简介一份基于Python与SQL Server开发的完整音乐管理系统源码面向个人音乐爱好者、小型音乐工作室及希望掌握音乐类项目开发的Python学习者。系统覆盖音乐分类、存储、检索、播放与推荐等核心功能具备清晰的前后台交互逻辑。 包体共127个文件压缩包大小约130.65MB。其中47个Python源文件构成系统业务逻辑39个字节码文件为编译产物18个MP3音频文件作为管理演示素材6个SQL脚本用于建库建表与存储过程另有XML和JSON配置数据文件支持灵活调整运行参数目录结构便于按模块查阅。 目前已有109人学习下载。整套代码展示了Python面向对象编程与SQL Server数据库协同工作的完整方案包含数据库设计脚本、可运行试听素材及工程配置文件可帮助读者快速上手音乐管理系统的开发思路与项目组织方法。1. 音乐管理系统为什么用 Python 配 SQL Server而不是 SQLite先说我拿到这套基于 Python 和 SQL Server 的音乐管理系统源码时的第一反应这不就是课程设计常见题嘛。但真正打开看了一遍建库脚本和连接代码之后我改了看法——它把 Python 的业务逻辑和 SQL Server 的数据库设计拆得很干净拿来当数据库课程设计的底子或者当 pyodbc 连接 SQL Server 的实战范本都是合适的。系统核心功能包括用户注册登录、歌曲信息管理、歌单维护和关键词检索界面用控制台交互数据全部落在 SQL Server 里。适合三类人正在做课程设计的学生、想把本地音乐库管起来又不想碰 Excel 的从业者以及想搞明白 Python 怎么连 SQL Server 的人。为什么偏偏是 SQL Server 而不是 SQLiteSQLite 单文件搞定确实省事但它扛不住多用户并发写入也没有成熟的权限体系。SQL Server 是企业里最常见的数据库之一事务、视图、存储过程这些能力都是现成的。用这套系统练手学到的表结构设计和事务写法可以直接平移到大一点的项目里。下面我按「建库 → 写业务 → 配环境 → 踩坑」的顺序把整份源码拆给你看。2. 数据库设计先行用户表、歌曲表、歌单表的关系建模2.1 表结构设计为什么拆成四张表而不是一张大表拿到源码先看 SQL 脚本这是我最推荐的习惯。整套系统围绕四张表转users 用户表、songs 歌曲表、playlists 歌单表、playlist_items 歌单明细表。这是典型的用户—歌单—歌曲多对多关系拆表的核心目的就一个降低数据冗余让修改和查询都变得可控。表名职责关键字段users用户账号user_id, username, password_hash, created_atsongs歌曲元数据song_id, title, artist, album, genre, duration_seconds, file_pathplaylists歌单头playlist_id, user_id, name, created_atplaylist_items歌单明细item_id, playlist_id, song_id为什么这么拆把歌曲、歌手、专辑信息全部塞进一张大表字段会膨胀到二十几个改一个歌手名要 UPDATE 几十行查“某用户某歌单里的歌”还得写一个结构混乱的 WHERE。拆开之后每张表只负责一件事查询用 JOIN 连接新增一个歌单只操作 playlists 和 playlist_items影响面小得多。有个细节值得学duration_seconds 用 INT 存秒数而不是字符串存“3:45”。存字符串排序和计算都不方便存 INT 显示时再格式化这是数据库设计里的基本功。文件路径 file_path 留了 NVARCHAR(200)实际使用中如果音乐文件在局域网共享目录这个字段可以存 UNC 路径长度也够。2.2 主键、外键与索引查询速度不是玄学主键用自增 INT IDENTITY(1,1)在 SQL Server 里默认同时是聚集索引行按主键顺序物理存储按主键查歌单明细效率最高。外键的作用不止是约束更重要的是让数据库自己保证引用完整性——你删一个用户得先处理他名下的歌单否则外键直接报错不会让你删出半个脏数据。再看检索场景。本系统最常用的操作是按标题搜歌、按歌手过滤所以源码给 songs 表建了两个非聚集索引CREATE INDEX idx_songs_title ON songs(title); CREATE INDEX idx_songs_artist ON songs(artist);索引不是越多越好。每一张索引在 INSERT 和 UPDATE 时都要同步维护写多读少的表加太多索引反而拖慢写入。这套系统的索引集中在 songs 表是因为歌曲查询频率远高于写入频率这个取舍是合理的。如果你要扩展最值得加的是 playlist_items.song_id 上的索引因为歌单 JOIN 歌曲时走的就是这个外键列。2.3 初始化脚本直接可跑的建库 SQL源码包里带一个 init_db.sql我本地跑了一遍结构可以拿来直接用-- 创建数据库忽略数据库已存在的报错 IF DB_ID(MusicDB) IS NULL CREATE DATABASE MusicDB; GO USE MusicDB; GO -- 用户表 CREATE TABLE users ( user_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password_hash NVARCHAR(64) NOT NULL, created_at DATETIME DEFAULT GETDATE() ); GO -- 歌曲表 CREATE TABLE songs ( song_id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(100) NOT NULL, artist NVARCHAR(50) NULL, album NVARCHAR(50) NULL, genre NVARCHAR(20) NULL, duration_seconds INT NULL, file_path NVARCHAR(200) NULL ); GO -- 歌单表 CREATE TABLE playlists ( playlist_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL FOREIGN KEY REFERENCES users(user_id), name NVARCHAR(50) NOT NULL, created_at DATETIME DEFAULT GETDATE() ); GO -- 歌单明细表 CREATE TABLE playlist_items ( item_id INT IDENTITY(1,1) PRIMARY KEY, playlist_id INT NOT NULL FOREIGN KEY REFERENCES playlists(playlist_id), song_id INT NOT NULL FOREIGN KEY REFERENCES songs(song_id), UNIQUE (playlist_id, song_id) ); GO注意建表顺序先 users再 playlists最后 playlist_items。因为外键必须先有被引用的表你把顺序反过来跑SQL Server 直接报“引用了不存在的对象”。GO 是批处理分隔符在 SSMS 里按批执行它不是 SQL 语句本身。字段全用 NVARCHAR 而不是 VARCHAR是为了中文不乱码这个细节在避坑章里会展开说。2.4 初始化数据两条路径别一行行手敲 INSERT初始化脚本建完表之后是空的系统跑起来没意思。源码包在 init_db.sql 末尾带了一段种子数据往里插了几首示例歌曲INSERT INTO songs (title, artist, album, genre, duration_seconds, file_path) VALUES (N晴天, N周杰伦, N叶惠美, N流行, 269, NC:\Music\晴天.mp3), (N海阔天空, NBeyond, N乐与怒, N摇滚, 326, NC:\Music\海阔天空.mp3), (N平凡之路, N朴树, N猎户星座, N民谣, 242, NC:\Music\平凡之路.mp3);字符串前面的 N 是 SQL Server 里表示 Unicode 字符串的前缀配合 NVARCHAR 列才能保证中文不出乱码。如果你已经有现成的音乐列表建议直接在 SSMS 里用导入向导把 CSV 或者 Excel 导进 songs 表比手写几百行 INSERT 快得多。导入时注意第一行如果是列名要在向导里勾选“第一行包含列名”不然会把标题行当数据插进去后面检索时脏数据全冒出来。3. 核心功能落地登录、歌单和检索的 Python 实现3.1 连接封装统一入口比到处写连接串强看 Python 部分之前我先扫了一遍代码里有没有重复的连接串。最怕看到的一种写法是每个函数里都写一遍pyodbc.connect(...)后面改个密码要全局替换。这份源码在这方面做得不错把连接封装成了一个函数import pyodbc SERVER localhost # 本机跑默认实例 DATABASE MusicDB USERNAME sa PASSWORD your_password # 改成你自己的 sa 密码 def get_conn(): conn_str ( DRIVER{ODBC Driver 17 for SQL Server}; fSERVER{SERVER}; fDATABASE{DATABASE}; fUID{USERNAME}; fPWD{PASSWORD}; CHARSETUTF8; ) return pyodbc.connect(conn_str, autocommitFalse)这里最值得说明的是两个参数。第一DRIVER必须填本机实际安装的 ODBC 驱动版本常见的还有ODBC Driver 13 for SQL Server驱动版本和 SQL Server 版本不匹配会出现“找不到驱动”的报错后面避坑章第一条就是它。第二autocommitFalse是刻意设的意思是每条 SQL 不会自动提交必须手动 commit这样才能在业务代码里做事务控制。如果你用的是 pymssql连接参数略有差异但封装思路是一样的。3.2 注册与登录密码哈希和参数化查询用户模块是系统的入口源码里注册和登录各写了一个函数。注册时密码不做明文存储用 hashlib 做 SHA-256 哈希登录时再对输入做同样哈希后比对import hashlib import pyodbc def hash_password(password: str) - str: # 实际项目中建议再加盐这里保持源码原样 return hashlib.sha256(password.encode(utf-8)).hexdigest() def register(username: str, password: str) - bool: pwd_hash hash_password(password) conn get_conn() cursor conn.cursor() try: cursor.execute( INSERT INTO users(username, password_hash) VALUES (?, ?), (username, pwd_hash) ) conn.commit() return True except pyodbc.IntegrityError: # username 有唯一约束重复注册会触发这个异常 return False finally: cursor.close() conn.close() def login(username: str, password: str): pwd_hash hash_password(password) conn get_conn() cursor conn.cursor() cursor.execute( SELECT user_id FROM users WHERE username ? AND password_hash ?, (username, pwd_hash) ) row cursor.fetchone() cursor.close() conn.close() return row[0] if row else None两个地方要注意。一是所有 SQL 都用了?占位符传参而不是把变量直接拼进 SQL 字符串。字符串拼接看起来方便一旦用户输入 OR 11 --这类内容整个登录逻辑就废了参数化查询是防注入最基础的一层。二是注册时捕捉了pyodbc.IntegrityError当用户名重复插入违反唯一约束时这个异常会被触发直接返回 False 提示用户换名字不用先查一遍再插入既省一次查询又避免并发下两个人同时注册同一个用户名的竞态。3.3 歌曲检索与歌单管理LIKE 模糊匹配歌曲检索是核心功能源码里用 LIKE 实现模糊匹配。这里同样走参数化查询把用户输入作为参数传入而不是拼进 SQLdef search_songs(keyword: str): conn get_conn() cursor conn.cursor() pattern f%{keyword}% cursor.execute( SELECT song_id, title, artist, duration_seconds FROM songs WHERE title LIKE ? OR artist LIKE ?, (pattern, pattern) ) rows cursor.fetchall() cursor.close() conn.close() return rowsLIKE 的%通配符放在参数里而不是 SQL 里这点容易被忽略。如果把%直接写进 SQL 字符串就得处理用户输入里本身就带%或_的情况那又是一层转义逻辑。用参数化写法后用户输入特殊字符时行为可预期得多。歌单管理的插入操作也值得看一眼比如往歌单里加一首歌def add_to_playlist(playlist_id: int, song_id: int): conn get_conn() cursor conn.cursor() try: cursor.execute( INSERT INTO playlist_items(playlist_id, song_id) VALUES (?, ?), (playlist_id, song_id) ) conn.commit() except pyodbc.IntegrityError: # UNIQUE(playlist_id, song_id) 约束触发说明重复添加 print(这首歌已经在歌单里了) finally: cursor.close() conn.close()重复添加这个业务规则约束直接写在数据库层应用层只需要捕捉异常。实际项目里这种设计很常见——能用数据库约束兜底的不要只靠应用层 if 判断因为并发场景下两个请求可能同时通过 if 检查然后一起插入最后靠数据库约束兜住。3.4 事务提交与回滚歌单创建不能做一半创建歌单并往里面加歌是典型的跨表事务场景。源码里 create_playlist 一次完成两步操作先往 playlists 插一条再把歌曲明细插进去。def create_playlist_with_songs(user_id: int, name: str, song_ids: list): conn get_conn() cursor conn.cursor() try: # OUTPUT INSERTED.playlist_id 用于拿到刚插入的自增主键 cursor.execute( INSERT INTO playlists(user_id, name) OUTPUT INSERTED.playlist_id VALUES (?, ?), (user_id, name) ) row cursor.fetchone() playlist_id row[0] for song_id in song_ids: cursor.execute( INSERT INTO playlist_items(playlist_id, song_id) VALUES (?, ?), (playlist_id, song_id) ) # 全部成功才提交 conn.commit() return playlist_id except Exception as e: # 任一步失败整体回滚数据库保持原样 conn.rollback() raise e finally: cursor.close() conn.close()这里有个识货点OUTPUT INSERTED.playlist_id是 SQL Server 里拿自增主键的推荐做法比先 SELECT MAX(id) 再插入可靠得多后者在多用户并发时必然出问题——两个用户同时建歌单MAX 查出来的可能是对方的歌单 ID。事务的意义也在这里歌单头插成功、明细插失败如果不回滚数据库里就多个空歌单后面列表展示和统计全是脏数据。带autocommitFalse的连接加上 commit/rollback把两步操作绑成一个原子动作。4. 环境配置与初始化驱动、连接串和建库脚本4.1 SQL Server 安装先开混合认证再建库这套系统跑起来的前提是 SQL Server 能用密码登录。如果你装的是默认 Windows 身份验证模式源码里UIDsa; PWD...这种连接串会被直接拒掉所以安装时要选混合模式或者装完以后再改。改法是在 SSMS 里右键服务器 → 属性 → 安全性 → 选中 SQL Server 和 Windows 身份验证模式然后重启 SQL Server 服务生效。sa 账号默认是禁用的需要手动启用并设置密码安全性 → 登录名 → sa → 右键属性状态选项卡里点“启用”再在常规选项卡里改密码。这一步做完才能在 Python 里用 sa 连库。建议只在开发环境这么干生产环境建一个最小权限账号只对这个库授权别把 sa 到处用。4.2 Python 侧准备pyodbc 依赖和 vscode 环境Python 侧的核心依赖只有一个 pyodbc。装之前先在命令行确认 pip 可用然后执行pip install pyodbc用 vscode 打开源码目录时建议先建虚拟环境再装依赖避免污染全局 Python 环境python -m venv .venv # Windows 下激活虚拟环境 .venv\Scripts\activate # 激活后再次安装 pip install pyodbcvscode 装完 Python 插件后按 CtrlShiftP 调出命令面板输入Python: Select Interpreter选中刚才创建的 .venv 解释器。这一步不做的话vscode 可能还在用系统级 Python包装到了虚拟环境里运行却用的是另一个解释器怎么 import 都报 ModuleNotFoundError。顺便确认 pyodbc 装成功命令行里跑一句python -c import pyodbc; print(pyodbc.version)能打出版本号就说明驱动绑定没问题。4.3 连接串的几种写法本机和局域网不一样源码里连接串写的是SERVERlocalhost这适用于本机直接跑。如果你要把数据库放到另一台机器上比如局域网里单独一台装了 SQL Server 的服务器连接串要改成 IP 加端口场景SERVER 写法说明本机默认实例localhost 或 127.0.0.1最省事本机调试用局域网服务器192.168.1.100,1433端口默认 1433逗号分隔命名实例192.168.1.100\SQLEXPRESS实例名用反斜杠注意转义连接串里端口写在 SERVER 字段里不要另写PORT1433pyodbc 不认识这个参数写了也白写。局域网连接还有一个前提SQL Server 配置管理器里要启用 TCP/IP 协议并把 TCP 端口设为 1433同时 Windows 防火墙放行 1433 入站规则。这个坑在避坑章第二条会细说。4.4 首次初始化建库、导数据、验证环境配好后初始化顺序建议固定成这样否则容易漏步骤在 SSMS 里打开 init_db.sql选中全部执行一遍建库建表插种子数据。回到源码目录先跑一遍主程序确认能启动到登录界面。注册一个测试账号登录后搜一首种子歌曲。SSMS 或者图形化工具里执行脚本时如果报错在某一行先看是不是 GO 分隔符没被正确识别——有些工具对 GO 的解析和 SSMS 不同逐段复制到查询窗口执行也能跑通。这一步做完Python 代码只要连接串的账号密码和 SQL Server 对得上系统就能直接跑起来。5. 避坑指南SQL Server 连接与数据写入的典型故障5.1 连接层驱动找不到、TCP/IP 没开、登录被拒现象pyodbc.connect()抛Cant open lib ODBC Driver 17 for SQL Server: file not found。原因本机装的是其他版本的 ODBC 驱动或者根本没装微软 ODBC Driver。pyodbc 是 ODBC 的封装系统里没有对应的驱动库文件连接就是黑匣子一样地报错。解决先查本机装了哪些驱动odbcinst -q -d # Windows 或 Linux 均可列出来的条目里有哪个版本就把连接串的DRIVER改成哪个比如{ODBC Driver 13 for SQL Server}。如果一条都没有去微软官网下 ODBC Driver 安装包17 和 18 两个大版本都可以装完再跑 odbcinst 确认。现象连接串填了局域网 IP报超时或者提示目标计算机积极拒绝端口不通。原因SQL Server 默认不开启 TCP/IP 协议或者防火墙拦了 1433。解决打开 SQL Server 配置管理器SQL Server 网络配置 → 实例名 → TCP/IP 右键启用然后在 IP 地址页里把 IPAll 的 TCP 端口改成 1433。改完必须重启 SQL Server 服务。Windows 防火墙里放行 1433命令行顺手测一下telnet 192.168.1.100 1433能进去说明端口通了进不去就继续查防火墙或 SQL 服务状态。现象连接串信息都对报Login failed for user sa错误号 18456。原因服务器还在 Windows 身份验证模式或者 sa 账号被禁用再或者密码不对。解决SSMS 里切到混合认证并重启服务启用 sa 并重设密码。这三步缺一不可只改其中一项仍然报 18456。5.2 数据层中文乱码、导入无效、类型转换失败现象歌曲名写入数据库后变成???读出来也是乱码。原因列类型用了 VARCHAR而 Python 传的是 Unicode 字符串反过来列是 VARCHAR 但代码里没加 N 前缀也会出问题。VARCHAR 按非 Unicode 编码存中文字符在某些排序规则下存不进去直接变问号。解决建表时字符串列一律用 NVARCHAR插入语句带 N 前缀。如果表已经建了直接把列类型 ALTER 成 NVARCHAR再重新插入数据。不要靠 Python 端 encode 来绕源头改列类型才是正解。现象SSMS 导入 CSV 时报“无法导入数据数据无效”或者导进去的行数是 0。原因最常见的两个原因一是 CSV 编码和数据库排序规则不一致文件是 UTF-8数据库默认排序规则按简体中文导致解析失败二是文件里有 BOM 头或特殊分隔符向导默认按逗号切分却没识别到。解决先用记事本或 vscode 把 CSV 另存为 UTF-8 with BOM导入向导的数据类型检测里把字符串列改成 NVARCHAR。如果单行数据里有逗号CSV 要做引号转义或者干脆改成用 Tab 分隔。现象从 SQL Server 里取出的字段是字符串想转成数字参与计算CONVERT(int, ...)直接报错转换失败。原因字符串里有空格、中文或者空值。SQL Server 的 CONVERT 遇到不可转换的值是直接抛错不会给你留余地。解决转换前先清洗去空格、过滤非数字。SQL Server 2012 以上可以用TRY_CAST和TRY_CONVERT转换失败时返回 NULL 而不是抛错SELECT TRY_CAST(ISNULL(NULLIF(column, ), 0) AS INT)如果连 TRY_CAST 都查不出来用ISNUMERIC先过滤一遍把明显不是数字的行剔掉再转。这套系统里 duration_seconds 本身就是 INT正常情况下不需要这种转换但你要扩展字段、导入外部 Excel 时这种清洗逻辑一定会用上。6. 进阶玩法把控制台程序升级成带界面的管理系统6.1 先写一个冒烟测试脚本再做扩展系统跑通之后最值得做的一件事不是马上加花哨功能而是写一个冒烟测试脚本把核心链路压一遍。我每次拿到新源码都会先干这个因为手动点控制台太容易漏环节了。# smoke_test.py from db_handler import register, login, search_songs, create_playlist_with_songs def run(): # 1. 注册一个新用户 ok register(test_user, test123) print(注册:, 成功 if ok else 失败(可能已存在)) # 2. 登录 uid login(test_user, test123) assert uid is not None, 登录失败 print(登录: 成功, user_id , uid) # 3. 搜一首种子歌曲 rows search_songs(晴天) assert len(rows) 0, 搜索无结果 print(搜索: 命中, len(rows), 条) # 4. 建歌单并加歌 pid create_playlist_with_songs(uid, 测试歌单, [r[0] for r in rows]) print(建歌单: 成功, playlist_id , pid) if __name__ __main__: run()这个脚本把注册、登录、检索、事务四条主链路一次性走完任何一步抛异常都能在 5 秒内暴露。数据库连接配置改了、表结构动了、密码换了先跑它比手动点半天菜单高效得多。6.2 加一个 Web 壳Flask 是最短路径控制台交互只适合练手真要给同学演示套一个 Flask Web 壳成本最低。把db_handler.py里的函数直接搬进 Flask 路由from flask import Flask, request, jsonify from db_handler import login, search_songs app Flask(__name__) app.route(/api/login, methods[POST]) def api_login(): data request.get_json() uid login(data[username], data[password]) if uid: return jsonify({code: 0, user_id: uid}) return jsonify({code: 1, msg: 用户名或密码错误}) app.route(/api/search, methods[GET]) def api_search(): keyword request.args.get(q, ) rows search_songs(keyword) return jsonify({code: 0, data: [list(r) for r in rows]}) if __name__ __main__: app.run(debugTrue)这样做的好处是数据层代码一行没改只在外面套了接口层前端的选型完全不用绑死在控制台里。我最初做课程设计的时候总想着一步到位上来就写界面结果界面写得难看业务逻辑也没调对交付前两天疯狂返工。从那以后我每次拿到新项目都强制先跑一遍冒烟测试确认数据层是稳的再碰任何界面代码。花十分钟写个测试脚本能省下后面两小时的排查时间这个习惯我建议你也养成希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?