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

428 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发规范
> 适用范围:`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 会授权遗漏):
```sql
INSERT INTO webpage_authority (web_id, authority_id, ...) VALUES
(@new_web_id, 1, ...),
(@new_web_id, 102, ...);
```
**正例(A:复用同 parent 下兄弟菜单授权并集)**:
```sql
-- 适用:新页面,没有 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:复用同页面已有授权)**:
```sql
-- 适用:往已有页面加按钮,按钮可见性应与页面可见性一致
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;
```
**例外:明确只给系统管理员**:
```sql
-- PM 明确说"只给管理员"时,可以写 authority_id=1
-- 这是有意识的决策,不是疏忽
INSERT INTO webpage_authority ... VALUES (@new_web_id, 1, ...);
```
> **为什么**:dev 简化数据让人误以为只有 1/102;test/prod 真实有 10+ 个权限组,写死会让大批用户看不到菜单/按钮。
### 2.2 永远不要硬编码自增主键
**反例**:
```sql
-- 假设 button.id 会是 789,前端/后端硬编码这个 id —— 锁死了这条记录
```
**正例**:
```sql
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 必须有校验段 + 事务
模板:
```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 注入风格
观察项目历史风格(保持一致即可):
```java
@Autowired
SomeService someService; // 字段级 @Autowired + 默认 package-private(无 private 修饰)
```
不要混入 `@Resource` 除非真的需要按 name 注入(如 RedisTemplate)。
### 3.4 用户身份
```java
User user = UserUtils.getUser(request);
Long userId = user == null ? 0L : user.getId();
```
不要假设 user 一定非 null,public 接口可能未登录。
### 3.5 软删 + 修改时间
业务表绝大多数有:
```java
@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 端点
```js
// 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 编译会挂):
```js
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` 同名):
```js
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 文档骨架
```markdown
# 开发文档:<需求名>
> 需求一句话描述
> 环境 / 涉及库 / 关键基础设施
## 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(不写磁盘):
```bash
git -c credential.helper='!f() { echo username=<u>; echo password=<p>; }; f' push origin dev
```
---
## 7. 编译部署
### 7.1 后端
```bash
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 前端
```bash
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
```sql
SELECT id, title, path, parent_id FROM webpage WHERE path='xxx' OR title='xxx';
```
### B. 查页面当前授权范围
```sql
SELECT DISTINCT authority_id FROM webpage_authority
WHERE web_id=<X> AND soft_delete=0 ORDER BY authority_id;
```
### C. 查同 parent 下兄弟菜单授权数(设计新菜单授权范围参考)
```sql
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
-- 跑新菜单 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