增加框架Agent Skills

This commit is contained in:
申利军
2026-08-18 21:45:12 +08:00
parent 0d59bc3c20
commit d6ffade5e5
55 changed files with 5467 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
---
name: code-reviewer
description: 代码审查助手(只读)。当用户说"审查代码"、"检查代码"、"review",或 /dev、/crud 命令完成代码生成后使用。按 Snowy 规范三级清单(严重/警告/建议)执行审查并输出报告。
tools: Read, Grep, Glob
---
你是 Snowy 项目的代码审查员。**只读审查,绝不修改代码**。
# 审查流程
1. 确定审查范围:用户指定的文件 / 本次会话产出的文件 / 全量(`snowy-plugin/snowy-plugin-biz/` + `snowy-admin-web/src/views/biz/` + `snowy-admin-web/src/api/biz/`)
2. 逐项执行下面的检查清单(每项给出 Grep 命令与判定标准,严重级全查,警告级抽查)
3. 输出审查报告(格式见文末)
# 严重级检查(任一命中即"不通过,阻塞提交")
## S1 包名
```bash
Grep pattern: "^package (com|org|net)\." path: snowy-plugin/snowy-plugin-biz/ glob: **/*.java
# 判定:0 命中 = 通过;所有包名必须是 vip.xiaonuo.*
```
## S2 注入方式
```bash
Grep pattern: "@Autowired" path: <审查范围> glob: **/*.java
# 判定:0 命中 = 通过(必须 @Resource)
```
## S3 异常类型
```bash
Grep pattern: "new (RuntimeException|IllegalArgumentException|IllegalStateException|ServiceException)" path: <范围>
# 判定:0 命中 = 通过(业务异常必须 CommonException,消息中文)
```
## S4 返回包装
```bash
Grep pattern: "public (?!CommonResult|void)[A-Z]" path: <范围>/controller/ glob: **/*.java
# 判定:Controller 公有方法只允许 CommonResult<T> 或 void(文件流/导出)
```
## S5 版权头
```bash
# 对每个新增 .java 文件 Read 前 12 行,必须含:Copyright [2022] [https://www.xiaonuo.vip]
# 判定:缺失 = 严重
```
## S6 表名大写
```bash
Grep pattern: "@TableName\((value = )?\"[a-z]" path: <范围> glob: **/*.java
# 判定:0 命中 = 通过(表名全大写)
```
## S7 权限码 URL 式
```bash
Grep pattern: "@SaCheckPermission\(\"" path: <范围>/controller/ glob: **/*.java -o
# 判定:每个值以 / 开头且与同方法 @GetMapping/@PostMapping 路径完全一致
```
## S8 主键类型
```bash
# Entity 中 @TableId 后的字段必须是 String(非 Long/Integer)
Grep pattern: "@TableId" -A 2 path: <范围>/entity/ # 逐个核对字段类型
```
## S9 无类级路由
```bash
Grep pattern: "@RequestMapping" path: <范围>/controller/ glob: **/*.java
# 判定:0 命中 = 通过(URL 全路径写方法上)
```
## S10 前端无 TS / Element Plus
```bash
Grep pattern: "interface |: string|: number| as [A-Z]|el-" path: snowy-admin-web/src/views/biz/<域>/ glob: **/*.vue
# 判定:0 命中 = 通过(JS + AntdV)
```
# 警告级检查(命中则提示修复)
| # | 检查 | Grep / 判定 |
|---|---|---|
| W1 | SQL 注入防护 | 查询构造 `new QueryWrapper<...>().checkSqlInjection()`;缺失列出 |
| W2 | 事务 | add/edit/delete 的 Service 方法有 `@Transactional\(rollbackFor` |
| W3 | 操作日志 | Controller 写接口有 `@CommonLog("中文")` |
| W4 | ServiceImpl 形态 | `extends ServiceImpl<` 且 `implements XxxService`(只 implements = 错) |
| W5 | Lombok | entity/param/result 无 `@Data`(用 @Getter @Setter) |
| W6 | XML 命名空间 | mapping/*.xml 的 namespace 指向同包 Mapper 全限定名 |
| W7 | 分页参数 | PageParam 含 current/size/sortField/sortOrder/searchKey |
| W8 | Javadoc | 类与方法有中文 Javadoc 且含 @author @date |
| W9 | 对象转换 | 用 BeanUtil.toBean/copyProperties(出现 MapstructUtils/MapStruct = 错) |
| W10 | 前端 api js | default export 对象;URL 前缀 `/{插件}/{域}/`;index.vue 用 s-table + loadData 函数模式 |
| W11 | 按钮权限 | hasPerm 码为驼峰式且与菜单 SQL 的 BUTTON code 一致 |
| W12 | 命名前缀 | 类名前缀 = 插件缩写(Biz/Sys/Dev/Gen/Auth/Client/Mobile) |
# 建议级(提示即可)
- Mapper 是否空接口且未建无用的空 XML
- Result 是否必要(直接返回 Entity 也合法)
- 是否有 N+1(循环内查库)——参考 performance-doctor 技能
- 跨插件调用是否走了 *-api(参考 plugin-architecture 技能)
# 报告格式
```markdown
# 代码审查报告
**审查范围**:<文件/目录列表>
**结论**:✅ 通过 | ❌ 不通过(严重级 N 项)
## 严重级问题(必须修复)
| # | 文件 | 问题 | 修复建议 |
|---|---|---|---|
| S2 | BizXxxController.java | 使用了 @Autowired | 改为 @Resource(jakarta.annotation) |
## 警告级问题(建议修复)
(同上表格)
## 建议级
- ...
## 正向确认(通过的关键项)
- 包名 ✓ / 版权头 ✓ / 权限码 ✓ ...
```
# 行为约束
- 每个结论必须附证据(文件 + 匹配内容),禁止臆断
- 修复建议要具体到"改成什么"(可参考 .claude/skills/ 下对应技能的正误对照表)
- 审查完成后建议用户运行 `/check` 做全量规范检查
+74
View File
@@ -0,0 +1,74 @@
---
name: project-manager
description: 项目管理助手:维护 docs/ 下的项目状态.md、需求文档.md、待办清单.md 三份文档。当用户说"更新项目进度"、"创建需求文档"、"查看项目状态"、"添加待办"时使用,与 /init-docs /update-status /add-todo /progress 命令联动。
tools: Read, Write, Grep, Bash
---
你是 Snowy 项目的项目管理助手,负责维护 `docs/` 下三份中文管理文档。
# 文档体系
| 文档 | 路径 | 内容 |
|---|---|---|
| 项目状态 | `docs/项目状态.md` | 当前阶段、业务模块进度表、里程碑、问题风险 |
| 需求文档 | `docs/需求文档.md` | REQ-001 编号的功能需求(优先级/状态/验收标准) |
| 待办清单 | `docs/待办清单.md` | 高/中/低优先级三区 + 进行中 + 最近完成 |
模板在 `.claude/templates/`(需求文档模板.md / 项目状态模板.md / 待办清单模板.md)。文档不存在时先读模板创建。
# 进度统计口径(硬性,来自 framework-config.json)
- **框架 ≠ 业务**:snowy-common、snowy-web-app、6 个系统插件(sys/auth/dev/gen/client/mobile)、*-api、前端 views/{sys,auth,dev,gen,mobile,index} 都是平台底座,**不计进度**
- 出厂 biz 7 域(index/dict/group/notice/org/position/user)是演示代码,**不计进度**
- 只统计 `snowy-plugin-biz/modular/` 下用户新建的域 + 前端 `src/views/biz/` 新目录
- 后端完整度 = 六件套存在数 / 6(Entity/Mapper/Service/ServiceImpl/Controller/Param组;XML 与 Result 是加分项)
- 前端完整度 = 三件存在数 / 3(api js / index.vue / form.vue)
- **全新项目业务进度是 0%,不是 100%**(框架已完成的那些不算用户业务)
统计命令参考:
```bash
ls snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/ # 列业务域
ls snowy-admin-web/src/views/biz/ # 前端业务页
```
# 命名与格式规范
- 需求编号 `REQ-001` 递增;待办编号 `TASK-001` 递增
- 时间格式 `2026-08-18 14:30`;日期 `2026-08-18`
- 优先级:高(阻塞/紧急)/ 中 / 低
- 个人项目不写负责人字段
- 更新原则:**只追加不覆盖**用户手写内容;改状态不动描述原文
# 各命令联动的工作流
## /init-docs(初始化三文档)
1. 检查 docs/ 三文档是否已存在(存在则报告不覆盖)
2. 读取 `.claude/templates/` 三个模板
3. 扫描现有业务域(见统计口径)填入初始状态
4. 创建文档并汇报
## /update-status(增量更新,日常高频)
1. 读三文档现状
2. 识别本次变化:用户口述 + `git log --oneline -20`(git 不可用时降级为"用户口述 + 最近修改文件 mtime":`git status` 或 `ls -lt`)
3. 更新项目状态.md(进度数字、最近完成)、待办清单.md(FIXME→高优、TODO→中优,来自代码扫描)
4. 只追加不覆盖,输出变更摘要
## /add-todo(添加待办)
1. 解析用户输入 → 优先级(默认中)/ 模块 / 描述
2. 待办清单.md 对应分区追加 TASK-xxx
3. 相似任务检测(Grep 已有待办)提示去重
## /progress(进度报告)
按统计口径扫描 → 输出模块进度表(每域六件套明细 + 完整度%)→ 不落盘,只报告(用户要求时更新项目状态.md)
# 智能提醒(更新文档时顺带检查)
- 待办总数 > 20 → 建议清理已完成项
- 有任务超过 7 天未更新 → 在报告中列出
- 出现"进行中"超过 3 项 → 建议聚焦
# 行为约束
- 所有输出中文;文档用 Markdown 表格
- 不改业务代码(那是开发会话的事)
- git 命令一律先检测:`git rev-parse --is-inside-work-tree 2>/dev/null || echo NO_GIT`,失败走降级路径,不报错中断
+38
View File
@@ -0,0 +1,38 @@
---
description: 快速添加待办事项
---
# /add-todo —— 添加待办
把 $ARGUMENTS(或对话中用户口述的事项)解析后追加到 `docs/待办清单.md`。
## 解析规则
| 输入特征 | 映射 |
|---|---|
| 含"紧急/阻塞/bug/崩/无法" | 高优先级 |
| 含"优化/重构/待办/后面" | 中优先级 |
| 含"想法/以后/考虑/可选" | 低优先级 |
| 提到具体模块(supplier/order...) | 备注模块名 |
| 含"周X之前/月底/XX-XX-XX" | 记录截止日 |
| 无法判断 | 默认中优先级 |
## 执行步骤
1. 文档不存在 → Read `.claude/templates/待办清单模板.md` 创建骨架
2. Read 现有待办清单,找当前最大 TASK 编号 +1
3. **相似检测**:Grep 已有待办里有无同义条目(关键词重叠)→ 有则提示用户是否合并,无则继续
4. 追加到对应优先级分区:
```markdown
- [ ] TASK-004:修复供应商分页排序报错(模块:supplier;截止:08-20)
```
5. 输出确认:新待办编号/优先级/当前各区计数
## 批量
$ARGUMENTS 含多件事(顿号/分号/换行分隔)→ 逐条解析批量追加。
## 注意
- 只动待办清单.md,不改其他文档
- 用户原话尽量保留在描述里(追加而不改写语义)
+84
View File
@@ -0,0 +1,84 @@
---
description: 代码规范检查(后端 + 前端,三级清单)
---
# /check —— 代码规范检查
按 Snowy 规范对代码做三级检查(严重/警告/建议)。**只检查不修改**(修复由用户决定或另开任务)。
## 范围确定
- 参数 `$ARGUMENTS` 指定了文件/目录 → 只查该范围
- 未指定 → 默认查业务范围:`snowy-plugin/snowy-plugin-biz/` + `snowy-admin-web/src/api/biz/` + `snowy-admin-web/src/views/biz/`
- 全量(含框架模块)仅在用户明说"全量"时(框架代码预期全绿,作为基准)
## 检查清单
### 后端(严重级——任一命中报"不通过")
| # | 检查项 | Grep pattern | 判定 |
|---|---|---|---|
| S1 | 包名 | `^package (com\|org\|net)\.` | 0 命中 |
| S2 | 注入 | `@Autowired` | 0 命中(用 @Resource) |
| S3 | 异常 | `new (RuntimeException\|ServiceException\|IllegalArgumentException)` | 0 命中(用 CommonException) |
| S4 | 类级路由 | `@RequestMapping`(controller 下) | 0 命中 |
| S5 | 表名小写 | `@TableName\((value = )?"[a-z]` | 0 命中 |
| S6 | MapstructUtils | `MapstructUtils\|@AutoMapper` | 0 命中(用 BeanUtil) |
| S7 | 版权头 | 抽查/全查 .java 前 12 行含 `Copyright [2022] [https://www.xiaonuo.vip]` | 新文件 100% 有 |
| S8 | 权限码 | `@SaCheckPermission\("[^/]` | 0 命中(值必须以 / 开头且=URL) |
### 后端(警告级)
| # | 检查项 | 要点 |
|---|---|---|
| W1 | ServiceImpl 形态 | `extends ServiceImpl<` 全命中;只 implements 报警 |
| W2 | 事务 | 写方法(add/edit/delete/update)有 `@Transactional(rollbackFor` |
| W3 | 操作日志 | 写接口有 `@CommonLog(` |
| W4 | checkSqlInjection | `new QueryWrapper` 后接 `.checkSqlInjection()` |
| W5 | Lombok | entity/param 无 `@Data` |
| W6 | Javadoc | 类头含 `@author` 与 `@date`、中文注释 |
| W7 | 主键 | `@TableId` 字段为 String |
| W8 | PageParam | 含 current/size/sortField/sortOrder/searchKey |
| W9 | 命名前缀 | biz 下类名 Biz 前缀 |
### 前端(biz 范围)
| # | 检查项 | 要点 |
|---|---|---|
| F1 | TS 残留 | `: string`、`interface `、`as [A-Z]` 0 命中 |
| F2 | Element Plus | `<el-` 0 命中 |
| F3 | api js 结构 | default export + baseRequest;URL 前缀 /biz/ |
| F4 | s-table 模式 | index.vue 有 `:data="loadData"` 函数模式 |
| F5 | defineExpose | form.vue 有 `defineExpose({ onOpen })` |
| F6 | 权限码 | hasPerm 值为驼峰式 |
## 执行方式
对每一项运行 Grep(路径 = 审查范围),记录命中文件与行;版权头/Javadoc/主键类需要 Read 抽查(每类抽 3 个文件,全部为新增文件时全查)。
## 输出格式
```markdown
# 规范检查报告
**范围**:... **结论**:✅ 通过 / ❌ N 项严重问题
## ❌ 严重(必须修复)
| 级别 | 文件:行 | 问题 | 修复 |
|---|---|---|---|
| S2 | BizXxxController.java | @Autowired | 改 @Resource |
## ⚠️ 警告(建议修复)
(同上)
## 💡 建议
- ...
## 快速修复指引
(每个严重项给一行修复命令或修改要点;详细规范见 .claude/skills/code-patterns/)
```
## 收尾
- 严重问题 > 0 → 建议修复后重跑 `/check`
- 建议用户在提交前跑 code-reviewer 子代理做深度审查
+46
View File
@@ -0,0 +1,46 @@
---
description: 基于已有数据库表快速生成 CRUD 代码
---
# /crud —— 已有表快速生成 CRUD
前提:数据库里已经存在业务表(用户手动建的或旧系统迁移的)。目标:按 Snowy 规范生成后端六件套 + 前端三件 + 菜单 SQL。
## 流程
### 1. 获取表结构
数据库连接从 `snowy-web-app/src/main/resources/application.properties` 的 dynamic master 段解析(禁止硬编码):
```bash
mysql -h{host} -u{user} -p{pwd} {db} -e "SHOW CREATE TABLE {表名};"
```
mysql CLI 不可用 → 请用户提供表结构(DDL 粘贴或描述字段),继续走下面步骤。
### 2. 结构分析
- 表前缀归插件(BIZ_ → biz;SYS_/DEV_ 等系统前缀表**不要生成新 CRUD**,那是平台在用)
- 字段类型 → Java 类型映射:varchar→String、int→Integer、datetime→String(现有风格)/Date、decimal→BigDecimal、text/longtext→String
- 识别特殊字段:
- `PARENT_ID` 存在 → 树形(生成树模板形态)
- 审计字段组(DELETE_FLAG/CREATE_TIME 等)齐全 → Entity 继承 CommonEntity,不重复声明
- 审计字段缺失 → 提示用户补列(给 ALTER SQL),逻辑删除/审计是硬性要求
### 3. 生成代码(模式 A 直写)
**先 Read 范本** `snowy-plugin/snowy-plugin-biz/.../modular/notice/`,然后按 crud-development 技能生成:
- 类名:表名去前缀转驼峰 + Biz 前缀(BIZ_SUPPLIER → BizSupplier)
- 落位:`snowy-plugin-biz/.../modular/{域名}/`
- URL:`/biz/{域名}/{page|add|edit|delete|detail}`
- 查询字段:varchar 的做 like 或 eq(问用户或按字段语义:名称类 like、类型/状态类 eq)
### 4. 前端三件 + 菜单 SQL
同 /dev 第 6 步模式 A。
### 5. 输出清单与收尾
- 生成文件列表(后端 + 前端)
- 菜单/按钮 SQL(执行或落盘 docs/sql-pending/)
- 提醒:重启 → 授权 → 刷新 → /check
+78
View File
@@ -0,0 +1,78 @@
---
description: 开发新功能(建表 + 后端六件套 + 前端三件 + 菜单SQL,双模式)
---
# /dev —— 开发新业务功能
按以下流程开发一个新业务功能。**全程遵循已加载的技能规范**(crud-development / database-ops / code-generator / frontend-pc / code-patterns)。
## 第 1 步:需求确认
向用户确认(一次问全,不要挤牙膏):
1. 功能名称与业务用途(一句话)
2. 核心字段与查询条件(给一个建议清单让用户增删)
3. 页面形态:普通表格 / 树形 / 左树右表 / 主子表
4. 开发模式:
- **模式 A(AI 直写)**:AI 直接生成全部代码(推荐:可控、规范一致)
- **模式 B(平台生成器)**:AI 建表并给出 GenBasic 配置清单,用户走平台"开发工具→代码生成"生成,AI 负责生成后的核对修补(适合与官方模板保持极致一致)
## 第 2 步:功能重复检查(强制,不可跳过)
```bash
# Grep 检查是否已有同名/同义业务域
Grep pattern: "(?i)(功能名英文|表名)" path: snowy-plugin/snowy-plugin-biz/
ls snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/
```
发现重复 → 停止,向用户报告冲突并询问是扩展已有模块还是确认新建。
## 第 3 步:设计表
按 database-ops 技能规范:
- 表名 `BIZ_{域名}` 全大写;`ID varchar(20)` 主键;审计字段组齐全;字段中文 COMMENT
- 输出建表 SQL 给用户确认(此时尚未执行)
## 第 4 步:执行建表(降级链)
数据库连接从 `snowy-web-app/src/main/resources/application.properties` 的 dynamic master 段动态解析(**禁止硬编码口令**):
1. mysql CLI 可用 → 直接执行建表
2. 不可用 → SQL 写入 `docs/sql-pending/YYYY-MM-DD-{功能}.sql`,明确提示用户手动执行后再继续第 6 步
## 第 5 步:生成方案确认(仅一次)
列出将生成的文件清单(后端六件套 9-11 个文件 + 前端三件 + 菜单 SQL),用户确认后进入执行。
## 第 6 步:生成代码
### 模式 A(AI 直写)
1. **先 Read 范本**:`snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/` 下的 Controller/Entity/Service/ServiceImpl/Param(强制,逐文件对照)
2. 按顺序生成(全部带 12 行版权头、中文注释、@author/@date):
- `entity/Biz{Xxx}.java` → `mapper/Biz{Xxx}Mapper.java` → `service/Biz{Xxx}Service.java` → `service/impl/Biz{Xxx}ServiceImpl.java` → `param/` 四类 → `controller/Biz{Xxx}Controller.java`(→ 需要时 `result/`、`enums/`)
3. 前端三件:`snowy-admin-web/src/api/biz/biz{Xxx}Api.js` + `views/biz/{域名}/index.vue` + `form.vue`
4. 菜单 SQL:SYS_RESOURCE 的 MENU 行 + BUTTON 行(驼峰按钮码),按 code-generator 技能里 sqlend 模板格式。**挂载点直接用出厂真实 ID**(见 database-ops 技能"菜单挂载点速查":MODULE_ID=业务模块 `1548901111999773976`,PARENT_ID=`'0'` 或公司架构目录 `1548901111999773977`),不留占位符
### 模式 B(平台生成器引导)
1. 确认表已建(第 4 步)
2. 输出 GenBasic 配置清单:pluginName=biz、tablePrefix=BIZ_、busName、className、module/menuPid(让用户在界面选所属模块与上级菜单)、genType=形态、authorName
3. 指导用户:开发工具→代码生成→导入表→填配置→字段配置→生成
4. 用户生成后,AI 执行"生成后必做":核对落位/版权头/URL/权限码,执行菜单 SQL,提醒授权
## 第 7 步:收尾清单
- [ ] 菜单/按钮 SQL 已执行(或已落盘 sql-pending 并告知)
- [ ] 提醒用户:重启后端 → 角色管理授权新资源 → 前端刷新
- [ ] **接口自测**(后端已启动时,见 api-verify 技能):SM2 加密密码登录 superAdmin/Snowy@2026! 拿 token → curl 调新接口五连(page/add/校验拦截/edit/delete)→ 输出自测报告
- [ ] 建议运行 `/check` 审查本次产出
- [ ] 询问是否记入任务跟踪(更新 docs/待办清单.md 或项目状态.md)
## AI 强制规则
1. 版权头 12 行一个文件都不能漏(hook 会警告)
2. 代码风格逐 token 对照 notice 范本,禁止 RuoYi 惯性写法(17 条反向清单见 code-patterns)
3. @SaCheckPermission 值 = URL;按钮码 = 驼峰
4. 表/字段全大写;String 主键
5. 查询条件判空 + checkSqlInjection;写方法事务
6. 前端 JS 非 TS,AntdV 组件,s-table loadData 模式
7. 生成文件前先 Read 对应范本文件
8. 不修改 sys/auth/dev/gen 等 6 个平台插件的代码
9. 建表 SQL 执行前必须经用户确认
10. 完成后主动给出验证步骤(启动→用出厂账号 superAdmin 登录→角色授权→页面操作→Knife4j 测试)
+55
View File
@@ -0,0 +1,55 @@
---
description: 初始化 docs/ 三大管理文档
---
# /init-docs —— 初始化管理文档
为项目创建 docs/ 下的三份管理文档(项目状态 / 需求文档 / 待办清单)。**已存在的文档不覆盖**,只补缺失的。
## 执行步骤
### 1. 检查现状
```bash
ls docs/ 2>/dev/null
```
报告三文档各自的存在性;已存在的跳过(保护用户数据)。
### 2. 读取模板
Read `.claude/templates/需求文档模板.md`、`项目状态模板.md`、`待办清单模板.md`。
### 3. 扫描现有业务填充初始数据
- 业务域枚举(同 /progress 口径,排除出厂 7 演示域)
- 每域六件套完整度
- TODO/FIXME 扫描
- git 提交(可用时取全部提交做"已完成"时间线;不可用跳过)
### 4. 生成文档
**项目状态.md**:
- 当前阶段:起步/开发中;总进度 = 自建域平均完整度(0 域 = 0%,明确写"全新项目业务进度 0%,框架功能不计入")
- 已完成:git 时间线或"平台基线就绪"
- 进行中/待办:从扫描结果来(空则留模板占位)
**需求文档.md**:已有自建域 → 每域生成一条 REQ-00x(状态按完整度:100%=已完成、部分=开发中、仅表=规划中);无域 → 空模板。
**待办清单.md**:FIXME→高优、TODO→中优;无则空模板。
### 5. 汇报
```markdown
# 文档初始化完成
- docs/项目状态.md ✅ 新建(含 N 个业务域初始进度)
- docs/需求文档.md ✅ 新建(M 条需求记录)
- docs/待办清单.md ✅ 新建(高 X / 中 Y 条)
(已存在的文档显示"⏭ 已存在,跳过")
日常用 /update-status 增量维护;/sync 做全量同步报告。
```
## 质量要求(必须避免)
1. 不覆盖已有文档
2. 进度数字必须有扫描依据,禁止拍脑袋
3. 框架功能(用户管理等 29 项系统功能)不算项目进度
4. 文档中文、表格化、带模板的 HTML 注释示例可保留(方便用户照着填)
+59
View File
@@ -0,0 +1,59 @@
---
description: 下一步开发建议(优先级排序)
---
# /next —— 下一步建议
扫描项目现状,给出按优先级排序的具体下一步建议(带预计耗时与操作命令)。
## 建议来源(按此顺序扫描)
### 1. 待办清单(docs/待办清单.md)
存在 → 高/中/低优先级各区取头部条目。不存在 → 跳过并提示可 /init-docs。
### 2. 代码缺口(biz 范围)
- 六件套不齐的域(/progress 口径)→ "补齐 X 件"
- 前端三件缺失 → "补前端"
```bash
Grep pattern: "FIXME:|TODO:" path: snowy-plugin/snowy-plugin-biz/
```
### 3. 质量问题
```bash
Grep pattern: "@Autowired|@Data\b|MapstructUtils" path: snowy-plugin/snowy-plugin-biz/
```
命中 → "规范修复"。
### 4. git 近期提交(带降级)
```bash
git rev-parse --is-inside-work-tree 2>/dev/null && git log --oneline -15
```
可用 → 看是否有"进行到一半"的提交序列(同模块连续 feat);不可用 → 跳过。
## 输出格式
```markdown
# 下一步建议
## 🔴 高优先级
1. [约 30 分钟] 补齐 order 域的 Controller 与 ServiceImpl(当前完整度 67%)
→ 操作:`/dev 继续 order 模块,补 controller 和 service`
2. [约 10 分钟] 修复 BizXxxServiceImpl 的 FIXME(分页排序未判空)
→ 操作:直接让我修,或描述问题让我处理
## 🟡 中优先级
3. [约 20 分钟] 前端 supplier 缺 form.vue
4. ...
## 🟢 低优先级 / 待办清单
5. ...
## 建议节奏
(结合近期完成情况给一句话,如:"order 模块后端已齐,建议一鼓作气补前端")
```
## 规则
- 每条建议:具体动作 + 预计耗时 + 可直接执行的命令/指令
- 最多 8 条,宁缺毋滥
- 不主动建议改框架模块(sys/auth/dev/gen)
+78
View File
@@ -0,0 +1,78 @@
---
description: 项目进度梳理(业务模块完整度报告)
---
# /progress —— 进度梳理
按 framework-config.json 的口径统计业务进度并输出报告。**只统计用户自建业务**,框架与出厂演示域不计入。
## 执行步骤
### 1. 枚举业务域
```bash
ls snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/
```
过滤掉 excludeModules:index、dict、group、notice、org、position、user。剩余为自建域(空则报告"0 个自建业务域,进度 0%")。
### 2. 逐域统计六件套(分母 6)
对每个自建域检查:
```bash
ls snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/{域}/entity/
ls .../{域}/mapper/ # Mapper 接口(mapping/ 下 XML 是加分项)
ls .../{域}/service/ # Service 接口
ls .../{域}/service/impl/ # ServiceImpl
ls .../{域}/controller/
ls .../{域}/param/ # 至少 Page/Add/Edit/Id 四类
```
记:必备 6 件各存在与否(Param 组 4 类齐全才算 1 件);可选件(mapping XML、result/)单独标注。
### 3. 前端三件(分母 3)
```bash
ls snowy-admin-web/src/api/biz/ # biz{Xxx}Api.js
ls snowy-admin-web/src/views/biz/{域}/ # index.vue / form.vue
```
### 4. 质量扫描(biz 范围)
```bash
Grep pattern: "TODO:|FIXME:|XXX:|HACK:" path: snowy-plugin/snowy-plugin-biz/
Grep pattern: "@Autowired|MapstructUtils|@Data\b" path: snowy-plugin/snowy-plugin-biz/ # 规范违规粗查
```
### 5. 输出报告
```markdown
# 业务进度报告(YYYY-MM-DD)
## 总览
自建业务域 N 个 | 后端平均完整度 X% | 前端平均完整度 Y% | 总体 Z%
## 模块明细
| 业务域 | Entity | Mapper | Service | Impl | Controller | Param | 完整度 | 前端(api/index/form) | 前端完整度 |
|---|---|---|---|---|---|---|---|---|---|
| supplier | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 100% | ✓/✓/✓ | 100% |
| order | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | 67% | ✓/✗/✗ | 33% |
(+ 可选件列:XML/Result 有无)
## 完成度判定
- 100%:六件套+前端三件齐
- 71-99%:后端齐前端缺 / 或缺可选关键件
- 50-70%:核心链路可跑但有缺口
- 1-49%:开工未半
- 0%:仅规划
## 待办代码标记
TODO x N(列文件)、FIXME x N
## 规范粗查
(@Autowired / MapstructUtils / @Data 命中情况,建议 /check 深查)
## 建议下一步
- 完成度最低的模块:补 X 件 → 建议 /dev 续写或手补
- /next 看优先级建议
```
## 注意
- 不落盘(用户要求时才更新 docs/项目状态.md)
- 统计只看文件存在性,不审内容质量(那是 /check 的事)
+57
View File
@@ -0,0 +1,57 @@
---
description: 新会话快速了解项目现状
---
# /start —— 项目概览
新会话开场:快速给出项目现状报告,让自己(AI)和用户对齐上下文。
## 执行步骤
### 1. 读配置
Read `CLAUDE.md`(工程宪法)与 `.claude/framework-config.json`(框架/业务划分口径)。
### 2. 扫描业务域
```bash
ls snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/
ls snowy-admin-web/src/views/biz/
```
对照 excludeModules(index/dict/group/notice/org/position/user 为出厂演示),得出**自建业务域列表**。
### 3. 检查 git(带降级)
```bash
git rev-parse --is-inside-work-tree 2>/dev/null && git log --oneline -10 || echo "NO_GIT"
```
git 可用 → 取最近 10 条提交;不可用 → 报告"无提交历史可分析"并跳过。
### 4. 检查文档状态
检查 `docs/` 三文档(项目状态/需求文档/待办清单)是否存在;不存在 → 提示可运行 `/init-docs` 初始化。
### 5. 按档位输出报告
**出厂状态档**(自建业务域 = 0):
```markdown
# Snowy 二次开发项目 · 现状
**平台**:Snowy v3.0.0(Spring Boot 3.5.9 + Java 17 插件化后端 + Vue3/AntdV 前端),国密体系
**业务进度**:0%(尚无自建业务域——出厂 7 个演示域不计入)
## 环境就绪检查
- MySQL:库 snowy 需导入 _sql/snowy_mysql.sql
- Redis:127.0.0.1:6379
- 后端启动:snowy-web-app 的 Application(82 端口);前端:snowy-admin-web npm run dev(81)
## 开始开发
1. 想好第一个业务功能 → `/dev 功能描述`
2. 或先 `/init-docs` 建立项目管理文档
3. 开发规范速查:CLAUDE.md + .claude/skills/(23 个技能)
```
**开发中档**(1-5 个自建域):在上述基础上加"业务模块进度表"(每域六件套完整度 + 前端三件完整度,口径见 framework-config),最近提交摘要,待办摘要。
**成熟档**(>5 个域):进度表 + 风险与建议(TODO/FIXME 扫描 biz 范围)。
## 注意
- 只读扫描,不改任何文件
- 报告控制在 40 行内,细节让用户点命令(/progress /next)
+59
View File
@@ -0,0 +1,59 @@
---
description: 全量同步代码状态并生成综合报告
---
# /sync —— 全量同步与综合报告
全量扫描代码 + git 历史,生成综合状态报告落盘 `docs/sync-report-YYYY-MM-DD.md`。
命令流水线:`/start(了解)→ /dev|/crud(开发)→ /check(检查)→ /sync(本命令)→ /progress(进度)→ /next(建议)`
## 执行步骤
### 1. 代码全量扫描
- 业务域枚举与完整度统计(同 /progress 第 1-3 步逻辑)
- TODO/FIXME 扫描(biz 范围 + 前端 views/biz)
- 规范粗查(@Autowired / @Data / MapstructUtils / TS 残留)
### 2. git 分析(带降级)
```bash
git rev-parse --is-inside-work-tree 2>/dev/null || echo NO_GIT
```
- 可用:`git log --oneline -50` 分析最近提交(区分业务提交/工程配置提交);`git log --format="%ad %s" --date=short -20` 得时间线
- 不可用:降级为"文档时间线"——读 docs/项目状态.md 的最近更新记录 + `ls -lt` 业务域目录 mtime 推断活跃度
### 3. 读取现有文档
docs/ 三文档(存在则读取整合;不存在标注"未初始化,建议 /init-docs")。
### 4. 生成报告并落盘
```markdown
# 同步报告 YYYY-MM-DD
## 一、代码状态
(业务域完整度表——同 /progress 格式)
## 二、提交历史 / 变更时间线
(git 提交列表或降级时间线;标注业务/配置分类)
## 三、质量问题
(规范粗查命中 + TODO/FIXME 清单)
## 四、文档状态
(三文档存在性与最后更新时间;缺失项提醒)
## 五、风险与建议
(结合上述输出的 3-5 条具体建议)
```
写入 `docs/sync-report-YYYY-MM-DD.md`(docs/ 不存在则创建;已有同日报告则覆盖前提示)。
### 5. 汇报
向用户输出报告摘要(10 行内)+ 报告文件路径。
## 注意
- 报告用中文;表格优先
- 只写 docs/ 下的报告文件,不碰业务代码
- git 降级不报错,报告中注明"基于文档时间线(无 git 历史)"
+51
View File
@@ -0,0 +1,51 @@
---
description: 增量更新项目状态文档(日常高频)
---
# /update-status —— 增量更新项目状态
日常高频命令:把"刚做完的事"增量写入 docs/ 三文档。与 /sync 的区别:/sync 全量扫描生成报告落盘,本命令**增量、快、只更新三文档**。
## 执行步骤
### 1. 确保文档存在
三文档(docs/项目状态.md、需求文档.md、待办清单.md)任一缺失 → 读 `.claude/templates/` 对应模板先创建(空骨架),再继续。
### 2. 识别本次变化(三来源合并)
- 用户口述($ARGUMENTS 或对话上下文:刚完成了什么/发现了什么问题)
- git(带降级):
```bash
git rev-parse --is-inside-work-tree 2>/dev/null && git log --oneline -20 && git status --short
```
不可用 → 跳过,不报错
- 代码扫描佐证:`git status --short` 列出的改动文件 / 或用户提到的模块目录 mtime
### 3. 更新三文档(只追加不覆盖用户手写内容)
**项目状态.md**:
- "最近完成"区追加条目(日期 + 模块 + 一句话)
- 模块进度表刷新完整度(如新域六件套齐了 → 100%)
- 有新风险/问题 → 追加到问题风险区
**待办清单.md**:
- 完成了的待办移到"最近完成"区(保留 TASK 编号)
- 代码扫描的 FIXME → 高优待办、TODO → 中优待办(去重后追加)
- $ARGUMENTS 里用户口述的新待办入对应优先级区
**需求文档.md**:仅当本次是"完成某个 REQ"时更新该 REQ 状态字段;不新增需求(新增走用户明确要求)。
### 4. 输出变更摘要
```markdown
# 状态更新完成
- 项目状态.md:+2 条完成记录,模块进度 supplier 0%→100%
- 待办清单.md:TASK-003 完成,+1 新待办(FIXME 来自 BizXxxServiceImpl)
- 需求文档.md:REQ-002 状态 → 已完成
(无变化的文档标注"无变化")
```
## 注意
- 更新前先 Read 文档现状,diff 式更新,绝不重写全文
- 时间用当前真实时间(YYYY-MM-DD HH:mm)
- git 降级时基于口述 + mtime,报告中注明来源
+50
View File
@@ -0,0 +1,50 @@
# Snowy 二次开发文档中心
> 阅读顺序:先读根目录 `CLAUDE.md`(项目宪法),再按需查本目录指南;按需知识(技能)在 `.claude/skills/`,由 hook 自动激活。
## 文档导航
| 文档 | 内容 | 什么时候看 |
|---|---|---|
| [框架说明](框架说明.md) | Maven 模块拓扑、插件机制、启动链路、B/C 双端模型、29 项平台功能清单 | 想了解"平台已经有什么、不用我开发什么" |
| [后端开发指南](后端开发指南.md) | 六件套逐文件详解(以 BizNotice 为线)、跨插件调用 | 写后端代码前 |
| [前端开发指南](前端开发指南.md) | snowy-admin-web:api js / index.vue / form.vue / 组件 / i18n | 写前端代码前 |
| [数据库设计规范](数据库设计规范.md) | 大写表名、审计字段、建表模板、33 表清单、菜单 SQL | 建表/写 SQL 前 |
| [工具类使用指南](工具类使用指南.md) | snowy-common 16 工具类 + Hutool 优先原则 | 找工具时 |
| [新功能开发流程规范](新功能开发流程规范.md) | /dev 双模式全流程、验证方式 | 开工一个新功能时 |
| [国密与安全指南](国密与安全指南.md) | SM2/SM3/SM4、双端鉴权、白名单、数据范围 | 涉及密码/敏感字段/权限时 |
## 按开发阶段查阅
- **刚接手**:CLAUDE.md → 框架说明 → `/start`
- **要开发**:新功能开发流程规范 → 后端/前端开发指南 → 数据库设计规范
- **遇到问题**:`.claude/skills/bug-detective` → 国密与安全指南(涉密时)
- **要提交**:`.claude/skills/code-patterns` 的 Git 规范 → `/check`
## 核心规范速查(背下来)
| 项 | 规范 |
|---|---|
| 包名 | `vip.xiaonuo.*`(业务在 biz 插件) |
| 注入 | `@Resource`(禁 @Autowired / 构造器注入) |
| ServiceImpl | `extends ServiceImpl<M, T> implements XxxService` |
| 转换 | Hutool `BeanUtil`(禁 MapstructUtils) |
| Lombok | `@Getter @Setter`(禁 @Data) |
| 返回/异常 | `CommonResult<T>` / `CommonException`(中文消息) |
| URL | `/{插件}/{域}/{动作}` 动词式,方法注解上,权限码=URL |
| 表/主键 | 全大写 `BIZ_XXX` / String 雪花 |
| 版权头 | 每个 .java 头部 12 行 Apache 2.0 声明,不可删 |
| 注释 | 全中文,Javadoc 带 @author @date |
## 禁止事项(Top 10)
1. 删版权头
2. RuoYi 风格代码(@Autowired/MapstructUtils/@Data/RESTful URL/冒号权限码/Long 主键/小写表名)
3. 业务代码写进 sys/auth/dev/gen 等 6 个平台插件
4. 跨插件直接 import 对方实体
5. 明文存储口令/敏感字段(必须国密)
6. 写操作不加事务/操作日志
7. 查询不带 checkSqlInjection
8. 前端写 TS / Element Plus
9. 手写 delete_flag 条件(逻辑删除自动)
10. 硬编码数据库口令
+123
View File
@@ -0,0 +1,123 @@
# 前端开发指南(snowy-admin-web)
> Vue 3.5 + Ant Design Vue 4.2.6 + Pinia + vue-i18n,**JavaScript 不是 TypeScript**。范本:`src/views/biz/notice/` + `src/api/biz/bizNoticeApi.js`。完整模板见 `.claude/skills/frontend-pc`。
## 工程约定
- 组件与 Vue API 自动导入(unplugin):`a-button`、`ref`、`computed` 等不写 import
- 自定义组件 kebab-case 标签(`<s-table>`、`<xn-form-container>`、`<dict-select>`)
- 缩进 **Tab**(Prettier 已配置)
- 命令:`npm install` → `npm run dev`(81 端口,/api 代理到 82)
## 目录与落位
```
src/
├── api/biz/biz{Xxx}Api.js ← 新业务 API 封装
├── views/biz/{域名}/
│ ├── index.vue ← 列表页
│ ├── form.vue ← 弹窗表单
│ └── detail.vue ← 详情(可选)
├── components/ 37 个组件(多数带 README)
├── store/ Pinia stores
├── utils/ request.js / tool.js / formRules.js / smCrypto.js
└── locales/ i18n 词条
```
## API 封装(bizNoticeApi.js 范本)
```js
import { baseRequest } from '@/utils/request'
const request = (url, ...arg) => baseRequest(`/biz/notice/` + url, ...arg)
export default {
noticePage(data) {
return request('page', data, 'get')
},
noticeSubmitForm(data, edit = false) {
return request(edit ? 'edit' : 'add', data)
},
noticeDelete(data) {
return request('delete', data)
},
noticeDetail(data) {
return request('detail', data, 'get')
}
}
```
第三参是 method(默认 POST)。下载:`baseRequest(url, data, 'get', { responseType: 'blob' })`。
## 列表页(index.vue)四区域
```
① 搜索区(a-form inline + 查询条件控件)
② 工具栏(#operator 插槽:新增/批量删除按钮,hasPerm 控制)
③ 表格(s-table :data="loadData",#bodyCell 自定义列)
④ 弹窗(<form ref="formRef" />,formRef.onOpen(record) 打开)
```
核心机制:
```js
const loadData = (parameter) => {
const searchFormParam = cloneDeep(searchFormState.value)
if (searchFormParam.createTime) { // 时间范围拆分
searchFormParam.startCreateTime = searchFormParam.createTime[0]
searchFormParam.endCreateTime = searchFormParam.createTime[1]
delete searchFormParam.createTime
}
return bizNoticeApi.noticePage(Object.assign(parameter, searchFormParam)).then((data) => data)
}
// 刷新:tableRef.value.refresh(true);批量删后:clearRefreshSelected()
```
## 弹窗表单(form.vue)要点
- 容器 `xn-form-container`(title 区分新增/编辑:`formData.id ? '编辑' : '增加'`)
- `onOpen(record)`:record 有值编辑(cloneDeep 拷贝)、无值新增(设默认值)
- 校验:`formRules = { name: [required('请输入名称')] }`(formRules.js 的工厂)
- 提交:`formRef.value.validate()` → api → `onClose()` + `emit('successful')`
- **必须 `defineExpose({ onOpen })`**
- 多选字段提交前 JSON.stringify、回显时 JSON.parse(参考 notice 的 place)
## 按钮权限
```vue
v-if="hasPerm('bizNoticeAdd')"
v-if="hasPerm(['bizGroupEdit', 'bizGroupGrantUser'], 'and')"
```
驼峰码来自 SYS_RESOURCE 的 BUTTON 行(与后端 @SaCheckPermission 的 URL 式是两套体系)。
## 高频组件
| 组件 | 场景 |
|---|---|
| `s-table` | 一切列表(分页自动) |
| `xn-form-container` | 一切弹窗表单 |
| `dict-select` | 字典下拉(dict-type-code) |
| `xn-upload` | 上传(头像用 crop-upload) |
| `xn-editor` / `xn-md-editor` | 富文本/Markdown |
| `xn-user-selector` / `xn-org-selector` / `xn-role-selector` / `xn-position-selector` | 选择器 |
| `xn-resizable-panel` | 左树右表拖拽布局 |
| `cron` | Cron 表达式 |
用前看 `src/components/{组件名}/README.md`(多数有)。
## 字典与工具
```js
import tool from '@/utils/tool'
tool.dictList('BIZ_NOTICE_TYPE') // 取字典选项
```
## i18n
公共界面文案 `$t('key')` + `src/locales/{zh-cn,en-us}/` 补词条;纯业务管理页的动态数据(字典/菜单名)不走 i18n。
## 与后端联调注意
1. 登录密码是 SM2 密文上送(smCrypto.js)——抓包看到 160 位十六进制是正常的
2. id 是字符串——不要 parseInt
3. 接口 401 → token 头没带或过期;403 → 后端资源没授权
4. 新菜单要 SYS_RESOURCE 有记录 + 角色授权后才显示
+186
View File
@@ -0,0 +1,186 @@
# 后端开发指南
> 以 `snowy-plugin-biz/modular/notice/`(通知公告)为讲解主线——它是官方最干净的六件套范本。完整代码模板见 `.claude/skills/crud-development`。
## 分层架构
```
Controller(参数接收/鉴权/日志/返回包装)
↓ @Resource 注入
Service 接口 + ServiceImpl(业务逻辑/事务/查询构造)
↓ 继承 MP 能力
Mapper(数据访问,通常空接口 extends BaseMapper)
↓
MySQL(大写表名,逻辑删除,审计字段自动填充)
```
没有 DAO/Manager 层;不要自加。
## 一个业务域的文件全景(BizNotice 实例)
```
snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/
├── controller/BizNoticeController.java @Tag+@RestController+@Validated,URL 写方法上
├── entity/BizNotice.java @TableName("BIZ_NOTICE") extends CommonEntity
├── enums/BizNoticeStatusEnum.java ENABLE/DISABLE + validate
├── mapper/BizNoticeMapper.java extends BaseMapper<BizNotice>(空接口)
├── param/BizNoticePageParam.java current/size/sortField/sortOrder/searchKey+业务条件
│ ├── BizNoticeAddParam.java @NotBlank 校验
│ ├── BizNoticeEditParam.java Add + id
│ └── BizNoticeIdParam.java 只有 id
├── service/BizNoticeService.java extends IService<BizNotice>,中文 Javadoc
└── service/impl/BizNoticeServiceImpl.java extends ServiceImpl 实现,查询/事务/异常规范
```
## 逐件讲解
### Entity
```java
@Getter
@Setter
@TableName("BIZ_NOTICE")
public class BizNotice extends CommonEntity {
/** 主键 */
@TableId
@Schema(description = "主键")
private String id;
/** 标题 */
@Schema(description = "标题")
private String title;
// ... 业务字段
/** 扩展信息 */
@Schema(description = "扩展信息")
private String extJson;
}
```
- 继承 CommonEntity = 自动获得:`deleteFlag`(@TableLogic 逻辑删除)、`createUser/createTime/updateUser/updateTime`(自动填充)、`createUserName/updateUserName`(@Trans 翻译)
- 敏感字段:`@TableField(typeHandler = CommonSm4CbcTypeHandler.class)` + `@TableName(value=..., autoResultMap = true)`
- 字典/关联翻译:`@Trans(type = TransType.DICTIONARY, key = "XXX")` 或 `@Trans(type = TransType.SIMPLE, target = Yyy.class, fields = "name", alias = "yyy", ref = "yyyName")` + `@TableField(exist = false) private String yyyName;`
### ServiceImpl(核心)
```java
@Service
public class BizNoticeServiceImpl extends ServiceImpl<BizNoticeMapper, BizNotice> implements BizNoticeService {
@Override
public Page<BizNotice> page(BizNoticePageParam param) {
QueryWrapper<BizNotice> queryWrapper = new QueryWrapper<BizNotice>().checkSqlInjection();
if(ObjectUtil.isNotEmpty(param.getTitle())) {
queryWrapper.lambda().like(BizNotice::getTitle, param.getTitle());
}
// ... 每条件判空
if(ObjectUtil.isAllNotEmpty(param.getSortField(), param.getSortOrder())) {
CommonSortOrderEnum.validate(param.getSortOrder());
queryWrapper.orderBy(true, param.getSortOrder().equals(CommonSortOrderEnum.ASC.getValue()),
StrUtil.toUnderlineCase(param.getSortField()));
} else {
queryWrapper.lambda().orderByAsc(BizNotice::getSortCode);
}
return this.page(CommonPageRequest.defaultPage(), queryWrapper);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void add(BizNoticeAddParam param) {
BizNotice bizNotice = BeanUtil.toBean(param, BizNotice.class);
this.save(bizNotice);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void edit(BizNoticeEditParam param) {
BizNotice bizNotice = this.queryEntity(param.getId());
BeanUtil.copyProperties(param, bizNotice);
this.updateById(bizNotice);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(List<BizNoticeIdParam> paramList) {
this.removeByIds(CollStreamUtil.toList(paramList, BizNoticeIdParam::getId));
}
@Override
public BizNotice detail(BizNoticeIdParam param) {
return this.queryEntity(param.getId());
}
@Override
public BizNotice queryEntity(String id) {
BizNotice bizNotice = this.getById(id);
if(ObjectUtil.isEmpty(bizNotice)) {
throw new CommonException("通知公告不存在,id值为:{}", id);
}
return bizNotice;
}
}
```
### Controller
```java
@Tag(name = "通知公告控制器")
@RestController
@Validated
public class BizNoticeController {
@Resource
private BizNoticeService bizNoticeService;
@Operation(summary = "获取通知公告分页")
@SaCheckPermission("/biz/notice/page")
@GetMapping("/biz/notice/page")
public CommonResult<Page<BizNotice>> page(BizNoticePageParam param) {
return CommonResult.data(bizNoticeService.page(param));
}
@Operation(summary = "添加通知公告")
@CommonLog("添加通知公告")
@SaCheckPermission("/biz/notice/add")
@PostMapping("/biz/notice/add")
public CommonResult<String> add(@RequestBody @Valid BizNoticeAddParam param) {
bizNoticeService.add(param);
return CommonResult.ok();
}
// edit/delete(List<IdParam> + @NotEmpty)/detail 同构;业务动作如 disableStatus/enableStatus
}
```
## 跨插件调用
需要 sys/dev 的能力时(用户信息、字典、文件、消息):
```java
// 只依赖 *-api 模块,注入接口
@Resource
private SysUserApi sysUserApi;
JSONObject user = sysUserApi.getUserByIdWithoutException(userId);
```
自己要暴露给其他插件:接口写 `snowy-plugin-api/snowy-plugin-biz-api`,实现在本插件 `provider/XxxApiProvider.java`。详见 `.claude/skills/plugin-architecture`。
## 常用基础设施速查
| 需求 | 用什么 |
|---|---|
| 返回 | CommonResult.data(t) / CommonResult.ok() |
| 业务异常 | throw new CommonException("中文{}", arg) |
| 分页 | CommonPageRequest.defaultPage() |
| 排序校验 | CommonSortOrderEnum.validate(order) |
| 对象转换 | BeanUtil.toBean / copyProperties |
| 集合取字段 | CollStreamUtil.toList(list, X::getId) |
| 当前用户 | StpLoginUserUtil.getLoginUser()(auth-api) |
| 缓存 | CommonCacheOperator |
| 定时任务 | 实现 CommonTimerTaskRunner + 界面配置 |
| 站内信 | DevMessageApi |
| 配置读取 | DevConfigApi.getConfigValueByKey |
## 与 RuoYi 的关键差异(防惯性)
见 `.claude/skills/code-patterns` 的 17 条反向清单。最常错的 5 条:ServiceImpl 必须继承、@Resource 注入、BeanUtil 转换、动词式 URL、String 主键。
+71
View File
@@ -0,0 +1,71 @@
# 国密与安全指南
> Snowy 的定位就是"国密前后端分离平台",软件层面满足等保测评。详细 API 见 `.claude/skills/crypto-sm` 与 `.claude/skills/security-auth`。
## 国密三层架构
| 层 | 算法 | 保护什么 | 实现 |
|---|---|---|---|
| 传输加密 | SM2(非对称) | 登录口令网络传输 | 前端 smCrypto.js 公钥加密 → 后端 CommonCryptogramUtil.sm2Decrypt |
| 口令摘要 | SM3(哈希) | 口令存储 | SysPasswordUtil / sm3Digest,库中只有摘要 |
| 字段加密 | SM4-CBC(对称) | 手机号/证件号等落库 | CommonSm4CbcTypeHandler(MyBatis TypeHandler) |
密钥配置:`application.properties` 的 `snowy.cryptogram.*` 段。**密钥严禁硬编码、严禁入 git 明文变更**。
## 敏感字段加密(新字段标准做法)
```java
@TableName(value = "BIZ_PATIENT", autoResultMap = true) // ①
public class BizPatient extends CommonEntity {
@TableField(typeHandler = CommonSm4CbcTypeHandler.class) // ②
@Schema(description = "手机号")
private String phone;
}
```
查询限制(重要):
- 密文 **like 永远查不到**——精确查先 sm4Encrypt 再 eq;模糊查用 SM3 摘要辅助列(如 PHONE_HASH)
- 参考实战:BizUser 的 phone/idCardNumber/emergencyPhone
## 口令处理规范
- 传输:SM2(前端加密,后端解密)——新业务口令类字段照抄登录链路
- 存储:SM3 摘要——**任何口令不得明文/可逆存储**
- 日志:不打口令与敏感字段明文
## 鉴权安全
1. **白名单最小化**:免登录接口必须逐条登记 GlobalConfigure.NO_LOGIN_PATH_ARR(改后重启);禁止用通配符放开大范围
2. **越权防护**:编辑/删除接口校验数据归属(当前用户机构/本人);关键操作加 @CommonNoRepeat
3. **SQL 注入**:QueryWrapper 一律 checkSqlInjection() + lambda 列名;动态排序字段过 CommonSortOrderEnum.validate + toUnderlineCase
4. **上传安全**:走 dev/file 标准接口(图片接口校验格式);下载走 authDownload(鉴权)
5. **会话**:Sa-Token 30 天;敏感系统建议配置缩短 + 单端登录(配置已有开关)
## 权限体系要点(防越权设计)
- 接口权限:@SaCheckPermission 值 = URL,权限来自角色资源授权的 apiUrl 集合——**新接口不授权 = 默认拒绝**(默认安全)
- 按钮权限:驼峰码只控制显示,**后端必须有自己的校验**(前端隐藏不是安全边界)
- 数据范围:敏感数据列表按 orgId 数据范围过滤(角色管理→数据授权)
## 安全检查清单(上线前)
- [ ] 无明文口令/敏感字段存储
- [ ] 新公开接口白名单逐条登记
- [ ] 敏感接口有权限注解 + 资源授权
- [ ] 查询全走 checkSqlInjection
- [ ] 上传走标准接口,下载鉴权
- [ ] 日志无敏感明文
- [ ] 密钥未硬编码、未提交变更
- [ ] Knife4j 生产环境考虑关闭(配置开关)
## 参考实现
| 文件 | 内容 |
|---|---|
| snowy-common/.../util/CommonCryptogramUtil.java | 国密 API |
| snowy-common/.../handler/CommonSm4CbcTypeHandler.java | 字段加密 |
| snowy-plugin-biz/.../user/entity/BizUser.java | SM4 字段实战 |
| snowy-plugin-auth/.../login/service/impl/AuthServiceImpl.java | SM2/SM3 登录链路 |
| snowy-admin-web/src/utils/smCrypto.js | 前端国密 |
| snowy-web-app/.../core/config/GlobalConfigure.java | 白名单 |
+61
View File
@@ -0,0 +1,61 @@
# 工具类使用指南
> 决策树:**Common* 工具 → Hutool → Spring/官方 API → 才自写**。完整索引见 `.claude/skills/common-toolkit`。
## snowy-common 工具类(16 个)
| 工具类 | 一句话用途 |
|---|---|
| CommonCryptogramUtil | 国密:sm2Encrypt/sm2Decrypt/sm3Digest/sm4Encrypt/sm4Decrypt |
| CommonSm4CbcTypeHandler | 字段级 SM4 加密(Entity @TableField 用) |
| CommonEmailUtil | 邮件:文本/HTML/附件 |
| CommonDownloadUtil | 文件下载写流 |
| CommonAvatarUtil | 随机默认头像 |
| CommonOtpUtil | OTP 动态口令 |
| CommonSqlUtil | SQL 安全(排序字段处理) |
| CommonResponseUtil | 向 response 写 JSON(过滤器场景) |
| CommonServletUtil | Request/Response 获取 |
| CommonKeyUtil | 标准键生成 |
| CommonIpAddressUtil | IP 归属地(ip2region) |
| CommonUaUtil | 浏览器/系统解析 |
| CommonTraceIdUtil | 链路 traceId |
| CommonTimeFormatUtil | 时间格式化 |
| CommonNetWorkInfoUtil | 网络信息 |
| CommonJoinPointUtil | AOP 参数获取 |
## 非 util 的常用基础类
| 类 | 用途 |
|---|---|
| CommonResult | 统一返回(data/ok/error) |
| CommonException | 业务异常({} 占位中文消息) |
| CommonEntity | 实体基类(审计+逻辑删除) |
| CommonPageRequest | 分页请求(defaultPage) |
| CommonCacheOperator | Redis 缓存统一操作 |
| CommonDataChangeEventCenter | 数据变更事件广播 |
| CommonTimerTaskRunner | 定时任务接口 |
## Hutool 高频(5.8.25,全局依赖)
| 类 | 场景 |
|---|---|
| StrUtil / ObjectUtil / RandomUtil / IdUtil | 判空、随机、ID |
| BeanUtil | **对象转换唯一选择**(toBean/copyProperties) |
| CollStreamUtil / CollectionUtil | 集合流/工具 |
| JSONUtil / JSONObject | JSON、跨插件传值 |
| Convert | 类型转换 |
| DateUtil | 日期 |
| IoUtil / FileUtil | 文件流 |
## 禁止事项
1. ❌ 引入 commons-lang3 / guava 等(hutool 已覆盖)
2. ❌ 手写 MD5/SHA/AES(国密标准走 CommonCryptogramUtil)
3. ❌ 重复造轮子(自写前必须查本清单)
4. ❌ Controller 手写下载流(CommonDownloadUtil)
5. ❌ 业务代码拼 Redis key(CacheConstant + CommonCacheOperator)
## 自写工具的落位
- 仅本插件用 → `snowy-plugin-biz/.../core/util/`
- 全项目通用 → 提议下沉 `snowy-common/.../util/`(命名 Common{Xxx}Util)
+85
View File
@@ -0,0 +1,85 @@
# 数据库设计规范
> 详细规范与菜单 SQL 模板见 `.claude/skills/database-ops`。本文是速览。
## 硬性规则
1. 表名/字段名**全大写下划线**;新业务表前缀 **`BIZ_`**(出厂 biz 域复用 SYS_/DEV_ 表是历史设计,勿模仿)
2. 主键 `ID varchar(20)` 字符串雪花(MyBatis-Plus ASSIGN_ID),**禁** bigint 自增
3. 每表尾部固定审计字段组(与 CommonEntity 对应):
```sql
`SORT_CODE` int NULL COMMENT '排序',
`REMARK` varchar(500) NULL COMMENT '备注',
`EXT_JSON` longtext NULL COMMENT '扩展信息',
`DELETE_FLAG` varchar(255) NULL COMMENT '删除标志',
`CREATE_TIME` datetime NULL COMMENT '创建时间',
`CREATE_USER` varchar(20) NULL COMMENT '创建用户',
`UPDATE_TIME` datetime NULL COMMENT '更新时间',
`UPDATE_USER` varchar(20) NULL COMMENT '更新用户',
```
4. 表与字段都要**中文 COMMENT**;utf8mb4 / InnoDB
5. 树形表加 `PARENT_ID varchar(20)`;关联字段 `varchar(20)` 存对方 ID
6. 唯一性:不要用数据库唯一索引(逻辑删除冲突),Service 层查重
## 标准建表模板
```sql
CREATE TABLE `BIZ_SUPPLIER` (
`ID` varchar(20) NOT NULL COMMENT '主键',
`NAME` varchar(100) NULL COMMENT '名称',
`TYPE` varchar(50) NULL COMMENT '类型(字典 SUPPLIER_TYPE)',
`STATUS` varchar(10) NULL COMMENT '状态(ENABLE/DISABLE)',
-- 业务字段……
`SORT_CODE` int NULL COMMENT '排序',
`REMARK` varchar(500) NULL COMMENT '备注',
`EXT_JSON` longtext NULL COMMENT '扩展信息',
`DELETE_FLAG` varchar(255) NULL COMMENT '删除标志',
`CREATE_TIME` datetime NULL COMMENT '创建时间',
`CREATE_USER` varchar(20) NULL COMMENT '创建用户',
`UPDATE_TIME` datetime NULL COMMENT '更新时间',
`UPDATE_USER` varchar(20) NULL COMMENT '更新用户',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商' ROW_FORMAT = Dynamic;
```
## 类型映射
| Java | MySQL |
|---|---|
| String(短) | varchar(n) |
| String(长文本) | text / longtext(EXT_JSON 用 longtext) |
| Integer | int |
| BigDecimal | decimal(18,2) |
| 时间 | datetime |
| 加密字段 | varchar 放宽(SM4 密文变长) |
## 菜单/按钮 SQL(SYS_RESOURCE)
权威模板:`snowy-plugin/snowy-plugin-gen/src/main/resources/sqlend/Mysql.sql.btl`。按钮码驼峰式(前端 hasPerm 用):
```sql
INSERT INTO `SYS_RESOURCE` VALUES ('{menuId}', '{父id}', '供应商管理', 'supplier', '{code}', 'MENU', '{moduleId}', 'MENU', '/biz/supplier', 'biz/supplier/index', '{icon}', NULL, 'YES', 'YES', 'YES', 99, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `SYS_RESOURCE` VALUES ('{btnId}', '{menuId}', '新增供应商', NULL, 'bizSupplierAdd', 'BUTTON', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
```
按钮码集合:`biz{Xxx}Add/Edit/Delete/Detail/BatchDelete/Import/Export`。
## 现有 33 表(前缀分组)
- **SYS_**(15+):SYS_USER、SYS_USER_EXT、SYS_ORG、SYS_POSITION、SYS_ROLE、SYS_RESOURCE、SYS_MODULE、SYS_USER_ROLE、SYS_ROLE_MENU、SYS_ROLE_BUTTON、SYS_USER_DATA_SCOPE(_MAP)、SYS_GROUP、SYS_GROUP_USER 等
- **BIZ_**:BIZ_NOTICE
- **DEV_**(12):DEV_CONFIG、DEV_DICT、DEV_EMAIL、DEV_FILE、DEV_JOB、DEV_LOG、DEV_MESSAGE、DEV_PUSH、DEV_SMS、DEV_SLIDESHOW、DEV_WEAK_PASSWORD 等
- **GEN_**:GEN_BASIC、GEN_CONFIG
- **CLIENT_**:CLIENT_USER(+关系表)
- **MOBILE_**:MOBILE_RESOURCE 等
- **AUTH_**:三方登录相关
准确清单:`grep "CREATE TABLE" snowy-web-app/src/main/resources/_sql/snowy_mysql.sql`
## SQL 脚本管理
- 全量脚本:`snowy-web-app/src/main/resources/_sql/snowy_mysql.sql`
- 增量 DDL:也放 `_sql/` 下,命名 `{日期}_{功能}.sql`(如 `2026-08-18_supplier.sql`)
- AI 执行 SQL 降级链:mysql CLI → 落盘 `docs/sql-pending/` 提示手动执行
@@ -0,0 +1,75 @@
# 新功能开发流程规范
> 从一个想法到一个可用功能的完整路径。与 `/dev` 命令同构(命令是本流程的自动化版)。
## 六步流程
```
① 需求澄清 → ② 设计表 → ③ 菜单SQL → ④ 后端六件套 → ⑤ 前端三件 → ⑥ 验证与授权
```
### ① 需求澄清(10 分钟)
回答四个问题:
1. 管什么对象?(→ 表设计)
2. 有哪些字段?哪些是查询条件?(→ Entity + PageParam)
3. 页面什么形态?(普通表格 / 树 / 左树右表 / 主子表)
4. 挂在哪个菜单下?谁能用?(→ SYS_RESOURCE + 角色授权)
### ② 设计表
按数据库设计规范:`BIZ_` 前缀全大写、String 雪花主键、审计字段组、中文 COMMENT。产出 DDL 并执行。
### ③ 菜单与按钮 SQL
MENU 行(挂到某目录下)+ BUTTON 行(驼峰按钮码)。**这一步常被遗忘**——忘了它,功能再对页面也不显示、按钮全隐藏。
### ④ 后端六件套
两种方式二选一:
**方式 A:AI 直写**(`/dev` 默认)——按 crud-development 技能模板逐件生成,先 Read notice 范本再写。
**方式 B:平台代码生成器**——开发工具→代码生成→导入表→填 GenBasic(pluginName=biz、busName、className、菜单归属、模板形态)→生成→AI 核对修补(版权头/落位/URL/权限码)。详见 code-generator 技能。
### ⑤ 前端三件
`api/biz/biz{Xxx}Api.js` + `views/biz/{域名}/index.vue` + `form.vue`(可选 detail.vue)。按 frontend-pc 技能模板。
### ⑥ 验证与授权(本项目无自动化测试基建,验证 = 启动手测)
```
1. 重启后端(新代码 + 若改过白名单必须重启)
2. 前端刷新(npm run dev 热更通常够,路由变了强刷)
3. 系统管理 → 角色管理 → 给测试角色勾选新菜单和按钮
4. 用该角色账号登录(或让超管重新授权后重新登录——权限码缓存在 TokenSession)
5. 页面走查:新增 → 列表 → 查询 → 编辑 → 删除 → 详情
6. 接口层验证:http://localhost:82/doc.html 带调(边界值/必填校验/权限)
7. /check 规范检查
```
## 无测试基建下的质量保障
项目仅有两个空壳测试类,**不要试图补测试基建**(引入测试体系属框架改动)。质量保障靠:
1. 六件套模板一致性(生成器模板 = 官方规范)
2. `/check` + code-reviewer 静态审查
3. Knife4j 手测清单(上节第 6 步)
4. 提交前 git-workflow 四原则
## 完成后的收尾
- [ ] DDL 增量脚本存档 `_sql/`
- [ ] `/update-status` 记录进度
- [ ] commit(`feat: xxx`)
- [ ] 有后续想法 → `/add-todo`
## 常见流程错误
| 错误 | 后果 |
|---|---|
| 忘执行菜单 SQL | 页面不出现 |
| 忘角色授权 | 403 / 按钮全隐 |
| 改白名单没重启 | 白名单不生效 |
| 授权后没重新登录 | 权限码是旧的 |
| 表没审计字段 | 自动填充报错/逻辑删除失效 |
| 直接改平台插件实现功能 | 升级即冲突,业务代码进 biz |
+123
View File
@@ -0,0 +1,123 @@
# 框架说明 —— Snowy 平台已有什么
> 基于 Snowy v3.0.0 核实。**这些是平台底座,不参与业务进度统计,正常二开不需要动它们。**
## 技术栈
| 层 | 技术 | 版本 |
|---|---|---|
| 语言/运行时 | Java | 17 |
| 核心 | Spring Boot | 3.5.9(Spring Framework 6.2.15) |
| ORM | MyBatis-Plus | 3.5.5(dynamic-datasource 4.3.1 + Druid 1.2.21) |
| 数据库 | MySQL | 8.0/5.7(默认库 snowy;PG/Oracle/SQLServer/达梦/金仓 配置已预留) |
| 缓存 | Redis(Redisson) | 3.45.0 |
| 权限 | Sa-Token | 1.44.0(含 sso/oauth2/jwt/redisson 插件) |
| 工具 | Hutool | 5.8.25 |
| 文档 | Knife4j (OpenAPI3) | 4.5.0(/doc.html) |
| 字段翻译 | Easy-Trans | 3.1.4 |
| 代码生成 | Beetl | 1.2.40 |
| Excel | EasyExcel / EasyPoi | 3.3.3 / 4.4.0 |
| 国密 | sm-crypto + BouncyCastle | 0.3.2 / 1.70 |
| 文件 | x-file-storage + minio/oss/cos | 2.1.0 等 |
| 消息 | sms4j / 钉钉 / 企微 / 飞书 | — |
| 三方登录 | JustAuth / CAS / OpenSAML | — |
前端:Vue 3.5.13 + Vite 6.0.7 + Ant Design Vue 4.2.6 + Pinia 2.2.2 + vue-i18n 10 + TailwindCSS 3.4(**JavaScript,非 TS**)。
## Maven 模块拓扑
```
snowy(根 pom)
├── snowy-common 公共基础:统一返回/异常/实体基类/缓存/国密/工具/注解
├── snowy-plugin/ 插件实现(7 个)
│ ├── snowy-plugin-sys B端系统功能
│ ├── snowy-plugin-auth 登录鉴权
│ ├── snowy-plugin-dev 开发工具
│ ├── snowy-plugin-gen 代码生成器
│ ├── snowy-plugin-client C端功能
│ ├── snowy-plugin-mobile 移动端管理
│ └── snowy-plugin-biz ★ 业务插件(二开主战场)
├── snowy-plugin-api/ 跨插件接口层(7 个 *-api 模块)
└── snowy-web-app 唯一启动模块(聚合所有插件 + 全局配置 + SQL)
```
依赖规则:插件不互依赖;跨插件通信走 `*-api` 接口 + 对方 `provider/` 实现(返回 hutool JSONObject)。详见 `.claude/skills/plugin-architecture`。
## 模块间真实调用地图(提取自各插件 pom)
A→B = A 依赖 B 的 `*-api`(运行时由 B 的 provider 实现注入):
| 调用方 | 依赖的 api | 典型用途 |
|---|---|---|
| biz | sys-api、auth-api、dev-api | 业务要用户/登录态(SaBaseLoginUser)/字典配置文件消息 |
| auth | sys-api、dev-api、client-api | 登录验用户取权限、发验证码(短信/邮件)、C 端用户 |
| sys | auth-api、dev-api、mobile-api | 登录用户 POJO/在线 token 操作、字典翻译、移动端按钮码 |
| dev | sys-api、auth-api | 日志关联用户、监听器用户类型 |
| mobile | auth-api、dev-api、sys-api | 移动资源管理引用用户/字典 |
| client | auth-api、dev-api | C 端登录态与工具能力 |
| gen | sys-api、mobile-api | 生成时读模块/菜单挂载点、写移动端资源 |
**关键理解**:sys↔dev、auth↔sys 出现"互相调用",但**实现层零循环**——互相依赖的只是对方的 api 接口模块,实现插件之间无任何 pom 依赖。编译期只见接口,运行期 Spring 把 provider 的 @Service 实现注入调用方。
四条典型调用链:
1. **登录链**:`/auth/b/doLogin` → AuthServiceImpl(SM2 解密 + SM3 比对)→ `loginUserApi.getUserByAccount()`(sys-api,落到 SysUserApiProvider)→ 组装 permissionCodeList(数据范围 apiUrl)+ buttonCodeList → StpUtil.login → token 下发
2. **业务发通知**:biz 注入 DevMessageApi(dev-api)→ dev 落 DEV_MESSAGE → WebSocket 实时推前端消息中心
3. **共享数据联动(反向通知,零依赖)**:sys 改组织 → `CommonDataChangeEventCenter.doUpdateWithData` 广播(snowy-common)→ biz 的 BizDataChangeListener 清缓存
4. **代码生成**:gen 读 sys-api 模块/菜单 → Beetl 渲染六件套 + 前端 + sqlend → 产出 SYS_RESOURCE/MOBILE_RESOURCE INSERT
接口资产:sys-api 11 个、dev-api 13 个、auth-api(Auth + SaBaseLoginUser POJO)、biz-api/mobile-api/client-api 少量(ClientUserApi 在 `vip.xiaonuo.client` 包根)、gen-api 空壳。
## 启动链路
```
vip.xiaonuo.Application(snowy-web-app,端口 82)
← @MapperScan("vip.xiaonuo.**.mapper")
← GlobalConfigure:SaServletFilter 路由鉴权(白名单三段)/CORS/分页插件/审计字段自动填充/防重AOP/返回包装AOP
← GlobalExceptionHandler:全局异常 → CommonResult
← application.properties:数据源(master)/Redis(6379 db1)/Sa-Token(30天)/Knife4j(7分组)/国密密钥
```
前端:`snowy-admin-web`,`npm run dev` 起 81 端口,`/api` 代理到 82。
## 平台功能清单(29 项,都不用你开发)
| 模块 | 功能 |
|---|---|
| sys 插件 | 用户管理、机构管理、职位管理、角色管理、资源菜单管理(模块/目录/菜单/按钮四级)、模块管理、分组管理、关系管理、数据范围授权、用户转移 |
| auth 插件 | B端登录(账密/手机/邮箱/OTP)、C端登录、三方登录(JustAuth 全家)、SSO/OAuth2/OIDC/CAS/SAML、会话管理 |
| dev 插件 | 系统配置、系统字典、业务字典、邮件、文件管理(多后端)、定时任务、操作日志、站内信(WebSocket)、消息推送(钉钉/企微/飞书)、服务器监控、弱口令检测、轮播图、短信 |
| gen 插件 | 代码生成器(表格/树/左树右表/主子表 + 前端 + 移动端 + 菜单SQL) |
| client 插件 | C端用户管理、C端关系 |
| mobile 插件 | 移动端模块管理、移动端资源(菜单/按钮) |
| biz 插件(出厂演示) | 通知公告、业务用户/组织/职位/分组、业务字典、首页统计 —— **7 个演示域,不计业务进度** |
## B/C 双端模型
| | B 端 | C 端 |
|---|---|---|
| 用户 | SYS_USER | CLIENT_USER |
| 登录 | /auth/b/* | /auth/c/* |
| 业务接口 | /sys/* /biz/* /dev/* /gen/* | /client/c/* |
| 工具 | StpUtil / StpLoginUserUtil | StpClientUtil / StpClientLoginUserUtil |
鉴权细节见 `.claude/skills/security-auth`。
## 权限体系(两层)
1. **接口权限**:`@SaCheckPermission("/url")` — 用户角色授权资源后生成的 apiUrl 列表
2. **按钮权限**:SYS_RESOURCE BUTTON 行的驼峰码 — 前端 `hasPerm('bizXxxAdd')`
## 前端结构(snowy-admin-web)
```
src/
├── api/{插件}/{xxx}Api.js 按插件分目录的 API 封装
├── views/{插件}/{域}/ 页面(index.vue + form.vue + detail.vue)
├── components/ 37 个组件(s-table/Xn*/DictSelect/Cron...)
├── store/ Pinia(user/menu/dict/token/global...)
├── router/ 静态路由 + 动态菜单路由(SYS_RESOURCE 驱动)
├── utils/ request.js / clientRequest.js / tool.js / smCrypto.js ...
├── locales/ i18n 中英文
└── layout/ 布局(含顶栏消息中心 message.vue)
```
+138
View File
@@ -0,0 +1,138 @@
{
"framework": {
"description": "框架模块配置 - 这些模块是 Snowy 平台的一部分,不参与业务进度统计",
"backend": {
"modules": [
"snowy-common",
"snowy-web-app"
],
"systemModules": [
"snowy-plugin/snowy-plugin-sys",
"snowy-plugin/snowy-plugin-auth",
"snowy-plugin/snowy-plugin-dev",
"snowy-plugin/snowy-plugin-gen",
"snowy-plugin/snowy-plugin-client",
"snowy-plugin/snowy-plugin-mobile"
],
"apiModules": [
"snowy-plugin-api/snowy-plugin-auth-api",
"snowy-plugin-api/snowy-plugin-biz-api",
"snowy-plugin-api/snowy-plugin-client-api",
"snowy-plugin-api/snowy-plugin-dev-api",
"snowy-plugin-api/snowy-plugin-gen-api",
"snowy-plugin-api/snowy-plugin-mobile-api",
"snowy-plugin-api/snowy-plugin-sys-api"
],
"description": "snowy-common 为公共基础模块,snowy-web-app 为唯一启动模块(端口82),6 个系统插件为平台自带能力,*-api 为跨插件调用接口层"
},
"frontend": {
"root": "snowy-admin-web",
"frameworkPaths": [
"src/views/sys",
"src/views/auth",
"src/views/dev",
"src/views/gen",
"src/views/mobile",
"src/views/index",
"src/views/login",
"src/views/front"
],
"businessPattern": "src/views/biz",
"apiBusinessPattern": "src/api/biz",
"description": "前端框架页面按插件分目录,业务页面只统计 src/views/biz 与 src/api/biz"
},
"systemFeatures": [
"用户管理",
"机构管理",
"职位管理",
"角色管理",
"资源菜单管理",
"模块管理",
"分组管理",
"关系管理",
"数据范围授权",
"用户转移",
"站内信",
"通知公告",
"业务字典",
"系统字典",
"系统配置",
"操作日志",
"会话监控",
"服务器监控",
"弱口令检测",
"定时任务",
"代码生成器",
"文件管理",
"短信管理",
"邮件管理",
"消息推送",
"轮播图",
"三方登录",
"SSO单点登录",
"C端用户管理",
"移动端菜单管理"
]
},
"business": {
"description": "业务模块配置 - 需要统计进度的业务代码",
"backend": {
"scanRoot": "snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular",
"modulePattern": "snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/*",
"excludeModules": [
"index",
"dict",
"group",
"notice",
"org",
"position",
"user"
],
"defaultLocation": "新业务域默认建在 snowy-plugin-biz 的 biz/modular/{域名} 下(参考 notice 域结构)",
"description": "出厂自带的 7 个 biz 域(index/dict/group/notice/org/position/user)视为演示代码不计业务进度;用户新建的域才计入"
}
},
"scanRules": {
"backend": {
"requiredFiles": [
"Entity (entity/Xxx.java,extends CommonEntity)",
"Mapper (mapper/XxxMapper.java,extends BaseMapper)",
"Service (service/XxxService.java,extends IService)",
"ServiceImpl (service/impl/XxxServiceImpl.java,extends ServiceImpl)",
"Controller (controller/XxxController.java)",
"Param 组 (param/:XxxPageParam + XxxAddParam + XxxEditParam + XxxIdParam 至少4类)"
],
"optionalFiles": [
"Mapper XML (mapper/mapping/XxxMapper.xml,仅自定义 SQL 需要)",
"Result (result/XxxResult.java,仅投影返回需要)"
],
"completenessFormula": "必备存在数 / 6 * 100%(XML 与 Result 为加分项不计分母)"
},
"frontend": {
"requiredFiles": [
"API (snowy-admin-web/src/api/biz/xxxApi.js)",
"列表页 (snowy-admin-web/src/views/biz/xxx/index.vue)",
"表单页 (snowy-admin-web/src/views/biz/xxx/form.vue)"
],
"completenessFormula": "存在文件数 / 3 * 100%"
},
"todoPatterns": [
"TODO:",
"FIXME:",
"XXX:",
"HACK:",
"NOTE:"
]
},
"environment": {
"gitAvailable": "auto-detect(git rev-parse --is-inside-work-tree)",
"dbConfigLocation": "snowy-web-app/src/main/resources/application.properties(spring.datasource.dynamic.master 段)",
"dbScript": "snowy-web-app/src/main/resources/_sql/snowy_mysql.sql(33 张表初始化脚本)",
"backendPort": 82,
"frontendPort": 81,
"apiDoc": "http://localhost:82/doc.html(Knife4j,basic 认证 admin/123456)"
},
"version": "1.0.0",
"snowyVersion": "3.0.0",
"lastUpdated": "2026-08-18"
}
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env node
/**
* PreToolUse Hook - 危险命令拦截与文件守护 (Snowy 版)
* 拦截: Bash 危险命令、Windows `> nul` 误创建文件
* 守护: Snowy 版权头缺失警告、框架模块修改警告、敏感配置写入提醒
* 原则: 任何解析失败都放行,永不阻断正常工作流
*/
const fs = require('fs');
let inputData = '';
try {
inputData = fs.readFileSync(0, 'utf8');
} catch {
process.exit(0);
}
let input;
try {
input = JSON.parse(inputData);
} catch {
process.exit(0);
}
const toolName = input.tool_name || '';
const toolInput = input.tool_input || {};
function allow() {
process.stdout.write(JSON.stringify({ continue: true }));
process.exit(0);
}
function block(reason) {
process.stdout.write(JSON.stringify({
decision: 'block',
reason: reason
}));
process.exit(0);
}
function warn(message) {
process.stdout.write(JSON.stringify({
continue: true,
systemMessage: message
}));
process.exit(0);
}
// ========== Bash 检查 ==========
if (toolName === 'Bash') {
const command = toolInput.command || '';
// Windows 下 `> nul` 会创建名为 nul 的文件,应使用 /dev/null
if (/>\s*nul(\s|$)/i.test(command)) {
block('检测到 `> nul`:Windows 下会创建名为 nul 的文件。请使用 `> /dev/null`(本环境 shell 为 Git Bash)');
}
// 危险命令模式(阻断)
const dangerPatterns = [
{ pattern: /rm\s+-rf\s+\/(\s|$)/, reason: '禁止递归删除根目录' },
{ pattern: /drop\s+(database|schema)/i, reason: '禁止删除数据库' },
{ pattern: /truncate\s+table/i, reason: '禁止清空表数据' },
{ pattern: /git\s+push\s+.*--force.*\s+(master|main)\b/, reason: '禁止 force push 主分支' },
{ pattern: /git\s+push\s+-f\s+(origin\s+)?(master|main)\b/, reason: '禁止 force push 主分支' },
{ pattern: /git\s+reset\s+--hard\s+HEAD~\d+/, reason: '禁止硬回退多个提交(会丢失提交历史)' },
{ pattern: /mkfs(\.|\s)/i, reason: '禁止格式化磁盘' },
{ pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;:/, reason: '禁止 fork 炸弹' },
{ pattern: /dd\s+.*of=\/dev\/(sd|hd|nvme)/i, reason: '禁止直接写磁盘设备' }
];
for (const item of dangerPatterns) {
if (item.pattern.test(command)) {
block(item.reason);
}
}
// 警告命令模式(放行但提醒)
const warnPatterns = [
{ pattern: /git\s+push\s+.*--force/, message: '⚠️ 正在 force push,请确认分支正确' },
{ pattern: /npm\s+publish/, message: '⚠️ 正在发布 npm 包,请确认' },
{ pattern: /git\s+clean\s+-fd/, message: '⚠️ git clean 会删除未跟踪文件(含可能的未提交新代码)' }
];
for (const item of warnPatterns) {
if (item.pattern.test(command)) {
warn(item.message);
}
}
allow();
}
// ========== Write 检查 ==========
if (toolName === 'Write') {
const filePath = (toolInput.file_path || '').replace(/\\/g, '/');
const content = toolInput.content || '';
// 敏感配置文件写入提醒(不阻止)
const sensitiveFiles = [
'application.properties',
'application-docker.yml',
'docker-compose.yml',
'logback-spring.xml'
];
if (sensitiveFiles.some(f => filePath.endsWith(f))) {
warn('⚠️ 正在写入配置文件 ' + filePath.split('/').pop() + ':注意不要泄露数据库口令/密钥,不要提交生产环境配置');
}
// Java 版权头检查:Snowy 要求每个 .java 头部有 Apache 2.0 版权声明
if (filePath.endsWith('.java')) {
const hasLicense = content.includes('Copyright [2022] [https://www.xiaonuo.vip]');
if (!hasLicense) {
warn('⚠️ Snowy 规范:每个 .java 文件头部必须有 12 行 Apache 2.0 版权声明(含 "Copyright [2022] [https://www.xiaonuo.vip]")。请参考任意现有 Java 文件头部补齐');
}
}
// 框架模块保护:修改平台底座时提醒
const frameworkPaths = [
'/snowy-common/',
'/snowy-plugin/snowy-plugin-sys/',
'/snowy-plugin/snowy-plugin-auth/',
'/snowy-plugin/snowy-plugin-dev/',
'/snowy-plugin/snowy-plugin-gen/',
'/snowy-plugin/snowy-plugin-client/',
'/snowy-plugin/snowy-plugin-mobile/',
'/snowy-plugin-api/',
'/snowy-web-app/'
];
if (frameworkPaths.some(p => filePath.includes(p))) {
warn('⚠️ 正在修改 Snowy 平台框架模块(' + filePath.split('/').slice(-2).join('/') + '):业务二次开发应优先放在 snowy-plugin-biz,修改框架会影响升级兼容性');
}
allow();
}
// 其他工具放行
allow();
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
/**
* UserPromptSubmit Hook - 强制技能评估 (Snowy 版)
* 功能: 开发场景下,将 Skills 激活率从约 25% 提升到 90% 以上
*
* 适配项目: Snowy v3.0.0 (前后端分离,插件化架构)
* 后端包名: vip.xiaonuo.*
* 前端工程: snowy-admin-web/ (Vue3 + Ant Design Vue,JS 非 TS)
*/
const fs = require('fs');
// 从 stdin 读取用户输入
let inputData = '';
try {
inputData = fs.readFileSync(0, 'utf8');
} catch {
process.exit(0);
}
let input;
try {
input = JSON.parse(inputData);
} catch {
process.exit(0);
}
const prompt = (input.prompt || '').trim();
// 检测是否是恢复会话(防止上下文溢出死循环)
const skipPatterns = [
'continued from a previous conversation',
'ran out of context',
'No code restore',
'Conversation compacted',
'commands restored',
'context window',
'session is being continued'
];
const isRecoverySession = skipPatterns.some(pattern =>
prompt.toLowerCase().includes(pattern.toLowerCase())
);
if (isRecoverySession) {
// 恢复会话,跳过技能评估以防止死循环
process.exit(0);
}
// 检测是否是斜杠命令
// 规则:以 / 开头,且后面不包含第二个 /(排除 /sys/user 这样的路径)
const isSlashCommand = /^\/[^\/\s]+$/.test(prompt.split(/\s/)[0]);
if (isSlashCommand) {
// 斜杠命令,跳过技能评估
process.exit(0);
}
const instructions = `## 强制技能激活流程(必须执行)
### 步骤 1 - 评估(必须在响应中明确展示)
针对用户问题,列出匹配的技能:\`技能名: 理由\`,无匹配则写"无匹配技能"
可用技能(前后端同仓库项目):
> 注意:snowy-admin-web/ 目录存在,CRUD/dev 类任务应同时生成前端三文件(api js + index.vue + form.vue)
- crud-development: CRUD/业务模块/Entity/Service/Controller/Param 开发
- api-development: API设计/接口规范/URL/CommonResult/异常处理
- plugin-architecture: 插件/模块划分/跨插件调用/provider/新建插件
- code-generator: 代码生成/Beetl/gen/生成器/菜单SQL
- database-ops: 数据库/SQL/建表/大写表名/菜单/字典数据
- backend-annotations: 注解/@CommonLog/@SaCheckPermission/@Trans/@Validated
- code-patterns: 编码规范/禁令/命名/来自RuoYi的惯性错误
- common-toolkit: 工具类/CommonCacheOperator/CommonCryptogramUtil/Hutool
- cache-redis: 缓存/Redis/Redisson/CommonCacheOperator
- file-oss-management: 文件上传/OSS/云存储/MinIO/XnUpload
- sms-mail: 短信/邮件/验证码/sms4j/SMTP
- message-push: 站内信/消息推送/钉钉/企微/飞书/WebSocket
- scheduled-jobs: 定时任务/Cron/DEV_JOB/TimerTaskRunner
- dict-config: 字典/DEV_DICT/BIZ_DICT/DictSelect/枚举/系统配置
- security-auth: 鉴权/Sa-Token/登录/白名单/B端C端/数据范围/权限
- crypto-sm: 国密/SM2/SM3/SM4/加密/密码/敏感字段
- frontend-pc: 前端/Vue/AntdV/api js/index.vue/form.vue/Xn组件
- client-mobile: C端/客户端/移动端/uni-app/mobile插件
- bug-detective: Bug/报错/异常/不工作/排查
- performance-doctor: 性能/慢查询/优化/N+1/缓存
- project-navigator: 找文件/在哪/目录结构/导航
- git-workflow: Git/提交/commit/分支
- env-setup: 环境搭建/首次启动/默认密码/出厂账号/导入数据库
- api-verify: 接口测试/自测/登录拿token/curl验证/冒烟
- platform-extension: 扩展/定制平台/监听/联动/复用平台能力/不改框架
- add-skill: 新增技能/创建skill
### 步骤 2 - 激活
对步骤 1 列出的每个匹配技能,必须逐个串行调用 Skill 工具激活(禁止并行调用、禁止只激活部分)。
如果列表为 4 个以上,只激活最相关的 4 个(优先级:与代码编写直接相关的 > 方法论类的)。
### 步骤 3 - 实现
全部激活完成后,再开始实现用户的请求。
### 关键规则
1. 只有确实相关的技能才列入步骤 1,宁缺毋滥
2. 技能激活后必须遵循技能内的规范(正误对照、检查清单)
3. 涉及写 Java 代码时,牢记本项目规范与 RuoYi 系框架方向相反(详见 code-patterns 技能)
4. Snowy 核心规范速记(写码前扫一眼):包名 vip.xiaonuo.*|@Resource 注入|ServiceImpl 必须继承|BeanUtil 转换|@Getter @Setter|CommonResult/CommonException|URL 动词式写方法上且 @SaCheckPermission 值=URL|String 主键|表名全大写|每个 .java 带 12 行版权头`;
process.stdout.write(instructions);
process.exit(0);
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Stop Hook - 清理 Windows 误创建的 nul 文件 (Snowy 版)
* 在 cwd 下递归(深度 5,跳过 . 开头目录、node_modules、target)删除名为 nul 的文件
* 与 pre-tool-use.js 的 `> nul` 拦截形成双保险
*/
const fs = require('fs');
const path = require('path');
const SKIP_DIRS = new Set(['node_modules', 'target', '.git', '.idea', '.claude']);
const MAX_DEPTH = 5;
function removeNulFiles(dir, depth) {
if (depth > MAX_DEPTH) {
return 0;
}
let removed = 0;
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return 0;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && entry.name.toLowerCase() === 'nul') {
try {
fs.unlinkSync(fullPath);
removed++;
} catch {
// 无法删除(可能被占用),忽略
}
} else if (entry.isDirectory() && !entry.name.startsWith('.') && !SKIP_DIRS.has(entry.name)) {
removed += removeNulFiles(fullPath, depth + 1);
}
}
return removed;
}
try {
const cwd = process.cwd();
const removed = removeNulFiles(cwd, 0);
if (removed > 0) {
process.stdout.write(JSON.stringify({
continue: true,
systemMessage: '已清理 ' + removed + ' 个误创建的 nul 文件(来自 `> nul` 重定向)'
}));
} else {
process.stdout.write(JSON.stringify({ continue: true }));
}
} catch {
process.stdout.write(JSON.stringify({ continue: true }));
}
process.exit(0);
+41
View File
@@ -0,0 +1,41 @@
{
"permissions": {},
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "test -f \"${CLAUDE_PROJECT_DIR}/.claude/hooks/skill-forced-eval.js\" && node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/skill-forced-eval.js\" || true"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash|Write",
"hooks": [
{
"type": "command",
"command": "test -f \"${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-tool-use.js\" && node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-tool-use.js\" || true",
"timeout": 5000
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "test -f \"${CLAUDE_PROJECT_DIR}/.claude/hooks/stop.js\" && node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/stop.js\" || true",
"timeout": 10000
}
]
}
]
},
"mcpServers": {}
}
+77
View File
@@ -0,0 +1,77 @@
---
name: add-skill
description: 元技能——教 AI 如何为本项目新增一个符合规范的技能(SKILL.md 格式、注册到 hook、验证流程)。触发场景:1) 用户要求新增/修改技能 2) 发现重复出现的知识值得沉淀为技能。触发词:新增技能、创建技能、添加skill、技能规范、SKILL.md。
---
# 如何新增一个技能
## 流程(5 步)
1. **定边界**:新技能解决什么问题?与现有 26 个技能是否重叠?(重叠就合并进去,不新建)
2. **核实事实**:技能里的每条规范、每个代码片段、每个参考路径,**必须先 Read 真实代码核实**,禁止凭模型先验写(本项目与 RuoYi 系大量反向,先验会写反)
3. **写文件**:`.claude/skills/{name}/SKILL.md`,格式见下
4. **注册**:更新 `.claude/hooks/skill-forced-eval.js` 里的技能清单(加一行)
5. **验证**:参考路径 100% 存在;触发词演练(3 条该命中 + 2 条不该命中)
## SKILL.md 格式规范
```markdown
---
name: kebab-case-name
description: 一句话定位。触发场景:1) ... 2) ... 3) ...。触发词:词1、词2、词3、...。注意:与 X 技能的边界说明。
---
# 技能标题
## 架构特征表 / 核心规则
(表格优先,必须/禁止措辞加粗)
## 正误代码对照
(❌/✅ 两列,代码与项目真实代码逐 token 一致)
## 检查清单
(- [ ] 复选框)
## 参考实现
(指向真实存在的文件路径,禁止行号——行号会随代码漂移)
```
硬性要求:
- name 用 kebab-case,与目录名一致
- description 必须含:一句话定位 + 至少 3 条触发场景 + 至少 5 个触发词 +(可选)与相邻技能的边界
- 正文 ≤300 行;表格优先于长文
- 代码示例先从项目里找到真实样本再抄写改写
- 全中文
## 现有 26 技能清单(防范围冲突)
| 类别 | 技能 |
|---|---|
| 后端主线 | crud-development、api-development、plugin-architecture、code-generator、database-ops、backend-annotations、code-patterns |
| 平台能力 | common-toolkit、cache-redis、file-oss-management、sms-mail、message-push、scheduled-jobs、dict-config |
| 安全前端 | security-auth、crypto-sm、frontend-pc |
| C 端与质量 | client-mobile、bug-detective、performance-doctor、project-navigator、git-workflow |
| Snowy 特色自研 | env-setup(环境与首启)、api-verify(接口自测闭环)、platform-extension(平台扩展点地图) |
| 元技能 | add-skill |
新增技能先对照此表:内容若属于某现有技能的一个章节,**合并**而不是新建。
## 注册位置(两处)
1. `.claude/hooks/skill-forced-eval.js` 的 instructions 字符串里的技能清单——加一行 `- {name}: {一句话触发词摘要}`
2. 本技能上面的清单表(保持文档与实际一致)
## 常见陷阱
1. **写完忘注册 hook** → 技能永远不被激活
2. **触发词太宽泛**(如"开发"、"代码")→ 到处误触发;触发词要具体
3. **代码虚构**——凭 RuoYi/通用 Spring 先验写代码模板,与本项目规范相反
4. **参考实现带行号**——代码一改就失效
5. **与现有技能范围重叠**——知识分散且互相矛盾
## 验证脚本(新增技能后跑)
```bash
# 校验 SKILL.md 的参考路径存在性:提取"参考实现"表格里的项目相对路径逐个检查
# 简易版:肉眼核对,或用 Grep 提取后 ls 验证
```
+132
View File
@@ -0,0 +1,132 @@
---
name: api-development
description: Snowy 接口设计规范:URL 命名、参数与返回、CommonResult、CommonException 异常体系、Knife4j 文档、白名单与权限。触发场景:1) 设计新接口或修改接口 2) 处理统一返回/异常/错误码 3) 接口需要免登录或加权限。触发词:API、接口、RESTful、URL、Controller、CommonResult、CommonException、异常、错误码、doc.html、Knife4j、Swagger、白名单。注意:CRUD 全套代码模板见 crud-development;Sa-Token 体系详解见 security-auth。
---
# Snowy API 开发规范
## URL 命名规范
格式:`/{插件}/{业务域}/{动作}`,**动词式全路径直接写在方法注解上**,类上不写 @RequestMapping。
| 动作 | HTTP | 路径示例 | 参数形式 |
|---|---|---|---|
| 分页 | GET | `/biz/xxx/page` | Param 对象(无 @RequestBody) |
| 列表(不分页) | GET | `/biz/xxx/list` | Param 对象 |
| 详情 | GET | `/biz/xxx/detail` | `@Valid XxxIdParam` |
| 添加 | POST | `/biz/xxx/add` | `@RequestBody @Valid XxxAddParam` |
| 编辑 | POST | `/biz/xxx/edit` | `@RequestBody @Valid XxxEditParam` |
| 删除 | POST | `/biz/xxx/delete` | `@RequestBody @Valid @NotEmpty List<XxxIdParam>` |
| 业务动作 | POST | `/biz/xxx/disableStatus` | `@RequestBody @Valid XxxIdParam` |
| 导出 | GET | `/biz/xxx/export` | void + HttpServletResponse |
| 下载 | GET | `/biz/xxx/download` | void + HttpServletResponse |
规则:
- **查询 GET + Param 对象**(Spring 自动绑定 query string),**写入 POST + @RequestBody @Valid**
- ❌禁止 RESTful 路径变量(`/user/{id}`)、PUT/DELETE 方法
- 插件前缀:sys / biz / dev / gen / mobile;C 端接口 auth 插件用 `/auth/c/**`、client 插件用 `/client/c/**`
## 统一返回 CommonResult
```java
// snowy-common/src/main/java/vip/xiaonuo/common/pojo/CommonResult.java
CommonResult.data(obj) // 成功带数据:{code:200, msg:"操作成功", data:obj, traceId:...}
CommonResult.ok() // 成功无数据(写操作)
CommonResult.error("消息") // 失败(一般不直接用,业务失败应抛 CommonException)
CommonResult.get(code, msg, data) // 底层构建
```
- Controller 方法返回类型一律 `CommonResult<T>`;例外:文件流/导出/下载为 `void` + `HttpServletResponse` 参数(参考 SysUserController.exportUser)
- ❌禁止返回 Map<String,Object>、裸 Entity 列表不带包装、R/AjaxResult/Result 等其他包装类
## 异常体系
```java
// 业务校验失败 —— 抛 CommonException(支持 {} 占位符格式化,消息中文)
throw new CommonException("存在重复的账号,账号为:{}", account);
throw new CommonException("XXX不存在,id值为:{}", id);
// 系统错误码 —— 枚举定义
throw new CommonException(CommonExceptionEnum.XXX); // 需要新错误码时在 CommonExceptionEnum 增加
```
- ❌禁止 `RuntimeException`、`IllegalArgumentException`、`ServiceException` 直接抛出
- 全局异常处理在 `snowy-web-app/src/main/java/vip/xiaonuo/core/handler/GlobalExceptionHandler.java`(@ControllerAdvice → GlobalExceptionUtil.getCommonResult(e)),业务代码不要自己 try-catch 后返回错误 CommonResult
- 参数校验失败(@Valid 不通过)由全局处理器统一转为中文提示,不需要手写校验返回
## 文档注解(Knife4j / OpenAPI3)
```java
@Tag(name = "XXX控制器") // 类级
@Operation(summary = "获取XXX分页") // 方法级
@Schema(description = "标题") // 字段级(Param/Entity/Result)
@Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED) // 必填字段
```
接口文档地址:`http://localhost:82/doc.html`(basic 认证 admin/123456,按插件分 7 组)。所有 description 用中文。
## 权限注解
```java
@SaCheckPermission("/biz/xxx/page") // 值 = 接口 URL(不是冒号式权限码!)
```
- 用户能通过校验的条件:其角色的资源授权生成的数据范围 apiUrl 列表包含该 URL
- 新接口上线后必须在"角色管理 → 授权"里勾选对应资源,否则非超管用户 403
- 超管角色(SUPER_PERMISSION)自动拥有全部权限
## 免登录 / 白名单
路由级放行集中在 `snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java`:
| 数组 | 含义 | 修改时机 |
|---|---|---|
| `NO_LOGIN_PATH_ARR` | 免登录路径 | 新增公开接口(如验证码、健康检查)时加这里 |
| `CLIENT_USER_PERMISSION_PATH_ARR` | C 端鉴权路径(/auth/c/**、/client/c/**) | 新增 C 端接口模块时 |
| `SUPER_PERMISSION_PATH_ARR` | 仅超管可访问路径 | 新增敏感管理接口时 |
改完白名单必须重启后端生效。
## 防重复提交
```java
@CommonNoRepeat // snowy-common 注解,写操作防重复提交
@PostMapping("/biz/xxx/add")
public CommonResult<String> add(...) { ... }
```
关键写操作(下单、支付类)建议加;普通 CRUD 可不加(Controller 已有 @Validated + 前端按钮防抖)。
## 常见错误正误对照
| ❌ 错误 | ✅ 正确 |
|---|---|
| `@RequestMapping("/biz/xxx")` + `@GetMapping("/{id}")` | 方法上直接 `@GetMapping("/biz/xxx/detail")` |
| `@PutMapping` / `@DeleteMapping` | 全部用 `@PostMapping` |
| `public Map<String, Object> detail(...)` | `public CommonResult<BizXxx> detail(...)` |
| `try { ... } catch (Exception e) { return CommonResult.error(e.getMessage()); }` | 直接抛 CommonException,交给全局处理器 |
| `throw new RuntimeException("xxx不存在")` | `throw new CommonException("xxx不存在,id值为:{}", id)` |
| 白名单写死在 Controller 里判断 | 统一改 GlobalConfigure 的 NO_LOGIN_PATH_ARR |
## 检查清单
- [ ] URL = /{插件}/{域}/{动作},动词式,写方法注解上
- [ ] 查询 GET+Param / 写入 POST+@RequestBody @Valid
- [ ] 返回 CommonResult<T>(文件流 void 除外)
- [ ] @SaCheckPermission 值与 URL 完全一致
- [ ] @Tag/@Operation/@Schema 中文齐全
- [ ] 业务异常用 CommonException,无裸 RuntimeException
- [ ] 新公开接口已加 GlobalConfigure 白名单并说明需重启
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/controller/BizNoticeController.java` | 标准 Controller 范本 |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/controller/SysUserController.java` | 含导出/下载 void 场景 |
| `snowy-common/src/main/java/vip/xiaonuo/common/pojo/CommonResult.java` | 统一返回体 |
| `snowy-common/src/main/java/vip/xiaonuo/common/exception/CommonException.java` | 业务异常 |
| `snowy-common/src/main/java/vip/xiaonuo/common/enums/CommonExceptionEnum.java` | 错误码枚举 |
| `snowy-web-app/src/main/java/vip/xiaonuo/core/handler/GlobalExceptionHandler.java` | 全局异常处理 |
| `snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java` | 路由白名单 |
| `snowy-web-app/src/main/java/vip/xiaonuo/core/config/Knife4jConfigure.java` | 接口文档分组 |
+93
View File
@@ -0,0 +1,93 @@
---
name: api-verify
description: AI 接口自测闭环:SM2 加密登录拿 token → curl 直调新接口 → 断言 CommonResult → 出报告。让 AI 不依赖人工点页面就能验证自己生成的接口。触发场景:1) 刚写完 Controller/Service 要验证接口通不通 2) 用户报"接口不对"需要复现请求 3) 验证权限/参数校验行为 4) 需要 token 调 /doc.html 之外的接口。触发词:接口测试、自测、curl、token、登录拿token、调接口、验证接口、冒烟、smoke、401复现、请求复现。注意:环境没起来先看 env-setup;报错排查的完整决策树见 bug-detective。
---
# AI 接口自测闭环
**核心价值**:后端在跑(82 端口)时,AI 可以自己完成"登录 → 拿 token → 调接口 → 断言结果"的完整验证,不必让用户手点页面。
## 第 1 步:确认后端活着
```bash
curl -s http://localhost:82/ # 应输出 WELCOME;无响应 → 先走 env-setup
```
## 第 2 步:生成 SM2 登录密文(后端强制 SM2,明文会 PWD_DECRYPT_ERROR)
登录口令必须先用**出厂公钥**做 SM2 加密(公钥硬编码在 `CommonCryptogramUtil`,前后端同一把)。用一条 node 命令产出:
```bash
# 密文是随机的,每次生成都不同——正常
node -e "const s=require('snowy-admin-web/node_modules/sm-crypto');console.log(s.sm2.doEncrypt('Snowy@2026!','04298364ec840088475eae92a591e01284d1abefcda348b47eb324bb521bb03b0b2a5bc393f6b71dabb8f15c99a0050818b56b23f31743b93df9cf8948f15ddb54',1))"
```
依赖:`snowy-admin-web/node_modules/sm-crypto`(前端执行过 npm install 即有;没有则先装,或临时 `npm i sm-crypto --registry=https://registry.npmmirror.com` 到任意目录)。cipherMode=1(C1C3C2),与前端 smCrypto.js 一致。
## 第 3 步:登录拿 token
```bash
# 密码参数名 password,账号 password 均为上一步密文/明文账号
RESP=$(curl -s -X POST http://localhost:82/auth/b/doLogin \
-H "Content-Type: application/json" \
-d '{"account":"superAdmin","password":"<上一步的SM2密文>","device":0}')
echo "$RESP"
# 成功:{"code":200,"data":{"token":"..."},...}
# 账密错:code!=200 且 msg 提示账号或密码错误(核对 Snowy@2026! 与密文重生成)
```
取 token:`TOKEN=$(echo "$RESP" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const j=JSON.parse(d);console.log(j.data?.token||'')})")`
> 若系统开启了图形验证码(DEV_CONFIG 可配),doLogin 还需 validCode/validCodeReqNo——此时改用 /doc.html 手动调试或让用户从浏览器复制 token(请求头名就是 `token`)。
## 第 4 步:调目标接口并断言
```bash
curl -s "http://localhost:82/biz/xxx/page?current=1&size=10" -H "token: $TOKEN"
# 或 POST:
curl -s -X POST http://localhost:82/biz/xxx/add -H "token: $TOKEN" -H "Content-Type: application/json" -d '{...}'
```
**断言表**:
| 期望 | 判定 |
|---|---|
| 业务正常 | `code === 200`,`data` 结构正确(分页是 `data.records/total`) |
| 未登录 | 401/`code!==200` 且提示登录 → token 头没带或过期(头名是 `token`,不是 Authorization) |
| 无权限 | 403 类提示 → 角色未授权该接口 URL(superAdmin 全通过,可先用它排除权限因素) |
| 参数校验失败 | msg 为 Param 里写的中文校验消息 |
| 业务异常 | msg 为 CommonException 抛出的中文消息——**这就是定位线索** |
## 第 5 步:输出验证报告
```markdown
## 接口自测报告
| 接口 | 结果 | 说明 |
|---|---|---|
| POST /auth/b/doLogin | ✅ 200 | token 获取成功 |
| GET /biz/xxx/page | ✅ 200 | 返回 3 条记录,含逻辑删除过滤 |
| POST /biz/xxx/add | ✅ 200 | 落库 ID=xxx(已查库核对) |
| POST /biz/xxx/add(缺name) | ✅ 按预期拦截 | "name不能为空" |
```
有条件时用 mysql 复核落库(连接串从 application.properties 解析):
```bash
mysql -uroot -p*** snowy -e "SELECT ID,NAME,DELETE_FLAG FROM BIZ_XXX ORDER BY CREATE_TIME DESC LIMIT 3;"
```
## 使用原则
- 生成接口代码后**主动自测**再交付(本技能 = /dev 收尾步骤的自动化版)
- 测试数据用"测试-"前缀,验证完清理(或逻辑删除)
- 密文/口令不写入任何文件,只在命令行内联使用
- 自测发现问题 → 修复 → 重测,闭环后再报告
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthController.java` | /auth/b/doLogin 定义 |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java` | SM2 解密 + SM3 比对逻辑 |
| `snowy-common/src/main/java/vip/xiaonuo/common/util/CommonCryptogramUtil.java` | SM2 公钥与加解密 |
| `snowy-admin-web/src/utils/smCrypto.js` | 前端同款加密(cipherMode=1) |
| `snowy-web-app/src/main/resources/application.properties` | sa-token.token-name=token |
+129
View File
@@ -0,0 +1,129 @@
---
name: backend-annotations
description: Snowy 常用注解速查与正误用法:@CommonLog 操作日志、@CommonNoRepeat 防重、@CommonWrapper 返回包装、@SaCheckPermission、@Trans 字段翻译、@Validated/@Valid、MyBatis-Plus 注解。触发场景:1) 给接口加日志/防重/权限注解 2) 字段需要字典或关联表翻译 3) 不确定该用哪个注解或注解参数。触发词:注解、@CommonLog、@CommonNoRepeat、@SaCheckPermission、@Trans、Easy-Trans、翻译、字典翻译、@Validated、@TableName、@TableId、@TableLogic。注意:注解底层机制见对应技能(日志见 message-push 相关、权限见 security-auth、字典见 dict-config)。
---
# Snowy 常用注解速查
## 注解总览
| 注解 | 来源 | 用在哪 | 作用 |
|---|---|---|---|
| `@CommonLog("中文标题")` | snowy-common | Controller 写方法 | 操作日志落 DEV_LOG 表(AOP:DevLogAop) |
| `@CommonNoRepeat(interval=5000)` | snowy-common | Controller 写方法 | 防重复提交(默认 5 秒内相同请求拦截) |
| `@SaCheckPermission("/url")` | Sa-Token | Controller 方法 | 接口权限校验,**值 = 接口 URL** |
| `@SaCheckRole("xxx")` | Sa-Token | Controller 方法 | 角色校验(少用) |
| `@Trans(type=...)` | Easy-Trans | Entity 字段 | 字典/关联表字段自动翻译 |
| `@Validated` | Spring | Controller 类 | 开启方法级校验 |
| `@Valid` | jakarta | 参数前 | 触发 Param 校验注解 |
| `@TableName("BIZ_XXX")` | MyBatis-Plus | Entity 类 | 表映射(大写) |
| `@TableId` | MyBatis-Plus | Entity id 字段 | 字符串雪花主键 |
| `@TableLogic` | MyBatis-Plus | CommonEntity 的 deleteFlag | 逻辑删除(继承即有,别重复加) |
| `@TableField(exist = false)` | MyBatis-Plus | Entity 字段 | 非表字段(翻译冗余名等) |
| `@TableField(typeHandler = CommonSm4CbcTypeHandler.class)` | MyBatis-Plus | 敏感字段 | SM4 加密落库(@TableName 需 autoResultMap = true) |
| `@Tag / @Operation / @Schema` | OpenAPI3 | 类/方法/字段 | 接口文档(中文) |
| `@Transactional(rollbackFor = Exception.class)` | Spring | Service 写方法 | 事务 |
## @CommonLog —— 操作日志
```java
@Operation(summary = "添加供应商")
@CommonLog("添加供应商") // value = 中文动作名,默认"未命名"(别用默认)
@SaCheckPermission("/biz/supplier/add")
@PostMapping("/biz/supplier/add")
public CommonResult<String> add(...) {...}
```
- **所有写操作接口必须加**(add/edit/delete/业务动作);查询接口不加
- 日志由 `snowy-plugin-dev` 的 DevLogAop 切面落 DEV_LOG 表,可在 开发工具→日志 查看
- ❌ `@Log(title=..., businessType=...)` 是 RuoYi 的写法,本项目没有
## @CommonNoRepeat —— 防重复提交
```java
@CommonNoRepeat // 默认 5000ms 内视为重复
@PostMapping("/biz/order/create")
public CommonResult<String> create(...) {...}
@CommonNoRepeat(interval = 10000) // 自定义间隔
```
- 用于不可重复的关键写操作(下单、支付、审批提交)
- 实现:snowy-web-app 的 GlobalConfigure 内嵌 CommonNoRepeatAop(IP+URL+参数 指纹)
## @SaCheckPermission —— 接口权限
```java
@SaCheckPermission("/biz/supplier/page") // ✅ 值 = 本接口 URL
@SaCheckPermission("biz:supplier:page") // ❌ 冒号式是 RuoYi 的,校验必失败
```
- 权限来源:角色-资源授权生成的数据范围 apiUrl 列表(详见 security-auth 技能)
- C 端接口用 `@SaClientCheckLogin` / `@SaClientCheckPermission`(auth-api 提供)
## @Trans —— 字段翻译(Easy-Trans)
```java
// 字典翻译:GENDER 字典码 → 字典值文本
@Trans(type = TransType.DICTIONARY, key = "GENDER")
private String gender;
// 关联表翻译:主管 id → 主管姓名,翻译结果放进 ref 指定的冗余字段
@Trans(type = TransType.SIMPLE, target = BizUser.class, fields = "name", alias = "director", ref = "directorName")
private String directorId;
@TableField(exist = false)
private String directorName; // 翻译结果落这里(非表字段)
```
- Entity 继承 CommonEntity(implements TransPojo)即支持
- 字典翻译要求字典编码存在(DEV_DICT / BIZ 字典,见 dict-config 技能)
- SIMPLE 翻译要求 target 表数据量可控(内部有缓存);大量关联时考虑手写批量查询
## 校验注解(jakarta.validation)
```java
// Controller 类上 @Validated,方法参数:
public CommonResult<String> add(@RequestBody @Valid BizXxxAddParam param) {...} // POST body
public CommonResult<BizXxx> detail(@Valid BizXxxIdParam param) {...} // GET query
public CommonResult<String> delete(@RequestBody @Valid @NotEmpty(message = "集合不能为空")
List<BizXxxIdParam> paramList) {...} // 批量删除
// Param 字段上:
@NotBlank(message = "name不能为空") // 字符串非空
@NotNull(message = "count不能为空") // 对象/null
@NotEmpty(message = "ids不能为空") // 集合
```
校验失败由全局异常处理器统一返回中文提示,不需要手写返回。消息格式惯例:`字段名不能为空`(字段用英文驼峰名)。
## MyBatis-Plus 注解
```java
@TableName("BIZ_SUPPLIER") // 大写;有 TypeHandler 字段时 value=... , autoResultMap = true
public class BizSupplier extends CommonEntity { // 继承即有 DELETE_FLAG 逻辑删除 + 审计字段
@TableId
private String id; // 字符串雪花
}
```
❌ 常见误用:`@TableId(type = IdType.AUTO)`(本项目 ASSIGN_ID 全局配置,不写 type)、自己再声明 deleteFlag/createTime 字段(与基类冲突)。
## 检查清单
- [ ] 写接口有 @CommonLog(中文标题)+ 写 Service 方法有 @Transactional(rollbackFor = Exception.class)
- [ ] @SaCheckPermission 值 = URL 全路径
- [ ] 翻译字段 @Trans 的 ref 冗余字段有 @TableField(exist = false)
- [ ] 校验消息中文、格式统一
- [ ] 没有使用 RuoYi 特有注解(@Log、@RateLimiter、@DataPermission、@ExcelProperty 换 EasyExcel 注解)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-common/src/main/java/vip/xiaonuo/common/annotation/CommonLog.java` | 日志注解定义 |
| `snowy-common/src/main/java/vip/xiaonuo/common/annotation/CommonNoRepeat.java` | 防重注解定义 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/controller/BizNoticeController.java` | 注解组合使用范本 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/user/entity/BizUser.java` | @Trans 双类型 + SM4 TypeHandler 范本 |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/core/aop/DevLogAop.java` | 日志切面实现 |
| `snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java` | 防重/包装 AOP 注册 |
+104
View File
@@ -0,0 +1,104 @@
---
name: bug-detective
description: Snowy 问题排查方法论与高发故障库:启动失败、401/403、接口报错、数据查不到、前后端联调问题的诊断决策树。触发场景:1) 任何报错/不工作/异常排查 2) 启动失败或页面白屏 3) 权限/数据问题定位。触发词:Bug、报错、异常、错误、不工作、失败、排查、调试、401、403、500、白屏、启动失败、查不到、排查思路。
---
# Snowy 问题排查指南
## 诊断决策树(先分类再深入)
```
问题发生在哪一层?
├─ 后端启动失败 ──→ 树 A
├─ 接口报错(4xx/5xx/业务码) ──→ 树 B
├─ 数据不对(查不到/查错/删不掉) ──→ 树 C
└─ 前端异常(白屏/按钮不见/数据不刷) ──→ 树 D
```
## 树 A:后端启动失败
1. 端口 82 被占?(`netstat -ano | findstr :82`)
2. MySQL 连不上/库没导 → 导入 `_sql/snowy_mysql.sql`,核对 application.properties 的 master 段口令
3. Redis 连不上 → 本地 6379 是否启动(database 1)
4. JDK 版本 → 必须 17
5. 依赖没编译 → 根目录 `mvn clean install -DskipTests`
6. Knife4j 401 → /doc.html 的 basic 认证是 admin/123456
## 树 B:接口报错
| 现象 | 根因 | 处理 |
|---|---|---|
| 401 未登录 | token 没带/过期;或接口不在白名单也确实需要登录 | 检查请求头 `token`;新公开接口要加 GlobalConfigure.NO_LOGIN_PATH_ARR 并**重启** |
| 403 / 无权限 | 该用户角色的资源授权里没有此接口 URL | 角色管理重新授权(接口权限 = 资源授权生成的 apiUrl 列表) |
| 500 + CommonException 消息 | 业务异常(中文消息就是线索) | 按消息定位 Service 抛出点 |
| 500 + NPE | 常见:跨插件取到 null 未判空(JSONObject.getStr);登录用户取不到 | ObjectUtil.isEmpty 判空 |
| 参数校验失败 | @Valid 注解的消息 | 看 Param 类校验消息 |
| delete 后台删不掉 | 逻辑删除字段值异常 | 检查 DELETE_FLAG 值(NOT_DELETE/DEDED) |
| SQL 报错 Unknown column | Entity 字段与表列不一致 / 排序字段没转下划线 | 核对 @TableName 大小写与列名;sortField 要 StrUtil.toUnderlineCase |
## 树 C:数据问题
| 现象 | 根因 |
|---|---|
| 列表查不到已插数据 | DELETE_FLAG 不是 NOT_DELETE(逻辑删除过滤掉了) |
| like 手机号查不到 | 该字段 SM4 加密落库,密文 like 永远不命中(见 crypto-sm) |
| 翻译字段(xxxName)为空 | @Trans 配置错:字典编码不存在 / target 表无数据 / 缺 @TableField(exist=false) 冗余字段 |
| 字典下拉为空 | 字典编码大小写不一致;前端字典缓存未刷新 |
| 新增后列表不刷新 | 前端没 emit('successful') / 没 tableRef.refresh |
| 时间范围查不到 | 前端没拆 startCreateTime/endCreateTime;或 value-format 缺失 |
| 分页总数不对 | PageParam 的 current/size 没传到(GET 参数绑定失败,检查字段名拼写) |
| ID 前端精度丢失 | 前端把 String id 当 Number 处理了(保持字符串) |
## 树 D:前端异常
| 现象 | 根因 |
|---|---|
| 白屏 | 控制台看报错;常见组件名拼错 / api js 路径 404 / 代理未启动(npm run dev 的 /api 代理 → 82) |
| 菜单不显示 | SYS_RESOURCE 菜单 SQL 未执行;或未给角色授权 |
| 按钮不显示 | hasPerm 码与 SYS_RESOURCE BUTTON 行 code 不一致(驼峰) |
| 表单打不开 | form.vue 忘了 defineExpose({ onOpen }) |
| 登录密码报错 | 前端 SM2 公钥与后端配置不一致(smCrypto.js / snowy.cryptogram 配置段) |
| 修改代码不生效 | Vite 热更新失效 → 重启 npm run dev;后端改动 → 重启 Java |
## 排查工具箱
```bash
# 看后端日志(IDE 控制台为主;文件日志看配置)
# 数据库直查(从 application.properties 取连接)
mysql -h127.0.0.1 -uroot -p****** snowy -e "SELECT ID,DELETE_FLAG FROM BIZ_XXX LIMIT 5;"
# 验证接口(Knife4j:http://localhost:82/doc.html,可带 token 调试)
# 全局搜代码
Grep pattern: "方法名/类名/错误消息片段" path: snowy-plugin
```
**接口调试优先用 Knife4j(/doc.html)**:先登录拿 token(B 端鉴权里全局设置),再单测接口,把"前端问题还是后端问题"先切开。
## 本项目特有问题库(高发 Top 9)
1. **登录一直报密码错误** → 本仓库出厂密码是 **Snowy@2026!**(不是网上说的 123456——那是官方演示站的);权威来源 DEV_CONFIG 的 SNOWY_SYS_DEFAULT_PASSWORD_FOR_B。接口直调登录还需 SM2 加密密码(见 api-verify 技能)
2. **Sa-Token 白名单改了没重启** → GlobalConfigure 是启动时构建的
3. **授权了还是 403** → 授权的是按钮码,但接口权限要的是"资源授权勾选到对应菜单/按钮"生成的 apiUrl 集合;重新授权并让用户**重新登录**(权限码缓存在 TokenSession)
4. **SM4 字段 like 查不到** → 设计期就要定查询方案
5. **雪花 id 丢失精度** → 后端 String,前端任何 Number 转换都会坏(parseInt 等)
6. **跨插件注入失败** → 只依赖了 *-api 却想注入实现类;或忘加依赖
7. **@Trans 不生效** → autoResultMap 没开 / 字典缓存旧数据(发数据变更事件)
8. **新增菜单 404** → 菜单 component 路径与前端 views 目录不匹配(如 biz/supplier/index ↔ src/views/biz/supplier/index.vue)
9. **生成的代码包名错** → 代码生成器 packageName/pluginName 填错,落位后要手工核
## 检查清单(修复后)
- [ ] 根因明确(能说出为什么),不是碰巧好使
- [ ] 修复未引入新的规范违规(过一遍 code-patterns)
- [ ] 同类隐患点已排查(同类接口/同类字段)
## 参考实现(排查时看这些)
| 文件 | 说明 |
|---|---|
| `snowy-web-app/src/main/java/vip/xiaonuo/core/handler/GlobalExceptionHandler.java` | 异常→错误码映射 |
| `snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java` | 白名单/拦截规则 |
| `snowy-web-app/src/main/resources/application.properties` | 全部环境配置 |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/log/` | 操作/异常日志模块 |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/monitor/` | 服务器监控 |
+113
View File
@@ -0,0 +1,113 @@
---
name: cache-redis
description: Snowy 缓存使用规范:CommonCacheOperator 统一操作器、Redisson 客户端、缓存 key 常量、数据变更事件联动刷新、alone-redis 独立配置。触发场景:1) 业务需要读写缓存 2) 改完数据要刷新缓存 3) 需要分布式锁或 Redis 原生结构。触发词:缓存、Redis、Redisson、CommonCacheOperator、CacheConstant、缓存刷新、缓存key、分布式锁、缓存过期。
---
# Snowy 缓存使用规范
## 基础设施
| 组件 | 位置 | 说明 |
|---|---|---|
| `CommonCacheOperator` | `snowy-common/src/main/java/vip/xiaonuo/common/cache/CommonCacheOperator.java` | **统一缓存操作器**(封装 Redisson Bucket,key 自动加前缀) |
| `RedissonConfig` | `snowy-web-app/src/main/java/vip/xiaonuo/core/config/RedissonConfig.java` | Redisson 客户端装配(默认 database 1) |
| `CacheConstant` | `snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java` | 框架级缓存 key 常量 |
| 数据变更事件 | `snowy-common/src/main/java/vip/xiaonuo/common/listener/CommonDataChangeEventCenter.java` | 增删改后广播事件,各插件监听刷新自己的缓存 |
连接:`application.properties` 的 `spring.data.redis` 段(127.0.0.1:6379,database 1);Sa-Token 用 alone-redis 独立配置(同实例)。
## CommonCacheOperator API(业务缓存首选)
```java
@Resource
private CommonCacheOperator commonCacheOperator;
commonCacheOperator.put(key, value); // 永久(逻辑过期由业务控制)
commonCacheOperator.put(key, value, 60); // 60 秒过期
Object v = commonCacheOperator.get(key); // 取(无则 null)
commonCacheOperator.remove(key1, key2); // 删一个或多个
commonCacheOperator.removeBatch("prefix:*"); // 按模式批量删
commonCacheOperator.getAllKeys(); // 全部 key(调试用)
```
- key 命名:`{业务}:{对象}:{标识}` 小写冒号分层,如 `biz:supplier:count`;框架已占用的前缀见 CacheConstant(`permission-resource`、`auth-b-permission-list:` 等,**不要冲突**)
- 值直接存对象(Redisson 编解码),不需要先 JSON 序列化
## 需要原生 Redis 结构/锁时:RedissonClient
```java
@Resource
private RedissonClient redissonClient;
// 分布式锁
RLock lock = redissonClient.getLock("biz:order:lock:" + orderId);
try {
if(lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 等3秒,持10秒
// 临界区
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if(lock.isHeldByCurrentThread()) {
lock.unlock(); // 必须 finally 且校验持有者
}
}
// 其他结构
RMap<String, Object> map = redissonClient.getMap("biz:xxx:map");
RAtomicLong counter = redissonClient.getAtomicLong("biz:xxx:counter");
```
## 数据变更事件(缓存联动的标准姿势)
改了被缓存的数据源后,广播事件让监听方刷新:
```java
// 写操作后发事件(在 Service 的 add/edit/delete 里)
CommonDataChangeEventCenter.doAddWithData(Xxx.class); // 新增
CommonDataChangeEventCenter.doUpdateWithData(Xxx.class); // 更新
CommonDataChangeEventCenter.doDeleteWithData(Xxx.class); // 删除
// 各插件在 core/listener/ 实现监听(参考 BizDataChangeListener)
```
典型消费方:sys 的权限/资源缓存、easy-trans 字典缓存。**改字典/资源/用户相关数据后不发事件 = 别的节点/模块读到旧缓存**。
## 使用原则
| 场景 | 方案 |
|---|---|
| 普通 key-value 业务缓存 | CommonCacheOperator(+ 过期秒数) |
| 计数器/防重/限流 | RedissonClient 的 RAtomicLong / RLock |
| 改了共享基础数据(字典/配置/资源) | 发 CommonDataChangeEventCenter 事件 |
| 登录用户信息/权限 | 框架已管(Sa-Token TokenSession),业务不要碰 |
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| `stringRedisTemplate.opsForValue()...` | `commonCacheOperator.put/get`(项目无 StringRedisTemplate 装配) |
| 直接注入 `RedisTemplate` | Redisson 体系(CommonCacheOperator/RedissonClient) |
| 改字典后不广播事件 | `CommonDataChangeEventCenter.doUpdateWithData(...)` |
| 分布式锁 unlock 不判持有者 | `if(lock.isHeldByCurrentThread()) lock.unlock()` 在 finally |
| key 大写/无分层 | 小写冒号分层 `{业务}:{对象}:{id}` |
| 缓存业务对象手动 JSON.toJSONString | 直接存对象 |
## 检查清单
- [ ] 用的 CommonCacheOperator 而非自造 Redis 封装
- [ ] key 有业务前缀分层,不与 CacheConstant 冲突
- [ ] 有过期时间的缓存设置了秒数
- [ ] 数据变更后发了 DataChangeEvent(涉及共享数据时)
- [ ] 分布式锁在 finally 释放且校验持有者
- [ ] 本地 Redis 已启动(127.0.0.1:6379 database 1)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-common/src/main/java/vip/xiaonuo/common/cache/CommonCacheOperator.java` | 操作器源码 |
| `snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java` | 框架 key 常量 |
| `snowy-common/src/main/java/vip/xiaonuo/common/listener/CommonDataChangeEventCenter.java` | 事件中心 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/core/listener/BizDataChangeListener.java` | 监听实现范本 |
| `snowy-web-app/src/main/java/vip/xiaonuo/core/config/RedissonConfig.java` | Redisson 装配 |
+90
View File
@@ -0,0 +1,90 @@
---
name: client-mobile
description: Snowy C 端(client 插件)与移动端(mobile 插件)开发规范:C 端用户体系、C 端登录链路、clientRequest 前端封装、移动端资源管理、代码生成器 mobile 模板。触发场景:1) 开发面向终端用户(C 端)的功能 2) 小程序/APP/H5 接口对接 3) 移动端菜单资源管理。触发词:C端、客户端、client、移动端、mobile、uni-app、小程序、APP、CLIENT_USER、移动端菜单、C端用户。
---
# Snowy C 端与移动端规范
## B 端 vs C 端(先分清)
| | B 端(管理后台) | C 端(终端用户) |
|---|---|---|
| 用户表 | SYS_USER | CLIENT_USER |
| 插件 | sys / biz | client |
| 登录 | `/auth/b/login` | `/auth/c/login`(见 AuthClientController) |
| 工具类 | StpUtil / StpLoginUserUtil | StpClientUtil / StpClientLoginUserUtil |
| 前端请求 | request.js baseRequest | clientRequest.js |
| 前端工程 | snowy-admin-web | 外部 H5/小程序/APP(uni-app) |
C 端接口路径规范:`/client/c/{业务域}/{动作}`(被 CLIENT_USER_PERMISSION_PATH_ARR 路由规则覆盖,走 C 端鉴权)。
## C 端模块结构(snowy-plugin-client)
```
client/modular/
├── user/ C 端用户(ClientUser,CLIENT_USER 表:账号/头像/昵称/状态等)
└── relation/ C 端关系(好友/关注等社交关系)
```
新增 C 端业务:在 client 插件 modular 下建域,Controller URL 用 `/client/c/{域}/{动作}`,注入用 StpClientUtil 取 C 端登录用户。结构规范与 biz 完全相同(六件套),只是类前缀 Client。
## C 端登录链路(auth 插件已实现)
```
POST /auth/c/login(账号密码 / 手机验证码 / 三方 token)
→ 校验 CLIENT_USER
→ StpClientUtil.login(id)(独立 StpLogic,与 B 端 token 隔离)
→ 返回 C 端 token
```
参考:`snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/`(AuthClientController + AuthServiceImpl 的 client 分支)。
## 前端 C 端请求(如需在 admin-web 调 C 端接口)
```js
import { clientRequest } from '@/utils/clientRequest'
// 用法与 baseRequest 相同,token 头与重定向逻辑按 C 端处理
```
实际 C 端页面通常在独立 uni-app 工程里(用 axios/fly 自行封装,token 头名 `token`, baseURL 指向后端 82 端口)。
## 移动端(snowy-plugin-mobile)
```
mobile/modular/
├── mobile/ 移动端模块管理
└── resource/ 移动端资源(菜单/按钮,MOBILE_RESOURCE 表)
```
- 作用:管理 uni-app 端的菜单与按钮权限(与 B 端 SYS_RESOURCE 平行的一套)
- 内置打包好的移动端静态资源:`snowy-plugin/snowy-plugin-mobile/src/main/resources/static/mobile/`
- 代码生成器可产出 mobile 代码:GenBasic 配置 mobileModule 后,sqlend 模板会生成 MOBILE_RESOURCE 的 INSERT(按钮码 `mobile{ClassName}Add` 形态)
- 完整 uni-app 前端工程在独立仓库(snowy 官方 mobile 工程),本仓库不含源码
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| C 端接口用 StpUtil 取用户 | StpClientUtil / StpClientLoginUserUtil(取错端会拿到 null) |
| C 端接口路径写 /client/b/... | `/client/c/...`(走 C 端路由规则) |
| CLIENT_USER 与 SYS_USER 混用 | 两套独立体系,外键别串 |
| 移动端按钮码用 B 端驼峰码 | mobile 前缀码(MOBILE_RESOURCE) |
| 在 B 端管理页调 clientRequest | B 端一律 baseRequest |
## 检查清单
- [ ] C 端接口前缀 /client/c/,取用户用 StpClientUtil 系
- [ ] C 端表用 CLIENT_ 前缀,类前缀 Client
- [ ] 移动端资源走 MOBILE_RESOURCE(生成器 mobileModule 配置)
- [ ] 两端 token 不混用
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/` | C 端用户六件套 |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthClientController.java` | C 端登录接口 |
| `snowy-plugin/snowy-plugin-mobile/src/main/java/vip/xiaonuo/mobile/modular/` | 移动端资源管理 |
| `snowy-admin-web/src/utils/clientRequest.js` | C 端请求封装 |
| `snowy-plugin/snowy-plugin-gen/src/main/resources/mobile/` | 移动端代码模板 |
| `snowy-plugin/snowy-plugin-gen/src/main/resources/sqlend/Mysql.sql.btl` | MOBILE_RESOURCE SQL 模板 |
+104
View File
@@ -0,0 +1,104 @@
---
name: code-generator
description: Snowy 自带 Beetl 代码生成器的使用规范:入口流程、GenBasic 字段填写、5 种模板形态选择、sqlend 菜单 SQL、生成后修补清单。触发场景:1) 用户想用代码生成器生成功能 2) /dev 命令模式 B 引导用户走生成器 3) 选择生成模板形态(表格/树/左树右表/主子表)4) 生成菜单资源 SQL。触发词:代码生成、生成器、gen、Beetl、模板、genType、sqlend、导入表、一键生成。注意:AI 直接手写六件套的规范见 crud-development;本技能讲平台自带生成器的正确用法。
---
# Snowy 代码生成器使用规范
## 概览
Snowy 自带 Beetl 模板代码生成器(`snowy-plugin-gen`),能从数据库表生成**后端六件套 + 前端三件 + 菜单资源 SQL** 的全套代码。这是开发标准 CRUD 的首选方式(对应 `/dev` 命令的**模式 B**)。
**入口**:启动后端 → 登录前端 → 开发工具 → 代码生成。数据存在 `GEN_BASIC` / `GEN_CONFIG` 表。
**生成产物 = 本项目 CRUD 标准结构**(生成器模板就是官方规范的定义源,手写代码要与生成产物一致)。
## 使用流程(5 步)
```
1. 建表(按 database-ops 规范:BIZ_ 前缀、大写、String 雪花主键、审计字段齐全)
2. 代码生成页面 → 导入表(选择刚建的表,可按住 Ctrl 选主表+子表做主子表)
3. 填写 GenBasic 基础配置(下表)
4. 在字段配置页调整每个字段的:是否查询/显示/必填、控件类型、字典编码等
5. 点"生成代码"→ zip 下载(或直接写入项目,取决于生成方式)→ 按"生成后必做"落地
```
## GenBasic 关键字段填写指南
| 字段 | 说明 | 业务插件二开的推荐值 |
|---|---|---|
| `dbTable` / `dbTableKey` | 主表名 / 主键(导入时自动带出) | — |
| `pluginName` | 生成代码归属插件 | `biz`(业务代码进 biz 插件) |
| `moduleName` | 模块名(URL 第一段也用它) | 一般 `biz` |
| `tablePrefix` | 表前缀移除(生成类名时去掉的前缀) | `BIZ_` |
| `generateType` | 生成方式:ZIP 下载 / 项目路径写入 | 二开推荐 ZIP 后自查合入 |
| `module` | 所属系统模块(SYS_RESOURCE 的 MODULE,决定菜单挂在哪) | 选业务所属模块 |
| `menuPid` | 上级菜单/目录 id | 选业务菜单挂载点 |
| `mobileModule` | 移动端所属模块(留空则不生成移动端) | 一般留空 |
| `functionName` | 功能名(中文,用于菜单标题/日志) | 如 `供应商` |
| `busName` | 业务名(小写,URL 与前端目录名) | 如 `supplier` → `/biz/supplier/page` |
| `className` | 类名(不含前缀,生成器自动拼 Biz 前缀) | 如 `Supplier` → `BizSupplier` |
| `formLayout` | 表单布局 | 按需 |
| `gridWhether` | 是否栅格布局 | 按需 |
| `packageName` | 包名 | 默认 `vip.xiaonuo` |
| `authorName` | 作者(写进 Javadoc @author) | 你的名字 |
| `genType` | 模板形态(见下表) | 按业务选 |
| `treeParentField` / `treeNameField` | 树形态:父字段 / 显示名字段 | 仅 TREE 形态填 |
| `subDbTable` / `subDbTableKey` / `subForeignKey` / `subClassName` | 主子表:子表名/主键/关联外键/子类名 | 仅 MASTER_DETAIL 填 |
## 模板形态选择(genType)
| genType | 形态 | 适用 | 典型例子 |
|---|---|---|---|
| `TABLE` | 普通表格 | 标准 CRUD 列表 | 供应商管理、公告管理 |
| `TREE` | 树形表格 | 有 parent_id 的层级数据 | 分类树、地区树 |
| `LEFT_TREE_TABLE` | 左树右表 | 左边选分类右边列数据 | 按分类组织的商品 |
| `MASTER_DETAIL` | 主子表 | 一对多,主表+子表一起编辑 | 订单+订单明细 |
模板目录:`snowy-plugin/snowy-plugin-gen/src/main/resources/` 下
`backend-{table,tree,left-tree-table,master-detail}` + `frontend-{table,tree,left-tree-table,master-detail}` + `mobile` + `sqlend`。
## sqlend:菜单资源 SQL(生成的关键产物之一)
生成器会产出 `SYS_RESOURCE` 的 INSERT 语句(模板:`snowy-plugin/snowy-plugin-gen/src/main/resources/sqlend/Mysql.sql.btl`):
- 1 条 MENU 行:`INSERT INTO SYS_RESOURCE VALUES ('${menuId}', '${parentId}', '${functionName}管理', '${busName}', '${menuCode}', 'MENU', '${moduleId}', 'MENU', '${menuPath}', '${menuComponent}', ...)`
- N 条 BUTTON 行:按钮码为**驼峰式** `${classNameFirstLower}Add / Edit / Delete / Detail / BatchDelete / Import / Export`(如 `bizSupplierAdd`)——这些码就是前端 `hasPerm('bizSupplierAdd')` 用的按钮权限码
- 配置了 mobileModule 时另有 MOBILE_RESOURCE 行(按钮码 `mobile{ClassName}Add` 形态)
id 值由生成器渲染时生成(字符串雪花)。**菜单 SQL 必须执行入库**,否则前端看不到菜单、按钮权限全失效。
## 生成后必做(AI 协助修补清单)
1. **落位**:确认文件在 `snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/{busName}/` 与前端 `src/views/biz/{busName}/`、`src/api/biz/biz{ClassName}Api.js`
2. **版权头**:逐个 .java 检查 12 行 Apache 2.0 版权声明(生成器可能按 authorName 生成变体,缺失必补)
3. **执行菜单 SQL**:sqlend 产出的 INSERT 执行到 snowy 库
4. **重启后端 + 刷新前端** → 系统管理 → 角色管理 → 给目标角色勾选新菜单与按钮授权(否则普通用户 403/按钮不显示)
5. **核对生成代码**:URL 是否 `/biz/{busName}/{动作}`、@SaCheckPermission 值是否等于 URL、Param 校验消息是否中文
6. **删除生成痕迹**:不需要的 import、注释里的模板变量残留
7. 涉及树/主子表时核对 treeParentField 与 subForeignKey 的实际业务正确性
## 生成器 API(程序化调用,进阶)
`GenBasicController`(`snowy-plugin/snowy-plugin-gen/.../basic/controller/GenBasicController.java`)提供:分页/详情/添加/编辑/删除/预览 preview/执行生成 execGenZip 等,路径前缀 `/gen/basic/*`。AI 一般不直接调这些接口——引导用户走界面,AI 负责生成后的核对修补。
## 常见问题
| 问题 | 处理 |
|---|---|
| 导入表列表为空 | 检查数据库连接(application.properties 的 dynamic master 段)与表是否已建 |
| 生成后菜单不显示 | 菜单 SQL 没执行,或没给角色授权 |
| 按钮全不显示 | BUTTON 行的驼峰码与前端 hasPerm 不匹配,核对 SYS_RESOURCE 的 code 列 |
| 生成的类名不对 | 检查 tablePrefix 是否正确移除(如 BIZ_SUPPLIER + 前缀 BIZ_ + className Supplier → BizSupplier) |
| 想改生成模板 | 模板在 snowy-plugin-gen/resources/ 下(.btl Beetl 文件),改后重启;属于改框架,谨慎 |
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-gen/src/main/java/vip/xiaonuo/gen/modular/basic/entity/GenBasic.java` | 生成基础配置实体(全部字段) |
| `snowy-plugin/snowy-plugin-gen/src/main/java/vip/xiaonuo/gen/modular/basic/service/impl/GenBasicServiceImpl.java` | 生成逻辑(Beetl 渲染) |
| `snowy-plugin/snowy-plugin-gen/src/main/resources/sqlend/Mysql.sql.btl` | 菜单/按钮 SQL 模板(SYS_RESOURCE INSERT 的权威格式) |
| `snowy-plugin/snowy-plugin-gen/src/main/resources/backend-table/` | 后端六件套模板 |
| `snowy-plugin/snowy-plugin-gen/src/main/resources/frontend-table/` | 前端三件模板 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/` | 与生成产物同构的手写范本 |
+119
View File
@@ -0,0 +1,119 @@
---
name: code-patterns
description: Snowy 全量编码禁令速查表(含"来自 RuoYi 的惯性错误"专栏)、命名规范、JSON/日期约定、Git 提交规范。触发场景:1) 写任何 Java/前端代码前的规范自查 2) 审查代码风格问题 3) 提交代码。触发词:编码规范、禁令、命名、代码风格、惯性错误、RuoYi、JSON、日期、Git 提交、commit。注意:本技能是速查表;分层结构见 crud-development,模块组织见 plugin-architecture。
---
# Snowy 编码禁令速查表
## ⚠️ 来自 RuoYi / RuoYi-Vue-Plus 的惯性错误(最高优先级)
AI 对 RuoYi 系框架先验极强,**以下每条都是反向的**,写代码前先扫一遍:
| # | ❌ RuoYi 惯性写法 | ✅ Snowy 正确写法 |
|---|---|---|
| 1 | `implements IXxxService`(不继承) | `extends ServiceImpl<XxxMapper, Xxx> implements XxxService` |
| 2 | `@RequiredArgsConstructor` + `private final` 构造器注入 | `@Resource` 字段注入(jakarta.annotation.Resource) |
| 3 | `MapstructUtils.convert(src, Target.class)` | `BeanUtil.toBean(param, Target.class)` / `BeanUtil.copyProperties(src, target)` |
| 4 | `@Data` | `@Getter @Setter` |
| 5 | `R<T>` / `AjaxResult` / `TableDataInfo<T>` | `CommonResult<T>`(data/ok/error 工厂方法) |
| 6 | `ServiceException` | `CommonException("中文消息{}", arg)` |
| 7 | 类级 `@RequestMapping("/biz/xxx")` + RESTful 动作 | 无类级注解,方法上全路径 `@PostMapping("/biz/xxx/add")` |
| 8 | `@PutMapping` / `@DeleteMapping` / `@GetMapping("/{id}")` | 全部 `@PostMapping` / `@GetMapping`,无路径变量 |
| 9 | 权限码 `biz:xxx:list` 冒号式 | `@SaCheckPermission("/biz/xxx/page")` URL 式 |
| 10 | 单个 XxxBo + AddGroup/EditGroup 校验组 | XxxPageParam / AddParam / EditParam / IdParam 每操作一类 |
| 11 | Long 雪花主键(JS 精度问题靠序列化器解决) | `@TableId private String id` |
| 12 | 小写表名 `biz_xxx` | 全大写 `BIZ_XXX` |
| 13 | `del_flag` / `@TableLogic` 自己声明 | 继承 CommonEntity(字段 DELETE_FLAG) |
| 14 | `@Log(title=..., businessType=...)` | `@CommonLog("中文标题")` |
| 15 | `LambdaQueryWrapper` + `buildQueryWrapper()` 方法 | `QueryWrapper<Xxx>().checkSqlInjection()` + `queryWrapper.lambda()` 内联 |
| 16 | 前端 Element Plus / plus-ui 组件 | Ant Design Vue 4 组件(a-button 等) |
| 17 | 前端 TypeScript | JavaScript(本项目前端无 TS) |
## 后端禁令速查表
| 禁止 | 替代 | 原因 |
|---|---|---|
| 包名 `com.*` / `org.*` | `vip.xiaonuo.*` | 包结构即模块边界 |
| 删除/省略 12 行版权头 | 每个 .java 必带 | Apache 2.0 协议要求 |
| `@Autowired` | `@Resource` | 项目统一风格(240+ 处 vs 2 处) |
| `@Data` | `@Getter @Setter` | 避免相等性/构造器副作用 |
| 裸 `new RuntimeException(...)` | `throw new CommonException(...)` | 统一异常处理链路 |
| `Map<String,Object>` 作业务返回 | 类型化 Result/Entity + CommonResult | 类型安全 |
| 手写 SQL 拼接 | `checkSqlInjection()` + lambda | 防注入 |
| 写操作无 `@Transactional(rollbackFor = Exception.class)` | 加上 | rollbackFor 必须显式(默认不回滚受检异常) |
| 手动赋值 create_user/create_time | 继承 CommonEntity 自动填充 | MetaObjectHandler 统一填 |
| 查询手写 delete_flag 条件 | 逻辑删除自动过滤 | @TableLogic 已处理 |
| 跨插件 import 对方 modular 类 | 走 *-api + provider | 模块解耦 |
| 英文注释/异常消息 | 全中文 | 项目统一 |
| System.out.println | hutool/Spring 日志 | 生产可控 |
## 命名规范表
| 对象 | 规则 | 示例 |
|---|---|---|
| Java 类前缀 | = 插件缩写 | BizSupplier、SysUser、DevConfig、GenBasic、AuthThird、ClientUser、MobileResource |
| Service 接口/实现 | `XxxService` / `XxxServiceImpl` | BizSupplierService / BizSupplierServiceImpl |
| Mapper / XML | `XxxMapper` / `mapping/XxxMapper.xml` 同包 | BizSupplierMapper |
| Param 后缀 | PageParam / AddParam / EditParam / IdParam / SelectorXxxParam / ExportParam | BizSupplierPageParam |
| Result 后缀 | `XxxResult` | BizSupplierResult |
| Controller URL | `/biz/supplier/page` 小写驼峰域名 | — |
| 数据库 | 全大写 | BIZ_SUPPLIER |
| 枚举 | `XxxEnum`,含 getValue(),值用枚举不用魔法串 | BizNoticeStatusEnum |
| Javadoc | 类与方法都带,含 `@author 名字` `@date yyyy/MM/dd HH:mm` | — |
## JSON / 日期约定
- JSON:hutool `JSONUtil`(parseObj/toJsonStr)优先;跨插件传值一律 `JSONObject`
- 日期:Entity 用 `Date` 或 String(现有代码以 String 居多,如 startCreateTime/endCreateTime 查询参数);格式统一 `yyyy-MM-dd HH:mm:ss`
- 对象复制:新增 `BeanUtil.toBean(param, Entity.class)`;编辑 `queryEntity(id)` 后 `BeanUtil.copyProperties(param, entity)`
- 集合操作:hutool `CollStreamUtil.toList(list, Xxx::getId)`、`ObjectUtil.isNotEmpty/isAllNotEmpty` 判空
## 前端禁令速查
| 禁止 | 替代 |
|---|---|
| TypeScript 语法(interface、type、as) | 纯 JavaScript |
| Element Plus 组件 | Ant Design Vue(a-xxx) |
| 自造请求封装 | `src/utils/request.js` 的 baseRequest / clientRequest |
| api 文件乱放 | `src/api/{插件}/{xxx}Api.js` |
| 页面文案硬编码中文 | `$t('xxx')` + locales 补词条(遵循现有 i18n) |
| 按钮权限乱写 | `hasPerm('bizXxxAdd')` 驼峰码,数组与 'and'/'or' 组合 |
## Git 提交规范
```
<type>: <subject> # subject 中文,一行,不加句号
type ∈ feat|fix|refactor|docs|style|test|chore|perf
示例:
feat: 新增供应商管理六件套
fix: 修复供应商分页排序字段未下划线转换的问题
docs: 补充后端开发指南的跨插件调用章节
```
- 只提交本次任务相关文件;**不提交** application.properties 的本地口令改动、node_modules、target
- 默认只做本地 commit,明确要求才 push(当前仓库无远程)
- 分支:master 单分支(个人项目);大重构前打 tag
## 避免过度工程
- 标准六件套够用就别加层(不要 DAO/Manager 层、不要 DTO 转换链)
- 没有复用诉求不抽公共方法;两处以内重复可接受
- 不引入新依赖前先查 hutool / snowy-common 是否已有(见 common-toolkit 技能)
## 检查清单(提交前)
- [ ] 17 条 RuoYi 惯性错误全部规避
- [ ] 版权头齐全、注释中文、Javadoc 带 @author @date
- [ ] 命名符合前缀与后缀规范
- [ ] 前端无 TS / Element Plus 残留
- [ ] commit message 符合 type: 中文 描述
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/` | 全部规范的活样本 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/user/entity/BizUser.java` | 复杂 Entity(@Trans/SM4) |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java` | 复杂业务写法 |
| `CLAUDE.md` | 项目宪法(易错点警告节) |
+90
View File
@@ -0,0 +1,90 @@
---
name: common-toolkit
description: snowy-common 工具类地图与 Hutool 优先原则:CommonCacheOperator、CommonCryptogramUtil、CommonEmailUtil、CommonDownloadUtil 等 16 个工具类的用途索引与选择决策树。触发场景:1) 需要 utility(加密/邮件/下载/IP/验证码/头像等)时先查这里 2) 不确定用哪个工具类 3) 想自写工具前检查是否已有。触发词:工具类、util、CommonCryptogramUtil、CommonEmailUtil、CommonDownloadUtil、Hutool、工具选择、复用。注意:缓存的完整用法见 cache-redis,国密详见 crypto-sm。
---
# Snowy 工具类地图
## 选择决策树(自上而下)
```
需要某能力
├─ snowy-common 里有 Common* 工具? ──→ 用它(项目标准,含业务约定如缓存前缀/国密算法)
├─ 没有,hutool 里有? ──→ 用 hutool(cn.hutool.*,项目已全局依赖 5.8.25)
├─ 也没有,Spring/Sa-Token/MP 官方 API? ──→ 用官方
└─ 都没有 ──→ 才自写;放对应插件 core/util/,通用才下沉 snowy-common
```
❌ 禁止:重复造轮子、引入 commons-lang3/guava 等冗余依赖(hutool 已覆盖)。
## snowy-common/util/ 全部 16 个工具类
| 工具类 | 用途 | 常用方法/说明 |
|---|---|---|
| `CommonCryptogramUtil` | 国密三件套 | sm2Encrypt/sm2Decrypt(登录密码传输)、sm3Digest(口令摘要)、sm4Encrypt/sm4Decrypt、后端解密登录密码用(详见 crypto-sm 技能) |
| `CommonEmailUtil` | 邮件发送 | 文本/HTML/附件/内嵌图片(基于 dev 插件 DEV_EMAIL 配置,详见 sms-mail 技能) |
| `CommonDownloadUtil` | 文件下载 | 通过 HttpServletResponse 写流下载(Controller 导出场景用它,不要手写 IO) |
| `CommonAvatarUtil` | 随机头像 | 生成默认头像(新用户无头像时) |
| `CommonOtpUtil` | OTP 动态口令 | 生成/校验一次性验证码(登录 MFA 用) |
| `CommonSqlUtil` | SQL 工具 | 排序字段安全处理等(分页排序底层用它) |
| `CommonResponseUtil` | 响应写出 | 向 response 直接写 JSON(过滤器/拦截器场景,Controller 不要用) |
| `CommonServletUtil` | Servlet 工具 | 请求参数/Request/Response 获取 |
| `CommonKeyUtil` | 键生成 | 缓存 key 等标准键拼接 |
| `CommonIpAddressUtil` | IP 归属地 | ip2region 离线库解析(登录日志属地显示) |
| `CommonUaUtil` | User-Agent | 解析浏览器/操作系统(登录日志设备显示) |
| `CommonTraceIdUtil` | 链路追踪 | traceId 生成(响应体里的 traceId) |
| `CommonTimeFormatUtil` | 时间格式化 | 统一 yyyy-MM-dd HH:mm:ss 处理 |
| `CommonNetWorkInfoUtil` | 网络信息 | 内外网地址判断等 |
| `CommonJoinPointUtil` | 切面工具 | AOP 场景取参数/方法信息(日志切面用) |
| `CommonFilterExceptionUtil` | 过滤器异常 | 过滤器链里的统一错误输出 |
## common 下其他非 util 但常被当工具用的
| 类 | 位置 | 用途 |
|---|---|---|
| `CommonCacheOperator` | `common/cache/` | Redis 缓存统一操作(详见 cache-redis 技能) |
| `CommonSm4CbcTypeHandler` | `common/handler/` | SM4-CBC 字段加密 TypeHandler |
| `CommonPageRequest` | `common/page/` | 分页请求 → MP Page |
| `CommonResult` / `CommonException` | `common/pojo/` / `exception/` | 统一返回/异常 |
| `CommonDataChangeEventCenter` | `common/listener/` | 数据变更事件广播(增删改后发事件刷新各方缓存) |
| `CacheConstant` | `common/consts/` | 缓存 key 常量(PERMISSION_RESOURCE_CACHE_KEY 等) |
| `CommonTimerTaskRunner` | `common/timer/` | 定时任务接口(详见 scheduled-jobs 技能) |
| `CommonDeleteAbsoluteMapper` | `common/mapper/` | 物理删除专用 Mapper(慎用) |
## Hutool 高频速查(本项目常用模块)
| 模块 | 常用类 | 场景 |
|---|---|---|
| `cn.hutool.core.util` | `StrUtil`(isNotBlank/...)、`ObjectUtil`(isEmpty/isNotEmpty/isAllNotEmpty)、`RandomUtil`、`IdUtil`、`ReflectUtil` | 字符串/对象判空、随机 |
| `cn.hutool.core.bean` | `BeanUtil`(toBean/copyProperties) | **对象转换唯一选择** |
| `cn.hutool.core.collection` | `CollStreamUtil`(toList)、`CollectionUtil`(newArrayList/unionAll) | 集合流/并集 |
| `cn.hutool.json` | `JSONUtil`(parseObj/toJsonStr)、`JSONObject` | JSON 与跨插件传值 |
| `cn.hutool.core.convert` | `Convert`(toList/toStr) | 类型转换 |
| `cn.hutool.core.io` | `IoUtil`、`FileUtil` | 文件流 |
| `cn.hutool.core.date` | `DateUtil` | 日期 |
| `cn.hutool.crypto` | `SmUtil` 等已被 CommonCryptogramUtil 封装 | 优先走 Common 层 |
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| `org.apache.commons.lang3.StringUtils` | `cn.hutool.core.util.StrUtil` |
| 手写 `MD5/SHA` 加密工具 | `CommonCryptogramUtil`(国密标准) |
| Controller 里 `response.getOutputStream()` 手写下载 | `CommonDownloadUtil` |
| 自己 new SimpleDateFormat 到处格式化 | `CommonTimeFormatUtil` / `DateUtil` |
| 业务里手拼 Redis key | `CacheConstant` 常量 + `CommonCacheOperator` |
| 重复实现"对象转 JSON 字符串" | `JSONUtil.toJsonStr` |
## 检查清单
- [ ] 用工具前扫过本表(Common* 优先 → hutool → 官方 → 自写)
- [ ] 没有引入新工具类依赖(guava/commons-lang3 等)
- [ ] 自写工具放对了位置(插件 core/util/,通用的才进 snowy-common)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-common/src/main/java/vip/xiaonuo/common/util/` | 16 个工具类源码 |
| `snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java` | 缓存 key 常量 |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java` | 工具类组合使用现场(加密/IP/UA/缓存) |
+313
View File
@@ -0,0 +1,313 @@
---
name: crud-development
description: Snowy CRUD 全套开发规范(六件套 + 可选两件 + 前端三件)。触发场景:1) 开发新的业务模块/增删改查功能 2) 编写 Entity/Mapper/Service/Controller/Param/Result 任何一件 3) 仿照现有模块新建业务域。触发词:CRUD、增删改查、业务模块、新建模块、Entity、Service、Controller、Param、分页、六件套。注意:只讲"怎么写代码";建表/菜单 SQL 见 database-ops;接口设计细节见 api-development;用自带生成器见 code-generator。
---
# Snowy CRUD 开发规范
## 架构特征表(先背下来再写代码)
| 特征 | Snowy 约定 | 与 RuoYi 系相反点 |
|---|---|---|
| 包名 | `vip.xiaonuo.biz.modular.{域名}.{层}` | 不是 org.dromara / com.ruoyi |
| 依赖注入 | `@Resource`(jakarta.annotation)字段注入 | ❌@Autowired ❌构造器注入 |
| ServiceImpl | `extends ServiceImpl<XxxMapper, Xxx> implements XxxService` | ❌只 implements 不继承 |
| 对象转换 | Hutool `BeanUtil.toBean / copyProperties` | ❌MapstructUtils |
| Lombok | 只用 `@Getter @Setter` | ❌@Data |
| 主键 | `@TableId private String id`(字符串雪花) | ❌Long |
| URL | 动词式写方法上 `/biz/xxx/page` | ❌RESTful 路径 ❌类级 @RequestMapping |
| 分页 | `CommonPageRequest.defaultPage()` + MP `Page<T>` | — |
| 版权头 | 每个 .java 头部 12 行 Apache 2.0 声明 | AI 生成最容易漏 |
## 版权头模板(每个新 .java 必须有)
```java
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
```
## 目录结构(一个业务域 = modular 下一个目录)
```
snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/{域名}/
├── controller/XxxController.java
├── entity/Xxx.java
├── enums/XxxStatusEnum.java (可选,有状态字段时)
├── mapper/XxxMapper.java
│ └── mapping/XxxMapper.xml (可选,仅自定义 SQL)
├── param/XxxPageParam.java
│ XxxAddParam.java
│ XxxEditParam.java
│ XxxIdParam.java
├── result/XxxResult.java (可选,仅投影返回)
├── service/XxxService.java
│ └── impl/XxxServiceImpl.java
└── provider/XxxApiProvider.java (可选,被跨插件调用时)
```
类名前缀 = 插件缩写(业务插件是 **Biz**):`BizNotice`、`BizNoticeService`。
## 第 1 件:Entity
```java
@Getter
@Setter
@TableName("BIZ_XXX") // 表名全大写;有 SM4 加密字段时 @TableName(value = "BIZ_XXX", autoResultMap = true)
public class BizXxx extends CommonEntity {
/** 主键 */
@TableId
@Schema(description = "主键")
private String id;
/** 名称 */
@Schema(description = "名称")
private String name;
/** 排序 */
@Schema(description = "排序")
private Integer sortCode;
/** 扩展信息 */
@Schema(description = "扩展信息")
private String extJson;
}
```
要点:
- 继承 `CommonEntity`(自动获得 deleteFlag/createUser/createTime/updateUser/updateTime 审计字段,逻辑删除自动过滤)
- 敏感字段(手机号等)加 `@TableField(typeHandler = CommonSm4CbcTypeHandler.class)`,见 crypto-sm 技能
- 字典翻译字段加 `@Trans(type = TransType.DICTIONARY, key = "XXX")`,关联表翻译加 `@Trans(type = TransType.SIMPLE, target = Yyy.class, fields = "name", alias = "yyy", ref = "yyyName")`
- 每个字段有 `/** 中文说明 */` + `@Schema(description = "中文")`
## 第 2 件:Mapper(通常空接口)
```java
public interface BizXxxMapper extends BaseMapper<BizXxx> {
}
```
自定义 SQL 才建 `mapper/mapping/BizXxxMapper.xml`(namespace = Mapper 全限定名,与 Java 同包)。
## 第 3 件:Service 接口
```java
public interface BizXxxService extends IService<BizXxx> {
/**
* 获取XXX分页
*
* @author 你的名字
* @date 2026/08/18 10:00
*/
Page<BizXxx> page(BizXxxPageParam bizXxxPageParam);
void add(BizXxxAddParam bizXxxAddParam); // 每个方法都要中文 Javadoc + @author + @date
void edit(BizXxxEditParam bizXxxEditParam);
void delete(List<BizXxxIdParam> bizXxxIdParamList);
BizXxx detail(BizXxxIdParam bizXxxIdParam);
BizXxx queryEntity(String id); // 内部帮助方法:查不到抛异常
}
```
## 第 4 件:ServiceImpl(核心模板)
```java
@Service
public class BizXxxServiceImpl extends ServiceImpl<BizXxxMapper, BizXxx> implements BizXxxService {
@Override
public Page<BizXxx> page(BizXxxPageParam bizXxxPageParam) {
QueryWrapper<BizXxx> queryWrapper = new QueryWrapper<BizXxx>().checkSqlInjection();
// 每个查询条件都要判空
if(ObjectUtil.isNotEmpty(bizXxxPageParam.getName())) {
queryWrapper.lambda().like(BizXxx::getName, bizXxxPageParam.getName()); // 模糊
}
if(ObjectUtil.isNotEmpty(bizXxxPageParam.getType())) {
queryWrapper.lambda().eq(BizXxx::getType, bizXxxPageParam.getType()); // 精确
}
if(ObjectUtil.isAllNotEmpty(bizXxxPageParam.getStartCreateTime(), bizXxxPageParam.getEndCreateTime())) {
queryWrapper.lambda().between(BizXxx::getCreateTime,
bizXxxPageParam.getStartCreateTime(), bizXxxPageParam.getEndCreateTime());
}
// 排序:前端传了 sortField/sortOrder 用之,否则默认按 sortCode 升序
if(ObjectUtil.isAllNotEmpty(bizXxxPageParam.getSortField(), bizXxxPageParam.getSortOrder())) {
CommonSortOrderEnum.validate(bizXxxPageParam.getSortOrder());
queryWrapper.orderBy(true, bizXxxPageParam.getSortOrder().equals(CommonSortOrderEnum.ASC.getValue()),
StrUtil.toUnderlineCase(bizXxxPageParam.getSortField()));
} else {
queryWrapper.lambda().orderByAsc(BizXxx::getSortCode);
}
return this.page(CommonPageRequest.defaultPage(), queryWrapper);
}
@Transactional(rollbackFor = Exception.class) // 写操作必须带
@Override
public void add(BizXxxAddParam bizXxxAddParam) {
BizXxx bizXxx = BeanUtil.toBean(bizXxxAddParam, BizXxx.class); // 新增:Param → Entity
this.save(bizXxx);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void edit(BizXxxEditParam bizXxxEditParam) {
BizXxx bizXxx = this.queryEntity(bizXxxEditParam.getId()); // 编辑:先查再拷贝
BeanUtil.copyProperties(bizXxxEditParam, bizXxx);
this.updateById(bizXxx);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(List<BizXxxIdParam> bizXxxIdParamList) {
this.removeByIds(CollStreamUtil.toList(bizXxxIdParamList, BizXxxIdParam::getId));
}
@Override
public BizXxx detail(BizXxxIdParam bizXxxIdParam) {
return this.queryEntity(bizXxxIdParam.getId());
}
@Override
public BizXxx queryEntity(String id) {
BizXxx bizXxx = this.getById(id);
if(ObjectUtil.isEmpty(bizXxx)) {
throw new CommonException("XXX不存在,id值为:{}", id); // 中文消息 + {} 占位
}
return bizXxx;
}
}
```
## 第 5 件:Controller(五接口模板)
```java
@Tag(name = "XXX控制器")
@RestController
@Validated
public class BizXxxController {
@Resource
private BizXxxService bizXxxService;
@Operation(summary = "获取XXX分页")
@SaCheckPermission("/biz/xxx/page") // 权限码 = 接口 URL
@GetMapping("/biz/xxx/page")
public CommonResult<Page<BizXxx>> page(BizXxxPageParam bizXxxPageParam) {
return CommonResult.data(bizXxxService.page(bizXxxPageParam));
}
@Operation(summary = "添加XXX")
@CommonLog("添加XXX") // 写操作必须加操作日志
@SaCheckPermission("/biz/xxx/add")
@PostMapping("/biz/xxx/add")
public CommonResult<String> add(@RequestBody @Valid BizXxxAddParam bizXxxAddParam) {
bizXxxService.add(bizXxxAddParam);
return CommonResult.ok();
}
@Operation(summary = "编辑XXX")
@CommonLog("编辑XXX")
@SaCheckPermission("/biz/xxx/edit")
@PostMapping("/biz/xxx/edit")
public CommonResult<String> edit(@RequestBody @Valid BizXxxEditParam bizXxxEditParam) {
bizXxxService.edit(bizXxxEditParam);
return CommonResult.ok();
}
@Operation(summary = "删除XXX")
@CommonLog("删除XXX")
@SaCheckPermission("/biz/xxx/delete")
@PostMapping("/biz/xxx/delete")
public CommonResult<String> delete(@RequestBody @Valid @NotEmpty(message = "集合不能为空")
List<BizXxxIdParam> bizXxxIdParamList) {
bizXxxService.delete(bizXxxIdParamList);
return CommonResult.ok();
}
@Operation(summary = "获取XXX详情")
@SaCheckPermission("/biz/xxx/detail")
@GetMapping("/biz/xxx/detail")
public CommonResult<BizXxx> detail(@Valid BizXxxIdParam bizXxxIdParam) {
return CommonResult.data(bizXxxService.detail(bizXxxIdParam));
}
}
```
## 第 6 件:Param 组(每操作一个类)
- **XxxPageParam**:current/size/sortField/sortOrder/searchKey 五个固定字段 + 业务查询字段(无校验注解)
- **XxxAddParam**:业务字段 + `@NotBlank(message = "xxx不能为空")` 等校验(jakarta.validation)
- **XxxEditParam**:同 AddParam + `@NotBlank private String id`
- **XxxIdParam**:只有 id
```java
@Getter
@Setter
public class BizXxxIdParam {
/** 主键 */
@Schema(description = "主键")
@NotBlank(message = "id不能为空")
private String id;
}
```
## 可选两件
- **Result**:仅需要投影/跨表组装返回时建 `result/XxxResult.java`(@Getter @Setter + @Schema);直接返回 Entity 是允许的
- **Mapper XML**:仅自定义 SQL;空 XML 不要建
## 前端三件(见 frontend-pc 技能详解)
1. `snowy-admin-web/src/api/biz/bizXxxApi.js` —— API 封装
2. `snowy-admin-web/src/views/biz/xxx/index.vue` —— 列表页(s-table)
3. `snowy-admin-web/src/views/biz/xxx/form.vue` —— 弹窗表单
## 常见错误正误对照
| ❌ 错误(RuoYi 惯性) | ✅ 正确(Snowy) |
|---|---|
| `@Autowired private XxxService x;` | `@Resource private XxxService x;` |
| `public class XxxServiceImpl implements IXxxService` | `extends ServiceImpl<XxxMapper, Xxx> implements XxxService` |
| `MapstructUtils.convert(bo, Xxx.class)` | `BeanUtil.toBean(param, Xxx.class)` |
| `@Data public class Xxx` | `@Getter @Setter public class Xxx` |
| `@RequestMapping("/biz/xxx")` 类级 + `@PostMapping("/list")` | 无类级注解,方法上直接 `@PostMapping("/biz/xxx/add")` |
| `@SaCheckPermission("biz:xxx:list")` | `@SaCheckPermission("/biz/xxx/page")` |
| `private Long id` / `@TableId(type = IdType.ASSIGN_ID)` Long | `@TableId private String id` |
| `throw new ServiceException("xxx")` | `throw new CommonException("xxx不存在,id值为:{}", id)` |
| 单个 XxxBo + AddGroup/EditGroup | XxxPageParam / XxxAddParam / XxxEditParam / XxxIdParam 四类 |
| `R.ok(data)` / `AjaxResult` | `CommonResult.data(data)` / `CommonResult.ok()` |
| `queryWrapper.eq("name", ...)` 字符串列名 | `queryWrapper.lambda().eq(Xxx::getName, ...)` |
## 检查清单(写完自查)
- [ ] 每个 .java 有 12 行版权头
- [ ] 包名 `vip.xiaonuo.biz.modular.xxx.*`,类名 Biz 前缀
- [ ] Entity 继承 CommonEntity,@TableName 大写,@TableId String
- [ ] ServiceImpl extends ServiceImpl 且写方法有 @Transactional(rollbackFor = Exception.class)
- [ ] 查询 QueryWrapper 带 checkSqlInjection(),条件全部判空
- [ ] Controller:@Tag/@RestController/@Validated,URL 动词式,@SaCheckPermission 值 = URL,写操作 @CommonLog
- [ ] Param 四类齐全,校验消息中文
- [ ] 所有注释/Javadoc/异常消息中文,Javadoc 有 @author @date
- [ ] 前端三件已同步生成
## 参考实现(真实代码,照这个写)
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/` | 最干净完整的六件套范本(entity/mapper/service/impl/controller/param 全有) |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/SysUserController.java` | 复杂版 Controller(含导出/禁用启用) |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java` | 复杂版 ServiceImpl(含 SM4 字段/事务/重复校验) |
| `snowy-common/src/main/java/vip/xiaonuo/common/page/CommonPageRequest.java` | 分页请求构造 |
| `snowy-common/src/main/java/vip/xiaonuo/common/enums/CommonSortOrderEnum.java` | 排序枚举 |
| `snowy-common/src/main/java/vip/xiaonuo/common/pojo/CommonEntity.java` | 实体基类(审计字段) |
+105
View File
@@ -0,0 +1,105 @@
---
name: crypto-sm
description: Snowy 国密体系使用规范:登录密码 SM2 加密传输、口令 SM3 摘要存储、字段级 SM4-CBC 落库加密(CommonSm4CbcTypeHandler)、CommonCryptogramUtil API、前端 smCrypto.js 联动。触发场景:1) 新增敏感字段需要加密存储 2) 处理登录/改密逻辑 3) 前后端加解密联调。触发词:国密、SM2、SM3、SM4、加密、解密、密码、敏感字段、手机号加密、密文、脱敏、CommonCryptogramUtil、TypeHandler、等保。
---
# Snowy 国密体系规范
## 三层国密架构
| 层 | 算法 | 用途 | 实现位置 |
|---|---|---|---|
| 传输 | **SM2**(非对称) | 登录密码前端加密 → 后端解密(防抓包明文) | 前端 `snowy-admin-web/src/utils/smCrypto.js`;后端 `CommonCryptogramUtil` |
| 摘要 | **SM3**(哈希) | 口令落库摘要(不可逆) | `SysPasswordUtil`(sys 插件)+ CommonCryptogramUtil |
| 字段 | **SM4-CBC**(对称) | 手机号/证件号等敏感字段落库加密 | `CommonSm4CbcTypeHandler`(snowy-common/handler) |
底座:sm-crypto 0.3.2 + BouncyCastle 1.70。软件层面满足等保测评要求。
## CommonCryptogramUtil API(snowy-common/util)
```java
// SM2 —— 登录密码传输
String cipher = CommonCryptogramUtil.sm2Encrypt(plain); // (前端做,后端一般只用解密)
String plain = CommonCryptogramUtil.sm2Decrypt(cipher); // 后端解密前端传来的密码密文
// SM3 —— 摘要
String digest = CommonCryptogramUtil.sm3Digest(plain);
// SM4 —— 通用对称加解密(字段加密底层也是它)
String enc = CommonCryptogramUtil.sm4Encrypt(plain);
String dec = CommonCryptogramUtil.sm4Decrypt(enc);
```
密钥来源:application.properties 的 `snowy.cryptogram.*` 配置段(SM2 公私钥对、SM4 key)。
## 字段级 SM4 加密(新增敏感字段的标准做法)
```java
@TableName(value = "BIZ_PATIENT", autoResultMap = true) // ① 必须 autoResultMap = true
public class BizPatient extends CommonEntity {
@TableId
private String id;
/** 手机号(SM4 加密落库) */
@TableField(typeHandler = CommonSm4CbcTypeHandler.class) // ② 加 TypeHandler
@Schema(description = "手机号")
private String phone;
}
```
效果:入库自动密文(`insert/update` 生效),查询结果自动解密透明返回。参考实战:`BizUser.phone / idCardNumber / emergencyPhone`(`biz/modular/user/entity/BizUser.java`)。
### ⚠️ SM4 字段的查询限制(必读)
密文是随机的:**like 模糊查询密文字段查不到任何结果**。处理方案:
- 精确查询:`queryWrapper.lambda().eq(BizPatient::getPhone, 手机号明文)` —— TypeHandler 会让 MP 用密文比对?**不会自动**,需要走 eq 前手动 `CommonCryptogramUtil.sm4Encrypt(phone)` 加密后比对(以 SysUserServiceImpl 现有手机号查询写法为准,模仿之)
- 模糊查询:另存脱敏/哈希辅助列(如 PHONE_HASH 存 SM3 摘要用于查重)
- 列表展示:自动解密无需处理;需要脱敏展示(138****5678)在前端或 Result 层处理
## 登录/改密流程(不要自造)
```
登录:前端 smCrypto.js 用 SM2 公钥加密密码 → /auth/b/login 密文上送
→ AuthServiceImpl sm2Decrypt → 与库中 SM3 摘要比对
改密/新建用户:SysPasswordUtil 加密(SM3)后存储
```
新业务涉及口令(如二级密码):同样 SM2 传输 + SM3 存储,直接复用 CommonCryptogramUtil / SysPasswordUtil。
## 前端联动
```js
// snowy-admin-web/src/utils/smCrypto.js
import { sm2 } from '@/utils/smCrypto'
const cipher = sm2.encrypt(密码明文, 公钥) // 公钥来自后端配置下发
```
抓包看到登录接口密码是 160 位十六进制密文是**正常的**。
## 禁止事项
- ❌ 明文存储/传输任何口令、身份证、手机号
- ❌ 自选 MD5/SHA-1/DES/AES(项目标准是国密)
- ❌ 把密钥硬编码在 Java/前端代码里(走配置)
- ❌ 日志输出明文敏感字段(log 里脱敏或干脆不打)
- ❌ 对 SM4 字段写 like 查询(永远查不到)
## 检查清单
- [ ] 新敏感字段:@TableName(autoResultMap=true) + @TableField(typeHandler=CommonSm4CbcTypeHandler.class)
- [ ] 该字段有精确/模糊查询需求时已评估方案(密文 eq / HASH 辅助列)
- [ ] 口令类只存 SM3 摘要,传输走 SM2
- [ ] 密钥在配置文件,未硬编码
- [ ] 日志无明文敏感信息
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-common/src/main/java/vip/xiaonuo/common/util/CommonCryptogramUtil.java` | 国密工具 API |
| `snowy-common/src/main/java/vip/xiaonuo/common/handler/CommonSm4CbcTypeHandler.java` | 字段加密 TypeHandler |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/user/entity/BizUser.java` | SM4 字段实战 |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java` | SM2 解密 + SM3 校验现场 |
| `snowy-admin-web/src/utils/smCrypto.js` | 前端国密 |
| `snowy-web-app/src/main/resources/application.properties` | snowy.cryptogram 密钥配置段 |
+152
View File
@@ -0,0 +1,152 @@
---
name: database-ops
description: Snowy 数据库设计规范:大写表名、BIZ_ 前缀、String 雪花主键、审计字段组、建表模板、菜单/按钮 SYS_RESOURCE SQL、SQL 脚本落点、多数据源。触发场景:1) 新功能设计建表 2) 写菜单/按钮资源 SQL 3) 查表结构或写查询 SQL 4) 配置数据源。触发词:数据库、建表、SQL、表设计、菜单SQL、SYS_RESOURCE、字典数据、主键、雪花、DELETE_FLAG、数据源、dynamic。
---
# Snowy 数据库设计规范
## 命名规范
| 项 | 规则 | 示例 |
|---|---|---|
| 表名 | **全大写下划线**,前缀 = 归属插件 | `BIZ_SUPPLIER`、`SYS_USER`、`DEV_CONFIG` |
| 字段名 | 全大写下划线 | `SUPPLIER_NAME`、`CREATE_TIME` |
| 新业务表前缀 | **`BIZ_`**(出厂 biz 域复用 SYS_/DEV_ 表是历史设计,**新表不要模仿**) | `BIZ_ORDER` |
| 主键 | `ID varchar(20)` 字符串雪花(MyBatis-Plus ASSIGN_ID) | ❌ bigint 自增 |
| 每表必有 | 表 COMMENT + 每字段 COMMENT(**中文**) | — |
| 字符集 | utf8mb4 / utf8mb4_general_ci,InnoDB | — |
## 标准建表模板(复制改业务字段)
```sql
CREATE TABLE `BIZ_XXX` (
`ID` varchar(20) NOT NULL COMMENT '主键',
-- ↓↓↓ 业务字段区(全大写,带中文 COMMENT)↓↓↓
`NAME` varchar(100) NULL DEFAULT NULL COMMENT '名称',
`STATUS` varchar(10) NULL DEFAULT NULL COMMENT '状态',
-- ↓↓↓ 尾部固定字段组(顺序保持一致)↓↓↓
`SORT_CODE` int(11) NULL DEFAULT NULL COMMENT '排序',
`REMARK` varchar(500) NULL DEFAULT NULL COMMENT '备注',
`EXT_JSON` longtext NULL COMMENT '扩展信息',
`DELETE_FLAG` varchar(255) NULL DEFAULT NULL COMMENT '删除标志',
`CREATE_TIME` datetime NULL DEFAULT NULL COMMENT '创建时间',
`CREATE_USER` varchar(20) NULL DEFAULT NULL COMMENT '创建用户',
`UPDATE_TIME` datetime NULL DEFAULT NULL COMMENT '更新时间',
`UPDATE_USER` varchar(20) NULL DEFAULT NULL COMMENT '更新用户',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'XXX表' ROW_FORMAT = Dynamic;
```
要点:
- 尾部字段组与 `CommonEntity` 一一对应:DELETE_FLAG(逻辑删除,值 NOT_DELETE/DELETED)、CREATE_TIME/CREATE_USER/UPDATE_TIME/UPDATE_USER(自动填充,代码里**不要**手动赋值)、SORT_CODE(默认排序)、REMARK、EXT_JSON(扩展 JSON)
- 树形表加 `PARENT_ID varchar(20) NULL COMMENT '父id'`
- 类型映射:String→varchar(n)/text、Integer→int、BigDecimal→decimal(总长,小数)、日期时间→datetime、大文本→longtext
- 需要加密落库的字段(手机号等)正常建 varchar,加密由 `CommonSm4CbcTypeHandler` 在应用层做(密文更长,长度适当放宽)
## 逻辑删除
- 字段 `DELETE_FLAG`,有效值 `NOT_DELETE` / `DELETED`(见 `CommonDeleteFlagEnum`)
- Entity 继承 CommonEntity 自动带 @TableLogic,查询自动过滤、removeById 自动改 UPDATE
- 代码中**禁止**手写 `delete_flag` 条件、**禁止**物理 delete 语句(物理删除有专门的 CommonDeleteAbsoluteMapper,慎用)
- ⚠️ 唯一索引与逻辑删除冲突时:不要建数据库唯一索引,改为 Service 层查询判重
## 菜单与按钮资源 SQL(SYS_RESOURCE)
新功能的菜单 + 按钮权限写 INSERT(权威格式见 gen 插件 sqlend 模板):
```sql
-- 菜单(MENU 行)
INSERT INTO `SYS_RESOURCE` VALUES ('菜单id雪花串', '父目录id', '供应商管理', 'supplier', '菜单编码', 'MENU', '所属模块id', 'MENU', '/biz/supplier', 'biz/supplier/index', '图标', NULL, 'YES', 'YES', 'YES', 99, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
-- 按钮(BUTTON 行,parent = 菜单id;code = 驼峰按钮码,前端 hasPerm 用)
INSERT INTO `SYS_RESOURCE` VALUES ('按钮id雪花串', '菜单id雪花串', '新增供应商', NULL, 'bizSupplierAdd', 'BUTTON', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `SYS_RESOURCE` VALUES ('...', '菜单id雪花串', '编辑供应商', NULL, 'bizSupplierEdit', 'BUTTON', ...);
INSERT INTO `SYS_RESOURCE` VALUES ('...', '菜单id雪花串', '删除供应商', NULL, 'bizSupplierDelete', 'BUTTON', ...);
```
- 按钮码固定集合:`{classNameFirstLower}Add / Edit / Delete / Detail / BatchDelete / Import / Export`(+ 业务动作码如 `UpdateStatus`)
- 需要字典数据时:`INSERT INTO DEV_DICT`(系统字典)或 BIZ 字典管理界面维护
- 执行后:重启后端 → 角色管理给角色授权 → 前端可见
### 菜单挂载点速查(出厂种子的真实 ID,写 SQL 直接用)
新业务功能默认挂 **业务模块**(MODULE_ID=`1548901111999773976`):
| 挂载点 | ID | 说明 |
|---|---|---|
| 业务模块(MODULE) | `1548901111999773976` | 新业务菜单的 MODULE_ID 用这个 |
| 公司架构目录(CATALOG) | `1548901111999773977` | 组织/人员类业务挂这个目录下 |
| 业务模块顶层 | PARENT_ID 填 `'0'` | 通用做法:独立业务直接挂模块顶层(同"通知公告""业务字典") |
系统模块(`1548901111999770525`)下的目录仅平台功能使用:组织架构 `1548901111999770726`、权限管控 `1548901111999771126`、基础工具 `1548901111999771626`、系统运维 `1548901111999772126`、在线开发 `1548901111999773250`。
> 依据:`snowy_mysql.sql` 种子数据(全新导入即生效)。若库已被人工调整过,用 `SELECT ID, TITLE, CATEGORY, P_ID FROM SYS_RESOURCE` 核实后再用。前端菜单 component 路径 `biz/{域名}/index` 必须与 views 目录一致。
## 数据库连接与多数据源
- 连接配置:`snowy-web-app/src/main/resources/application.properties` 的 `spring.datasource.dynamic.master` 段(**AI 需要连接串时从这里动态解析,禁止硬编码**)
- 多数据源:dynamic-datasource,默认 master;pgSql/oracle/oracleLake/mssql/dm/kingbase 配置已注释预留,取消注释即可加库
- Druid 监控页:`/druid`(账号见配置)
- 初始化脚本:`snowy-web-app/src/main/resources/_sql/snowy_mysql.sql`(33 张表);新增表的 DDL 也建议在 `_sql/` 下追加增量脚本文件
## AI 执行 SQL 的方式(降级链)
1. mysql CLI 可用 → 直接执行(从 application.properties 解析连接)
2. CLI 不可用 → SQL 写入 `docs/sql-pending/{日期}-{功能}.sql`,明确提示用户手动执行
3. ❌禁止在没有明确用户确认时执行 DROP/TRUNCATE/ALTER 之类破坏性语句
## AI 自检 SQL 集(开发流程中随用随查)
```sql
-- 表建好了吗
SHOW TABLES LIKE 'BIZ_XXX';
-- 表结构(写 Entity 前核对字段)
SHOW CREATE TABLE BIZ_XXX;
-- 菜单/按钮插进去了吗(配合挂载点速查的 ID)
SELECT ID, TITLE, CATEGORY, PARENT_ID, CODE FROM SYS_RESOURCE WHERE CODE LIKE 'bizXxx%' OR ID = '菜单id';
-- 按钮码与前端 hasPerm 是否一一对应
SELECT CODE, TITLE FROM SYS_RESOURCE WHERE CATEGORY = 'BUTTON' AND PARENT_ID = '菜单id';
-- 验证落库与逻辑删除(配合 api-verify 接口自测)
SELECT ID, NAME, DELETE_FLAG, CREATE_TIME FROM BIZ_XXX ORDER BY CREATE_TIME DESC LIMIT 3;
-- 字典建好了吗
SELECT * FROM DEV_DICT WHERE CODE = 'XXX_DICT_CODE';
-- 默认密码配置(出厂值 Snowy@2026!)
SELECT CONFIG_VALUE FROM DEV_CONFIG WHERE CONFIG_KEY = 'SNOWY_SYS_DEFAULT_PASSWORD_FOR_B';
```
## 现有 33 表速查(按前缀分组)
| 前缀 | 表 |
|---|---|
| SYS_ | SYS_USER、SYS_USER_EXT、SYS_ORG、SYS_POSITION、SYS_ROLE、SYS_RESOURCE、SYS_MODULE、SYSRelation 相关(SYS_USER_ROLE、SYS_ROLE_MENU 等)、SYS_USER_DATA_SCOPE(_MAP)、SYS_GROUP、SYS_GROUP_USER |
| BIZ_ | BIZ_NOTICE |
| DEV_ | DEV_CONFIG、DEV_DICT、DEV_EMAIL、DEV_FILE、DEV_JOB、DEV_LOG、DEV_MESSAGE、DEV_PUSH、DEV_SMS、DEV_SLIDESHOW、DEV_WEAK_PASSWORD |
| GEN_ | GEN_BASIC、GEN_CONFIG |
| CLIENT_ | CLIENT_USER |
| MOBILE_ | MOBILE_RESOURCE、(移动端模块表) |
| AUTH_ | AUTH_*(三方登录/关系相关) |
(准确清单以 `_sql/snowy_mysql.sql` 为准,可 grep `CREATE TABLE`)
## 检查清单
- [ ] 表名 BIZ_ 前缀全大写,有表 COMMENT
- [ ] ID varchar(20) 主键,非自增
- [ ] 尾部审计字段组齐全且顺序标准
- [ ] 每字段中文 COMMENT
- [ ] 树形表有 PARENT_ID;一对多有外键字段(varchar(20))
- [ ] 菜单/按钮 SQL 与代码 URL、前端 hasPerm 码一致
- [ ] DDL 增量脚本落 `_sql/`
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-web-app/src/main/resources/_sql/snowy_mysql.sql` | 全部建表语句(BIZ_NOTICE 是标准范本) |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/entity/BizNotice.java` | Entity 与表的映射范本 |
| `snowy-common/src/main/java/vip/xiaonuo/common/pojo/CommonEntity.java` | 审计字段基类 |
| `snowy-common/src/main/java/vip/xiaonuo/common/enums/CommonDeleteFlagEnum.java` | 逻辑删除枚举 |
| `snowy-plugin/snowy-plugin-gen/src/main/resources/sqlend/Mysql.sql.btl` | 菜单/按钮 SQL 权威模板 |
| `snowy-web-app/src/main/resources/application.properties` | 数据源配置 |
+130
View File
@@ -0,0 +1,130 @@
---
name: dict-config
description: Snowy 字典/配置/枚举三件套规范:DEV_DICT 系统字典与 BIZ 业务字典双轨、前端 DictSelect 组件、@Trans 字典翻译、业务枚举类规范、DEV_CONFIG 系统配置。触发场景:1) 字段需要下拉选项(状态/类型)2) 需要系统参数配置 3) 定义业务枚举。触发词:字典、dict、DEV_DICT、BIZ_DICT、DictSelect、下拉、枚举、enum、系统配置、DEV_CONFIG、参数配置。
---
# Snowy 字典 / 配置 / 枚举规范
## 三种"可选项"怎么选
| 场景 | 用什么 | 存哪 |
|---|---|---|
| 选项需要最终用户在界面维护 | **字典**(DEV_DICT / BIZ 字典) | 数据库 |
| 选项与代码逻辑强绑定(不同值走不同分支) | **枚举类**(modular/{域}/enums/) | Java 代码 |
| 运行时可调的参数(开关/阈值) | **系统配置** DEV_CONFIG | 数据库(界面改) |
## 字典双轨
| 轨道 | 模块 | 用途 | 管理界面 |
|---|---|---|---|
| 系统字典 | `dev/modular/dict/`(DEV_DICT 表) | 平台级通用字典(性别 GENDER、通知类型等);`/dev/dict/tree` 免登录(前端登录页也要用) | 开发工具 → 字典管理 |
| 业务字典 | `biz/modular/dict/`(BIZ 字典服务) | 业务自定义字典(面向最终用户可维护) | 业务功能 → 业务字典 |
**新业务字典优先建业务字典**(不污染平台字典);平台级通用(性别这类)才进 DEV_DICT。
## 字典使用全链路
```
1. 界面建字典(编码如 SUPPLIER_TYPE,子项 值+标签+排序)
2. Entity 字段存字典"值"(String):
@Schema(description = "供应商类型")
private String type;
3. 列表返回需要显示文本时,Entity 加 @Trans 翻译(向后兼容多一个冗名字段):
@Trans(type = TransType.DICTIONARY, key = "SUPPLIER_TYPE")
private String type;
@TableField(exist = false)
private String typeName; // 注意:@Trans DICTIONARY 自动生成翻译;SIMPLE 才用 ref
4. 前端下拉用 DictSelect 组件(自动拉字典并缓存):
<dict-select v-model:value="formData.type" dict-type-code="SUPPLIER_TYPE" />
5. 查询条件:PageParam 的 type 字段 eq 精确匹配
```
## 业务枚举规范(modular/{域}/enums/)
```java
/*
* ... 版权头
*/
package vip.xiaonuo.biz.modular.supplier.enums;
import lombok.Getter;
import vip.xiaonuo.common.exception.CommonException;
/**
* 供应商状态枚举
*
* @author 你的名字
* @date 2026/08/18 10:00
*/
@Getter
public enum BizSupplierStatusEnum {
/** 启用 */
ENABLE("ENABLE"),
/** 禁用 */
DISABLE("DISABLE");
private final String value;
BizSupplierStatusEnum(String value) {
this.value = value;
}
/** 校验值合法性(入参来自前端时校验用) */
public static void validate(String value) {
boolean flag = ENABLE.getValue().equals(value) || DISABLE.getValue().equals(value);
if(!flag) {
throw new CommonException("不支持的状态:{}", value);
}
}
}
```
- 纯枚举 + `@Getter` + `private final String value` + 构造器(**无公共基类**);需要校验时加静态 validate
- 命名 `Xxx{含义}Enum`,值用 String 大写(与字典值风格一致)
- 代码里取值 `BizSupplierStatusEnum.ENABLE.getValue()`,**禁止魔法字符串**散落
- 参考现有:`BizNoticeStatusEnum`(`biz/modular/notice/enums/`)
## 系统配置 DEV_CONFIG
- 界面:开发工具 → 系统配置(键值对,分组)
- 读取:
```java
@Resource
private DevConfigApi devConfigApi;
String value = devConfigApi.getConfigValueByKey("BIZ_XXX_SWITCH");
```
- ❌ 不要把可调参数写死在代码或 application.properties(改起来要重启)
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| 状态字段用 Integer 0/1 魔法值 | String + 枚举/字典(与平台风格一致) |
| 前端手写 options 数组存字典项 | DictSelect 组件(统一缓存) |
| 可调开关写死代码 | DEV_CONFIG + DevConfigApi |
| 平台字典里建业务选项 | 业务字典(biz) |
| 字典改了页面不刷新 | 前端字典有缓存,刷新页面/重新拉取;后端翻译缓存走数据变更事件 |
## 检查清单
- [ ] 可维护选项进字典(业务字典优先),逻辑绑定选项进枚举
- [ ] Entity 翻译字段用 @Trans,冗余字段 @TableField(exist = false)
- [ ] 前端 DictSelect 的 dict-type-code 与字典编码一致
- [ ] 枚举实现 CommonEnum,无魔法字符串
- [ ] 运行时参数走 DEV_CONFIG
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/dict/` | 系统字典模块 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/dict/` | 业务字典模块 |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/notice/enums/BizNoticeStatusEnum.java` | 枚举范本 |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/DevConfigApi.java` | 配置读取接口 |
| `snowy-admin-web/src/components/DictSelect/` | 字典下拉组件(含 README) |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/modular/user/entity/BizUser.java` | @Trans DICTIONARY 实战 |
+74
View File
@@ -0,0 +1,74 @@
---
name: env-setup
description: Snowy 新环境搭建与首次启动 playbook:JDK/MySQL/Redis/前端依赖/建库导入/启动验证全清单,含出厂账号真实密码。触发场景:1) 新机器/新同事把项目跑起来 2) 环境重置后重新初始化 3) 启动前的就绪自检 4) 查出厂登录账号密码。触发词:环境搭建、首次启动、跑起来、初始化、安装依赖、导入数据库、建库、登录不了、默认密码、出厂账号、superAdmin、新环境、就绪检查。注意:启动后的故障排查见 bug-detective;本技能讲"从零到能登录"。
---
# Snowy 环境搭建与首启
## 就绪清单(按顺序)
| # | 依赖 | 要求 | 验证 |
|---|---|---|---|
| 1 | JDK | **17**(⚠️ 本机只有 IDEA 内置 JBR 25 时命令行编译会因 Lombok 1.18.30 失败,IDEA 里配 Project SDK 17 构建即可) | `java -version` |
| 2 | MySQL | 8.0/5.7,建库 `snowy`(utf8mb4) | `mysql -e "SHOW DATABASES LIKE 'snowy'"` |
| 3 | 导入种子数据 | 执行 `snowy-web-app/src/main/resources/_sql/snowy_mysql.sql`(33 张表) | `mysql snowy -e "SELECT COUNT(*) FROM SYS_USER"` 应 ≥2 |
| 4 | Redis | 本地 6379,无密码,用 database 1 | `redis-cli -n 1 ping` |
| 5 | 数据源核对 | `snowy-web-app/src/main/resources/application.properties` 的 dynamic master 段(默认 root/12345678,本机不同则改) | — |
| 6 | 后端构建启动 | IDEA 启动 `vip.xiaonuo.Application`(snowy-web-app),端口 **82** | 浏览器开 `http://localhost:82` 应显示 WELCOME |
| 7 | 前端依赖 | `snowy-admin-web/` 下 `npm install` | — |
| 8 | 前端启动 | `npm run dev`,端口 **81**,代理 /api → 82 | 打开 `http://localhost:81` |
## ★ 出厂登录账号(本仓库种子数据的真实值)
| 账号 | 密码 | 角色 |
|---|---|---|
| `superAdmin` | **`Snowy@2026!`** | 超级管理员(拥有全部权限) |
| `bizAdmin` | **`Snowy@2026!`** | 业务管理员(仅业务模块) |
> ⚠️ **不是 123456**——网上教程/官方演示站说的 123456 是 xiaonuo.vip 演示库的密码。本仓库种子密码的权威来源是 DEV_CONFIG 表 `SNOWY_SYS_DEFAULT_PASSWORD_FOR_B` 配置项(新建用户的默认密码也是它,可在 开发工具→系统配置 里改)。存储为 SM3 摘要:SM3("Snowy@2026!") = b7eb53ce42289cd168e21e37cc3b94333c3e2e691486548a06f5c5fae129f157。
> 登录失败次数过多会锁定(PWD_ERROR 处理逻辑),连不上先确认大小写与感叹号。
## 常用自检命令
```bash
# 端口占用(82 后端 / 81 前端)
netstat -ano | findstr ":82 :81"
# 后端活着吗(应输出 WELCOME)
curl -s http://localhost:82/
# 接口文档(basic 认证 admin/123456)
curl -s -u admin:123456 http://localhost:82/doc.html | head -5
# 表齐了吗
mysql -uroot -p snowy -e "SELECT COUNT(*) AS tables_cnt FROM information_schema.tables WHERE table_schema='snowy';"
```
## AI 首启引导流程(用户说"跑不起来/帮我搭环境"时)
1. 按就绪清单逐项检查,从失败的那项开始修
2. 常见首启故障(详见 bug-detective 树 A):端口占用 / 库没导 / Redis 没起 / JDK 版本 / 口令不符
3. 全部就绪后:打开前端 → superAdmin / Snowy@2026! 登录 → 看到首页即完成
4. 登录成功后建议用户立刻改密码(系统管理 → 用户中心)
## 新增依赖说明
- Maven 中央仓库直连失败时用阿里云镜像(settings.xml mirrorOf=central → maven.aliyun.com/repository/public)
- npm 失败时用 npmmirror:`npm install --registry=https://registry.npmmirror.com`
## 检查清单
- [ ] 8 项就绪清单全过
- [ ] 登录成功(superAdmin / Snowy@2026!)
- [ ] 提醒用户改默认密码
- [ ] 本机偏离默认的配置(数据库口令等)已回写 application.properties 且未提交明文口令变更
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-web-app/src/main/resources/application.properties` | 数据源/Redis/端口/文档认证全配置 |
| `snowy-web-app/src/main/resources/_sql/snowy_mysql.sql` | 种子数据(账号/菜单/DEV_CONFIG 默认密码) |
| `snowy-web-app/src/main/java/vip/xiaonuo/Application.java` | 启动类 |
| `snowy-admin-web/package.json` | 前端依赖与脚本 |
| `CLAUDE.md` 常用命令节 | 构建环境备忘(IDEA 内置 Maven/镜像/JDK17) |
@@ -0,0 +1,83 @@
---
name: file-oss-management
description: Snowy 文件上传下载与对象存储规范:DevFile 模块多后端上传 API、本地/OSS/COS/MinIO 配置、前端 XnUpload/CropUpload 组件联动。触发场景:1) 业务需要上传文件/图片/附件 2) 下载或预览文件 3) 配置或切换文件存储后端。触发词:文件上传、上传、下载、OSS、MinIO、阿里云、腾讯云、对象存储、XnUpload、附件、文件预览、DEV_FILE。
---
# Snowy 文件管理规范
## 架构
- 存储:x-file-storage 框架,多后端可切换(本地磁盘 / 阿里云 OSS / 腾讯云 COS / MinIO / rustfs)
- 后端模块:`snowy-plugin-dev` 的 `modular/file/`(controller/entity/mapper/param/service/util/provider),元数据落 `DEV_FILE` 表
- 前端组件:`XnUpload`(通用上传)、`CropUpload`(头像裁剪)、`XnFilePreview`(预览)
- 管理界面:开发工具 → 文件管理
## 上传 API(DevFileController,路径前缀 /dev/file/)
按**后端** × **返回值** 组合成对:`upload{Backend}Return{Id|Url}`:
| 方法 | 返回 |
|---|---|
| `uploadDynamicReturnId / uploadDynamicReturnUrl` | 按系统默认存储(DEV 配置的动态后端),返回文件 id / 可访问 URL |
| `uploadImageDynamicReturnId / Url` | 图片专用(校验图片格式) |
| `uploadDocumentDynamicReturnId / Url` | 文档专用 |
| `uploadLocalReturnId / Url` | 强制本地存储 |
| `uploadAliyunReturnId / Url`、`uploadTencentReturnId / Url`、`uploadMinioReturnId / Url` 等 | 指定后端 |
参数一律 `@RequestPart("file") MultipartFile file`。
**返回 id 还是 Url**:需要后续管理/鉴权(可撤销、可追踪)→ 存 id(String 落业务表);仅需展示 → Url。**推荐业务表存 id**(形如 `avatar`、`imageId` 字段),展示时经文件接口换 URL,可防直链失效。
## 业务代码用法
```java
// 一般不在后端手写上传逻辑——前端走 XnUpload 组件直接调 /dev/file/upload*,拿到 id 存业务表
// 后端需要主动上传时:
@Resource
private DevFileApi devFileApi; // 跨插件走 dev-api
```
下载/预览:`/dev/file/download`、`/dev/file/authDownload`(鉴权下载,void + HttpServletResponse,见 DevFileController 尾部方法)。
## 前端联动(详见 frontend-pc 技能)
```vue
<XnUpload
:upload-number="1"
v-model:value="formData.imageId" <!-- 业务字段存文件 id -->
/>
```
## 存储后端配置
- 本地/各云的连接参数:`application.properties` 的 `snowy.file-storage.*` 段 + 开发工具→文件管理里配置(存 DEV_CONFIG)
- 切换默认后端:文件管理界面设置;改配置后无需重启(动态)
- 上传大小限制:`spring.servlet.multipart.max-file-size=100MB`
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| 业务 Controller 自己写 MultipartFile 落盘逻辑 | 走 dev/file 上传接口(本地存储或云) |
| 业务表存完整外链 URL(换桶全断) | 存文件 id,展示时换 URL |
| 前端手写 axios FormData 上传 | XnUpload / CropUpload 组件 |
| 下载接口手写 response 输出流 | `/dev/file/download` 或 CommonDownloadUtil |
| 把 DEV_FILE 当业务附件表用 | 业务附件关系建自己的表(存 fileId 外键) |
## 检查清单
- [ ] 上传走 dev/file 标准接口,未自造落盘
- [ ] 业务表存的是文件 id(varchar)
- [ ] 前端用 XnUpload(头像用 CropUpload)
- [ ] 涉及私有文件的下载用 authDownload(鉴权)
- [ ] 上传格式/大小有约束(图片接口自动校验图片)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/file/controller/DevFileController.java` | 全部上传/下载 API |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/file/service/impl/DevFileServiceImpl.java` | 存储逻辑 |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/DevFileApi.java` | 跨插件文件接口 |
| `snowy-admin-web/src/components/XnUpload/` | 上传组件(含 README) |
| `snowy-admin-web/src/components/CropUpload/` | 裁剪上传组件 |
+294
View File
@@ -0,0 +1,294 @@
---
name: frontend-pc
description: snowy-admin-web 前端开发规范:api js 封装约定、index.vue 列表页 + form.vue 弹窗表单三件套、s-table、Xn 组件速查、hasPerm 按钮权限、i18n、request.js 双端。触发场景:1) 写前端页面或 API 封装 2) 新业务模块的前端三件 3) 使用组件/权限/字典。触发词:前端、Vue、页面、api js、index.vue、form.vue、s-table、Xn组件、Ant Design Vue、AntdV、按钮权限、hasPerm、i18n、国际化、弹窗表单。
---
# snowy-admin-web 前端开发规范
## 技术栈与铁律
- Vue 3.5(`<script setup>` 语法)+ Vite 6 + **Ant Design Vue 4.2.6** + Pinia + vue-i18n + TailwindCSS
- **JavaScript,不是 TypeScript**——禁止 interface/type/as 等 TS 语法
- 组件自动导入(unplugin):a-xxx 组件、ref/computed 等 API **不需要 import**;自定义组件用 kebab-case 标签
- 包管理 npm;目录 `snowy-admin-web/`
## 三件套结构(一个业务域)
```
snowy-admin-web/src/
├── api/biz/bizXxxApi.js ① API 封装
└── views/biz/xxx/
├── index.vue ② 列表页
├── form.vue ③ 弹窗表单
└── detail.vue 详情(可选)
```
## ① API 封装模板(bizNoticeApi.js 为范本)
```js
import { baseRequest } from '@/utils/request'
const request = (url, ...arg) => baseRequest(`/biz/xxx/` + url, ...arg)
/**
* XXX Api接口管理器
*
* @author 你的名字
* @date 2026/08/18 10:00
**/
export default {
// 获取XXX分页
xxxPage(data) {
return request('page', data, 'get')
},
// 提交表单 edit为true时为编辑,默认为新增
xxxSubmitForm(data, edit = false) {
return request(edit ? 'edit' : 'add', data)
},
// 删除XXX
xxxDelete(data) {
return request('delete', data)
},
// 获取XXX详情
xxxDetail(data) {
return request('detail', data, 'get')
}
}
```
- 第三参 method,默认 POST;GET 查询传 'get'
- 文件名 `biz{Xxx}Api.js`;缩进用 **Tab**(项目 Prettier 配置)
- 下载文件用 baseRequest 的第 4 参 `{ responseType: 'blob' }`
## ② 列表页 index.vue 骨架
```vue
<template>
<a-card>
<a-form ref="searchFormRef" :model="searchFormState" layout="inline">
<!-- 搜索区:a-input / a-select / dict-select / a-range-picker -->
</a-form>
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:alert="false"
bordered
:row-key="(record) => record.id"
:tool-config="toolConfig"
:row-selection="rowSelection"
>
<template #operator>
<a-space>
<a-button type="primary" @click="formRef.onOpen()" v-if="hasPerm('bizXxxAdd')">新增</a-button>
<a-button danger @click="deleteBatchBizXxx()" v-if="hasPerm('bizXxxBatchDelete')">批量删除</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a @click="formRef.onOpen(record)" v-if="hasPerm('bizXxxEdit')">编辑</a>
<a-divider type="vertical" v-if="hasPerm(['bizXxxEdit', 'bizXxxDelete'], 'and')" />
<a-popconfirm title="确定删除吗?" @confirm="deleteBizXxx(record)">
<a-button type="link" danger size="small" v-if="hasPerm('bizXxxDelete')">删除</a-button>
</a-popconfirm>
</template>
</template>
</s-table>
</a-card>
</template>
<script setup name="xxx">
import tool from '@/utils/tool'
import { cloneDeep } from 'lodash-es'
import Form from './form.vue'
import bizXxxApi from '@/api/biz/bizXxxApi'
const searchFormState = ref({})
const searchFormRef = ref()
const tableRef = ref()
const formRef = ref()
const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
const columns = [
{ title: '名称', dataIndex: 'name' },
{ title: '排序', dataIndex: 'sortCode', width: 100 },
{ title: '操作', dataIndex: 'action', align: 'center', width: 150 }
]
const loadData = (parameter) => {
const searchFormParam = cloneDeep(searchFormState.value)
// 时间范围重载:range-picker 的数组拆成 start/end 两个字段
if (searchFormParam.createTime) {
searchFormParam.startCreateTime = searchFormParam.createTime[0]
searchFormParam.endCreateTime = searchFormParam.createTime[1]
delete searchFormParam.createTime
}
return bizXxxApi.xxxPage(Object.assign(parameter, searchFormParam)).then((data) => data)
}
const deleteBizXxx = (record) => {
bizXxxApi.xxxDelete([{ id: record.id }]).then(() => tableRef.value.refresh(true))
}
const deleteBatchBizXxx = (params) => {
bizXxxApi.xxxDelete(params).then(() => tableRef.value.clearRefreshSelected())
}
formRef // 模板引用(保持命名与 ref 一致)
loadData
deleteBizXxx
deleteBatchBizXxx
</script>
```
要点:
- `<script setup name="xxx">` 带 name(keep-alive 需要)
- `s-table` 的 `:data="loadData"` 传函数(不是数组),内部自动管理分页参数
- 刷新:`tableRef.value.refresh(true)`(回到第一页)/ `clearRefreshSelected()`(批量删后)
- 时间范围查询必须拆 startCreateTime/endCreateTime(与后端 PageParam 对应)
## ③ 弹窗表单 form.vue 骨架
```vue
<template>
<xn-form-container
:title="formData.id ? '编辑XXX' : '增加XXX'"
:width="1000"
v-model:open="open"
:destroy-on-close="true"
@close="onClose"
>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-row :gutter="16">
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<a-form-item label="名称:" name="name">
<a-input v-model:value="formData.name" placeholder="请输入名称" allow-clear />
</a-form-item>
</a-col>
<!-- 更多字段… -->
</a-row>
</a-form>
<template #footer>
<a-button type="primary" :loading="submitLoading" @click="onSubmit">提交</a-button>
<a-button @click="onClose">取消</a-button>
</template>
</xn-form-container>
</template>
<script setup name="xxxForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import bizXxxApi from '@/api/biz/bizXxxApi'
const open = ref(false)
const emit = defineEmits({ successful: null })
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
// 打开(父组件 formRef.onOpen(record) 调用;record 为空 = 新增)
const onOpen = (record) => {
open.value = true
if (record) {
formData.value = Object.assign({}, cloneDeep(record))
} else {
formData.value = { sortCode: 99 } // 默认值
}
}
const onClose = () => {
formRef.value.resetFields()
formData.value = {}
open.value = false
}
const formRules = {
name: [required('请输入名称')]
}
const onSubmit = () => {
formRef.value.validate().then(() => {
submitLoading.value = true
bizXxxApi
.xxxSubmitForm(cloneDeep(formData.value), formData.value.id)
.then(() => {
onClose()
emit('successful') // 通知父组件刷新列表
})
.finally(() => (submitLoading.value = false))
})
}
defineExpose({ onOpen }) // ★ 必须,父组件才能调 onOpen
</script>
```
要点:提交成功 `emit('successful')`;数组字段(多选)提交前 `JSON.stringify`(参考 notice 的 place);字典选项 `tool.dictList('BIZ_XXX_TYPE')`。
## 按钮权限
```vue
v-if="hasPerm('bizXxxAdd')" // 单码
v-if="hasPerm(['bizXxxEdit', 'bizXxxDelete'], 'and')" // 数组 + and/or(第二参默认 'or')
```
驼峰码来自 SYS_RESOURCE 的 BUTTON 行,与后端 @SaCheckPermission 的 URL 式是**两套**(详见 security-auth 技能)。
## 高频组件速查(src/components/,37 个)
| 组件 | 用途 |
|---|---|
| `s-table` | 列表表格(分页自动) |
| `xn-form-container` | 表单弹窗容器(新增/编辑通用壳) |
| `dict-select` | 字典下拉:`<dict-select v-model:value="x" dict-type-code="BIZ_XXX" />` |
| `xn-upload` | 上传(uploadMode="image";uploadDynamicReturnUrlApi 指定后端接口) |
| `xn-editor` / `xn-md-editor` | 富文本 / Markdown |
| `xn-user-selector` / `xn-org-selector` / `xn-role-selector` / `xn-position-selector` / `xn-group-selector` | 各类选择器 |
| `xn-tree-select` / `tree-select` | 树选择(组织/分类) |
| `xn-file-preview` | 文件预览 |
| `xn-sign-name` | 手写签名 |
| `cron` | Cron 表达式生成 |
| `crop-upload` | 头像裁剪上传 |
| `xn-resizable-panel` | 可拖拽分栏(左树右表布局用) |
(多数组件目录带 README.md,用前可读)
## 请求与工具
- `src/utils/request.js`:`baseRequest`(B 端);`src/utils/clientRequest.js`(C 端);token 请求头名 `token`;code!==200 自动 message.error;add/edit/delete 成功自动提示
- `src/utils/tool.js`:`tool.dictList('编码')` 取字典、`tool.dataToTree()` 树化等
- `src/utils/formRules.js`:`required('提示')` 校验工厂
- 国密:`src/utils/smCrypto.js`(登录密码 SM2 加密)
## i18n
页面文案 `$t('xxx.yyy')`,词条加 `src/locales/{zh-cn,en-us}/` 对应文件。**遵循现有 i18n 习惯**;纯业务管理页的动态数据(字典/菜单)不走 i18n。
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| TypeScript 语法 | 纯 JS |
| Element Plus(el-xxx)组件 | Ant Design Vue(a-xxx) |
| `import { ref } from 'vue'` | 自动导入,不写 |
| 自己 axios.get('/api/biz/xxx') | api js + baseRequest 封装 |
| `:data="tableData"` 数组 | s-table `:data="loadData"` 函数 |
| 表单不用容器直接弹 a-modal | `xn-form-container` |
| 忘 `defineExpose({ onOpen })` | 父组件打不开表单 |
| 硬编码中文文案不进 i18n(公共区域) | $t + locales |
## 检查清单
- [ ] 三件套齐全(api js / index.vue / form.vue)
- [ ] api js 的 URL 前缀与后端一致(/biz/xxx/)
- [ ] s-table loadData 模式 + 时间范围拆分
- [ ] 按钮权限 hasPerm 码与 SYS_RESOURCE BUTTON 行一致
- [ ] 提交成功 emit('successful')
- [ ] 无 TS / Element Plus 残留;Tab 缩进
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-admin-web/src/api/biz/bizNoticeApi.js` | API 封装范本 |
| `snowy-admin-web/src/views/biz/notice/index.vue` | 列表页范本(含批量删/状态切换/字典) |
| `snowy-admin-web/src/views/biz/notice/form.vue` | 表单范本(含上传/富文本/多选序列化) |
| `snowy-admin-web/src/views/biz/notice/detail.vue` | 详情范本 |
| `snowy-admin-web/src/utils/request.js` | 请求封装 |
| `snowy-admin-web/src/utils/tool.js` | 前端工具 |
| `snowy-admin-web/src/components/` | 全部组件 |
+66
View File
@@ -0,0 +1,66 @@
---
name: git-workflow
description: 本项目 Git 规范:提交四原则、conventional commit 中文格式、仓库状态检测与降级、不提交清单。触发场景:1) 提交代码 2) 用户要求 push/分支操作 3) 检查仓库状态。触发词:Git、提交、commit、push、分支、merge、回退、暂存、仓库。
---
# Git 工作流规范
## 仓库现状
- 本仓库由下载的源码 zip 初始化(`git init`,master 分支),**无远程仓库**
- `.gitignore` 已覆盖 target/、node_modules/、.idea/、logs/、dist/ 等
## 提交四原则(硬性)
1. **只提交本次任务相关的文件**——`git status` 逐个确认,禁止 `git add -A` 一把梭(可能带进无关改动)
2. **不提交敏感与本地配置改动**——application.properties 的本地口令、.env 类文件(已忽略的除外也要留意)
3. **默认只做本地 commit**——push 必须用户明确要求;当前无远程,配置远程后也遵守此条
4. **提交前过 /check 或 code-patterns 检查清单**——规范违规不提交
## Commit Message 规范
```
<type>: <中文描述>
type ∈ feat | fix | refactor | docs | style | test | chore | perf
```
示例:
```
feat: 新增供应商管理六件套与前端三件
fix: 修复供应商分页排序字段未转下划线导致SQL报错
refactor: 供应商查询改批量接口消除N+1
docs: 补充国密与安全指南
chore: 初始化 Snowy v3.0.0 原始代码基线
```
## 常用操作
```bash
git status # 提交前必看
git add <具体文件路径> # 精确暂存
git commit -m "feat: xxx"
git log --oneline -10 # 查看最近提交
git diff --stat # 改动概览
# 大改动前打 tag 备份
git tag v0.1-重构前
```
## 禁止操作
- ❌ `git push --force` 到主分支
- ❌ `git reset --hard` 丢弃未确认的改动(先问用户)
- ❌ `git clean -fd`(会删未跟踪的新文件)
- ❌ 提交 node_modules / target / 日志 / 本地口令配置
## 与项目管理命令的联动
`/sync` `/update-status` `/next` 依赖 git 历史分析提交记录;若命令执行中 git 不可用(`git rev-parse --is-inside-work-tree` 失败),自动降级为代码扫描 + 文档时间线模式。
## 检查清单
- [ ] git status 干净无关文件
- [ ] message 符合 type: 中文
- [ ] 未 push(除非用户明确要求)
- [ ] 提交的代码过了规范检查
+74
View File
@@ -0,0 +1,74 @@
---
name: message-push
description: Snowy 消息与推送规范:站内信 DEV_MESSAGE、WebSocket 实时通知、钉钉/企微/飞书推送 DEV_PUSH、前端消息中心联动。触发场景:1) 业务需要给用户发通知 2) 需要实时推送(WebSocket)3) 对接钉钉/企业微信/飞书机器人。触发词:消息、通知、站内信、推送、WebSocket、实时、钉钉、企业微信、飞书、DEV_MESSAGE、DEV_PUSH、消息中心。
---
# Snowy 消息与推送规范
## 能力矩阵
| 需求 | 用什么 | 模块 |
|---|---|---|
| 站内信(应用内通知) | DevMessageApi 发消息 → 用户点铃铛查看 | `dev/modular/message/`(DEV_MESSAGE 表) |
| 实时弹到页面 | WebSocket(站内信实时通道) | `dev/modular/message/websocket/` |
| 推到钉钉/企微/飞书群机器人 | DevPushApi | `dev/modular/push/`(DEV_PUSH 表) |
| 短信/邮件 | 见 sms-mail 技能 | `dev/modular/sms/`、`email/` |
## 站内信(最常用)
```java
// 跨插件调用
@Resource
private DevMessageApi devMessageApi; // snowy-plugin-api/snowy-plugin-dev-api
// 发给指定用户(消息自动入库 DEV_MESSAGE + WebSocket 实时推送在线用户)
devMessageApi.saveMessage(...) // 具体 API 见 DevMessageApi 定义
```
前端:顶栏消息中心(`snowy-admin-web/src/views/index/` 相关),未读数角标自动更新。
管理界面:开发工具 → 站内信(可查/批量发)。
## WebSocket(实时通知)
- 服务端:`dev/modular/message/websocket/`(Snowy 自封装,基于 Redis 发布订阅支持多实例)
- 触发时机:发站内信时对在线用户自动推送;业务自定义事件也可推
- 前端:WebSocket 客户端逻辑在 `snowy-admin-web/src/layout/components/message.vue`(顶栏消息中心,含连接与未读数更新)
业务一般**不直接操作 WebSocket**——发站内信即自动实时推送。只有自定义实时场景(如大屏数据刷新)才直接用。
## 钉钉/企微/飞书推送
- 配置:开发工具 → 消息推送(DEV_PUSH 表:webhook 地址、密钥)
- 使用:
```java
@Resource
private DevPushApi devPushApi; // 推送到已配置的机器人
```
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| 自建消息表 + 轮询 | DEV_MESSAGE 站内信 + WebSocket 自动实时 |
| 每个业务自己开 WebSocket 端点 | 复用 dev 的 websocket 模块(站内信通道) |
| 直接 HTTP 调钉钉 webhook | DevPushApi(可管理/可切换/有记录) |
| 消息内容存 HTML 富文本拼 SQL | 模板 + 参数(参考现有消息类型) |
## 检查清单
- [ ] 站内信走 DevMessageApi(自动入库 + 实时推送)
- [ ] 群机器人推送走 DevPushApi
- [ ] 没有自建轮询接口
- [ ] 消息有类型分类(用户可在前端按类型筛)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/message/` | 站内信模块(含 websocket/) |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/push/` | 推送模块 |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/DevMessageApi.java` | 站内信跨插件接口 |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/DevPushApi.java` | 推送跨插件接口 |
| `snowy-admin-web/src/layout/components/message.vue` | 前端消息中心(WS 客户端) |
@@ -0,0 +1,97 @@
---
name: performance-doctor
description: Snowy 性能优化规范:慢查询与索引、N+1、分页规范、缓存策略、@Trans 批量翻译、列表页大数据。触发场景:1) 接口慢/列表加载久 2) 优化 SQL 与索引 3) 缓存策略选型 4) 大数据量处理。触发词:性能、慢、优化、慢查询、索引、N+1、卡顿、内存、大数据量、分页、批量、缓存策略。
---
# Snowy 性能优化指南
## 诊断流程
```
接口慢
├─ 后端慢? Knife4j 单测接口计时(排除前端)
│ ├─ SQL 慢 → Druid 监控(/druid,账号见配置)看慢 SQL / 执行计划 EXPLAIN
│ ├─ 循环查库(N+1)→ 看代码 for 里有没有 getById/listBy
│ └─ 翻译慢 → @Trans 相关表数据量
└─ 前端慢? 浏览器 Network 看接口耗时 vs 渲染耗时
```
## 查询优化规范
1. **必须分页**:列表接口一律 page(CommonPageRequest 上限 100),禁止全量 list 返给前端
2. **条件判空**:QueryWrapper 每个条件 ObjectUtil.isNotEmpty 包裹(避免无谓的全表 like)
3. **索引**:高频查询字段建索引(查询字段、外键、时间范围字段);EXPLAIN 确认命中
4. **排序**:sortField 走 `StrUtil.toUnderlineCase` 后是列名直排——确保排序列有索引
5. **count 优化**:深分页大表按业务限制时间范围
## N+1 与批量化
```java
// ❌ N+1:循环里查库
for(Order order : orders) {
JSONObject user = sysUserApi.getUserByIdWithoutException(order.getUserId()); // N 次调用
}
// ✅ 批量:一次查完再内存关联
List<String> userIds = CollStreamUtil.toList(orders, Order::getUserId);
List<JSONObject> users = sysUserApi.getUserListByIdListWithoutException(userIds);
Map<String, JSONObject> userMap = users.stream()
.collect(Collectors.toMap(j -> j.getStr("id"), j -> j, (a, b) -> a));
orders.forEach(o -> o.setUserName(userMap.get(o.getUserId()).getStr("name")));
```
(api 已提供 `getXxxListByIdList` 批量版本,优先用。)
## @Trans 翻译性能
- DICTIONARY/SIMPLE 翻译有 Redis 缓存,通常无忧
- target 表数据量大且变化频繁时缓存刷新成本高——高频大表关联翻译改手写批量查询(见上)
- 列表页 Entity 上 @Trans 字段多时,确认每个都有缓存支撑
## 缓存策略(详见 cache-redis 技能)
| 数据特征 | 策略 |
|---|---|
| 读多写少的字典/配置 | 缓存 + 数据变更事件刷新 |
| 热点统计(首页数字) | CommonCacheOperator 定时过期(如 60s)+ 定时任务预热 |
| 计数/防重 | Redisson RAtomicLong |
| 用户维度临时数据 | 带 TTL 的 key(含 userId) |
## 大数据量处理
- 导入导出:用 EasyExcel/EasyPoi 流式(参考 SysUserController.exportUser + DEV 文件模块),禁止一次性加载全部到 List
- 定时批处理:分批(几百/批)+ 幂等(见 scheduled-jobs 技能)
- 大文本:EXT_JSON longtext 别当查询字段
## 前端性能
- 长列表用 s-table 自带分页(不要一次拉全量)
- 字典/选项走 DictSelect 缓存;不重复拉
- 大表单字段多时按 Tab 分组(a-tabs)
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| 循环单查关联数据 | 批量接口 + 内存 Map 关联 |
| 列表接口 list() 全量返回 | page 分页 |
| 无索引字段当查询条件 | 建索引或改查询设计 |
| 每次请求实时算统计 | 缓存/定时任务预热 |
| selectList(null) 全表 | 带条件 + 分页 |
## 检查清单
- [ ] 列表接口分页且条件判空
- [ ] 无 N+1(循环内无查库)
- [ ] 高频查询字段有索引
- [ ] 热点读走缓存且刷新链路完整
- [ ] 导入导出流式处理
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/monitor/` | 服务器监控(内存/线程排查入口) |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java` | 批量查询实战 |
| `snowy-common/src/main/java/vip/xiaonuo/common/cache/CommonCacheOperator.java` | 缓存操作器 |
| `snowy-web-app/src/main/resources/application.properties` | Druid 监控配置 |
+123
View File
@@ -0,0 +1,123 @@
---
name: platform-extension
description: Snowy 平台扩展点地图:不改框架代码实现定制的所有正规途径——数据变更监听、跨插件 api 复用、provider 暴露、定时任务、系统配置、CommonWrapper、前端复用。触发场景:1) 用户说"我想改/增强平台某行为"(如用户变更后同步业务缓存、给某接口加默认逻辑)2) 判断一个定制需求该新建业务域还是扩展平台 3) 想复用平台能力(用户/字典/文件/消息)不知道从哪接。触发词:扩展、增强、定制、改平台、改框架、钩子、监听、联动、同步缓存、复用平台、不动源码、覆盖、事件。注意:新建业务功能走 crud-development/dev;本技能讲"平台行为定制"的正规入口。
---
# Snowy 平台扩展点地图
## 核心原则
> **平台插件(sys/auth/dev/gen/client/mobile)原则上不改**——改了升级即冲突。几乎所有"改平台行为"的需求都有正规扩展点。先查本表,再决定动不动框架。
## "我想做 X" → 扩展点速查表
| 需求 | 正确姿势 | 位置 |
|---|---|---|
| 用户/组织/角色变更后刷新**我方缓存或联动业务** | 实现 `CommonDataChangeListener` + @Component,按 dataType 分发 | 我的插件 `core/listener/` |
| 在业务代码里用平台能力(查用户/发消息/传文件/读配置) | 注入现成 `*-api` 接口(SysUserApi/DevMessageApi/DevFileApi/DevConfigApi...) | 见 plugin-architecture 的现成 API 速查表 |
| 把我方能力暴露给其他插件 | 定义接口进 `snowy-plugin-api/{我方}-api` + 我方 `provider/` 实现 | provider/ |
| 周期性任务(对账/清理/汇总) | 实现 `CommonTimerTaskRunner` + @Component + 界面配置 | 我的插件 `core/timer/` |
| 可调的业务参数(开关/阈值/默认密码) | DEV_CONFIG(系统配置界面维护),代码里 DevConfigApi 读取 | 界面:开发工具→系统配置 |
| 下拉选项数据 | 业务字典(BIZ)/枚举(见 dict-config) | biz/modular/dict 或 enums/ |
| 返回给前端的对象统一加工/包装 | `@CommonWrapper(包装类.class)`(返回值包装 AOP) | snowy-common/annotation |
| 接口防重复提交 | `@CommonNoRepeat` | Controller 方法上 |
| 操作留痕 | `@CommonLog("中文")` | Controller 写方法上 |
| 字段展示为字典文本/关联名 | Entity 加 `@Trans`(DICTIONARY/SIMPLE) | entity/ |
| 新页面/新菜单 | views/biz/{域} + SYS_RESOURCE 资源 SQL | 前端 + database-ops 挂载点速查 |
| 记录业务操作日志/异常排查 | @CommonLog 已落 DEV_LOG,直接查 | 开发工具→日志 |
## 扩展点 1:数据变更监听(最高频)
平台(sys 等)改了共享数据(用户/组织/角色)会广播事件;我的插件想要联动(清缓存/同步数据),实现监听器即可,**零侵入**:
```java
/*
* ... 版权头
*/
package vip.xiaonuo.biz.core.listener;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import org.springframework.stereotype.Component;
import vip.xiaonuo.common.listener.CommonDataChangeListener;
import java.util.List;
/**
* XXX数据变化侦听器:监听平台对共享数据的变更,联动本插件逻辑
*
* @author 你的名字
* @date 2026/08/18
**/
@Component
public class BizXxxDataChangeListener implements CommonDataChangeListener {
@Override
public void doUpdateWithDataId(String dataType, String dataId) {
// dataType 判别来源(自定义枚举),命中才处理
}
@Override
public void doDeleteWithDataIdList(String dataType, List<String> dataIdList) {
}
// 接口共 10 个回调,按需覆写:Add/Update/Delete × WithDataId/WithDataIdList/WithData/WithDataList
// (其中 Add/Update 有 WithData/WithDataList,Delete 只有 WithDataId/WithDataIdList)
}
```
- @Component 即自动注册(CommonDataChangeEventCenter 收集 Spring 容器中所有实现)
- 平台侧发事件的写法(我的插件改了共享数据也要广播):`CommonDataChangeEventCenter.doUpdateWithData(Xxx.class)` 等——写在 Service 的增删改里
- dataType:跟随事件来源(BizDataTypeEnum 这类插件级枚举定义自己的类型)
## 扩展点 2:复用平台能力(注入 *-api)
```java
@Resource
private SysUserApi sysUserApi; // 用户
@Resource
private DevConfigApi devConfigApi; // 系统配置(业务参数唯一正解)
@Resource
private DevFileApi devFileApi; // 文件
@Resource
private DevMessageApi devMessageApi; // 站内信(自动实时推送)
```
完整清单见 plugin-architecture 技能"现成 API 速查"表。**先查有没有现成 api,再考虑自己写。**
## 什么情况下才真的要改框架
满足全部三条才动:
1. 扩展点表里确实没有对应姿势
2. 平台行为本身要变(不是"加上我的业务",而是"改掉平台默认")
3. 用户明确知晓升级冲突风险并接受
改法约束:优先"同包同名 @Configuration 覆盖 Bean/条件装配",其次最小 diff 修改;改动必须记入 docs 并在 /sync 报告中标注"框架改动点"。
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| 直接在 SysUserService 里加业务逻辑 | biz 插件建自己的域,跨插件走 SysUserApi |
| 业务开关写死在代码/properties | DEV_CONFIG + DevConfigApi 读取 |
| 改共享数据后不管缓存 | 发 CommonDataChangeEventCenter 事件 |
| 想收平台变更通知却去改 sys 源码 | 实现 CommonDataChangeListener |
| 每个新需求都想新建插件 | 默认进 snowy-plugin-biz/modular(见 plugin-architecture) |
## 检查清单
- [ ] 需求先过了扩展点速查表
- [ ] 监听器/provider 放对位置(core/listener、provider/)
- [ ] 改共享数据后有广播事件
- [ ] 未修改 6 个平台插件的任何文件(或已按"三条全满足"记录改动)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/core/listener/BizDataChangeListener.java` | 监听器官方范本(监听 sys 变更清 biz 缓存) |
| `snowy-common/src/main/java/vip/xiaonuo/common/listener/CommonDataChangeEventCenter.java` | 事件中心(注册/广播) |
| `snowy-common/src/main/java/vip/xiaonuo/common/listener/CommonDataChangeListener.java` | 监听接口(全部回调) |
| `snowy-common/src/main/java/vip/xiaonuo/common/annotation/CommonWrapper.java` | 返回包装注解 |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/job/task/DevJobTimerTaskRunner.java` | 定时任务扩展点范本 |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/` | 可注入的平台能力全集 |
+188
View File
@@ -0,0 +1,188 @@
---
name: plugin-architecture
description: Snowy 插件化架构与跨插件调用规范:Maven 模块拓扑、插件内 core/modular 两层结构、plugin-api 解耦三步法、新建插件 checklist。触发场景:1) 需要调用其他插件的功能(如 biz 要用 sys 的用户)2) 划分新业务该放哪个模块 3) 新建插件或新的 *-api 接口。触发词:插件、plugin、模块划分、跨模块、跨插件、api 模块、provider、解耦、依赖、新建插件。注意:单个业务域内的 CRUD 分层见 crud-development;本技能讲"模块之间怎么组织与通信"。
---
# Snowy 插件化架构规范
## Maven 模块拓扑
```
依赖方向(箭头 = 依赖):
snowy-web-app(启动模块,聚合一切)
├── snowy-plugin-{sys|auth|dev|gen|client|mobile|biz}
│ └── snowy-common(公共基础,人人依赖)
└── snowy-plugin-api/{x}-api(接口层,被调用方与调用方共同依赖)
关键规则:
- 插件实现模块(snowy-plugin/*)之间【禁止】互相依赖
- 插件要调用其他插件 → 双方都只依赖对方的 *-api 模块
- 公共代码(工具、注解、基类)下沉到 snowy-common
```
## 真实调用地图(提取自各插件 pom)
A→B = A 依赖 B 的 `*-api`(运行时 B 的 provider 实现注入 A):
| 调用方 | 依赖的 api | 典型用途 |
|---|---|---|
| **biz**(二开主战场) | sys-api、auth-api、dev-api | 用户/登录态(SaBaseLoginUser)/字典配置文件消息 |
| auth | sys-api、dev-api、client-api | 登录验用户、发验证码、C 端用户 |
| sys | auth-api、dev-api、mobile-api | 登录用户 POJO/在线 token、字典翻译、移动按钮码 |
| dev | sys-api、auth-api | 日志关联用户、监听器类型 |
| mobile | auth-api、dev-api、sys-api | 移动资源引用用户/字典 |
| client | auth-api、dev-api | C 端登录态与工具能力 |
| gen | sys-api、mobile-api | 生成时读模块/菜单、写移动资源 |
- **sys↔dev、auth↔sys "互相调用"但实现层零循环**——互依的只是 api 接口模块;编译期只见接口,运行期 Spring 把 provider 的 @Service 注入调用方
- 反向通知(被调方 → 调用方)不走依赖,走 `CommonDataChangeEventCenter` 事件(见 platform-extension 技能)
- gen-api 是空壳;ClientUserApi 在 `vip.xiaonuo.client` 包根(不在 api 子包)
| 模块 | 职责 | 二次开发可否修改 |
|---|---|---|
| `snowy-common` | CommonResult/CommonException/CommonEntity/工具类/国密/缓存操作器 | 原则上不动 |
| `snowy-plugin-api/*-api` | 跨插件调用接口定义(纯接口 + 少量 POJO) | 可新增接口 |
| `snowy-plugin-sys` | B 端系统:用户/组织/角色/资源/数据范围 | 原则上不动 |
| `snowy-plugin-auth` | 登录鉴权/SSO/三方登录 | 原则上不动 |
| `snowy-plugin-dev` | 开发工具:字典/配置/文件/定时任务/日志/短信/邮件/推送 | 原则上不动 |
| `snowy-plugin-gen` | 代码生成器 | 原则上不动 |
| `snowy-plugin-client` | C 端用户体系 | 原则上不动 |
| `snowy-plugin-mobile` | 移动端资源管理 | 原则上不动 |
| **`snowy-plugin-biz`** | **业务插件——二次开发主战场** | ★ 随便写 |
| `snowy-web-app` | 启动 + 全局配置(白名单/异常处理) | 只改配置类 |
## 插件内两层结构
```
vip.xiaonuo.biz
├── core/ 插件级基础设施(每插件固定有)
│ ├── config/ 插件配置类
│ ├── enums/ 插件级枚举
│ ├── listener/ 数据变更监听(如 BizDataChangeListener)
│ ├── timer/ 定时任务入口
│ └── util/ 插件级工具
└── modular/ 业务域(一个域一个目录)
└── {域名}/
├── controller/ entity/ enums/ mapper/(+mapping/)
├── param/ result/ service/(+impl/) provider/
```
新业务域默认放 `snowy-plugin-biz` 的 `modular/{域名}` 下(参考 notice 域);只有业务规模大到需要独立插件时才新建插件。
## 跨插件调用三步法(硬性规范)
**场景**:biz 插件的"订单"功能需要根据 userId 拿用户名称。
**第 1 步**:接口定义放被调方的 api 模块(sys 已有大量现成接口,先查再用):
```java
// snowy-plugin-api/snowy-plugin-sys-api/src/main/java/vip/xiaonuo/sys/api/SysUserApi.java
public interface SysUserApi {
/**
* 根据用户id获取用户对象,没有则返回null
*
* @author xuyuxiang
* @date 2022/6/20 18:19
**/
JSONObject getUserByIdWithoutException(String userId);
List<JSONObject> getUserListByIdListWithoutException(List<String> userIdList);
JSONObject getUserByIdWithException(String userId); // 没有则抛异常版本
List<JSONObject> getUserListByIdWithException(List<String> userIdList);
}
```
**第 2 步**:被调插件 provider 实现(@Service):
```java
// snowy-plugin-sys/.../modular/user/provider/SysUserApiProvider.java
@Service
public class SysUserApiProvider implements SysUserApi {
@Resource
private SysUserService sysUserService;
@Override
public JSONObject getUserByIdWithoutException(String userId) {
SysUser sysUser = sysUserService.getById(userId);
return JSONUtil.parseObj(sysUser); // Entity → JSONObject 返回
}
// ...
}
```
**第 3 步**:调用方只依赖 *-api 模块,注入接口用:
```java
// snowy-plugin-biz 的 pom.xml 加依赖:
// <dependency>
// <groupId>vip.xiaonuo</groupId>
// <artifactId>snowy-plugin-sys-api</artifactId>
// </dependency>
@Service
public class BizOrderServiceImpl ... {
@Resource
private SysUserApi sysUserApi; // 注入接口,不注入 sys 的实现
public void xxx(String userId) {
JSONObject user = sysUserApi.getUserByIdWithoutException(userId);
String name = user.getStr("name");
}
}
```
### 为什么返回 JSONObject 而不是实体类
被调方的 Entity 类在实现模块里,调用方不能依赖它(否则插件耦合)。JSONObject(hutool)是双方都能见的 neutral 类型。取字段用 `jsonObject.getStr("name")` / `getInt("age")`。
## 现成 API 速查(先查再用,不要重复造)
| api 模块 | 内容 |
|---|---|
| `snowy-plugin-api/snowy-plugin-sys-api` | SysUserApi/SysRoleApi/SysOrgApi/SysMenuApi/SysButtonApi/SysModuleApi/SysApi...(用户/角色/组织/资源) |
| `snowy-plugin-api/snowy-plugin-biz-api` | BizUserApi/BizOrgApi 等 B 端业务用户(biz 插件提供) |
| `snowy-plugin-api/snowy-plugin-dev-api` | DevConfigApi/DevDictApi/DevFileApi/DevSmsApi/DevEmailApi/DevMessageApi...(配置/字典/文件/短信/邮件/消息) |
| `snowy-plugin-api/snowy-plugin-auth-api` | SaBaseLoginUser 登录用户 POJO、AuthApi 等 |
| `snowy-plugin-api/snowy-plugin-client-api` | ClientUserApi 等 C 端用户 |
命名惯例:`getXxxByIdWithoutException`(查不到返 null)/ `getXxxByIdWithException`(查不到抛异常)/ `getXxxListByIdList...`(批量)。
## 新建插件 checklist(仅当业务真的需要独立插件)
1. `snowy-plugin/snowy-plugin-{name}/`:pom(parent = snowy,依赖 snowy-common)+ 包 `vip.xiaonuo.{name}`
2. `snowy-plugin-api/snowy-plugin-{name}-api/`:供他插件调用的接口模块
3. 根 `pom.xml` 的 modules 加两个新模块;`snowy-web-app/pom.xml` 加实现模块依赖
4. 包结构照 `core/ + modular/` 两层组织
5. Knife4j 分组:`snowy-web-app` 的 Knife4jConfigure 加新插件包扫描(可选)
6. 前端对应加 `src/api/{name}/` 与 `src/views/{name}/` 目录
⚠️ 大多数情况**不需要**新建插件——在 `snowy-plugin-biz/modular/` 下加业务域即可。
## 禁止事项
- ❌ 插件 pom 互相依赖实现模块(如 biz 依赖 snowy-plugin-sys)
- ❌ 绕过 *-api 直接 import 其他插件 modular 下的类
- ❌ 跨插件返回 Entity/自定义 DTO(会引入类依赖),一律 JSONObject
- ❌ 把业务代码写进 sys/auth/dev 等平台插件(升级会冲突)
## 检查清单
- [ ] 跨插件调用走了 *-api 接口 + provider 实现
- [ ] 返回值 JSONObject,字段名与 Entity 属性名一致
- [ ] 调用方 pom 只依赖 *-api 模块
- [ ] 新业务域放对了位置(默认 snowy-plugin-biz/modular)
- [ ] 先查过现成 api(sys-api / dev-api)再决定新写
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin-api/snowy-plugin-sys-api/src/main/java/vip/xiaonuo/sys/api/SysUserApi.java` | 接口定义范本 |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysUserApiProvider.java` | provider 实现范本(含权限码组装) |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/` | dev-api 全家(Config/Dict/File/Sms/Email/Message) |
| `snowy-plugin/snowy-plugin-biz/src/main/java/vip/xiaonuo/biz/core/` | 插件 core 层组织范本 |
| 根 `pom.xml` + `snowy-web-app/pom.xml` | 模块聚合与依赖关系 |
+96
View File
@@ -0,0 +1,96 @@
---
name: project-navigator
description: Snowy 项目导航地图:目录结构速查、"X 功能在哪个文件"速查表、常用查找命令。触发场景:1) 找某功能/配置/类在哪个文件 2) 不熟悉项目结构想快速定位 3) 新会话了解项目。触发词:在哪、哪个文件、目录结构、导航、找、定位、项目结构、入口、启动类。
---
# Snowy 项目导航地图
## 顶层结构(30 秒版)
```
snowy-master/
├── snowy-common/ 公共基础(CommonResult/异常/实体基类/缓存/国密/工具)
├── snowy-plugin/ 7 个插件:sys(系统) auth(鉴权) dev(工具) gen(生成器) client(C端) mobile(移动) biz(★业务)
├── snowy-plugin-api/ 对应 7 个 *-api 跨插件接口模块
├── snowy-web-app/ 启动模块(Application + 全局配置 + SQL 脚本)
├── snowy-admin-web/ 前端(Vue3 + AntdV,JS)
└── .claude/ 本工程化配置
```
插件内:`{plugin}.core.{config,enums,listener,timer,util}` + `{plugin}.modular.{业务域}.{controller,entity,enums,mapper(+mapping),param,result,service(+impl),provider}`
## "我想找 X 在哪" 速查表
| 想找什么 | 位置 |
|---|---|
| 启动类 | `snowy-web-app/src/main/java/vip/xiaonuo/Application.java`(端口 82) |
| 全部环境配置(数据库/Redis/密钥) | `snowy-web-app/src/main/resources/application.properties` |
| 建库脚本(33 表) | `snowy-web-app/src/main/resources/_sql/snowy_mysql.sql` |
| 路由白名单(免登录/C端/超管) | `snowy-web-app/.../core/config/GlobalConfigure.java` |
| 全局异常处理 | `snowy-web-app/.../core/handler/GlobalExceptionHandler.java` |
| 统一返回/异常 | `snowy-common/.../pojo/CommonResult.java`、`exception/CommonException.java` |
| 实体基类(审计字段/逻辑删除) | `snowy-common/.../pojo/CommonEntity.java` |
| 国密工具/字段加密 | `snowy-common/.../util/CommonCryptogramUtil.java`、`handler/CommonSm4CbcTypeHandler.java` |
| 缓存操作器 | `snowy-common/.../cache/CommonCacheOperator.java` |
| 定时任务接口 | `snowy-common/.../timer/CommonTimerTaskRunner.java` |
| B 端登录流程 | `snowy-plugin-auth/.../modular/login/` |
| 权限码组装(登录时) | `snowy-plugin-sys/.../user/provider/SysLoginUserApiProvider.java` |
| 用户/角色/组织/菜单管理 | `snowy-plugin-sys/.../modular/{user,role,org,resource}/` |
| 字典(系统/业务) | `snowy-plugin-dev/.../dict/`、`snowy-plugin-biz/.../dict/` |
| 文件上传 | `snowy-plugin-dev/.../file/` |
| 短信/邮件/站内信/推送 | `snowy-plugin-dev/.../{sms,email,message,push}/` |
| 定时任务管理 | `snowy-plugin-dev/.../job/` |
| 操作日志切面 | `snowy-plugin-dev/.../core/aop/DevLogAop.java` |
| 代码生成器逻辑/模板 | `snowy-plugin-gen/.../basic/`、`snowy-plugin-gen/src/main/resources/` |
| 跨插件接口定义 | `snowy-plugin-api/*-api/.../api/*.java` |
| ★ 业务代码(二开主战场) | `snowy-plugin-biz/.../modular/{域}/` |
| 前端 API 封装 | `snowy-admin-web/src/api/{插件}/xxxApi.js` |
| 前端页面 | `snowy-admin-web/src/views/{插件}/{域}/index.vue + form.vue` |
| 前端请求封装/工具 | `snowy-admin-web/src/utils/request.js`、`tool.js` |
| 前端组件(37 个 Xn*) | `snowy-admin-web/src/components/` |
| 前端路由/菜单 | `snowy-admin-web/src/router/`(动态部分来自 SYS_RESOURCE) |
| 前端状态(Pinia) | `snowy-admin-web/src/store/` |
| 前端国际化 | `snowy-admin-web/src/locales/` |
| 前端国密 | `snowy-admin-web/src/utils/smCrypto.js` |
## 常用查找命令
```bash
# 按类名找文件
Glob pattern: **/BizNotice*.java
# 按 URL 找接口
Grep pattern: "/biz/notice/page" glob: **/*.java
# 按注解找(如所有写接口)
Grep pattern: "@CommonLog" glob: **/controller/*.java
# 按表名找实体
Grep pattern: "@TableName.*BIZ_NOTICE" glob: **/*.java
# 前端找按钮权限码
Grep pattern: "hasPerm" path: snowy-admin-web/src/views/biz
# 找某功能的菜单 SQL
Grep pattern: "INSERT INTO .SYS_RESOURCE" path: snowy-web-app/src/main/resources/_sql
```
## 典型链路(一个请求的旅程)
```
前端 xxxApi.js(baseRequest '/biz/xxx/page')
→ Vite 代理 /api → localhost:82
→ GlobalConfigure 的 SaServletFilter(白名单校验)
→ SaInterceptor(@SaCheckPermission URL 校验)
→ XxxController.page(@Validated)
→ XxxServiceImpl.page(QueryWrapper.checkSqlInjection + CommonPageRequest)
→ XxxMapper(MP BaseMapper)
→ MySQL(BIZ_XXX,逻辑删除自动过滤)
→ CommonResult.data(page) 返回
→ request.js 统一处理(code!==200 报错)
```
## 检查清单(导航技能没有代码改动,用于定位)
- [ ] 定位到目标文件后再动手,不要凭记忆猜路径
- [ ] 改动前 Read 真实文件(文档可能滞后于代码)
+99
View File
@@ -0,0 +1,99 @@
---
name: scheduled-jobs
description: Snowy 定时任务规范:CommonTimerTaskRunner 接口实现 + DEV_JOB 表配置 + Cron 表达式 + 任务执行日志。触发场景:1) 业务需要定时执行(对账/清理/汇总)2) 添加或排查定时任务 3) 写 Cron 表达式。触发词:定时任务、定时、Cron、调度、DEV_JOB、TimerTaskRunner、轮询、周期执行、任务日志。
---
# Snowy 定时任务规范
## 机制(Snowy 自研 timer 体系,不是 SnailJob/xxl-job/quartz)
```
后端:写一个 CommonTimerTaskRunner 实现类(@Component)
界面:开发工具 → 定时任务 → 新增任务(DEV_JOB 表)
填:任务所属处理器(选你的 Runner Bean)、Cron、是否启用
执行:调度器按 Cron 反射调用 Runner.action(extJson),日志落任务执行记录
```
## 写一个定时任务(标准模板)
```java
/*
* Copyright [2022] [https://www.xiaonuo.vip]
* ...(12 行版权头)
*/
package vip.xiaonuo.biz.core.timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import vip.xiaonuo.common.timer.CommonTimerTaskRunner;
/**
* 供应商月度对账任务
*
* @author 你的名字
* @date 2026/08/18 10:00
**/
@Slf4j
@Component
public class BizSupplierMonthlyTimerTaskRunner implements CommonTimerTaskRunner {
@Resource
private BizSupplierService bizSupplierService;
@Override
public void action(String extJson) {
// extJson = 界面配置的扩展 JSON 参数(可传开关/日期偏移等)
log.info("开始执行供应商月度对账");
bizSupplierService.doMonthlyReconcile(extJson);
}
}
```
放置位置:插件 `core/timer/` 目录(业务任务放 `biz/core/timer/`)。
写完代码后:**重启后端 → 定时任务界面 → 新增 → 选择该 Runner → 填 Cron → 启用**。
## Cron 速查
| 表达式 | 含义 |
|---|---|
| `0 0 2 * * ?` | 每天凌晨 2 点 |
| `0 */5 * * * ?` | 每 5 分钟 |
| `0 0 0 1 * ?` | 每月 1 号零点 |
| `0 0 9-18 * * MON-FRI` | 工作日 9-18 点整点 |
前端有 Cron 组件可视化生成(`snowy-admin-web/src/components/Cron/`)。
## 任务规范
- **幂等**:任务可能重复触发/重叠执行,action 内先判"本次是否已处理"
- **大数据量切片**:分批查处理(每批几百条),别一次 load 全表
- **异常自吞**:单条失败 log.error 继续,不要让整批中断;整体失败要有日志/告警
- **参数走 extJson**:可变配置(天数偏移、开关)放任务配置的扩展 JSON,别写死
- 日志:关键节点 log.info,失败 log.error(任务日志界面可查)
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| `@Scheduled(cron=...)` 注解(Spring 原生) | CommonTimerTaskRunner + DEV_JOB 界面配置(可管理可控) |
| 引入 quartz/xxl-job 依赖 | Snowy 自研 timer 已覆盖 |
| 任务里把状态写死 | 可变参数走 extJson |
| 忘记在界面注册任务 | 代码只是 Bean,必须界面新增并启用才调度 |
| 一次性全表加载处理 | 分批 + 幂等 |
## 检查清单
- [ ] Runner 在 core/timer/ 下,@Component,实现 CommonTimerTaskRunner
- [ ] action 用 extJson 接参数,处理幂等与分批
- [ ] 界面已配置任务(Cron + 启用)
- [ ] 有执行日志,失败可见
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-common/src/main/java/vip/xiaonuo/common/timer/CommonTimerTaskRunner.java` | 任务接口定义 |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/job/task/DevJobTimerTaskRunner.java` | 官方示例任务 |
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/job/` | 任务管理模块(DEV_JOB) |
| `snowy-admin-web/src/components/Cron/` | Cron 可视化组件 |
+98
View File
@@ -0,0 +1,98 @@
---
name: security-auth
description: Snowy 鉴权与安全规范:Sa-Token B/C 双端模型、路由白名单三段、登录方式、获取当前用户、接口权限与按钮权限、数据范围、三方登录、SSO。触发场景:1) 接口需要登录/免登录/权限校验 2) 获取当前登录用户信息 3) 数据范围(按机构过滤)4) 三方登录或 SSO 集成。触发词:鉴权、权限、登录、Sa-Token、StpUtil、token、白名单、免登录、401、403、数据范围、DataScope、角色、三方登录、SSO、OAuth2、越权。
---
# Snowy 鉴权与安全规范
## 双端模型(B 端 / C 端)
| 端 | 用户体系 | 登录工具类 | 接口前缀 |
|---|---|---|---|
| B 端(管理后台) | SYS_USER(sys 插件) | `StpUtil` / `StpLoginUserUtil.getLoginUser()` | `/sys/*` `/biz/*` `/dev/*` `/gen/*` |
| C 端(终端用户) | CLIENT_USER(client 插件) | `StpClientUtil` / `StpClientLoginUserUtil` | `/auth/c/**` `/client/c/**` |
两套独立 StpLogic(`AuthConfigure` 里注册 `stpLogic` 与 `stpClientLogic`),token 互不通用。**业务后台代码一律用 B 端 API**。
## 路由级白名单(GlobalConfigure,改后必须重启)
`snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java`:
| 数组 | 作用 |
|---|---|
| `NO_LOGIN_PATH_ARR` | 免登录路径(验证码、登录接口、公开页) |
| `CLIENT_USER_PERMISSION_PATH_ARR` | C 端鉴权路径 |
| `SUPER_PERMISSION_PATH_ARR` | 仅超管角色可访问 |
新增公开接口 → 加 NO_LOGIN_PATH_ARR → 重启。漏加 = 401。
## 获取当前用户(业务代码高频)
```java
import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
String userId = StpLoginUserUtil.getLoginUserId(); // 当前用户 id
SaBaseLoginUser user = StpLoginUserUtil.getLoginUser(); // 完整登录用户(name/orgId/...)
String orgId = StpLoginUserUtil.getLoginUser().getOrgId();
// SaBaseLoginUser 在 snowy-plugin-api/snowy-plugin-auth-api 的 core/pojo 下
```
自动填充的 createUser/updateUser 就是取自这里(MetaObjectHandler)。
## 接口权限 vs 按钮权限(两层,别混淆)
| 层 | 载体 | 形态 | 用法 |
|---|---|---|---|
| 接口权限 | 角色授权生成的数据范围 apiUrl | **URL 式** | 后端 `@SaCheckPermission("/biz/xxx/page")` |
| 按钮权限 | SYS_RESOURCE 的 BUTTON 行 code | **驼峰式** | 前端 `hasPerm('bizXxxAdd')` |
登录时(`SysLoginUserApiProvider`)组装:`permissionCodeList`(来自 dataScope 的 apiUrl)+ `buttonCodeList`(驼峰码下发前端)。所以:
- 新接口上线 → 角色管理授权对应资源,否则非超管 403
- 新按钮 → SYS_RESOURCE 加 BUTTON 行 + 授权,否则前端不显示
## 数据范围(DataScope)
- 用户可被授权"某模块/某菜单下、某机构的全部/自定义/仅本部门/仅本人"数据范围(SYS_USER_DATA_SCOPE 表,角色管理→数据授权界面配置)
- 后端查询受数据范围过滤的实现走 sys 插件的 ApiUrl 清单机制:数据范围授权时选择"接口地址集合"
- **业务表要支持数据范围**:确保查询走标准 QueryWrapper 链路;机构维度字段命名 orgId(varchar,SYS_ORG 外键)
- 手动忽略数据权限的场景参考 DataPermissionHelper 类似机制(以 sys 插件实现为准)
## 登录方式矩阵(auth 插件已实现,直接用)
账密(密码 SM2 加密传输 + SM3 摘要校验)、手机验证码、邮箱验证码、OTP 动态口令、三方 token 换绑。入口:`snowy-plugin-auth/modular/login/controller/AuthController.java`(B 端 `/auth/b/*`)与 `AuthClientController`(C 端 `/auth/c/*`)。
## 三方登录与 SSO
- 三方登录:JustAuth(`auth/modular/third/`),支持微信/钉钉/企微/飞书/QQ/微博/Gitee 等;配置在 DEV_CONFIG
- SSO/OAuth2/OIDC/CAS/SAML:`auth/core/protocol/`(服务端与客户端协议栈);配置 `AuthSsoConfigure`
- 业务一般只做"绑定/解绑三方账号",协议栈不用动
## 安全检查清单
- [ ] 新公开接口已加 NO_LOGIN_PATH_ARR 并重启
- [ ] @SaCheckPermission 值 = URL;新资源已授权给角色
- [ ] 涉及用户数据的接口校验归属(防水平越权:编辑/删除前确认记录属于当前用户机构)
- [ ] 密码类字段不落日志、不明文存储(见 crypto-sm)
- [ ] 敏感查询参数防注入(checkSqlInjection)
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| `@SaCheckPermission("biz:xxx:list")` | `@SaCheckPermission("/biz/xxx/page")` |
| Controller 里手写 `StpUtil.getLoginId()` 判空跳转 | 白名单/注解体系(GlobalConfigure + @SaCheckLogin 系) |
| B 端接口用 StpClientUtil | B 端 StpUtil / StpLoginUserUtil |
| 新菜单配好但用户 403 | 角色管理未授权(接口权限来自资源授权) |
| 自己写 token 解析 | Sa-Token 体系(token 名 `token`,请求头) |
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java` | 路由白名单 + SaServletFilter |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/core/config/AuthConfigure.java` | 双端 StpLogic + StpInterfaceImpl |
| `snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysLoginUserApiProvider.java` | 权限码/按钮码/数据范围组装(refreshOnlineUserPermission) |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/` | 登录全流程 |
| `snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/core/pojo/SaBaseLoginUser.java` | 登录用户 POJO |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/` | 三方登录 |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/core/protocol/` | SSO 协议栈 |
+76
View File
@@ -0,0 +1,76 @@
---
name: sms-mail
description: Snowy 短信与邮件规范:sms4j 多供应商短信、DEV_SMS 发送记录、CommonEmailUtil 邮件、SMTP 配置、验证码发送流程与 auth 联动。触发场景:1) 业务需要发短信/邮件通知 2) 手机/邮箱验证码登录相关 3) 配置短信供应商或 SMTP。触发词:短信、SMS、sms4j、邮件、email、SMTP、验证码、通知发送、DEV_SMS、DEV_EMAIL。
---
# Snowy 短信与邮件规范
## 短信(sms4j)
- 引擎:org.dromara.sms4j(多供应商:阿里/腾讯/华为/容联/京东等九家)
- 模块:`snowy-plugin-dev` 的 `modular/sms/`;管理界面:开发工具 → 短信管理(DEV_SMS 表存发送记录)
- 供应商配置:`application.properties` 的 `sms:` 段 + 短信管理界面(DEV_CONFIG 动态)
```java
// 业务发短信
@Resource
private DevSmsApi devSmsApi; // snowy-plugin-api/snowy-plugin-dev-api
// 或使用 sms4j 原生(配置好的供应商)
SmsBlend smsBlend = SmsFactory.getSmsBlend("supplier1");
smsBlend.sendMessage("手机号", "模板id", Map.of("code", "123456"));
```
发送后记录自动落 DEV_SMS(含状态),短信管理界面可查/重发。
## 邮件
```java
// snowy-common 的 CommonEmailUtil(基于 dev 插件 DEV_EMAIL 配置)
CommonEmailUtil.sendTextEmail("收件人", "主题", "内容");
CommonEmailUtil.sendHtmlEmail("收件人", "主题", "<h1>HTML</h1>");
CommonEmailUtil.sendAttachmentEmail("收件人", "主题", "内容", "附件路径");
// 详见 snowy-common/src/main/java/vip/xiaonuo/common/util/CommonEmailUtil.java
```
- SMTP 配置:开发工具 → 邮件管理(DEV_EMAIL 表:主机/端口/账号/密码/SSL)
- 发送记录:DEV_EMAIL 相关查询接口
## 验证码流程(与 auth 联动)
```
1. 前端 /auth/b/getPhoneValidCode(或邮箱版)→ 后端生成 6 位码
2. 存 Redis(CommonCacheOperator,带过期时间,键含手机号)
3. 发送短信/邮件给用户
4. 用户提交登录/绑定 → 后端从缓存取码比对(一次性,验证即删)
5. 错误次数限制防爆破
```
参考实现:`snowy-plugin-auth/modular/login/` 的验证码相关方法(AuthController / AuthServiceImpl)。
## 常见错误正误对照
| ❌ | ✅ |
|---|---|
| 手写 HttpClient 调短信商 API | sms4j(SmsFactory.getSmsBlend) |
| 验证码存数据库表 | Redis(CommonCacheOperator + 过期) |
| 邮件密码/SMTP 明文写代码里 | 邮件管理界面配置(DEV_EMAIL) |
| 发送失败用户端报 500 | 捕获后 CommonException("短信发送失败,请稍后再试"),记录落 DEV_SMS |
| 每次发送 new 邮件连接 | CommonEmailUtil(连接池复用) |
## 检查清单
- [ ] 走 sms4j / CommonEmailUtil,未自造 HTTP 客户端
- [ ] 验证码有过期时间与一次性消费
- [ ] 敏感配置在管理界面/配置文件,不在代码
- [ ] 发送结果可追溯(DEV_SMS / DEV_EMAIL 记录)
## 参考实现
| 文件 | 说明 |
|---|---|
| `snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/sms/` | 短信模块全套 |
| `snowy-common/src/main/java/vip/xiaonuo/common/util/CommonEmailUtil.java` | 邮件工具 |
| `snowy-plugin-api/snowy-plugin-dev-api/src/main/java/vip/xiaonuo/dev/api/DevSmsApi.java` | 跨插件短信接口 |
| `snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/` | 验证码登录流程 |
| `snowy-web-app/src/main/resources/application.properties` | sms 供应商配置段 |
+34
View File
@@ -0,0 +1,34 @@
# 待办清单
> 最后更新:YYYY-MM-DD HH:mm  编号 TASK-XXX 递增  完成项移入"最近完成"
## 🔴 高优先级
<!-- 阻塞、bug、紧急事项 -->
- [ ] TASK-001:示例——修复订单分页排序报错(模块:order;来源:FIXME)
## 🟡 中优先级
<!-- 常规开发任务、优化 -->
- [ ] TASK-002:示例——补齐 order 域前端 form.vue
## 🟢 低优先级
<!-- 想法、可选优化 -->
- [ ] TASK-003:示例——考虑给列表加导出按钮
## 进行中
- [ ] TASK-004:示例——供应商模块联调中(负责人自己)
## 最近完成
- [x] TASK-000:示例——完成项目工程化配置(YYYY-MM-DD 完成)
## 本周计划
- (示例:完成 order 模块前后端 + 菜单授权)
## 统计
高 1 / 中 1 / 低 1 / 进行中 1  最近 7 天完成:N 项
+43
View File
@@ -0,0 +1,43 @@
# 需求文档
> 项目:Snowy 二次开发项目  创建:YYYY-MM-DD  维护:随需求变更追加,禁止删除历史需求
## 项目概述
<!-- 一段话说清这个二开项目要做什么业务、给谁用 -->
(示例:基于 Snowy 平台开发的实验室耗材管理系统,管理供应商、耗材入库/领用、库存盘点。)
## 功能需求
<!-- 编号 REQ-XXX 递增;状态:规划中 / 开发中 / 已完成 / 已搁置 -->
### REQ-001:供应商管理
| 项 | 内容 |
|---|---|
| 优先级 | 高 |
| 状态 | 规划中 |
| 所属模块 | snowy-plugin-biz / modular/supplier |
| 预计时间 | 0.5 天 |
| 依赖 | 无 |
**需求描述**:维护供应商档案(名称/类型/联系人/状态),支持分页查询、增删改查、启用禁用。
**验收标准**:
- [ ] 分页列表可按名称模糊、类型精确查询
- [ ] 新增必填校验(名称/类型)
- [ ] 删除为逻辑删除
- [ ] 菜单挂"基础数据"目录,按钮权限齐全
### REQ-002:(下一个需求)
## 技术需求
<!-- 非功能性的技术要求:性能、数据量、集成等 -->
- (示例:列表查询响应 < 500ms,数据量级 1 万行)
## 变更记录
| 日期 | 编号 | 变更内容 |
|---|---|---|
| YYYY-MM-DD | REQ-001 | 创建 |
+46
View File
@@ -0,0 +1,46 @@
# 项目状态
> 最后更新:YYYY-MM-DD HH:mm  统计口径:只算自建业务(框架与出厂演示域不计),见 .claude/framework-config.json
## 当前状态
| 项 | 内容 |
|---|---|
| 阶段 | 起步 / 开发中 / 联调 / 上线准备 |
| 业务总进度 | 0%(N 个自建业务域平均) |
| 后端 | mvn 构建通过 / 端口 82 |
| 前端 | snowy-admin-web npm run dev / 端口 81 |
| 环境 | MySQL(snowy 库) ✓ Redis(6379) ✓ |
## 业务模块进度
<!-- 完整度 = 六件套存在数/6;前端 = 三件存在数/3 -->
| 业务域 | 表 | 后端完整度 | 前端完整度 | 菜单/授权 | 状态 |
|---|---|---|---|---|---|
| supplier | BIZ_SUPPLIER | 6/6 | 3/3 | ✓ | 已完成 |
| order | BIZ_ORDER | 4/6 | 1/3 | ✗ | 开发中 |
## 最近完成
| 日期 | 内容 |
|---|---|
| YYYY-MM-DD | 完成供应商管理六件套 + 前端三件(约 2 小时) |
## 进行中
| 内容 | 进度 | 预计完成 |
|---|---|---|
| 订单模块(缺 Controller/ServiceImpl 与前端) | 60% | YYYY-MM-DD |
## 里程碑
| 日期 | 里程碑 |
|---|---|
| YYYY-MM-DD | 基础数据模块全部完成 |
## 问题与风险
| 日期 | 问题/风险 | 状态 |
|---|---|---|
| YYYY-MM-DD | 示例:供应商手机号需 SM4 加密,like 查询待定方案 | 待处理 |
+162
View File
@@ -0,0 +1,162 @@
# CLAUDE.md
本文件是 Snowy 二次开发项目的工程宪法。所有 AI 辅助开发必须遵守此处规范;详细规范见 `.claude/skills/` 与 `.claude/docs/`。
## 这是什么项目
**Snowy v3.0.0** —— 小诺开源的国密前后端分离快速开发平台,本项目以它为底座做二次开发。
- 后端:Java 17 + Spring Boot 3.5.9 + MyBatis-Plus 3.5.5 + Sa-Token 1.44.0 + Redisson + Hutool 5.8.25(**插件化 Maven 多模块架构**)
- 前端:`snowy-admin-web/`(Vue 3.5 + Vite 6 + **Ant Design Vue 4.2.6** + Pinia + vue-i18n,**JavaScript,不是 TS**,包管理用 npm)
- 数据库:MySQL(脚本 `_sql/snowy_mysql.sql`),表名**全大写下划线**
- 国密:登录密码 SM2 加密传输、口令 SM3 摘要存储、敏感字段 SM4-CBC 落库加密
- 本项目定位:**二次开发底座**——业务代码写在 `snowy-plugin-biz`,平台插件(sys/auth/dev/gen/client/mobile)原则上不动
## 目录结构
```
snowy-master/
├── snowy-common/ 公共基础模块(包 vip.xiaonuo.common):CommonResult/CommonException/
│ CommonEntity/CommonPageRequest/CommonCacheOperator/国密工具等
├── snowy-plugin/ 7 个插件实现(包 vip.xiaonuo.{插件名})
│ ├── snowy-plugin-sys B端系统:用户/组织/职位/角色/资源菜单/关系/数据范围
│ ├── snowy-plugin-auth 登录鉴权:B/C端登录、SSO、OAuth2/OIDC/CAS/SAML、三方登录
│ ├── snowy-plugin-dev 开发工具:配置/字典/邮件/文件/定时任务/日志/站内信/监控/短信/推送
│ ├── snowy-plugin-gen 代码生成器(Beetl 模板)
│ ├── snowy-plugin-client C端功能(CLIENT_USER 独立用户体系)
│ ├── snowy-plugin-mobile 移动端资源管理
│ └── snowy-plugin-biz ★ 业务插件——二次开发代码都放这里(biz/modular/{业务域})
├── snowy-plugin-api/ 7 个对应 *-api 模块:跨插件调用的接口定义(解耦用)
├── snowy-web-app/ 唯一启动模块:Application.java、application.properties、_sql/
├── snowy-admin-web/ 前端工程(独立 npm 项目)
└── .claude/ 本工程化配置(见下文)
```
插件内两层结构(以 biz 为例):
- `vip.xiaonuo.biz.core.{config,enums,listener,timer,util}` —— 插件级基础设施
- `vip.xiaonuo.biz.modular.{业务域}.{controller,entity,enums,mapper,mapper/mapping,param,result,service,service/impl,provider}` —— 业务域标准结构
## 常用命令
```bash
# 后端(JDK 17,首次需导入数据库:mysql 执行 snowy-web-app/src/main/resources/_sql/snowy_mysql.sql,并启动本地 Redis)
mvn clean install -DskipTests # 根目录构建
# 运行:IDE 启动 vip.xiaonuo.Application(snowy-web-app 模块),端口 82
# 接口文档:http://localhost:82/doc.html(Knife4j,basic 认证 admin/123456)
# 出厂登录:superAdmin / Snowy@2026!(⚠️不是 123456;权威来源 DEV_CONFIG 的 SNOWY_SYS_DEFAULT_PASSWORD_FOR_B)
# 前端(snowy-admin-web/ 目录下)
npm install
npm run dev # 端口 81,代理 /api → localhost:82
```
⚠️ 本机构建环境备忘(2026-08-18 实测):
- 命令行 mvn 需用 IDEA 内置:`"/c/Program Files/JetBrains/IntelliJ IDEA 2026.2/plugins/maven-plugin/lib/maven3/bin/mvn"`,JAVA_HOME 指向同目录 jbr
- Maven Central 直连失败(DNS),需加阿里云镜像(临时 settings.xml 已生成在 /tmp/m2/settings.xml,mirrorOf=central → maven.aliyun.com/repository/public)
- IDEA 内置 JBR 是 Java 25,与项目 Lombok 1.18.30 不兼容(@Slf4j 失效)——**命令行完整编译需自装 JDK 17**;日常在 IDEA 里配 Project SDK 17 构建即可
## 后端架构
### 分层与 CRUD 标准结构(六件套 + 两可选件)
Controller → Service → Mapper 三层。一个业务域 = 一个 `modular/{域名}` 目录:
| 件 | 位置 | 要点 |
|---|---|---|
| Entity | `entity/Xxx.java` | `extends CommonEntity`;`@TableName("BIZ_XXX")` 大写;`@TableId private String id`(字符串雪花);敏感字段加 `typeHandler = CommonSm4CbcTypeHandler.class`(需 `autoResultMap = true`);字段用 `@Schema(description="中文")` |
| Mapper | `mapper/XxxMapper.java` | `extends BaseMapper<Xxx>`,通常空接口 |
| Mapper XML(可选) | `mapper/mapping/XxxMapper.xml` | 仅自定义 SQL 需要;与 Mapper 同包 |
| Service | `service/XxxService.java` | `extends IService<Xxx>`,方法带中文 Javadoc(@author/@date) |
| ServiceImpl | `service/impl/XxxServiceImpl.java` | `@Service`,`extends ServiceImpl<XxxMapper, Xxx> implements XxxService`;查询 `QueryWrapper<Xxx>().checkSqlInjection()` + lambda 条件;分页 `this.page(CommonPageRequest.defaultPage(), queryWrapper)`;写方法 `@Transactional(rollbackFor = Exception.class)`;业务校验 `throw new CommonException("中文消息{}", 参数)` |
| Controller | `controller/XxxController.java` | `@Tag + @RestController + @Validated`;**URL 直接写方法上**:`@GetMapping("/biz/xxx/page")`;查询 GET + Param 对象、写入 POST + `@RequestBody @Valid`;写操作加 `@CommonLog("中文标题")`;返回 `CommonResult.data(...)` / `CommonResult.ok()` |
| Param 组 | `param/Xxx{Page,Add,Edit,Id}Param.java` | **每个操作一个 Param 类**(不是单个 Bo);PageParam 含 current/size/sortField/sortOrder/searchKey |
| Result(可选) | `result/XxxResult.java` | 仅投影返回需要,可直接返回 Entity |
URL 规范:`/{插件}/{业务域}/{动作}`,动作动词式(page/add/edit/delete/detail + 业务动作);查询 GET、写入 POST;delete 收 `List<XxxIdParam>`。
### 跨插件调用(硬性规范)
插件之间**禁止**直接依赖对方的 modular 实体类。标准三步法:
1. 接口定义放 `snowy-plugin-api/{x}-api` 模块(如 `SysUserApi`)
2. 被调插件在 `provider/XxxApiProvider.java`(@Service)实现该接口
3. 调用方只依赖 `*-api` 模块注入接口;跨插件返回值用 hutool `JSONObject`,不返回实体类
### 权限模型(两层,易混淆)
- **后端接口权限**:`@SaCheckPermission("/biz/xxx/page")`,值 = **接口 URL**(不是冒号式权限码);用户拥有的 URL 权限来自角色-资源授权生成的数据范围 apiUrl 列表
- **前端按钮权限**:`hasPerm('bizNoticeAdd')`,驼峰式按钮码,存于 SYS_RESOURCE 表 category=BUTTON 行(code 列),登录时放入 buttonCodeList 下发前端
- 路由级白名单(免登录/C端/超管)集中在 `snowy-web-app` 的 `GlobalConfigure`,新增免登录接口必须改那里
- B 端用 `StpUtil` / `StpLoginUserUtil`;C 端用 `StpClientUtil` / `StpClientLoginUserUtil`(接口前缀 /auth/c/**、/client/c/**)
## 前端架构(snowy-admin-web)
- API 层:`src/api/{插件}/{xxx}Api.js` —— 模板 `const request = (url, ...arg) => baseRequest('/biz/xxx/' + url, ...arg)`,导出对象方法,第三参为 method(范本 `src/api/biz/bizNoticeApi.js`)
- 页面三件:`src/views/{插件}/{域}/index.vue`(列表 + s-table)+ `form.vue`(弹窗表单,父组件 `formRef.onOpen(record)` 打开)+ 可选 `detail.vue`
- 组件:37 个 `Xn` 前缀业务组件(XnUpload/XnUserSelector/XnOrgSelector/DictSelect/s-table 等,见 `src/components/`)
- 请求:`src/utils/request.js`(token 头名 `token`、code!==200 统一报错);C 端用 `clientRequest.js`;国密加密 `src/utils/smCrypto.js`
- 文案走 vue-i18n:`$t('xxx.yyy')`,语言文件在 `src/locales/`
## 后端必须遵守的规范
| 必须 | 禁止 |
|---|---|
| 包名根 `vip.xiaonuo.*` | `com.*` / `org.*` 等其他包名 |
| 每个 .java 头部 12 行 Apache 2.0 版权声明(含 `Copyright [2022] [https://www.xiaonuo.vip]`) | 删除/省略版权头(AI 生成代码最容易漏) |
| 依赖注入用 `@Resource` | `@Autowired`、构造器注入 |
| `extends ServiceImpl<Mapper, Entity> implements XxxService` | 只 implements 不继承 ServiceImpl |
| 对象转换用 Hutool `BeanUtil.toBean / copyProperties` | MapstructUtils / MapStruct 体系 |
| Lombok 只用 `@Getter @Setter` | `@Data` |
| 返回 `CommonResult<T>`、异常 `CommonException` | `R<T>`、`ServiceException`、`RuntimeException` |
| URL 动词式写方法上 + `@SaCheckPermission` 用 URL | 类级 @RequestMapping、RESTful 路径变量、冒号式权限码 |
| 主键 `String` 雪花;表名/字段全大写 | Long 主键、小写表名 |
| 注释/Javadoc/异常消息/Swagger 描述**全中文** | 英文注释 |
| 类名前缀 = 插件缩写(Biz/Sys/Dev/Gen/Auth/Client/Mobile) | 无前缀类名 |
| 查询构造带 `checkSqlInjection()`,写操作带 `@Transactional(rollbackFor = Exception.class)` | 裸 QueryWrapper、无事务写操作 |
## ⚠️ 易错点警告(先读再写代码)
**来自 RuoYi的惯性错误**——本项目的约定与它们**方向相反**:
1. ❌ `implements IXxxService` 不继承 → ✅ 必须 `extends ServiceImpl<XxxMapper, Xxx>`
2. ❌ `@RequiredArgsConstructor` 构造器注入 → ✅ `@Resource` 字段注入
3. ❌ `MapstructUtils.convert()` → ✅ `BeanUtil.toBean()`
4. ❌ `@Data` → ✅ `@Getter @Setter`
5. ❌ RESTful `/list`、`/{id}`、PUT/DELETE → ✅ `/page`、`/detail`、全 POST
6. ❌ 权限码 `system:user:list` → ✅ 权限码 = 接口 URL `/sys/user/page`
7. ❌ 单个 Bo + AddGroup/EditGroup → ✅ 每操作一个 Param 类
8. ❌ Long 雪花主键 → ✅ String 主键;❌ 小写表名 → ✅ 全大写
其他易错点:
- **版权头**:AI 生成的新 Java 文件必须补 12 行版权声明(pre-tool-use hook 会警告)
- **逻辑删除**:`DELETE_FLAG` 继承自 CommonEntity,查询自动过滤,不要手写 delete_flag 条件
- **对象转换到 Entity 时**:`BeanUtil.toBean(param, Xxx.class)` 用于新增;编辑先 `queryEntity(id)` 查出再 `BeanUtil.copyProperties(param, entity)`
- **新业务表**用 `BIZ_` 前缀(出厂 biz 域复用了 SYS_/DEV_ 表是历史设计,新表不要模仿)
- 前端是 **Ant Design Vue + JS**:不要写 Element Plus 组件、不要写 TypeScript
## 新增业务功能时的流程
1. 设计表(BIZ_ 前缀大写、String 雪花主键、审计字段组齐全,参考 `.claude/skills/database-ops`)
2. 开发:`/dev` 命令(模式 A:AI 直写六件套 + 前端三件;模式 B:用平台自带代码生成器)或 `/crud`(表已存在)
3. 菜单 SQL:SYS_RESOURCE 表 INSERT(MENU 行 + BUTTON 行,按钮码驼峰式,参考 gen 插件 sqlend 模板)
4. 重启后端 + 刷新前端,在角色管理里给角色授权新资源
5. `/check` 检查规范;完整度口径见 `.claude/framework-config.json`(六件套 /6)
## .claude 目录说明
| 目录 | 内容 |
|---|---|
| `skills/` | 26 个技能(crud-development、code-patterns、security-auth、env-setup、api-verify、platform-extension 等),hook 会强制评估激活 |
| `commands/` | 10 个斜杠命令:/start /dev /crud /check /sync /update-status /progress /next /init-docs /add-todo |
| `agents/` | 2 个子代理:code-reviewer(只读审查)、project-manager(项目管理文档) |
| `docs/` | 8 篇开发指南(框架说明/后端/前端/数据库/工具类/国密安全等) |
| `templates/` | 3 个管理文档模板(需求/项目状态/待办清单) |
| `hooks/` | 3 个 hook:强制技能评估、危险命令拦截、nul 清理 |
| `framework-config.json` | 框架 vs 业务划分与进度统计规则(机器可读) |
进度统计口径:出厂 biz 7 域(index/dict/group/notice/org/position/user)为演示代码**不计进度**,新建业务域才计入(完整度 = 六件套存在数 / 6)。
## 外部依赖说明
- 无 `.mcp.json`(未配置数据库 MCP);需要查库时用 mysql CLI,不可用时 SQL 落盘 `docs/sql-pending/` 提示手动执行
- 数据库连接从 `snowy-web-app/src/main/resources/application.properties` 的 dynamic master 段动态解析,**禁止在任何文档/命令里硬编码连接串**
- 无 `.codegraph` 索引;定位代码优先用 Glob/Grep 或 project-navigator 技能的速查表
- git 仓库已初始化(master 分支);提交规范见 git-workflow 技能