作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合, 仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
14 KiB
开发规范
适用范围:
xiaoqu-franchisee/xiaoqu-publicbeanandmap/vue-manage-system三个仓库的 dev 阶段开发。 本文档由近期三次需求(出库单上传凭证 / 代理商消息通知 / 客户产品清单查询)的实战提炼而来,每条规则后面附「为什么」。
1. 多环境数据库
1.1 schema 对应关系
| 环境 | 权限库(菜单/字段/按钮配置) | 业务库(业务表) |
|---|---|---|
| dev | xiaoqu_comples_d |
xiaoqu_franchisee_d |
| test | test |
xiaoqu_franchisee_d(与 dev 共享) |
| prod | xiaoqu_comples_p |
xiaoqu_franchisee_p |
- 后端 datasource 命名:m0 = 权限库(b0/s0/m0),m1 =
xiaoqu_mall_*(b1/s1/m1),m2 = 业务库(b2/s2/m2) - DB host:dev/test 都在
192.168.1.181:3306(root/kaixinjiuhao),prod 单独
1.2 环境差异要点
- dev 数据被简化过,权限组通常只有
authority_id=1, 102 - test 是从线上导的快照,包含 8~26 个真实 authority(按业务条线/区域划分)
- prod 上线前要现查,不能假设和 dev/test 一致
⚠️ dev 上 SQL 跑通不代表 test/prod 跑得对。任何涉及
authority_id的 SQL 必须想清楚跨环境表现。
2. 权限 SQL 约定
2.1 永远不要硬编码 authority_id
反例(dev 跑得通,test/prod 会授权遗漏):
INSERT INTO webpage_authority (web_id, authority_id, ...) VALUES
(@new_web_id, 1, ...),
(@new_web_id, 102, ...);
正例(A:复用同 parent 下兄弟菜单授权并集):
-- 适用:新页面,没有 webpage_authority 自身记录可参考
INSERT INTO webpage_authority (web_id, authority_id, title, path, parent_id, ...)
SELECT @new_web_id, sa.authority_id, '消息通知', 'messageNotice', 131, ...
FROM (
SELECT DISTINCT authority_id FROM webpage_authority
WHERE web_id IN (SELECT id FROM webpage WHERE parent_id=131 AND id <> @new_web_id)
AND soft_delete=0
) sa;
正例(B:复用同页面已有授权):
-- 适用:往已有页面加按钮,按钮可见性应与页面可见性一致
INSERT INTO button_webpage_authority (web_id, authority_id, button_id, ...)
SELECT 211, wa.authority_id, @new_button_id, ...
FROM (
SELECT DISTINCT authority_id FROM webpage_authority
WHERE web_id=211 AND soft_delete=0
) wa;
例外:明确只给系统管理员:
-- PM 明确说"只给管理员"时,可以写 authority_id=1
-- 这是有意识的决策,不是疏忽
INSERT INTO webpage_authority ... VALUES (@new_web_id, 1, ...);
为什么:dev 简化数据让人误以为只有 1/102;test/prod 真实有 10+ 个权限组,写死会让大批用户看不到菜单/按钮。
2.2 永远不要硬编码自增主键
反例:
-- 假设 button.id 会是 789,前端/后端硬编码这个 id —— 锁死了这条记录
正例:
INSERT INTO button (...) VALUES (...);
SET @new_button_id := LAST_INSERT_ID(); -- 拿到本次插入的真实 id
-- 后续都用 @new_button_id 引用
为什么:dev / test / prod 自增起点不同(实测 dev=789, test=790, prod 待定)。前后端代码也不应耦合 button.id 这种 surrogate key。
2.3 SQL 必须有校验段 + 事务
模板:
USE xiaoqu_comples_d; -- 切环境只改这行
START TRANSACTION;
-- ① INSERT webpage ...
-- ② INSERT webpage_authority ...
-- ③ INSERT page_field ...
-- ④ INSERT field_webpage_authority ...
-- ⑤ 校验 - 跑完核对数字符合预期
SELECT id, parent_id, title, path FROM webpage WHERE id = @new_web_id;
SELECT COUNT(*) AS auth_grants FROM webpage_authority WHERE web_id = @new_web_id;
SELECT COUNT(*) AS field_count FROM page_field WHERE web_id = @new_web_id;
SELECT COUNT(*) AS field_grants FROM field_webpage_authority WHERE web_id = @new_web_id;
-- 期待 field_grants = field_count × auth_grants
COMMIT;
-- 失败请改 ROLLBACK
2.4 上线流程
| 步骤 | 操作 |
|---|---|
| 1. dev 跑通 | USE xiaoqu_comples_d,事务跑完看校验段 |
| 2. test 跑通 | USE test,同一段 SQL 直接跑(动态化的好处);记录跨环境差异(id 值、授权数等) |
| 3. prod 上线前 sanity check | 跑 SELECT 校验:parent_id 是否一致 / 当前页面/按钮是否已存在(防重复执行) / 兄弟菜单授权范围 |
| 4. prod 跑 | USE xiaoqu_comples_p,整段 SQL 照跑 |
| 5. 文档回写 | 把三环境实测结果(id 值、授权数、字段数)记到开发文档对应位置 |
3. 后端代码约定
3.1 包结构
xiaoqu-publicbeanandmap/src/main/java/me/iiv/iivframework/
├── beans/
│ ├── b0/ # 权限库实体(webpage / button / authority / ...)
│ ├── b1/ # mall 库实体
│ └── b2/ # 业务库实体(warehouse / customer_product_list / ...)
├── mapper/
│ ├── m0/ / m1/ / m2/ # MyBatis-Plus BaseMapper,按库分包
└── service/
├── s0/ / s1/ / s2/ # IService 接口
└── s0/Impl/ / s1/Impl/ / s2/Impl/ # ServiceImpl(注意 Impl 首字母大写)
xiaoqu-franchisee/src/main/java/me/iiv/iivframework/controller/<业务域>/
├── XxxController.java # @Controller + @RequestMapping
├── input/XxxQueryInput.java # @RequestBody 接收的入参
├── output/XxxOutput.java # 列表/详情出参(class_name 必须与 page_field 一致)
└── 如有导出 → controller/export/XxxExport.java(@Excel 注解)
3.2 class_name 一致性
page_field.class_name 必须与后端 DTO 全限定类名严格一致。
反例:
- SQL:
me.iiv.iivframework.controller.customerProduct.output.CustomerProductListOutput - 代码:
me.iiv.iivframework.controller.customer.output.CustomerProductListOutput← 包名不同
后台「权限组管理 → 配置页面内容权限」会用反射加载这个类,路径不一致直接报错。
强制规则:
- 先定 SQL 里的 class_name → 后端按这个全限定名创建文件
- 改包名 / 改类名 → 必须同步改 SQL(或反过来)
3.3 Controller 注入风格
观察项目历史风格(保持一致即可):
@Autowired
SomeService someService; // 字段级 @Autowired + 默认 package-private(无 private 修饰)
不要混入 @Resource 除非真的需要按 name 注入(如 RedisTemplate)。
3.4 用户身份
User user = UserUtils.getUser(request);
Long userId = user == null ? 0L : user.getId();
不要假设 user 一定非 null,public 接口可能未登录。
3.5 软删 + 修改时间
业务表绝大多数有:
@TableField("soft_delete")
private Integer softDelete; // 0=正常 1=删除
@TableField("modify_id")
private Long modifyId;
@TableField("modify_time")
private Date modifyTime;
约定:
- 查询永远
WHERE soft_delete=0(用WrapperUtils.getWrapper(false)已自带) - "删除"=软删(setSoftDelete(1) + setModifyTime(new Date()) + setModifyId(userId))
- 更新一定要 setModifyTime + setModifyId
3.6 文件/图片上传
- 通用文件上传接口:
POST /uploadFile/warehouseCertificate,返回{fileId, url} - 通用接口不要在里面硬编码
aggregate_id / file_type / in_out_allocation_type(这会污染装机/送货/凭证等多个业务流) - 业务侧的"绑定"应在业务提交时再 UPDATE 这三个字段
- 用户取消未提交的图片 → 留在 DB 里
aggregate_id=0→ 由定时任务清理(孤儿) - 注意:
OSSClientUtil.uploadImageStream2OSS名字带 Image 但实际支持 jpg/png/gif/bmp/pdf/doc/zip 等所有类型(看getContentType实现),不要被命名误导
4. 前端代码约定
4.1 API 端点
// src/api/api.js — 按业务域聚合所有 URL 常量
outManage: {
listByPage: '/myWarehouseOut/listByPage',
certificateGet: '/myWarehouseOut/certificate/get',
...
}
// src/api/warehouseService.js — 按业务域聚合 service
export const outManageService = {
listByPage: (postData) => request.post($api.outManage.listByPage, postData),
certificateGet: (params) => request.get($api.outManage.certificateGet, { params }),
...
};
避免重复定义同一个 URL(典型例子:/uploadFile/warehouseCertificate 在 inWarehouse / outManage / allotManage 三处重复定义;新加时复用而非新增)。
4.2 表格页面骨架
参考 outAllDoc.vue / siteMoveLog.vue / deviceManageSearch.vue:
mixins: [standTableMixin]— 提供 currentForm / pageData / tableHeight 等tableHeader/tableButtons/rowButtons来自 Vuex(allPageFields/allPageButtons),由后端配置驱动buttonsClickFun是 button enName → handler 的映射,rowButtonClick / tableButtonClick 会按 enName 自动分发
4.3 永远不要 deep import 进 element-ui 内部模块
反例(CI Linux 编译会挂):
import ElImageViewer from 'element-ui/lib/image-viewer'; // ❌ 内部模块
正例:
- 用公开组件:
<el-image :preview-src-list>、<el-dialog>、<el-popover>等 - 用第三方公开库(如
v-viewer) - 自己手写一个简易组件(参考 outCertificateForm.vue 的 lightbox 实现)
为什么:macOS 文件系统大小写不敏感能解析,Linux CI 严格大小写匹配,常常构建报
Module not found。
4.4 表单 / 弹窗规则
- 弹窗
z-index< 9999(侧边栏的 z-index 是 9999) - 弹窗如果
append-to-body,必须watch route+onBeforeUnmount主动 close,否则路由切走弹窗还在
4.5 下拉级联
公司 → 物业 → 项目 三级下拉接口已存在(dropDownService 同名):
dropDownService.selectAllAgent()
dropDownService.selectPropertyByAgentId({ agentId })
dropDownService.selectDistrictByPropertyId({ propertyId })
参考 deviceManageSearch.vue 660-700 行的 watcher 模式(agentId 变 → 清空 propertyId/districtId → 重拉下级列表)。
5. 开发文档约定
5.1 文档位置
xiaoqu-franchisee/doc/<feature-name>-dev.md
5.2 文档骨架
# 开发文档:<需求名>
> 需求一句话描述
> 环境 / 涉及库 / 关键基础设施
## 1. 数据库变更
### 1.1 业务表(建表 / 复用现有表)
### 1.2 菜单 + 字段 + 权限(事务 SQL)
### 1.3 字段配置说明(表格)
## 2. 后端改动
### 2.1 包结构(标注新增 / 复用)
### 2.2 API
### 2.3 查询/业务逻辑
## 3. 前端改动
### 3.1 路由
### 3.2 API
### 3.3 页面
## 4. 上线执行顺序
- DEV → DEV 冒烟 → TEST → PRD sanity check → PRD 上线
## 5. 风险与注意(表格)
## 6. PM 确认结论(敲定的决议清单)
5.3 文档里必须写
- 跨环境实测结果(dev=X, test=Y, prod 待查)— 跑过的写实测值
- PM 决议(用表格固化,避免后期扯皮)
- 风险与处理(已知坑要落字)
5.4 不要写
- 完整代码(一两段 snippet 示意即可,详细看仓库)
- 设计动机的 "why" 散落各处(统一到一节 PM 决议)
6. Git 约定
6.1 分支
xiaoqu-publicbeanandmap/xiaoqu-franchisee:dev 分支vue-manage-system:feature 分支- 三个 repo 不共享 commit,要分别 commit
6.2 Commit message
中文 + 模块前缀,简短主题 + 详细说明:
docs(消息通知): SQL 改为动态复用兄弟菜单授权,兼容 dev/test/prod
- §1.2 webpage_authority/field_webpage_authority INSERT 用子查询替代硬编码
- §4 上线步骤增加 TEST 段
- 风险表 authority 硬编码条目标记已修复
6.3 推送
- Gitea remote
http://39.102.42.135:8280/...,凭证存 macOS Keychain(GUI session 才有) - ssh 远程操作时若 keychain 拿不到,用一次性 inline credential helper(不写磁盘):
git -c credential.helper='!f() { echo username=<u>; echo password=<p>; }; f' push origin dev
7. 编译部署
7.1 后端
cd xiaoqu-publicbeanandmap && mvn clean install -DskipTests # 先装公共模块
cd xiaoqu-franchisee && mvn clean package -DskipTests # 打 WAR
# 部署到外部 Tomcat(war 名 ROOT.war)
xiaoqu-publicbeanandmap 改动后必须先 install,否则上层模块用旧 jar 编译。
7.2 前端
yarn dev # 本地起服务(端口 8079,proxy /api → VUE_APP_API_ORIGIN)
yarn build-test # test 环境包
yarn build-pre # 预生产
yarn build # 生产
环境 URL(来自 .env.*):
| 环境 | API_ORIGIN |
|---|---|
| develop | http://192.168.1.182:8093(通过 vite proxy) |
| test | http://192.168.1.182:8093 |
| pre-production | http://192.168.1.182:8094 |
| production | http://webserver.ququtech.cn:9480 |
7.3 CI(Jenkins)
构建机是 Linux,严格大小写。本地 macOS 跑得通 + Jenkins 失败的常见原因:
- import 路径大小写
- 文件名大小写不一致
- deep import 内部模块(见 §4.3)
附:常用查询脚本
A. 查页面 web_id
SELECT id, title, path, parent_id FROM webpage WHERE path='xxx' OR title='xxx';
B. 查页面当前授权范围
SELECT DISTINCT authority_id FROM webpage_authority
WHERE web_id=<X> AND soft_delete=0 ORDER BY authority_id;
C. 查同 parent 下兄弟菜单授权数(设计新菜单授权范围参考)
SELECT w.id AS web_id, w.title, COUNT(DISTINCT wa.authority_id) AS auth_cnt
FROM webpage w
LEFT JOIN webpage_authority wa ON wa.web_id=w.id AND wa.soft_delete=0
WHERE w.parent_id=<Y>
GROUP BY w.id, w.title;
D. 防重复执行
-- 跑新菜单 SQL 前先 sanity check
SELECT id, title, path FROM webpage WHERE path='<new_path>'; -- 应返回空
SELECT id, en_name FROM button WHERE en_name='<new_en_name>' AND web_id=<X>; -- 应返回空
维护
本文档基于以下需求实战提炼,遇到新的规范点请追加:
- 出库单上传凭证(
doc/out-certificate-dev.md) - 代理商消息通知(
doc/agent-message-notice-dev.md) - 客户产品清单查询(
doc/customer-product-list-dev.md)
版本:v1.0 / 2026-05-14