Skip to content

新模块开发规范(从建表到上线)

唯一事实源是本仓库(framework)实际代码与根目录 CLAUDE.md。分层硬规则的权威表述见根目录 AGENTS.md,完整分层与事件系统细节见 docs/ai/architecture.mdmake:crud 用法与产物清单见 docs/ai/crud.md——本文只在流程节点上引用,不重复展开。

1. 新模块完整流程

新建一个业务模块,从建表到前端可见,按以下顺序推进:

  1. 设计表结构:先写 CREATE TABLE,字段命名遵循第 2 章约定。字段注释必须逐一填写COMMENT '...')——make:crud 的字段类型推断、表单控件映射、搜索性判定都读取真实表结构和注释,注释缺失或含糊会直接降低生成代码的可用性(例如 status 无注释仍可被识别为状态字段,但业务专属字段如 audit_status 若无注释,生成器只能按类型兜底成 input,需要人工在骨架上二次调整表单控件与说明文案)。
  2. 执行 make:crud 生成骨架:命令行 php think make:crud <table> --module=<module> --model=<Model> 或管理后台「系统管理 → 代码生成」页面。生成产物清单(Model/Repository/Service/Controller/Validate/Route/前端 API/列表页/表单组件等 11 类)、参数、字段类型映射规则见 docs/ai/crud.md,本文不重复。
  3. 业务增量:审核流转、状态机、跨表编排等骨架不覆盖的逻辑,在生成的骨架文件上追加方法,不得修改骨架已有方法签名,不得绕过生成器手写整套基础 CRUD(详见 docs/ai/crud.md 第 4、5 章的增量原则与示例)。
  4. Listener + event.php:业务增量中如果有失败不影响主流程的副作用(日志、通知、缓存清理),通过 $this->trigger('event.name', $data) 交给新建的 Listener 处理,并在 server/app/event.phplisten 数组中注册映射;必须成功的操作(如同一事务内的强关联写入)留在 Service 内联,不要拆到 Listener。
  5. 菜单与权限数据写入 init.sql:新模块的导航菜单、按钮权限写入 server/public/install/data/init.sqlmenus 表 INSERT 语句。写入前必须检查所有已用 ID(含按钮 ID),避免主键冲突,规则见第 3 章。
  6. schema.sql 同步:新表结构必须同步写入 server/public/install/data/schema.sql,否则全新安装会缺表,触发 DI 注入失败(Repository/Service 构造时找不到对应 Model 映射的表)。
  7. admin 构建admin/ 源码(新增页面、API 文件)变更后执行:
    bash
    cd admin && pnpm run build
    构建产物 server/public/admin/ 是 git 跟踪的编译产物,需与源码变更一并提交,否则线上 admin 静态资源与源码不同步。

2. 数据库约定

  • 时间戳字段统一使用 created_at / updated_at / deleted_at禁止使用 create_time / update_time / delete_time 等旧字段名。
  • 状态字段统一命名为 status,取值 1 表示启用、0 表示禁用。
  • 软删除统一使用 deleted_at 字段(不做物理删除),make:crud 生成的 Model 默认支持软删除。
  • 日志类表(只追加不修改的表,如登录日志、操作日志)Model 中设置:
    php
    protected $updateTime = false;
    protected $deleteTime = false;
    对应地,日志类表的表结构不需要updated_at / deleted_at 字段(真实示例见 server/app/model/system/AdminLoginLog.phpadmin_login_logs 表只有 created_at)。

3. 菜单与权限数据

菜单存储在 menus 表(结构见 server/public/install/data/schema.sql:77),关键字段:parent_id(父级 ID,0 表示一级菜单)、type1=目录,2=菜单,3=按钮)、permission(权限标识字符串)、sort(排序)。

3.1 一级菜单 ID 实地核实结果

server/public/install/data/init.sqlmenus 表 INSERT 语句为准(用 grep -oE '^\s*\([0-9]+, 0,' 提取 parent_id = 0 的行核实),当前实际已用的一级菜单 ID 是 1、2、3、4、7、8、9(共 7 个)

ID标题name
1控制台Workbench
2系统管理System
3开发工具DevTools
4渠道管理Channel
7内容管理Content
8应用管理Application
9用户管理User

根目录 CLAUDE.md 写的是「一级菜单 ID 分配:1-8 已用,9=用户管理」,与 init.sql 实际数据核对后不完全准确——ID 5(公众号)、6(小程序)、15(开放平台)实际是二级菜单parent_id = 4,挂在「渠道管理」下),并不是一级菜单,也不是"未用"的空档。menus.id 是全局主键,5615 已被上述二级菜单占用,新增一级菜单不能复用这三个 ID。以本文实地核实的结果为准。

3.2 子菜单 ID 分配惯例

多数模块的二级菜单(功能页面)按 x00 段分配,三级菜单(按钮权限)在该段内顺序累加:

模块(一级菜单 ID)二级菜单段示例
系统管理(2)10-114管理员管理 10,其按钮 11-14;角色管理 20,其按钮 21-25
渠道管理(4)400/500/550(二级菜单本身用 5/6/15,例外见 3.1)公众号配置 400,其按钮 401
内容管理(7)700-744协议管理 700,公告管理 710,反馈管理 720,文章栏目 730,文章管理 740
应用管理(8)800-813区域管理 800,应用版本 810
用户管理(9)900-920用户列表 900,余额记录 910,积分记录 920

3.3 新增菜单前的强制检查

新增任何菜单/按钮记录前,必须先核对 init.sqlmenus 表已用的全部 ID(不只是一级菜单,二级菜单、按钮 ID 都要查),避免 INSERT 主键冲突:

bash
grep -oE '^\s*\([0-9]+,' server/public/install/data/init.sql | grep -oE '[0-9]+' | sort -n | uniq

新模块的菜单写入建议:一级菜单沿用 3.1 表中未出现的个位数(若确需新增一级模块);二级/三级菜单沿用 3.2 的 x00 段惯例,在目标模块段内取下一个未使用的号段。写完后重新执行上面的 grep 命令确认无重复。

4. Model 访问器规则

Model 中定义 getXxxAttr() 访问器时,必须同时声明 protected $append = ['xxx'],才会出现在 toArray() 输出中。

make:crud 生成的 Model 会在表含 status 字段时自动加上这一对——protected $append = ['status_text']getStatusTextAttr()(见 docs/ai/crud.md 产物清单第 1 项)。AI 在骨架基础上手写新增访问器时必须遵循同一约定,漏写 $append 是访问器"写了但前端拿不到值"的常见原因。

5. 文件上传约定

  • 上传接口:POST /adminapi/upload/image(图片,最大 2MB,限 jpg/png/gif/webp)与 POST /adminapi/upload/file(普通文件,最大 10MB),路由定义于 server/app/adminapi/route/upload.php,对应 server/app/adminapi/controller/v1/upload/UploadController.php
  • 本地存储路径:server/public/storage/uploads/images/server/public/storage/uploads/files/(按 Ymd 再分子目录),对应 URL 前缀 /storage/...;配置云存储驱动时改为上传到云端并返回云端 URL(驱动切换逻辑见 core\storage\StorageManager)。
  • 接口返回值同时包含完整 URLurl 字段,用于前端直接展示)与相对路径path 字段,用于数据库存储),业务表中涉及图片/文件的字段应存相对路径,便于后续域名迁移或切换存储驱动。
  • 前端展示图片时统一通过 admin/src/store/modules/app.store.tsappStore.getImageUrl(url) 处理 URL 拼接——它会判断是否已是完整 URL、是否开发环境(走 Vite proxy 用相对路径)、生产环境按 oss_domain 等配置项拼接域名前缀,业务代码不要自行拼接。

6. 系统配置

  • 系统配置统一存储在 system_configs 表(结构见 server/public/install/data/schema.sql:192),核心字段:config_key(唯一键)、config_group(分组)、config_typestring/number/boolean/json/file)、config_options(下拉选项 JSON)、config_depends(显示依赖 JSON)、statussort_order
  • 新模块如需可配置项,直接在该表新增记录(按 config_group 分组管理),不需要新建专门的配置表。
  • 配置更新后,SystemConfigService 通过 $this->trigger('config.changed', ['keys' => [...]]) 触发事件(见 server/app/service/system/SystemConfigService.php),由 ConfigChangedListenerserver/app/listener/system/ConfigChangedListener.php)负责:
    • Cache::tag('config')->clear() 清除标签化的配置缓存
    • StorageManager::reset() 重置存储驱动单例,使新的存储配置立即生效
  • 前端在应用启动时通过 auth.guard.ts 拉取全局配置(/adminapi/system/config/global),业务代码新增配置项后无需改前端拉取逻辑,读取 appStore.config 即可拿到最新值。

7. 前端约定

  • 页面目录:页面组件放在 admin/src/views/{module}/{kebab-model}/ 下(如 admin/src/views/content/article/index.vue),列表页 index.vue + 表单弹窗 components/{Model}Form.vue禁止把页面放到 admin/src/pages/——该目录不是本项目使用的目录(与 docs/ai/architecture.md 第 7 章禁令一致)。
  • API 目录:一个模块对应 admin/src/api/ 下一个文件(如 admin/src/api/article.ts),文件内导出 getList/getDetail/create/update/delete/batchDelete 等请求方法及配套 TS 接口定义({Model}CreateReq/{Model}UpdateReq/{Model}Info),不要把多个模块的请求方法混写在一个文件里。
  • 请求封装:统一使用 admin/src/utils/request.ts 中导出的 myRequest 发起 API 调用,响应拦截器已处理 token 静默刷新、过期跳转、错误提示,业务代码不要自行处理这些边界情况,避免出现循环跳转。
  • 动态路由:路由不是前端静态声明的,而是应用启动时从后端 /adminapi/system/config/global 拉取的菜单数据,经 admin/src/router/index.ts 中的 filterAsyncRoutes() 过滤转换、loadRouteView()component 字符串动态导入组件后注册。这意味着新模块的页面只有在 menus 表(第 3 章)中正确配置了 component 路径(对应 views/ 下的真实文件路径)后,才能通过菜单点击访问到;component 字段与页面文件路径不一致是"菜单能看见、点击 404"的常见原因。

基于 MIT 许可发布