新模块开发规范(从建表到上线)
唯一事实源是本仓库(
framework)实际代码与根目录CLAUDE.md。分层硬规则的权威表述见根目录AGENTS.md,完整分层与事件系统细节见docs/ai/architecture.md,make:crud用法与产物清单见docs/ai/crud.md——本文只在流程节点上引用,不重复展开。
1. 新模块完整流程
新建一个业务模块,从建表到前端可见,按以下顺序推进:
- 设计表结构:先写
CREATE TABLE,字段命名遵循第 2 章约定。字段注释必须逐一填写(COMMENT '...')——make:crud的字段类型推断、表单控件映射、搜索性判定都读取真实表结构和注释,注释缺失或含糊会直接降低生成代码的可用性(例如status无注释仍可被识别为状态字段,但业务专属字段如audit_status若无注释,生成器只能按类型兜底成input,需要人工在骨架上二次调整表单控件与说明文案)。 - 执行
make:crud生成骨架:命令行php think make:crud <table> --module=<module> --model=<Model>或管理后台「系统管理 → 代码生成」页面。生成产物清单(Model/Repository/Service/Controller/Validate/Route/前端 API/列表页/表单组件等 11 类)、参数、字段类型映射规则见docs/ai/crud.md,本文不重复。 - 业务增量:审核流转、状态机、跨表编排等骨架不覆盖的逻辑,在生成的骨架文件上追加方法,不得修改骨架已有方法签名,不得绕过生成器手写整套基础 CRUD(详见
docs/ai/crud.md第 4、5 章的增量原则与示例)。 - Listener + event.php:业务增量中如果有失败不影响主流程的副作用(日志、通知、缓存清理),通过
$this->trigger('event.name', $data)交给新建的 Listener 处理,并在server/app/event.php的listen数组中注册映射;必须成功的操作(如同一事务内的强关联写入)留在 Service 内联,不要拆到 Listener。 - 菜单与权限数据写入
init.sql:新模块的导航菜单、按钮权限写入server/public/install/data/init.sql的menus表 INSERT 语句。写入前必须检查所有已用 ID(含按钮 ID),避免主键冲突,规则见第 3 章。 schema.sql同步:新表结构必须同步写入server/public/install/data/schema.sql,否则全新安装会缺表,触发 DI 注入失败(Repository/Service 构造时找不到对应 Model 映射的表)。- admin 构建:
admin/源码(新增页面、API 文件)变更后执行:bash构建产物cd admin && pnpm run buildserver/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.php,admin_login_logs表只有created_at)。
3. 菜单与权限数据
菜单存储在 menus 表(结构见 server/public/install/data/schema.sql:77),关键字段:parent_id(父级 ID,0 表示一级菜单)、type(1=目录,2=菜单,3=按钮)、permission(权限标识字符串)、sort(排序)。
3.1 一级菜单 ID 实地核实结果
以 server/public/install/data/init.sql 的 menus 表 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实际数据核对后不完全准确——ID5(公众号)、6(小程序)、15(开放平台)实际是二级菜单(parent_id = 4,挂在「渠道管理」下),并不是一级菜单,也不是"未用"的空档。menus.id是全局主键,5、6、15已被上述二级菜单占用,新增一级菜单不能复用这三个 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.sql 中 menus 表已用的全部 ID(不只是一级菜单,二级菜单、按钮 ID 都要查),避免 INSERT 主键冲突:
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)。 - 接口返回值同时包含完整 URL(
url字段,用于前端直接展示)与相对路径(path字段,用于数据库存储),业务表中涉及图片/文件的字段应存相对路径,便于后续域名迁移或切换存储驱动。 - 前端展示图片时统一通过
admin/src/store/modules/app.store.ts中appStore.getImageUrl(url)处理 URL 拼接——它会判断是否已是完整 URL、是否开发环境(走 Vite proxy 用相对路径)、生产环境按oss_domain等配置项拼接域名前缀,业务代码不要自行拼接。
6. 系统配置
- 系统配置统一存储在
system_configs表(结构见server/public/install/data/schema.sql:192),核心字段:config_key(唯一键)、config_group(分组)、config_type(string/number/boolean/json/file)、config_options(下拉选项 JSON)、config_depends(显示依赖 JSON)、status、sort_order。 - 新模块如需可配置项,直接在该表新增记录(按
config_group分组管理),不需要新建专门的配置表。 - 配置更新后,
SystemConfigService通过$this->trigger('config.changed', ['keys' => [...]])触发事件(见server/app/service/system/SystemConfigService.php),由ConfigChangedListener(server/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"的常见原因。