系统设计三件套:功能结构图(做什么)、目录结构(放哪)、界面预览(长什么样)。 目录按功能分还是按文件类型分,是 PHP 项目第一个分岔,也是后期好不好维护的分水岭。 把「模块 × 表」叠成一张矩阵,就知道改哪张表要动最多地方——这是最便宜的改动评估。
教材 22.3 讲系统设计,五个小节:系统目标、系统功能结构、系统预览、开发环境、文件夹组织结构。 这一篇把前四个压成一节, 重点放在第五个「文件夹组织结构」上——因为它最能体现「这个项目好不好维护」。 另外加了一节教材没有的内容:模块和表的对照矩阵。 这东西在需求变更的时候特别有用:动手前先数一下这张表被几个模块引用, 引得多就先把方案写清楚。 演示里用真实的模块清单统计了「哪几张表被最多模块碰到」,结果和直觉一致: 订单表和车辆表排在最前面。
一、系统目标:一句话说清边界
教材写的系统目标是「实现物流配送信息的信息化管理」。这句话太宽,宽到没法验收。 把它拆成能验收的几条,才会对设计有约束力:
| 目标 | 能验收的样子 |
|---|---|
| 车源可查 | 输入车型、吨位,三秒内出结果 |
| 单可填 | 一张单从打开到保存,不超过十次点击 |
| 状态可追 | 任何一张单,能看到它现在到哪一步 |
| 回执可存 | 签收信息拍照留档,能按单号反查 |
右边这一列的价值在于「能吵架」:做不到就是没做到,不含糊。
而「信息化管理」这种目标,做完之后没人能说它没达成,也没人能说它达成了。
第二条「不超过十次点击」看着像产品要求,其实直接影响表设计:
要少点几次,就得让常用字段有默认值、让能带出的信息自动带出。
这些「默认值从哪来」,最后都落在表的 DEFAULT 和几张字典表上。
二、功能结构:一棵树,不是一张清单
教材给的功能结构图是一棵树,三层:模块 → 子功能 → 操作。 树和清单的区别在于「归属」:清单里「删除发货单」是一个独立功能, 树里它挂在「发货单管理」下面,你一眼就知道它属于哪个模块、该改哪个目录。
| 层 | 内容 | 落到哪 |
|---|---|---|
| 第一层 | 车源、发货单、基础信息、报表 | 一个目录 |
| 第二层 | 填单、查询、删除、打印 | 目录下的一个文件 |
| 第三层 | 保存、取消、导出 | 文件里的一个分支 |
这三层的粒度不是随便定的,它对应三种不同的「改动影响面」: 改第一层是加功能,要动目录和导航;改第二层是改功能,动一个文件; 改第三层是调细节,动几行代码。 分不清这三层,就会把「加一个导出按钮」当成「加一个模块」来做。
三、目录结构:按功能分,别按类型分
PHP 项目有两种常见分法,差别在「改一个需求要翻几个目录」:
| 分法 | 结构 | 改「发货单」要翻 |
|---|---|---|
| 按类型分 | js/ css/ php/ |
php/ 里几十个文件全看一遍 |
| 按功能分 | order/ vehicle/ |
只看 order/ 这一个目录 |
第一行在小型项目里也能活,因为文件少。
但文件一过三十个,就开始出现「这个文件是干嘛的」这种问题。
第二行的代价是「公共文件放哪」——连接、页头、函数库需要一个 inc/ 之类的目录。
这个代价是值得付的:公共文件的修改频率远低于业务文件。
教材这个项目用的是第二种,结构大致是:
四、界面预览:先画再写
教材的「系统预览」给的是几张界面截图。这一步常被跳过,理由是「画图浪费时间」。 但界面预览真正的作用不是好看,是「逼你把字段列全」。 画一张填单页,你会发现「发货单明细」这个区域放不下——因为一张单可能有多件货。 这个发现的价值在于:它直接决定了「要不要一张明细表」。 如果不画,很可能建完表、写完代码,才发现一张单只能填一件货。 同样,画查询结果页会发现「要显示客户名和车牌,但订单表里只有 id」。 这就逼出「查询要连表」这个结论——而这个结论又回头影响索引怎么建。 所以「先画再写」不是流程洁癖,是让问题在便宜的时候暴露。
五、模块和表的对照矩阵
教材没有这一节,但它是需求变更时最省事的一张表。 做法很简单:每个模块列出它要碰的表,然后横向统计。
| 表 | 被几个模块用到 | 含义 |
|---|---|---|
tb_vehicle |
5 | 车源、填单、查询、首页都要它 |
tb_order |
4 | 核心单据 |
tb_client |
3 | 填单、查询要连 |
tb_admin |
1 | 只在后台 |
排在前面的就是「地基表」。改它们的结构,要动的地方最多,也最容易漏。 所以一个实用的规矩是:动前三名之前,先把「哪些地方要跟着改」列成清单,而不是边改边想。 这条规矩在真实项目里救过很多次火。 因为「改表」这个动作本身很快,几十秒; 出事的是「忘了某个模块也在用它」,而那个模块往往是最久没人访问的那个。
六、动手 Demo:模块与表:谁碰谁,碰得最多的是哪张
这一段做三件事:把目录结构打印出来对照、把「模块 × 表」叠成矩阵统计热度、 再把首页要的四个指标用一条聚合查询算出来,验证「一次往返能拿齐」。
import sqlite3
c = sqlite3.connect(u"des.db", isolation_level=None)
c.executescript(u"""
CREATE TABLE tb_admin (id INTEGER PRIMARY KEY, uname TEXT);
CREATE TABLE tb_client (id INTEGER PRIMARY KEY, cname TEXT);
CREATE TABLE tb_vehicle (id INTEGER PRIMARY KEY, plate TEXT, state TEXT);
CREATE TABLE tb_order (id INTEGER PRIMARY KEY, order_no TEXT,
client_id INTEGER, vehicle_id INTEGER, freight REAL, state TEXT,
send_date TEXT);
CREATE TABLE tb_order_detail (id INTEGER PRIMARY KEY, order_id INTEGER,
gname TEXT);
CREATE TABLE tb_receipt (id INTEGER PRIMARY KEY, order_id INTEGER,
signer TEXT);
""")
# ① 目录结构
for line in (u"wuliu/", u" ├─ conn/ 连接", u" ├─ inc/ 公共函数",
u" ├─ admin/ 后台", u" ├─ order/ 发货单",
u" ├─ vehicle/ 车源", u" └─ index.php 首页"):
print(line)
# ② 模块 × 表,统计每张表被几个模块用到
mods = {
u"首页": [u"tb_order", u"tb_vehicle"],
u"车源查询": [u"tb_vehicle"],
u"发货单填单": [u"tb_order", u"tb_order_detail", u"tb_client", u"tb_vehicle"],
u"发货单查询": [u"tb_order", u"tb_client", u"tb_vehicle"],
u"明细查看": [u"tb_order_detail"],
u"回执验收": [u"tb_receipt", u"tb_order"],
u"管理员管理": [u"tb_admin"],
u"客户管理": [u"tb_client"],
u"车辆管理": [u"tb_vehicle"],
}
cnt = {}
for tables in mods.values():
for t in tables:
cnt[t] = cnt.get(t, 0) + 1
for t, k in sorted(cnt.items(), key=lambda kv: (-kv[1], kv[0])):
print(u"%-18s %d" % (t, k))
# ③ 首页四个指标,一条聚合查询拿齐
c.execute(u"BEGIN")
c.executemany(u"INSERT INTO tb_order VALUES (?,?,?,?,?,?,?)",
[(i, u"WL%06d" % i, i % 120 + 1, i % 60 + 1,
200.0 + (i % 40) * 25.0,
[u"待发车", u"在途", u"已到达", u"已验收"][i % 4],
u"2026-09-%02d" % (i % 28 + 1)) for i in range(1, 601)])
c.execute(u"COMMIT")
once = c.execute(u"""
SELECT (SELECT COUNT(*) FROM tb_order WHERE send_date='2026-09-01'),
(SELECT COUNT(*) FROM tb_order WHERE state='在途'),
(SELECT COUNT(*) FROM tb_order WHERE state='已到达'),
(SELECT SUM(freight) FROM tb_order WHERE state='已验收')""").fetchone()
print(u"今日 %d / 在途 %d / 待验收 %d / 运费 %.0f 元" % once)
c.close()
实跑输出:
--------------------------------------------------------------------
模块与表:谁碰谁,碰得最多的是哪张
--------------------------------------------------------------------
**教材 22.3 讲系统设计:系统目标、功能结构、系统预览、开发环境、文件夹组织结构。**
**这一节把「功能结构」和「数据库」两张图叠在一起看——哪几张表被最多模块碰,就是系统的地基。**
① 文件夹组织结构(照教材的写法,按功能分)
--------------------------------------------------------------------
wuliu/
├─ conn/ 数据库连接(conn.php)
├─ inc/ 公共函数、页头页脚
├─ admin/ 后台:基础信息管理
├─ order/ 发货单:填单、查询、删除、明细、打印
├─ vehicle/ 车源查询
├─ client/ 客户端(查询自己的单)
└─ index.php 首页
**「按功能分目录」还是「按文件类型分目录」,是 PHP 项目里第一个分岔。**
**按类型分就是 `js/`、`css/`、`php/` 三个目录,**
**所有业务文件堆在 `php/` 里——文件一多就找不到。**
**按功能分,改「发货单」这个需求时,所有相关文件都在 `order/` 里,不用满仓库翻。**
② 模块和表的对照
--------------------------------------------------------------------
表 被几个模块用到
-------------------------------
tb_vehicle 5
tb_order 4
tb_client 3
tb_order_detail 2
tb_admin 1
tb_receipt 1
**排在最上面的那两张,就是系统的地基。改它们的表结构,要动的地方最多。**
**这条规律可以直接用在「改动评估」上:动手前先数一下这张表被几个模块引用,**
**引得多就先把改动方案写清楚,别边改边想。**
③ 首页要显示的四个数,一条 SQL 能不能算出来
--------------------------------------------------------------------
指标 值
-------------------------
今日发单 21
在途 150
待验收 150
已验收运费合计 108750 元
**四个子查询塞进一条语句,好处只有一条:**
**一次往返拿齐,不用四次。**
**在本地几乎没有差别,但在「PHP 每次请求都要新建连接」的前提下,**
**少三次往返就是少三次连接开销。**
**这就是第 21 章那个「连接占 88%」的结论在这里的用处——它决定了「合并查询」值不值得做。**
第二段那个统计结果就是「改动评估表」:排在前面的表,改它要动的地方最多。
注意 tb_vehicle 排第一,不是因为它重要,是因为它被最多模块顺手用到——这类表最容易被低估。
第三段证明了「四个指标一条 SQL 拿齐」是可行的。
这个写法在本地看不出优势(往返不要钱),但在 PHP 里每次请求都要新建连接的前提下,
少三次往返就是实打实的省。
顺带说一句这四个子查询的代价:它们各自扫一遍表,只是省了往返,没省计算。
数据量大起来之后,更划算的做法是「一次扫描分桶统计」,但那要改写成 GROUP BY,
可读性会下降。
30 秒实验
- 在第二段的清单里加一个「报表」模块,看统计结果怎么变。
- 把第三段的四个子查询拆成四条独立语句,各测一次耗时,和合并版对比。
- 给
tb_order.state和send_date各建一个索引,重跑第三段看快多少。
常见误区
- 目录按文件类型分。文件少的时候没事,过三十个就开始「这个文件是干嘛的」。 按功能分,改需求时只看一个目录。
- 跳过界面预览直接写代码。画一遍会发现「一张单能填几件货」「查询页要显示客户名」这类问题, 它们直接改表结构。
- 改表之前不数引用。改表本身几十秒,出事的是「忘了某个模块也在用」, 而那个模块往往最久没人访问。
自测三问(点开对答案)
1. 功能结构为什么画成树而不是清单? —— 树表达了「归属」。 清单里「删除发货单」是独立功能,树里它挂在「发货单管理」下面,一眼知道该改哪个目录。 三层还对应三种改动影响面。 2. 目录按功能分的主要代价是什么? —— 公共文件(连接、页头、函数库)没地方自然归位,得单独开一个 `inc/`。 但这个代价值得付,因为公共文件的修改频率远低于业务文件。 3. 「模块 × 表」矩阵有什么用? —— 它是改动评估表。排在前面的表被最多模块引用,改它最容易漏。 动手前先数引用,把「哪些地方要跟着改」列成清单,而不是边改边想。设计定了,下一篇进数据库设计:七张表怎么从 E-R 图落成建表语句,以及约束到底能拦住哪些错误。
- ☐ (本篇完,下一篇 22.4 数据库设计)