简介《通讯录管理系统数据库设计与实现.doc》是一份围绕通讯录管理场景的数据库设计完整文档面向数据库初学者、高校学生及需要完成课程设计或毕业设计的开发者。内容从需求简介、功能概述与数据项说明出发系统梳理数据字典并覆盖分组、用户、联系人、拥有四类核心数据结构同时展开E-R图向关系模型的转换、数据库模式、用户子模式以及物理结构与索引设计子模式部分还涉及用户分组显示、联系人查询、账号管理等典型界面与权限场景。附有SQL Server环境下建库建表的SQL语句便于读者按文档步骤对照练习。资源为1个doc文件压缩包大小422KB虽是单文档但编排完整可作为数据库课程设计报告模板或入门练习材料。已有997人学习下载适合希望快速掌握数据库设计全流程的读者参考。1. 这标题看着像交作业实际上是让你把“通讯录”当业务系统来设计刚拿到“通讯录管理系统数据库设计与实现”这个题目时多数人会以为这是最水的课程设计——三张表、几个 CRUD一晚上搞定。真动手才发现通讯录恰恰是“看起来简单、做起来到处是坑”的典型重名联系人怎么区分一个联系人多个电话号码怎么存分组和标签是不是一回事删除联系人之后聊天记录和通话记录要不要跟着删这些问题你光靠一张contacts表根本回答不了。这篇笔记打算从数据库设计的角度把通讯录管理系统的表结构、关系建模、SQL 落地和常见坑完整过一遍。适合正在做课程设计的学生也适合要在一个星期内交付内部通讯录功能的后端开发。内容不是泛泛讲理论而是按“需求分析 → 关系模式 → 建表 SQL → 查询与维护 → 避坑”这条路径走争取让你照着做完就能跑跑完能答辩答辩完能直接改造成生产可用。2. 通讯录的业务建模先想清楚“联系人”到底是什么再谈建表2.1 从真实使用场景反推实体不只是“姓名电话”通讯录系统如果只存“姓名 手机号”那不叫设计叫 Excel 搬家。真实的通讯录使用场景至少包括这样几种一个人有多个电话手机、办公座机、家庭座机一个电话可能属于多个联系人家庭共享号码联系人之间有亲属、同事、同学这类关系用户要按分组管理联系人但一个联系人可以出现在多个分组用户希望给联系人打标签比如“重要客户”“周末打球”标签与分组的行为并不完全相同。把这些场景摊开我们就能从中间抽出候选实体用户系统使用者、联系人通讯录条目、联系方式手机、座机、邮箱等多个值、分组用户自建的分类目录、标签跨分组的语义标记、联系人与分组的关联、联系人与标签的关联。这里最关键的建模决策是联系方式不直接作为联系人表的字段而是拆成独立的“联系方式表”。这样设计后一个联系人拥有任意多个电话、任意多个邮箱都能在关系模型里表达而不是靠phone1、phone2、phone3这种字段去堆。分组和标签也容易混淆。分组更像文件夹语义是“组织”标签更像便签语义是“标记”。一个人同时属于“同事”和“家人”两个分组这在业务上是矛盾的——现实中你会建一个“家人同事”的混合分类吗但打上“同事”和“家人”两个标签就很合理。所以分组用“多对多关联表”支持一个联系人属于多个分组标签同样用关联表但两者在查询语义、统计口径上分开处理避免后续改需求时牵连表结构。2.2 从实体到关系模式三范式怎么落进通讯录场景关系模式设计就是把实体变成二维表把实体之间的关系变成外键。按照三范式的习惯我把整个通讯录管理系统的核心关系模式拆成六个部分用户表sys_user存系统登录账号联系人表contact存联系人姓名、性别、头像、备注这些“本身属于联系人”的属性联系方式表contact_way存电话、邮箱等可多值属性分组表contact_group与联系人的关联表contact_group_mapping负责分组关系标签表tag与联系人的关联表contact_tag_mapping负责标签关系。这里要特别注意第二范式的约束联系人表和联系方式表必须分离。如果把电话号码放进联系人表一个号码对应三个联系人时你是重复存三行还是只存一个重复存三行导致更新困难只存一个又丢失了“哪个联系人拥有它”的信息。拆表之后联系方式表一行就是一个电话号码外键指向联系人主键干净利落。这正是“通讯录管理系统数据库设计”这道题想考察的核心点。2.3 字段设计的取舍冗余、默认值、逻辑删除与时间戳具体字段设计有很多细节直接决定后续好不好用。主键我一般用BIGINT自增不推荐UUID原因是通讯录数据量不大自增主键在 InnoDB 里走聚簇索引性能稳定UUID带来的随机写入会造成页分裂。联系人姓名字段用VARCHAR(64)覆盖绝大多数中文姓名和英文名手机号要按 E.164 规范考虑86前缀所以给VARCHAR(20)而不是BIGINT——电话号码不是数字是字符串开头可以有还可能包含分机号-。“逻辑删除”这个设计我会强烈建议你加上。物理删除就是DELETE FROM一行数据直接没了但如果这个联系人是被另一个功能引用着比如后续接入通话记录删除后会出现一堆悬空引用。加一个is_deleted TINYINT DEFAULT 0字段删除时执行UPDATE ... SET is_deleted 1所有查询默认带上WHERE is_deleted 0就相当于给了自己一颗后悔药。代价是每个查询都要多一个条件但对通讯录这种低频写入、高频读取的系统来说完全值得。时间戳字段create_time和update_time建议类型用DATETIME并设置默认值。注意 MySQL 5.7 之后的写法是DEFAULT CURRENT_TIMESTAMP更新时用ON UPDATE CURRENT_TIMESTAMP自动维护修改时间。这个字段看似不起眼但做“最近新增联系人”“最近修改的记录”这类功能时直接就当索引条件用省一次全表扫描。3. 核心表结构落地把 ER 模型转成 MySQL 建表 SQL3.1 建库与基础配置字符集、排序规则和存储引擎的一次性决定我们先把数据库和基础选项定下来。下面这段 SQL 是建库的标准操作我一般直接在 MySQL 8.0 的mysql命令行客户端里执行也会把同样的脚本放到 Navicat 的查询窗口里跑。CREATE DATABASE IF NOT EXISTS address_book DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;字符集必须选utf8mb4因为它才是真正的“全量 Unicode”连 emoji 和冷僻汉字都能存。utf8mb4_unicode_ci是排序规则ci表示 case insensitive也就是比较字符串时忽略大小写这样查“ZhangSan”和“zhangsan”就像同一个词对通讯录这种大量人名搜索的场景更友好。如果用老旧的utf8遇到用户备注里存一个生僻字就报错这是最常见的建库翻车点。存储引擎选 InnoDB不用纠结。通讯录系统有事务需求批量导入联系人时要么全成功要么全回滚有外键需求联系方式指向联系人InnoDB 是唯一能同时满足这两点的默认选择。MyISAM 主打全文索引和读性能但现在 MySQL 8.0 的 InnoDB 已经支持全文索引MyISAM 在事务和外键上的短板反而更致命。3.2 用户表与联系人主表权限边界和核心属性分开建下面先建用户表和联系人主表。用户表对应“谁在用这个系统”联系人主表对应“一条通讯录记录本身”。这段 SQL 是整个设计的地基字段注释直接写在建表语句后面。CREATE TABLE sys_user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 用户ID自增主键, username VARCHAR(32) NOT NULL COMMENT 登录账号唯一, password_hash VARCHAR(128) NOT NULL COMMENT 加密后的密码不允许明文, nickname VARCHAR(32) NOT NULL DEFAULT COMMENT 昵称展示用, status TINYINT NOT NULL DEFAULT 1 COMMENT 账号状态1正常0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 最后修改时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE InnoDB COMMENT 系统用户表;sys_user表最需要注意的两个设计一是password_hash给VARCHAR(128)这是为 bcrypt 等哈希算法预留的长度存明文密码是通讯录系统评分时最致命的错误二是username上建了唯一索引保证同一系统里不会出现两个相同的登录账号。实际做多用户版本时所有联系人数据都应该带一个user_id外键表示“这份通讯录属于谁”但要注意user_id不要和联系人主键混在一起。联系人主表的设计依我看是整套数据库好坏的分水岭CREATE TABLE contact ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 联系人ID, user_id BIGINT UNSIGNED NOT NULL COMMENT 所属用户ID, name VARCHAR(64) NOT NULL COMMENT 联系人姓名, pinyin VARCHAR(128) NOT NULL DEFAULT COMMENT 姓名拼音用于A-Z索引, initial CHAR(1) NOT NULL DEFAULT # COMMENT 姓氏首字母非A-Z一律按#处理, gender TINYINT NOT NULL DEFAULT 0 COMMENT 性别0未知1男2女, avatar_url VARCHAR(512) NOT NULL DEFAULT COMMENT 头像地址空串表示未设置, company VARCHAR(128) NOT NULL DEFAULT COMMENT 公司或组织名称, remark VARCHAR(255) NOT NULL DEFAULT COMMENT 联系人备注, is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0未删除1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_deleted_name (user_id, is_deleted, name), CONSTRAINT fk_contact_user FOREIGN KEY (user_id) REFERENCES sys_user (id) ) ENGINE InnoDB COMMENT 联系人主表;这张表里pinyin和initial是典型的“冗余字段换查询性能”。通讯录 App 几乎都提供 A-Z 字母索引如果每次查询都调用数据库函数去拼拼音效率很低更现实的做法是应用层在新增联系人时用现成的拼音库算好拼音写入这两个字段查询时直接WHERE initial A就能按字母索引。user_id和is_deleted上建了联合索引普通查询都是“先按用户过滤再排除已删除记录最后按姓名排序”这个联合索引正好覆盖。3.3 联系方式、分组、标签把“多值”和“多对多”拆干净接下来三张表解决“一个人有多个号码”“一个联系人属于多个分组多个标签”这两个核心业务问题。CREATE TABLE contact_way ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 联系方式ID, contact_id BIGINT UNSIGNED NOT NULL COMMENT 所属联系人ID, way_type TINYINT NOT NULL COMMENT 类型1手机号2座机3邮箱4微信号5其他, way_value VARCHAR(128) NOT NULL COMMENT 联系方式的值如手机号或邮箱地址, is_default TINYINT NOT NULL DEFAULT 0 COMMENT 是否默认联系方式1是0否, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_contact_way (contact_id), CONSTRAINT fk_way_contact FOREIGN KEY (contact_id) REFERENCES contact (id) ON DELETE CASCADE ) ENGINE InnoDB COMMENT 联系方式表; CREATE TABLE contact_group ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 分组ID, user_id BIGINT UNSIGNED NOT NULL COMMENT 所属用户ID, group_name VARCHAR(32) NOT NULL COMMENT 分组名称如同学、家人, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序权重数值越小越靠前, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_group_user (user_id, group_name) ) ENGINE InnoDB COMMENT 联系人分组表; CREATE TABLE contact_group_mapping ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 关联ID, contact_id BIGINT UNSIGNED NOT NULL COMMENT 联系人ID, group_id BIGINT UNSIGNED NOT NULL COMMENT 分组ID, join_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_contact_group (contact_id, group_id), KEY idx_group_id (group_id), CONSTRAINT fk_cgm_group FOREIGN KEY (group_id) REFERENCES contact_group (id) ON DELETE CASCADE ) ENGINE InnoDB COMMENT 联系人与分组关联表;联系方式表与联系人表之间用了ON DELETE CASCADE意思是删除一个联系人时他的所有联系方式自动删除避免留下孤儿数据。分组表里UNIQUE KEY uk_group_user (user_id, group_name)限制“同一个用户不能建两个同名分组”这是很自然的业务规则。联系人与分组关联表则是一个标准的多对多连接表主键用自增id同时给(contact_id, group_id)建唯一索引防止同一个人被加进同一个组两次。标签相关的两张表结构几乎可以复用分组表的设计只是语义上“分组用于组织标签用于标记”。如果项目时间紧张这一节的分组和标签可以合并成一张“分类表 分类关联表”用type字段区分是分组还是标签但这样操作在统计卡片的实现上会更绕所以我会分成两套表。3.4 外键到底加不加InnoDB 下的选择与替代方案业内对外键有过不少分歧常见说法是“互联网大厂不用外键影响并发”。但通讯录管理系统是典型的内部工具型系统写入频率低查询频率高加外键利大于弊。外键能保证数据库层面的引用完整性比如你想删除一个仍有联系方式指向它的联系人ON DELETE CASCADE外键会自动处理如果用物理删除DELETE FROM contact WHERE id 3同时忘记删contact_way里的数据就会留下无主记录。假如你的项目明确要求不用外键至少要在应用层保证删除顺序先删关联表数据再删联系方式最后删联系人。这个顺序一旦错了就产生脏数据。外键加上之后批量导入联系人这类批量操作如果一次插入数千条外键检查会带来额外开销但通讯录系统达不到这个量级因此不用为并发性能过度设计。你在答辩时如果能主动讲清楚“我为什么加了外键、什么时候可以不加”比背概念拿分更容易。4. 通讯录核心操作从“新增联系人”到“按字母索引搜索”的完整 SQL4.1 新增联系人事务里一次写四张表顺序不能乱新增一个联系人是通讯录系统最重要的写操作它同时涉及联系人主表、联系方式表、分组关联表和标签关联表。下面这段 SQL 演示的是“新增一个带电话、并加入同事分组的联系人”我用事务包起来保证任何一步失败都能整体回滚。START TRANSACTION; INSERT INTO contact (user_id, name, pinyin, initial, gender, remark) VALUES (1, 张伟, zhangwei, Z, 1, 产品部对接人); SET new_contact_id LAST_INSERT_ID(); INSERT INTO contact_way (contact_id, way_type, way_value, is_default) VALUES (new_contact_id, 1, 13800138000, 1), (new_contact_id, 2, 010-12345678, 0); INSERT INTO contact_group_mapping (contact_id, group_id) SELECT new_contact_id, id FROM contact_group WHERE group_name 同事 AND user_id 1; COMMIT;这段 SQL 有几个点值得细看。LAST_INSERT_ID()在同一个会话中直接拿到上一条 INSERT 生成的主键避免了先查询再插入的两次往返如果并发高用LAST_INSERT_ID()可能拿到别的连接插入的 ID但那是建立在新连接不共享该函数的前提下单连接事务里安全。分组的插入用了SELECT ... FROM contact_group WHERE group_name 同事而不是硬编码group_id 5这样当分组 ID 变化时代码不用改。注意START TRANSACTION和COMMIT是成对出现的。实际项目中至少还要加一个ROLLBACK分支比如伪代码里try { 事务 } catch { ROLLBACK; throw }我这里为了展示核心 SQL 没把这段写成程序代码。判断新增成功的标准不是 INSERT 不报错而是事务提交后你用联系人 ID 查联系方式能查到 2 条记录。4.2 查询设计联系人列表、关键字搜索和字母索引的三类 SQL 模板列表查询是性能优化的重点很多通讯录系统越用越卡就是因为列表查询写了SELECT *然后分页后再去循环查联系方式。正确的做法是主查询只查联系人主表联系方式按需批量查出在应用层做组装。下面的查询返回第一页联系人按创建时间倒序只返回未删除记录SELECT id, name, initial, company, remark FROM contact WHERE user_id 1 AND is_deleted 0 ORDER BY create_time DESC LIMIT 20 OFFSET 0;LIMIT 20 OFFSET 0是 MySQL 的标准分页写法第一个参数是页大小第二个是偏移量。我的习惯是让前端传入页码和每页条数然后在后端把“页码”换算成“偏移量”offset (page - 1) * size而不是直接把 page 拼进 SQL。按姓名模糊搜索时索引基本失效但通讯录数据量不大全表扫描也能接受SELECT id, name, company FROM contact WHERE user_id 1 AND is_deleted 0 AND (name LIKE CONCAT(%, 张, %) OR pinyin LIKE CONCAT(%, zhang, %));这里同时匹配name和pinyin覆盖“输入汉字搜姓名”和“输入拼音搜汉字”两种搜索习惯。注意LIKE %zhang%这种前导通配符会使索引失效但对几千行的通讯录数据影响极小。真正要优化时可以改成WHERE pinyin LIKE zhang%让索引生效代价是用户输入中间部分拼音时搜不到这个取舍看具体场景。字母索引查询是通讯录的招牌功能SQL 非常直接SELECT id, name, initial FROM contact WHERE user_id 1 AND is_deleted 0 AND initial A ORDER BY pinyin ASC;initial A配合idx_user_deleted_name联合索引user_idis_deletedname能走索引快速过滤。排序用pinyin而不是name是因为拼音是连续的、可预测的排序键而中文姓名按数据库的排序规则排出来的顺序通常不符合用户预期。4.3 更新与删除物理删除的限制和批量导入的思路普通更新联系人信息注意UPDATE语句要带上is_deleted 0条件否则可能把已经逻辑删除的记录误改回来UPDATE contact SET company 新公司名称 WHERE id 10 AND user_id 1 AND is_deleted 0;删除操作用逻辑删除保留历史数据UPDATE contact SET is_deleted 1 WHERE id 10 AND user_id 1;这里要特别指出如果用物理删除DELETE FROM contact WHERE id 10联系方式表里关联的行会因为ON DELETE CASCADE被自动清理看起来没什么问题。但如果你以后想接“通话记录”模块让通话记录指向联系人 ID物理删除就会把这些记录也连带删掉导致统计数据不准。所以只要是商业项目或者要做产品迭代的课程设计我都建议用逻辑删除。批量导入联系人的场景在通讯录系统里极为常见比如从旧手机导出 CSV 再导入。实现思路是用LOAD DATA INFILE或编程语言循环插入但 SQL 层的关键经验是“先降索引再导入导完重建索引”。索引的维护成本是逐条写入时的额外开销批量导入时可以先ALTER TABLE contact DISABLE KEYS导入完毕再ENABLE KEYS。不过这个操作在 InnoDB 上并不是对所有索引都有效对唯一索引和主键无效。我自己更常用的做法是每条数据先查重再插入每 200 条提交一次事务这样既避免单条失败导致全部回滚的尴尬又不会因为每一条都提交一次事务导致提交日志爆炸。5. 通讯录数据库设计的 5 个踩坑记录现象、原因与解决办法5.1 手机号字段建了 BIGINT00 开头的固话直接丢号这是本项目最普遍的低级错误。有同学图省事把contact_way.way_value设计成BIGINT测试时输入手机号没问题某天录入一个0086-10-12345678格式的固话数据库报错或者数字溢出再看存进去的值开头的0丢了用户投诉“号码少了一位”。原因是BIGINT是数值类型数值类型不保存前导零0086会转成86。电话号码看似数字实际是字符串里面可能包含、-、空格、分机号。解决方法是把way_value定义为VARCHAR(32)并且在应用层写一个正则校验函数只允许数字、加号、减号和括号。为了彻底根治数据库层还能加一个CHECK约束MySQL 8.0.16 之后才真正强制执行CHECK约束8.0 以前只是“语法上接受、事实上跳过”所以这层的校验主要靠应用层把关。5.2 外键约束导致插入失败我在“分组映射”上踩的慢查询坑曾经有个需求是“给已存在的 1000 个联系人统一加进同事分组”。我直接写了INSERT INTO contact_group_mapping (contact_id, group_id) SELECT id, 5 FROM contact结果执行时报错 “Cannot add or update a child row: a foreign key constraint fails”。原因是有几个联系人处于逻辑删除状态当联系人属于逻辑删除状态时业务上不允许再建立关联。解决办法是把查询条件加上is_deleted 0INSERT ... SELECT id, 5 FROM contact WHERE is_deleted 0。这个坑本质上是“逻辑删除字段和外键约束打架”——外键只认物理行不管逻辑状态所以应用层必须把逻辑状态过滤职责扛起来。5.3 分组关联表不建唯一索引同一联系人被插进了同一分组两次删除分组、再重新建一个同名分组后再把联系人加进去发现重复。原因是分组删除时如果用的是物理删除DELETE FROM contact_group WHERE id 5关联表里的记录会被级联删掉但分组表自增 ID 不会复用新分组拿到id 100这时候再往关联表插入(contact_id10, group_id100)不会触发任何唯一索引冲突——因为旧记录确实已经没了但业务视角上“同一个分组出现了两次”。根本原因是在分组表上缺了唯一约束。解决方法是最初建表时加上UNIQUE KEY uk_contact_group (contact_id, group_id)只要同一个联系人和同一个分组 ID 不能建立第二行关系物理删分组后重建出的新 ID 反而不会挡住用户把联系人加进新分组这是好事。真正要防的是用户在同一个分组内重复添加联系人这一层唯一索引直接兜底。5.4 逻辑删除字段没进联合索引A-Z 索引页越翻越慢系统运行三个月后联系人从 200 条涨到 20000 条含已删除的字母索引查询忽然从 10 毫秒飙升到 500 毫秒。看 EXPLAIN 发现WHERE user_id1 AND is_deleted0 AND initialA在contact表上做了全表扫描。原因是最初只建了KEY idx_user_id (user_id)单列索引它只能过滤user_id然后还要回表逐行检查is_deleted和initial。当表里积累了几万条删除数据时回表的行数变大性能当然下降。解决办法是建联合索引(user_id, is_deleted, initial)让三个过滤条件都在索引命中回表行数约等于最终返回行数。这里有个经验联合索引的字段顺序要遵循“等值条件在前、范围条件在后”user_id是等值is_deleted是等值initial也是等值按字段顺序排下来正好跳过排序导致的额外文件排序。5.5ON DELETE CASCADE与逻辑删除策略互相矛盾物理删分组把通讯录删穿了还有一个隐蔽问题分组表的外键写了ON DELETE CASCADE然后程序里删除分组时用的是物理删除DELETE FROM contact_group WHERE id 5结果这个分组下所有关联联系人确实还在但关联关系全没了。用户界面上再看这批联系人分组列变成空以为是联系人丢了工单炸掉。原因很简单ON DELETE CASCADE联动的是“关联表里的行”不是联系人本身。整个系统用了逻辑删除策略但分组用物理删除两套策略并存导致语义混乱。解决方法是统一删除策略分组也做成逻辑删除加一个is_deleted字段查询时默认过滤已删分组。或者反过来联系人、分组、联系方式全部走物理删除靠外键级联清理。二选一不能混着用。从数据可恢复性的角度我站逻辑删除统一方案代价是每张业务表都多个is_deleted查询条件多一个AND换来的是误操作后还能救回来。6. 从课程设计到能用的通讯录软删除、隐私与数据迁移的进阶改造课程设计版本的数据库跑通“增删改查 分组 标签”之后想让它能真正投入使用还需要把三件事补上。第一件事是回收站设计。当前逻辑删除只是把is_deleted置 1用户界面上没有入口去恢复。我一般会在contact表上再加一个deleted_at DATETIME NULL字段为 NULL 表示未删除有值表示删除时间同时把is_deleted作为冗余字段保留这样“最近删除的 30 条记录”可以WHERE deleted_at IS NOT NULL AND deleted_at NOW() - INTERVAL 30 DAY查出来恢复时直接把deleted_at置成 NULL。第二件事是隐私字段的处理。通讯录里存的号码、邮箱属于敏感数据数据库层面一旦泄露就是事故。常见做法是contact_way.way_value不存明文而是用 AES_ENCRYPT 加密存储查询时用 AES_DECRYPT 解密程序里统一加密钥匙从环境变量读取不能写进 SQL 脚本里。这个方案的问题是排序和搜索不方便但通讯录本来就不需要按号码排序按姓名和拼音搜就行影响可控。如果只是课程设计至少要保证密码字段用password_hash而不是明文。第三件事是数据迁移脚本。从 Excel 导入联系人时Excel 里的“姓名 电话”两列不能直接映射到联系人表和联系方式表而是要先在程序里做一次拆分。经验数据表里的联系人可能有多列电话导入的 SQL 是先插入contact主表拿到主键再循环插入contact_way每 200 条一个批次提交。如果数据量上万可以用LOAD DATA LOCAL INFILE导入临时表再用INSERT ... SELECT从临时表拆到业务表能省十分钟级别的时间。最后说一个我从这个项目里沉淀下来的习惯数据库设计文档一定要包含“字段注释”和“关系说明”而不是只扔一个建表脚本。很多课程设计做完就忘半年后再打开address_book库看到contact_way.way_type的1、2、3、4、5根本不知道对应什么。我的做法是把这些注释直接写进建表 SQL 的COMMENT里再额外用一个 Markdown 文件记录“为什么 phone 用 VARCHAR 不用 BIGINT”“为什么加逻辑删除”这类决策过程。这比代码本身更能说明设计能力。希望这个从业务建模到物理表结构的完整思路能帮你少走我当年走过的弯路。照这套结构把表建好再把查询模板套进你的后端接口通讯录管理系统的核心就稳了。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?