57 张表的门业业务工作台:用 CodeIgniter 4 把口述的行规写成代码
一个面向工业门销售团队的内贸外贸一体化内部工作台:57 张表、63 个迁移、30 个控制器,把口述的报价行规、单据生成和一个只读 MCP 网关装进同一套 CodeIgniter 4 系统。
项目背景
一家做工业门的中小型制造企业,产品是提升门,以及软质、堆积、涡轮硬质三款快速门,内贸和外贸两条线都在跑。做系统之前,业务是这么运转的:
- 报价靠一份 Word 模板,价格规则一半写在文档里,一半在老板口述里;
- 客户资料和跟进记录散在各人的表格、聊天记录里;
- 外贸的 PI、CI、销售合同各自维护模板,改一处忘三处;
- 新人反复问同样的问题,答案埋在历史聊天里。
目标因此定得很窄:不做通用 CRM,只把这条链路上重复的动作收进一个系统——线索从哪来、客户跟到哪一步、报价怎么算、合同怎么出、外贸单据怎么开、资料和口径放在哪。
最终交付的是一个服务端渲染的内部工作台,单人开发维护。它不是 SaaS,代码里明确写了不做多租户;真正的隔离维度是账号级业务线(内贸 / 外贸)加三级角色数据范围(管理 / 主管 / 员工)。
做了什么
技术栈:CodeIgniter 4.7.5 + PHP 8.2 + MySQL 5.7,前端 Tailwind CSS 3.4 + Alpine.js,服务端渲染,前端资源本地构建,线上不依赖 Node。选这套而不是前后端分离,理由很朴素:使用者是业务员,页面以表单、列表、打印文档为主,SSR 能省掉一整层接口和状态管理。
| 模块 | 做的事 |
|---|---|
| 线索公海 | 录入、领取、指派、转客户;领取用事务加条件更新,受影响行数为 1 才算领到 |
| 客户与成交 | 跟进记录、成交要素清单、资信核查项、成交 / 流失判定 |
| 报价 | 报价计算器(只算价不落库)、报价单生成、版本、预览 |
| 合同 | 结构化合同、版本回滚、网页预览、Word / PDF 输出 |
| 外贸单据 | PI / CI / 销售合同,美元计价,EXW / FOB / CIF / DDP 四种术语,英 / 俄文切换 |
| 项目中心 | 内贸 8 个阶段、外贸 14 个阶段,每阶段带检查清单,各类单据挂在项目下 |
| 产品库 | 参数、配件、图集、配置、部件示图 |
| 知识库 | 资料 / 话术 / 案例条目,带附件、版本和修改建议审核 |
| 业务答疑 | 提问、回答、点赞、评论、采纳,采纳后沉淀为知识条目 |
| 工作任务 | 四象限指派、提交、验收 |
| 站内信 | 顶部铃铛,业务侧一行调用即可发提醒 |
| AI 网关 | MCP 端点,对外提供查产品、查知识、搜问答、试算价格、看待办 |
关键技术或难点
1. 把口述的行规变成可维护的规则
这个项目最难的从来不是写代码,而是把「行规」翻译成规则。面积 7 ㎡ 和 9 ㎡ 是两档分界;宽度到不到 6 米决定规格档;小门、视窗、加强筋按件另计;高位提升超过一定高度还要加价。这些先落进一张规则表,再由一个计价引擎解释,规则内容可以在后台改,不需要改代码。
工程上做了三件事,让规则不至于「算错了还不吭声」:
- 精确匹配不到规格时,按面积就近匹配,同时往结果里写一条告警;
- 区间价不硬编码,业务员可以确认成交单价,超出区间就提示「请确认是否符合特批」;
- 规则缺失时用文档里的固定价兜底,并在返回值里标明来源。
这里的关键取舍是:宁可多弹提示,也不静默取默认值。报价错了赔的是真金白银,一个看起来正常的数字比一个明显报错更危险。
2. 一套账号,两条业务线
内外贸的字段、单据、话术差别很大,但没必要做两套系统。做法是账号表记一个业务线字段,登录后写进 session;控制器入口统一校验,跨线直接 302 跳回本线并提示;菜单按业务线生成,普通账号无法通过改 URL 参数切换;单据层物理分家,内贸报价合同和外贸 PI/CI/SC 各走各的表,互不污染。管理员可以在两条线之间切换,切换结果会持久化。
3. 权限:可以脱库单测的数据范围
三级角色的数据范围由一个独立的范围计算类负责。它被刻意设计成不访问数据库——下属 ID 由外部注入,查询侧统一走「拿到 owner 列表,然后 whereIn」。好处很直接:权限逻辑可以用纯数组单测,不用起数据库、不用造数据。主管对团队数据只读,写操作单独校验归属。
4. 一份 HTML,三处输出
合同要同时出现在网页预览、Word、PDF 三个地方。做法是把内容渲染成一份 HTML,Word 用 PhpWord 按同一份数据重排,PDF 用 Dompdf 输出。PDF 的中文字体是个坑:默认字体不带中文,最后把 TTF 放进受控目录并在 CSS 里声明字体才解决。外贸单据没有现成模板,改成用代码生成 docx;后来又加了箱单和报关单,是同一套路子。
5. AI 接入:工具是数据,令牌是哈希
系统内做了一个 MCP 网关(JSON-RPC 2.0),让外部 AI 客户端能查产品、查知识、搜历史问答、试算价格、看自己的待办。三条约束:
- 工具是数据,不是硬编码。工具目录存在表里,目前登记 14 个,11 个已实现、3 个只标记为「规划中」;角色和工具的授权关系也是一张表,后台勾选即可。
- 令牌只存 sha256,明文只在生成那一次出现。
- 写操作一律走草案。AI 出结构,进草案表,人确认后才落业务表。
还有一个副产品:调用日志里命中数为 0 的记录,正好就是「知识库没有覆盖的问题」,可以直接反哺内容建设。
6. 文件不裸奔
所有上传和生成的文档都落在非公开目录,只能经一个受控控制器按数据范围输出,不暴露真实路径。富文本用自研编辑器,服务端做 DOMDocument 白名单净化,前端不承担安全职责。
结果与数据
下面这些数字都是从仓库里数出来的实测值,不是估计。没有放业务效果指标——这类数据不适合公开,也不该由开发者单方面宣称。
| 指标 | 数量 |
|---|---|
| 控制器 | 30(29 个业务 + 1 个抽象基类) |
| 模型 | 54 |
| 业务库(Libraries) | 22 |
| 助手函数 | 12 |
| 过滤器 | 3(登录 / 角色 / MCP 鉴权) |
| 视图 | 92 |
| 数据库迁移 | 63 |
| 数据表 | 57 |
| 单元测试 | 15 个测试类 / 96 个用例 |
测试集中压在「容易错、又不好人工回归」的地方:提升门面积档与加价(12 例)、三款快速门的计价算例(10 例)、成交要素判定(14 例)、问答富文本净化(11 + 3 例)、数据范围计算(4 例)、MCP 工具目录与角色授权(5 例)。
复盘
做对的:先把「不做什么」写下来。 设计文档里有一节专门列放弃项:不做多租户、不做外键约束、不做软删除、不引入 ORM 关联、不做 i18n 框架,每条都写明了代价。事后看,正是这些放弃让单人维护的项目没有失控。需要多语言的时候(比如合同要出英文和俄文版),就在单据层加一个语言字段局部解决,而不是把整个系统改成多语言。
教训一:文档里的规模数字会漂移。 设计文档写的是 39 张表、22 个控制器,而现在实际是 57 张表和 30 个控制器。数字是手写进 Markdown 的,每加一张表都忘了回去改。结论是:这类统计要么自动生成,要么干脆不写,文档只负责讲「为什么」。
教训二:计价系统的默认值必须可见。 最早的版本在规则缺失时会静默返回一个兜底价,报价单看起来完全正常,只有当事人知道那个价格是猜的。改成所有兜底、就近匹配、超出区间都写告警并带进结果页之后,问题才真正暴露出来。
仍在进行中的: AI 网关目前是「只读 + 试算 + 草案」的形态,新建客户、新建项目、新建单据这三个工具只在目录里登记为规划中,没有接。不是做不出来,而是这三类写操作一旦出错,纠正成本比省下的时间高得多。等草案的确认流程在更多工具上跑顺了再开放。
如果给这个项目一句话总结:把业务里那些「只有老员工知道」的东西,变成代码里能被测试、能被审计、能被新人读到的东西。
评论(0)
还没有评论,来抢占沙发吧~