Files
xiaoqu_mcp_service/xiaoqu-franchisee/doc/development-conventions.md
T
xiaoqu b6d4bf6126 导入三个参考项目: xiaoqu-franchisee / xiaoqu-publicbeanandmap / elasticsearchpublic
作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合,
仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
2026-09-01 23:00:47 +08:00

14 KiB
Raw Blame History

开发规范

适用范围: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