# 开发规范 > 适用范围:`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'; // ❌ 内部模块 ``` **正例**: - 用公开组件:``、``、`` 等 - 用第三方公开库(如 `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/-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=; echo password=

; }; 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= 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= GROUP BY w.id, w.title; ``` ### D. 防重复执行 ```sql -- 跑新菜单 SQL 前先 sanity check SELECT id, title, path FROM webpage WHERE path=''; -- 应返回空 SELECT id, en_name FROM button WHERE en_name='' AND web_id=; -- 应返回空 ``` --- ## 维护 本文档基于以下需求实战提炼,遇到新的规范点请追加: - 出库单上传凭证(`doc/out-certificate-dev.md`) - 代理商消息通知(`doc/agent-message-notice-dev.md`) - 客户产品清单查询(`doc/customer-product-list-dev.md`) **版本**:v1.0 / 2026-05-14