项目:ERP-CRM | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析(v1.2:2026-10-06 一致性回刷——SOP-3 验收表数 38→40,与模板实测一致)
本篇是六类高频扩展作业的标准作业程序(SOP):新增业务模块、用代码生成器产出模块、新增租户、新增前端页面、编写增量迁移、新增定时任务与业务指标。每条 SOP 都标注涉及文件与验收命令。开工前请先读《01-系统架构》第 4 节的十条设计决策与 AGENTS.md——本篇假设你已理解"租户键 ThreadLocal + 三处同步"这两个基本盘。
适用:模块含自定义业务逻辑(不只是 CRUD),或需要精确控制 SQL。整体动作链如下:
erp-crm/src/main/resources/init-sql-script/erp-crm-tenant-sample.sql 中按现有表风格追加 CREATE TABLE(utf8mb4、create_by/create_time/update_by/update_time/remark 公共列、必要的 idx_* 索引)。命名沿用 crm_*/erp_* 前缀。表注释写中文。erp-crm/src/main/java/com/ec/crm/domain/(或 erp/domain/)新建 Xxx.java 继承 com.ec.common.core.domain.BaseEntity;导出字段加 @Excel(name="中文列头");join 注入的非表字段加 @Transient(参照生成产物 CrmReceivable.java 的 contractNo/customerName)。mapper/,XML 放 src/main/resources/mapper/crm/(mybatis.mapperLocations 扫描 classpath*:mapper/**/*Mapper.xml)。XML 必含四段:resultMap、<sql id="selectXxxVo">、list(<where> 动态条件)、CRUD 语句。全部用 #{} 参数化;需要防越权时照抄 CrmCustomerMapper.xml 第 394-415 行的 restrictOwner <if> 模式。IXxxService + 实现 XxxServiceImpl。写操作涉及多表时加 @Transactional;方法内不得切数据源(跨主库需求拆成独立非事务方法,见 AGENTS.md 陷阱条款)。BaseController,五端点命名固定(GET /list、GET /{id}、POST、PUT、DELETE /{ids}),每个端点必须显式 @PreAuthorize("@ss.hasPermi('crm:xxx:动作')")(不声明则仅要求登录,任何租户用户可调);写端点带 @Log(title="中文模块名", businessType=...);列表端点 startPage() 后立即查询(防分页串染,AGENTS.md 约定)。db-backup/upgrade_business_modules.sql 第 71 行起):目录 menu_type='M' → 页面 C → 按钮 F(perms 逐个落);然后 INSERT INTO sys_role_menu (role_id=3, ...) 给普通销售角色补授权(WHERE NOT EXISTS 探重)。menu_id 从空闲段取(目前 2100-2138 已用 ⚠️ 待确认后续段位分配约定,建议在本文档追加登记)。db-backup/upgrade_xxx.sql 让存量租户库跟上。mvn package -DskipTests && java -jar target/erpcrm.jar 重启;登录 tenant1 确认菜单出现、五端点全通、Swagger(/swagger-ui/index.html)显示新接口;mvn test 确认无回归。适用:纯 CRUD 模块(无自定义端点、无条件为等值过滤可接受)。步骤:
erp-crm/gen_business_modules.py:把头部 BASE/PKG/RES 三个绝对路径改为你本机仓库路径(当前写死作者机器路径 ⚠️);在 MODULES 数组追加一项——entity(类名)、table、cn(中文名)、route(URL 片段)、cols(四元组列表:java 名/类型/中文注释/db 列,主键行第 4 位写 True)、extra_transient(join 注入字段)。JOIN/JOIN_COLS/EXTRA_RESULT 三个字典(键为 entity 名);单跳 join 照抄 CrmOpportunity 条目,二跳照抄 CrmReceivable(两行 join 字符串)。python3 gen_business_modules.py,确认输出 6 行 Xxx: 6 files written。sys_dict_* 种子(含 refreshCache);② 特殊端点(审核/入库/提交类动词接口)手工加到 Controller;③ 非等值查询条件(like/日期范围)手工改 XML 的 <where>;④ 归属校验按《08-安全与权限设计》5.2 补 restrictOwner(生成器四模块至今没有,是已知债)。红线:不要对已手工修改过的生成产物重跑生成器——它会整文件覆盖(如 CrmContract 的 review_status 就不在生成器定义里,重跑即丢失)。"生成后即固化",后续演进走 SOP-5 的增量迁移 + 手工改码。
两条路径,按是否需要自动化选择:
/register 页提交 {tenantName, adminName, adminPass, packageId}。系统自动完成:重名校验 → CREATE DATABASE ec_xxx(utf8mb4)→ 建最小授权 MySQL 用户 → 跑租户模板 SQL → 参数化插入部门与管理员 → 登记 master_tenant(套餐定有效期,无套餐 7 天)。全程零手工,失败自动回滚防孤儿库。注册限流 5 次/时/IP。CREATE DATABASE 后灌模板 SQL,再修 master_tenant 登记行(url/username/password/expiration_date)并补插 admin 用户。注意口令列:配了 EC_TENANT_PASSWORD_KEY 时必须写经 TenantDbPasswordCipher.encrypt 的密文(或留明文让启动迁移器改写),写错密文格式会得到 JDBC Access denied。验收:用新租户名 + 管理员账号登录,确认 40 张表齐全(SHOW TABLES 对照模板)、菜单/字典/角色种子就位、/system/tenant/quota 返回配额。若模板刚被改过而新租户缺表,说明 SOP-1 第 1 步漏改模板(最常见翻车点,《00-项目总览》第 6 节早有预警)。
erpcrm-ui/src/api/{crm,erp,system}/ 新建 xxx.js,从 @/utils/request 导入默认实例,导出与后端五端点一一对应的函数(listXxx/getXxx/addXxx/updateXxx/delXxx),写操作如需跳过防重复提交可传 headers: { repeatSubmit: false }。src/views/{crm,erp}/xxx/list.vue,一律 <script setup>。标准骨架照抄 src/views/crm/customer/list.vue:inline 搜索表单(v-show="showSearch")→ 按钮行(每个按钮带 v-hasPermi,权限串与后端一字不差)→ el-table + pagination → 编辑弹窗。列表状态全部来自 useTable(listXxx, { 初始查询参数 });字典下拉用 useDict(["字典类型"]);弹窗开关用 useModal。sys_menu.component 列(如 erp/xxx/list),经 GET /getRouters 下发后由 src/api/routes.ts 动态注册。纯静态页才去 src/router/modules/remaining.ts 登记并注意 handleAsyncRoutes 的执行时机。@/plugins/echarts 导入;只用 element-plus;禁止 console.log 残留与 getCurrentInstance();新工具逻辑必须配 vitest 用例;导出走全局 download()(其 60 秒超时与 blob 校验已处理好错误分支)。pnpm lint && pnpm typecheck && pnpm test 全绿;手工过一遍增删改查 + 无权限账号确认按钮隐藏且直连接口被 403 挡住。适用:给存量租户库加表/加列/补种子。项目的迁移机制没有 Flyway/Liquibase(技术债分析见《13-技术债务与演进建议》第 2 节),靠幂等脚本手工/CI 执行,已沉淀五批范本(db-backup/upgrade_{business_modules,approval,supply_finance,warehouse_transfer,stock_adjust}.sql)。写作规则:
CREATE TABLE IF NOT EXISTS;字典与菜单插入用 INSERT ... SELECT ... FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM ...) 探重(范本见 upgrade_approval.sql 全文);菜单插入前先 DELETE FROM sys_menu WHERE menu_id IN (...) 清残留(防半截执行留下的旧行阻断新插入)。ADD COLUMN IF NOT EXISTS,重复执行会报 Duplicate column——范本如实记录了这一点,重复执行前需人工判断。若要彻底幂等,可包一层存储过程检查 INFORMATION_SCHEMA.COLUMNS ⚠️ 待确认(项目尚未采用该模式,引入前先与维护者对齐风格)。erp-crm-tenant-sample.sql、新 upgrade 脚本、(涉及主库时)sql/erp-crm-master.sql。漏改模板 = 新租户缺结构;漏写 upgrade = 存量租户漂移。@Excel 注解(04-数据模型.md 5.4 的既定清单)。ops/backup.sh --out /tmp)→ 对每个存量租户库执行(库清单取 master_tenant.database_name,批量游标可改造 backup.sh 第 65-67 行的 UNION 查询)→ 应用冒烟(登录后打开相关页面)。模板已含最新结构,新注册租户不需要跑 upgrade。db-backup/ryt_nelub7xr_before_demo_upgrade.sql 的做法),便于回滚比对。com.ec.<域>.job.XxxJob,把执行体拆成公开的 sweepOnce()(返回结果 record,幂等可重跑)+ @Scheduled(cron = "${ec.xxx.job-cron:默认值}") 的薄壳入口——这是 TenantExpirationJob 确立的模式(javadoc 明写测试约定)。cron 参数走 ec.* 配置项 + application.yml 默认值,不硬编码。common/metrics/ErpMetrics.java 加方法:Counter 用 counter("erp.<域>.<动作>.total", "标签", ...),Gauge 用 holder 对象注册;标签值必须过既有的正则归一([a-zA-Z0-9_-]{1,32},防随机值制造无上限时间序列);命名用点号(Prometheus 自动转下划线)。TenantExpirationJobIntegrationTest 直调 sweepOnce() 写集成测试,并断言 MeterRegistry 里新指标已接线;本地 curl http://localhost:9091/actuator/prometheus | grep erp_ 确认暴露。mvn test(47 例基线不许红);不要用管道吞 mvn package 退出码(AGENTS.md 警告)。pnpm lint && pnpm typecheck && pnpm test。