简介:这是一份面向数据库开发者、管理员及项目负责人的新闻发布系统数据库设计模板,围绕数据库环境说明、命名规则、逻辑设计、物理设计等关键内容展开,可直接作为新闻类站点或内容管理系统数据建模的参考规范。资源包为单个 doc 文档,大小约 241KB,结构紧凑、便于查阅;文档先给出数据库环境与命名规则,再从表汇总入手,对用户信息表、新闻表、留言表、新闻类别表等核心数据表进行字段级说明,包含表结构、索引、视图及存储与检索思路,可帮助读者快速理解新闻发布业务的数据关系并迁移到同类系统中。目前已有 114 人学习该资源,适合正在搭建新闻发布系统、撰写数据库设计文档,或希望系统规范数据库设计流程的初中级开发者学习使用。
1. 新闻发布系统数据库设计:一份 2012 年设计文档的现代复现
接手老项目最头疼的不是读代码,而是把版本库里那份数据库设计文档重新变成能跑的库。新闻发布系统是典型的“四张表”业务:用户、新闻、留言、类别,规模不大,却完整覆盖了主键设计、外键关联、文本字段选型、密码加密和权限系统集成这几类绕不开的问题。
下面以一份 2012 年的《新闻发布系统数据库设计报告》为底稿,把逻辑设计、物理结构和安全性方案逐个拆开讲,并给出可直接执行的 T-SQL 脚本和核对查询。正在做课程设计、接手老系统维护、或者需要把历史设计文档反推成库表的人,都能找到对应章节直接照着抄。
2. 逻辑设计与命名规则:从业务动作推导四张表
拿到需求直接写 CREATE TABLE,是最容易翻车的一种开始方式。新闻发布系统的数据量级和业务边界决定了模型的上限:游客浏览新闻、管理员发布稿件、访客留言,本质上只有三个动作。把动作映射成实体,就是新闻、类别、留言和后台用户四张表。先画业务流程图再定表,比对着页面输入框建字段要稳得多。
2.1 为什么四张表够用
这个系统里,管理员是唯一的内容生产者,所以用户表不需要角色字段,权限判断交给 ASP.NET 的成员管理系统;游客是唯一的内容消费者,留言表只需要记录 IP 和内容,不需要为游客建独立档案。很多初学者会把管理员和普通注册用户塞进同一张用户表,导致表结构里出现一堆可空字段(头像、积分、等级),这些字段对新闻发布场景毫无意义。四张表的设计之所以成立,是因为它严格按业务流程划分实体边界,没有引入“未来可能会用”的占位字段。这个取舍看起来朴素,却是区分数据库设计好坏的分水岭。
2.2 dbo 前缀与表命名规则的实际含义
原文档的命名规则写了两条:数据库命名全部由英文小写字母组成、单词之间用下划线分割;表命名为 dbo_表义名。这里有一个容易误解的点需要澄清:SQL Server 里dbo是默认架构名(schema),不是数据库名前缀。文档里写的dbo.News,完整访问路径是“数据库名.架构名.表名”,也就是news.dbo.News。
实际建库时数据库名是news,所有表挂在dbo架构下,表名直接用News、Category这类业务词即可。原文档写的dbo_表义名,本质是强调用架构限定名区分同名业务表,而不是让表名本身带 dbo 后缀。下表是四张表的实体划分与关联关系,建表顺序依赖关系一目了然:
表 2.1 四张表的实体划分与关联
| 表名 | 实体含义 | 核心字段 | 与其他表的关联 |
|---|---|---|---|
| dbo.User | 后台管理员 | UserID, UserName, UserCode | 业务鉴权字段独立 |
| dbo.Category | 新闻分类 | CategoryID, CategoryName | 被 News 引用 |
| dbo.News | 新闻内容 | NewsID, NewsTitle, NewsContent | CategoryID 引用 Category |
| dbo.Comment | 游客留言 | CommentID, CommentContent, CreateTime | NewsID 引用 News |
2.3 先建库再建表:从 PowerDesigner 生成脚本到手工初始化
原文档用 PowerDesigner 9.0 画 ER 图,再用 SQL Server 查询分析器执行生成的脚本。PowerDesigner 的物理数据模型可以直接生成带扩展属性的 T-SQL 脚本,直接执行没问题,但脚本很啰嗦,不容易看出建库顺序。我更愿意手动写初始化脚本,结构清晰,也方便团队评审:
-- 新闻发布系统数据库初始化:不存在才创建,避免重复执行报错 IF DB_ID(N'news') IS NULL BEGIN CREATE DATABASE news ON PRIMARY ( NAME = N'news_data', FILENAME = N'D:\SQLData\news_data.mdf', SIZE = 16MB, FILEGROWTH = 16MB ) LOG ON ( NAME = N'news_log', FILENAME = N'D:\SQLData\news_log.ldf', SIZE = 8MB, FILEGROWTH = 8MB ); END GO这段脚本先通过DB_ID(N'news')判断数据库是否已存在,防止重复执行时抛错。ON PRIMARY指定主数据文件,LOG ON指定日志文件。FILEGROWTH设成固定 16MB 而不是默认的百分比,是为了避免数据库在快速增长阶段出现大量碎片化的小幅度扩展。
实际部署时,数据文件与日志文件应当放在不同物理磁盘上,这里为了演示写在了同一目录。建库之后,按依赖顺序建表:先Category,再News(因为外键引用 Category),最后Comment(因为外键引用 News)。顺序反了会出现“引用对象不存在”的报错,这也是手工脚本比 PowerDesigner 一次生成更不容易出意外的地方。
3. 物理表结构拆解:类型选型、主外键与字段命名陷阱
逻辑设计回答“有哪些实体”,物理设计回答“字段用什么类型、是否可空、默认值怎么设”。文档里的四个表结构,任何一张单独拿出来看都有值得展开的设计点,这里按用户、新闻、留言、类别的顺序依次拆开,顺带指出哪些字段设计放到今天需要修正,哪些可以保留原样不动。
3.1 用户表 dbo.User:CHAR(20) 存不下哈希后的密码
文档里用户表以UserID为主键,字段有UserName、UserCode(密码)、UserQQ、UserAge、UserEmail。整理后的结构如下,右侧是结合当前安全要求的修正建议:
表 3.1 用户表结构与原设计对比
| 字段 | 原设计类型 | 可空 | 建议修正 | 说明 |
|---|---|---|---|---|
| UserID | INT | 否 | INT IDENTITY | 主键,自增 |
| UserName | VARCHAR(10) | 否 | NVARCHAR(20) | 登录名,支持中文 |
| UserCode | CHAR(20) | 否 | NVARCHAR(128) | 密码,哈希存储 |
| UserQQ | VARCHAR(20) | 是 | NVARCHAR(20) | 备用联系方式 |
| UserAge | INT | 是 | 可删 | 年龄不是刚性字段 |
| UserEmail | VARCHAR(50) | 是 | NVARCHAR(100) | 邮箱 |
最需要修正的是UserCode的类型。CHAR(20)是定长字符,存明文密码勉强够 20 个字符,但如果按文档 6.2 节的要求把密码做加密后再存储,MD5 需要 32 位十六进制、SHA-1 需要 40 位、SHA-256 需要 64 位,加上盐值更长,CHAR(20)直接放不下。类型和加密算法必须绑定在一起考虑,这是一个容易在设计阶段漏掉、开发阶段爆雷的坑。
3.2 新闻表 dbo.News:TEXT 升级为 NVARCHAR(MAX),外键要显式声明
新闻表字段简单,但建表脚本里藏着三个值得注意的决策。先看脚本:
CREATE TABLE dbo.News ( NewsID INT IDENTITY(1,1) NOT NULL, NewsTitle NVARCHAR(100) NOT NULL, NewsContent NVARCHAR(MAX) NOT NULL, CreateTime DATETIME NOT NULL CONSTRAINT DF_News_CreateTime DEFAULT(GETDATE()), CategoryID INT NOT NULL, CONSTRAINT PK_News PRIMARY KEY CLUSTERED (NewsID), CONSTRAINT FK_News_Category FOREIGN KEY (CategoryID) REFERENCES dbo.Category(CategoryID) ); GO提示:原文档把标题类型写成
VACHAR(100),这是VARCHAR(100)的笔误。这里直接升级为NVARCHAR(100),因为新闻标题含中文和全角符号,用 Unicode 存储可以避免排序规则导致的乱码。
脚本逻辑说明:NewsID INT IDENTITY(1,1)从 1 开始自增,避免应用层手工维护主键;CONSTRAINT DF_News_CreateTime DEFAULT(GETDATE())让不传时间的 INSERT 语句自动取服务器当前时间;FK_News_Category强制新闻必须属于已存在的类别,这一条比在应用代码里判断“类别是否存在”更可靠——数据库层面的约束不会被上层逻辑绕过。
同时,NewsContent没有沿用原文档的TEXT类型。TEXT、NTEXT、IMAGE是 SQL Server 2005 之前的遗留类型,2012 还能用但已标记为弃用,新代码一律用VARCHAR(MAX)或NVARCHAR(MAX)代替。两者的行为差异主要在存储管理和查询计划方面,MAX类型与现有字符串函数(如LEN、SUBSTRING)兼容性更好。
3.3 留言表 dbo.Comment:UserID 还是 UserIP,命名语义必须一致
留言表在原文档里的关键字段是CommentID、CommentContent、CreateTime、UserID和NewsID。最有争议的是UserID——字段注释写的是“用户 IP 地址”,但字段名却是 UserID。
这是设计文档里很典型的一类问题:命名与语义不一致。游客留言没有登录,唯一能追踪的身份就是来源 IP,所以这个字段本质上是UserIP,类型VARCHAR(15)(IPv4 最大长度 15 个字符)。如果坚持叫UserID,后续维护的人会下意识认为它关联用户表,可能错误地加入外键约束。碰到这种名不副实的字段,最省事的做法是直接重命名,不要带着歧义往下传。
键约束方面,Comment.NewsID必须引用News.NewsID。外键建立后,删除已有留言的新闻时数据库会拦截,这从产品逻辑上是合理的:新闻报道发布后被删除,其留言应当一并处理,而不是出现松散的孤儿数据。
3.4 类别表 dbo.Category:CategoryName 与 Type 到底留哪个
类别表结构为CategoryID主键、CategoryName类别名、Type类别类型。CategoryName和Type的语义高度重叠,一个叫“新闻类别名”,另一个叫“新闻类别类”,如果不是原作者解释,很难说清区别。如果重建这张表,底稿可以这样写:
CREATE TABLE dbo.Category ( CategoryID INT IDENTITY(1,1) NOT NULL, CategoryName NVARCHAR(20) NOT NULL, -- 原文档里的 Type 字段与 CategoryName 语义重叠,建表时不保留 CONSTRAINT PK_Category PRIMARY KEY CLUSTERED (CategoryID) ); GO我的建议是二选一:如果Type仅表示分类层级,比如“一级分类、二级分类”,那应该拆成ParentID做树形结构而非用Type字符串字段;如果Type只是另一个名字,则直接删掉,保留CategoryName。多一个语义模糊的字段,维护时就要为“它为什么存在”付一次成本。
4. 安全性设计与 ASP.NET Membership 集成:从密码哈希到权限表对接
安全边界可以分三层看:游客能不能碰到数据库、管理员密码以什么形式落库、后台登录权限怎样复用现成的成员管理机制。原文档第 6 节讲了密码加密,第 7 节讲了成员系统映射,但这两块要落到真实环境,还要补一层数据库访问隔离。这一章把三层串起来讲,从端口监听、账号授权开始,最后落到你在代码里如何组织密码校验。
4.1 三层隔离:端口、账号、连接串
文档只规定游客不能直接操作数据库,具体怎么实施是开发者的活。常见做法是:
- 数据库服务器只监听内网 IP,Web 服务器作为唯一访问源;如果 Web 服务和数据库服务装在同一台机器,SQL Server 只监听 127.0.0.1,不对公网开放 1433 端口。
- 为 Web 应用单独创建登录账号,只授予
db_datareader和db_datawriter固定数据库角色,不授予db_owner,更不能用sa。 - 连接字符串里不写明文密码,改用 Windows 身份验证或者加密后的配置项。文档里把 Web 服务和数据库服务放同一台机器,这种情况下用服务账号的 Windows 身份验证,比在连接串里写账号密码更可控。
原文档提到 SQL Server 登录模式为混合身份验证、sa 密码为 123。这是开发环境的配置,上线前必须改掉:要么关闭 SQL 身份验证,要么至少把 sa 密码替换为强密码并且禁止远程登录。数据库账号最小化授权这条,做起来成本最低,收益却最直接——即使连接串泄露,攻击者拿到的也只是读写权限,不能改表结构。
4.2 密码存储:从明文到加盐哈希的落地写法
2012 年的系统常用 MD5 做密码摘要,当时不觉得有问题,但现在 MD5 和 SHA-1 都属于被攻破的哈希算法。密码字段如果还没有固定长度,正好借着数据库设计稿定型的时机改成 PBKDF2:
public static string HashPassword(string password, byte[] salt, int iterations = 10000) { // PBKDF2 迭代生成 32 字节哈希,输出为 salt+hash 的 Base64 using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, iterations)) { byte[] hash = deriveBytes.GetBytes(32); byte[] combined = new byte[salt.Length + hash.Length]; Buffer.BlockCopy(salt, 0, combined, 0, salt.Length); Buffer.BlockCopy(hash, 0, combined, salt.Length, hash.Length); return Convert.ToBase64String(combined); } }salt应该随机生成 16 字节,每次注册都不一样;iterations决定计算成本,10000 次是 OWASP 建议的下限,性能有余量再往上加。验证时把库里取出的 Base64 解出来,前 16 字节作 salt,剩余 32 字节作 hash,用同样的迭代次数重新计算再比较。这样即使两个用户密码相同,库里的值也不一样,彩虹表攻击失效。
4.3 业务用户表与 ASP.NET Membership 的字段映射
文档第 7 节给出的是 2012 年前后 Web Forms 项目很常见的做法:后台用户信息存在自定义表BBC_EMPLOYEE,登录认证走 ASP.NET 自带的成员管理系统。两边的映射如下:
表 4.1 自定义用户表与 ASP.NET Membership 的关联键
| 自定义表字段 | ASP.NET 表字段 | 关联作用 |
|---|---|---|
| BBC_EMPLOYEE.E_NAME | aspnet_Users.UserName | 保存后台管理的用户编号 |
| BBC_EMPLOYEE.E_PASSWORD | aspnet_Membership.Password | 共同保存用户的密码 |
这样做的好处是少改业务代码,坏处是用户名一旦变更,业务表和aspnet_Users的关联链路就断了。文档里默认用户名不可改,所以映射关系能成立。
如果你现在用的是 ASP.NET Core Identity,不建议照搬这套双表映射。更合理的做法是在业务表加一列AspNetUserId,直接存 Identity 表的 Guid 主键,从根上避免“名字改了到处跟着改”的问题。老代码维护时保留映射关系即可;新项目优先考虑在业务表里关联主键,而不是依赖“名字相同”这种软关联。
5. 用系统视图核对建库结果:三条 SQL 把设计文档验证一遍
设计文档写完、建库脚本跑完,不等于工作结束。真正要做的是用 SQL 把库里的表结构拉出来,跟文档逐行比对。下面这三条查询是我核对老项目时固定会跑的,适用的场景是手头既有设计文档又有数据库访问权限,但不确定脚本执行结果和文档是否完全一致。
5.1 字段级核对:一次查询列出全部表和列的元数据
SELECT t.name AS 表名, c.name AS 字段名, ty.name AS 数据类型, c.max_length AS 长度字节, c.is_nullable AS 可空, CASE WHEN pk.column_id IS NOT NULL THEN 1 ELSE 0 END AS 主键, CASE WHEN fk.parent_column_id IS NOT NULL THEN 1 ELSE 0 END AS 外键 FROM sys.tables t JOIN sys.columns c ON t.object_id = c.object_id JOIN sys.types ty ON c.user_type_id = ty.user_type_id LEFT JOIN sys.index_columns pk ON pk.object_id = c.object_id AND pk.column_id = c.column_id AND pk.index_id = 1 LEFT JOIN sys.foreign_key_columns fk ON fk.parent_object_id = c.object_id AND fk.parent_column_id = c.column_id WHERE t.schema_id = SCHEMA_ID('dbo') ORDER BY t.name, c.column_id;sys.tables、sys.columns、sys.types是 SQL Server 系统视图,分别保存表、列、类型元数据。pk.index_id = 1指向聚集索引,如果主键没有建在聚集索引上,这个条件要改成按sys.indexes.is_primary_key判断。max_length对NVARCHAR返回的是字节数,除以 2 才是字符数,比对文档时要记得换算。
5.2 外键完整性检查:缺一条就删除时报主从冲突
很多项目因为导入数据顺序的问题,外键没建成但没人发现。直到运营一段时间后,删除一个分类被数据库拦截,才暴露问题。检查脚本:
SELECT fk.name AS 外键名, tp.name AS 外键所在表, ref.name AS 引用表 FROM sys.foreign_keys fk JOIN sys.tables tp ON fk.parent_object_id = tp.object_id JOIN sys.tables ref ON fk.referenced_object_id = ref.object_id ORDER BY ref.name;正常结果应该看到至少两条:FK_News_Category和FK_Comment_News。如果少了,说明建表顺序错了或者脚本生成环节丢失约束。补外键用ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY REFERENCES ...,注意被引用表必须存在且对应的列有索引,否则加约束时会很慢。
5.3 清理 TEXT/NTEXT 的残留,一步到位不导数据
老项目从 SQL 2000 升级上来的,最容易留下TEXT、NTEXT类型。用下面的查询扫描残留:
SELECT t.name AS 表名, c.name AS 列名 FROM sys.columns c JOIN sys.tables t ON c.object_id = t.object_id WHERE c.system_type_id IN (35, 99) -- 35 对应 TEXT,99 对应 NTEXT查出来之后执行类型转换:
ALTER TABLE dbo.News ALTER COLUMN NewsContent NVARCHAR(MAX) NOT NULL;ALTER COLUMN能在原表上直接完成类型转换,不需要导出再导入,但要理解它会锁住该表的写操作,应该安排在维护窗口执行。转换完成后,重新跑一遍 5.1 的查询,确认max_length变成 -1(MAX类型的长度在元数据里显示为 -1),这一步验证就闭环了。
本文还有配套的精品资源,点击获取