feat(education): complete Flyway migration and atomic submit

This commit is contained in:
2026-07-30 12:06:55 +08:00
parent ce02f8acb4
commit 4edf83de94
231 changed files with 32753 additions and 1376 deletions

View File

@@ -0,0 +1,709 @@
# API 设计规范知识库
convention:
id: "api-designer"
name: "API 设计规范"
category: "设计规范"
description: "定义项目 REST API 的设计规范,包括接口命名、请求响应格式、权限控制、文档注解等,确保 API 设计的统一性和规范性"
# ============================================
# 第一部分:设计原则
# ============================================
philosophy:
design_principles:
- principle: "操作式 URL 设计"
description: "使用动词表达操作意图,而非 RESTful 资源式命名"
example: "/system/user/create 而非 POST /users"
- principle: "统一响应格式"
description: "所有接口使用 CommonResult 包装,确保响应格式一致"
- principle: "权限分级控制"
description: "通过 @PermitAll 和 @PreAuthorize 实现公开接口与权限接口的分离"
- principle: "文档即代码"
description: "使用 OpenAPI 3.0 注解,接口文档与代码同步维护"
# ============================================
# 第二部分:接口规范
# ============================================
interface_conventions:
# URL 命名规范
url_naming:
format: "/{模块}/{功能}/{操作}"
rules:
- "模块名与功能名使用小写字母"
- "操作名使用动词或动词短语"
- "多词使用连字符分隔(如 update-status"
examples:
- path: "/system/user/create"
description: "创建用户"
- path: "/system/user/page"
description: "用户分页列表"
- path: "/pay/order/get"
description: "获取订单详情"
- path: "/system/user/update-status"
description: "更新用户状态"
# HTTP 方法映射
http_methods:
- method: "POST"
operations: ["create", "import", "send", "login", "logout"]
description: "创建类操作"
idempotent: false
- method: "PUT"
operations: ["update", "update-status", "update-password"]
description: "更新类操作"
idempotent: true
- method: "DELETE"
operations: ["delete", "delete-list"]
description: "删除类操作"
idempotent: true
- method: "GET"
operations: ["get", "page", "list", "export-excel", "get-import-template"]
description: "查询类操作"
idempotent: true
safe: true
# 标准端点定义
standard_endpoints:
- endpoint: "/create"
method: "POST"
purpose: "创建单个实体"
request: "@RequestBody @Valid XxxSaveReqVO"
response: "CommonResult<Long>"
permission: "{模块}:{功能}:create"
- endpoint: "/update"
method: "PUT"
purpose: "更新单个实体"
request: "@RequestBody @Valid XxxSaveReqVO"
response: "CommonResult<Boolean>"
permission: "{模块}:{功能}:update"
- endpoint: "/delete"
method: "DELETE"
purpose: "删除单个实体"
request: "@RequestParam(\"id\") Long id"
response: "CommonResult<Boolean>"
permission: "{模块}:{功能}:delete"
parameter_annotation: "@Parameter(name = \"id\", description = \"编号\", required = true)"
- endpoint: "/delete-list"
method: "DELETE"
purpose: "批量删除实体"
request: "@RequestParam(\"ids\") List<Long> ids"
response: "CommonResult<Boolean>"
permission: "{模块}:{功能}:delete"
parameter_annotation: "@Parameter(name = \"ids\", description = \"编号列表\", required = true)"
- endpoint: "/get"
method: "GET"
purpose: "获取单个实体详情"
request: "@RequestParam(\"id\") Long id"
response: "CommonResult<XxxRespVO>"
permission: "{模块}:{功能}:query"
parameter_annotation: "@Parameter(name = \"id\", description = \"编号\", required = true)"
- endpoint: "/page"
method: "GET"
purpose: "分页查询列表"
request: "@Valid XxxPageReqVO"
response: "CommonResult<PageResult<XxxRespVO>>"
permission: "{模块}:{功能}:query"
- endpoint: "/list"
method: "GET"
purpose: "列表查询(不分页)"
request: "@RequestParam(\"ids\") List<Long> ids 或无参数"
response: "CommonResult<List<XxxRespVO>>"
permission: "{模块}:{功能}:query"
- endpoint: "/export-excel"
method: "GET"
purpose: "导出 Excel"
request: "@Valid XxxPageReqVO"
response: "void直接写入 HttpServletResponse"
permission: "{模块}:{功能}:export"
special_annotation: "@ApiAccessLog(operateType = EXPORT)"
- endpoint: "/get-import-template"
method: "GET"
purpose: "获取导入模板"
request: "无参数"
response: "void直接写入 HttpServletResponse"
permission: "通常无需权限或 {模块}:{功能}:import"
- endpoint: "/import"
method: "POST"
purpose: "导入 Excel"
request: "@RequestParam(\"file\") MultipartFile file, @RequestParam(\"updateSupport\") Boolean updateSupport"
response: "CommonResult<XxxImportRespVO>"
permission: "{模块}:{功能}:import"
parameter_annotation: "@Parameters({...})"
# ============================================
# 第三部分:注解规范
# ============================================
annotation_standards:
# Controller 类注解
class_annotations:
- annotation: "@Tag"
format: "@Tag(name = \"管理后台 - {模块名}\")"
purpose: "OpenAPI 3.0 文档分组"
import: "io.swagger.v3.oas.annotations.tags.Tag"
- annotation: "@RestController"
purpose: "REST 控制器"
import: "org.springframework.web.bind.annotation.RestController"
- annotation: "@RequestMapping"
format: "@RequestMapping(\"/{模块}/{功能}\")"
purpose: "路由前缀"
import: "org.springframework.web.bind.annotation.RequestMapping"
- annotation: "@Validated"
purpose: "参数校验支持"
import: "org.springframework.validation.annotation.Validated"
# 方法注解
method_annotations:
- annotation: "@Operation"
format: "@Operation(summary = \"{操作描述}\")"
purpose: "接口文档说明"
import: "io.swagger.v3.oas.annotations.Operation"
examples:
- "@Operation(summary = \"创建用户\")"
- "@Operation(summary = \"获得用户分页列表\")"
- "@Operation(summary = \"导出用户 Excel\")"
- annotation: "@PermitAll"
purpose: "公开接口(无需登录)"
import: "cn.iocoder.yudao.framework.security.core.annotations.PermitAll"
when_to_use: "登录、登出、注册、验证码等无需认证的接口"
- annotation: "@PreAuthorize"
format: "@PreAuthorize(\"@ss.hasPermission('{模块}:{功能}:{操作}')\")"
purpose: "权限控制"
import: "org.springframework.security.access.prepost.PreAuthorize"
examples:
- "@PreAuthorize(\"@ss.hasPermission('system:user:create')\")"
- "@PreAuthorize(\"@ss.hasPermission('pay:order:query')\")"
- annotation: "@Parameter"
format: "@Parameter(name = \"{参数名}\", description = \"{描述}\", required = {是否必填}, example = \"{示例}\")"
purpose: "单参数文档"
import: "io.swagger.v3.oas.annotations.Parameter"
when_to_use: "单个 @RequestParam 参数时使用"
- annotation: "@Parameters"
format: "@Parameters({ @Parameter(name = \"xxx\", ...), @Parameter(name = \"yyy\", ...) })"
purpose: "多参数文档"
import: "io.swagger.v3.oas.annotations.Parameters"
when_to_use: "多个 @RequestParam 参数时使用"
- annotation: "@ApiAccessLog"
format: "@ApiAccessLog(operateType = EXPORT)"
purpose: "操作日志记录"
import: "cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog"
when_to_use: "导出、导入等需要记录日志的操作"
constants_import: "static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT"
# 参数注解
parameter_annotations:
- annotation: "@RequestBody"
purpose: "接收 JSON 请求体"
import: "org.springframework.web.bind.annotation.RequestBody"
usage: "@RequestBody @Valid XxxReqVO reqVO"
- annotation: "@RequestParam"
purpose: "接收 URL 查询参数"
import: "org.springframework.web.bind.annotation.RequestParam"
usage: "@RequestParam(\"id\") Long id"
- annotation: "@Valid"
purpose: "参数校验"
import: "javax.validation.Valid"
usage: "@Valid @RequestBody XxxReqVO reqVO"
# ============================================
# 第四部分:请求响应规范
# ============================================
request_response:
# 统一响应结构
common_result:
class: "cn.iocoder.yudao.framework.common.pojo.CommonResult<T>"
structure:
code: "Integer0 表示成功)"
msg: "String错误提示信息"
data: "T返回数据"
usage:
success: "return success(data)"
success_boolean: "return success(true)"
success_id: "return success(id)"
import: "static cn.iocoder.yudao.framework.common.pojo.CommonResult.success"
# 分页响应结构
page_result:
class: "cn.iocoder.yudao.framework.common.pojo.PageResult<T>"
structure:
total: "Long总记录数"
list: "List<T>(当前页数据列表)"
usage: "CommonResult<PageResult<XxxRespVO>> getXxxPage(...)"
empty: "new PageResult<>(pageResult.getTotal())"
# 分页请求参数
page_param:
class: "cn.iocoder.yudao.framework.common.pojo.PageParam"
fields:
pageNo: "Integer页码从 1 开始,默认 1最小值 1"
pageSize: "Integer每页条数默认 10范围 1-200"
constants:
PAGE_NO: "1"
PAGE_SIZE: "10"
PAGE_SIZE_NONE: "-1不分页用于导出等场景"
usage: "分页查询 VO 继承 PageParam"
# 请求 VO 类型
request_vo_types:
- type: "SaveReqVO"
purpose: "新增/修改请求"
naming: "XxxSaveReqVO"
features:
- "修改时包含 id 字段"
- "包含 @NotBlank/@NotNull 校验注解"
- "使用 @Schema 注解描述字段"
- type: "PageReqVO"
purpose: "分页查询请求"
naming: "XxxPageReqVO"
features:
- "继承 PageParam"
- "包含查询条件字段"
- "时间字段使用 @DateTimeFormat"
- type: "ImportReqVO"
purpose: "导入请求"
naming: "XxxImportReqVO 或使用 @RequestParam 接收 file"
features:
- "包含 file 和 updateSupport 参数"
- type: "RespVO"
purpose: "响应对象"
naming: "XxxRespVO"
features:
- "包含 @Schema 注解描述字段"
- "包含 createTime 等只读字段"
# VO 注解规范
vo_annotations:
- annotation: "@Schema"
purpose: "OpenAPI 3.0 字段描述"
format: "@Schema(description = \"{字段描述}\", requiredMode = Schema.RequiredMode.REQUIRED, example = \"{示例}\")"
import: "io.swagger.v3.oas.annotations.media.Schema"
- annotation: "@ExcelProperty"
purpose: "Excel 导出字段"
format: "@ExcelProperty(\"{列名}\")"
import: "com.alibaba.excel.annotation.ExcelProperty"
- annotation: "@DictFormat"
purpose: "字典转换"
format: "@DictFormat(\"{字典类型}\")"
# ============================================
# 第五部分:权限标识规范
# ============================================
permission_naming:
format: "{模块}:{功能}:{操作}"
rules:
- "模块名与功能名使用小写字母"
- "操作名使用标准动词"
standard_operations:
- operation: "query"
description: "查询权限(包括 get、page、list"
- operation: "create"
description: "新增权限"
- operation: "update"
description: "修改权限"
- operation: "delete"
description: "删除权限"
- operation: "export"
description: "导出权限"
- operation: "import"
description: "导入权限"
examples:
- "system:user:query"
- "system:user:create"
- "system:user:update"
- "system:user:delete"
- "system:user:export"
- "system:user:import"
- "pay:order:query"
- "pay:order:create"
# 权限 SQL 配置
permission_sql:
menu_format: |
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('{功能名称}管理', '', 2, 0, {父菜单ID}, '{功能}', 'ep:document', '{模块}/{功能}/index', 0);
button_format: |
INSERT INTO system_menu (name, permission, type, sort, parent_id, status) VALUES
('{功能名称}查询', '{模块}:{功能}:query', 3, 1, @menuId, 0),
('{功能名称}新增', '{模块}:{功能}:create', 3, 2, @menuId, 0),
('{功能名称}修改', '{模块}:{功能}:update', 3, 3, @menuId, 0),
('{功能名称}删除', '{模块}:{功能}:delete', 3, 4, @menuId, 0),
('{功能名称}导出', '{模块}:{功能}:export', 3, 5, @menuId, 0);
# ============================================
# 第六部分:错误码体系
# ============================================
error_codes:
format: "1_模块编号_功能编号_错误序号"
success_code: 0
# 全局错误码
global_errors:
- code: 0
message: "成功"
- code: 400
message: "请求参数不正确"
- code: 401
message: "账号未登录"
- code: 403
message: "没有该操作权限"
- code: 404
message: "请求未找到"
- code: 405
message: "请求方法不正确"
- code: 423
message: "请求失败,请稍后重试"
- code: 429
message: "请求过于频繁"
- code: 500
message: "系统异常"
- code: 501
message: "功能未实现/未开启"
- code: 502
message: "错误的配置项"
- code: 900
message: "重复请求"
- code: 901
message: "演示模式,禁止写操作"
- code: 999
message: "未知错误"
# 模块编号速查
module_codes:
- module: "infra"
code_prefix: "1_001"
description: "基础设施模块"
- module: "system"
code_prefix: "1_002"
description: "系统管理模块"
- module: "member"
code_prefix: "1_003"
description: "会员模块"
- module: "pay"
code_prefix: "1_007"
description: "支付模块"
- module: "product"
code_prefix: "1_008"
description: "商品模块"
- module: "trade"
code_prefix: "1_011"
description: "交易模块"
- module: "promotion"
code_prefix: "1_013"
description: "促销模块"
# 错误码定义模板
error_code_template: |
// ========== {实体名称} 相关错误码 1_XXX_XXX_XXX ==========
ErrorCode XXX_NOT_EXISTS = new ErrorCode(1_002_001_000, "{实体名称}不存在");
ErrorCode XXX_CODE_DUPLICATE = new ErrorCode(1_002_001_001, "已存在该编码的{实体名称}");
ErrorCode XXX_NAME_DUPLICATE = new ErrorCode(1_002_001_002, "已存在该名称的{实体名称}");
ErrorCode XXX_CAN_NOT_DELETE = new ErrorCode(1_002_001_003, "{实体名称}不能删除,原因:{}");
ErrorCode XXX_STATUS_ERROR = new ErrorCode(1_002_001_004, "{实体名称}状态不正确");
# ============================================
# 第七部分Controller 模板
# ============================================
controller_template:
# 基础 CRUD 模板
basic_crud: |
package cn.iocoder.yudao.module.{模块}.controller.admin.{功能};
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.{模块}.controller.admin.{功能}.vo.*;
import cn.iocoder.yudao.module.{模块}.dal.dataobject.{功能}.XxxDO;
import cn.iocoder.yudao.module.{模块}.service.{功能}.XxxService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - {实体名称}")
@RestController
@RequestMapping("/{模块}/{功能}")
@Validated
public class XxxController {
@Resource
private XxxService xxxService;
@PostMapping("/create")
@Operation(summary = "创建{实体名称}")
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:create')")
public CommonResult<Long> createXxx(@Valid @RequestBody XxxSaveReqVO createReqVO) {
return success(xxxService.createXxx(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新{实体名称}")
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:update')")
public CommonResult<Boolean> updateXxx(@Valid @RequestBody XxxSaveReqVO updateReqVO) {
xxxService.updateXxx(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除{实体名称}")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:delete')")
public CommonResult<Boolean> deleteXxx(@RequestParam("id") Long id) {
xxxService.deleteXxx(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得{实体名称}")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:query')")
public CommonResult<XxxRespVO> getXxx(@RequestParam("id") Long id) {
XxxDO xxx = xxxService.getXxx(id);
return success(BeanUtils.toBean(xxx, XxxRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得{实体名称}分页")
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:query')")
public CommonResult<PageResult<XxxRespVO>> getXxxPage(@Valid XxxPageReqVO pageReqVO) {
PageResult<XxxDO> pageResult = xxxService.getXxxPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, XxxRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出{实体名称} Excel")
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportXxxExcel(@Valid XxxPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<XxxDO> list = xxxService.getXxxPage(pageReqVO).getList();
List<XxxExcelVO> excelList = BeanUtils.toBean(list, XxxExcelVO.class);
ExcelUtils.write(response, "{实体名称}.xls", "数据", XxxExcelVO.class, excelList);
}
}
# 公开接口模板
public_endpoint: |
@PostMapping("/login")
@PermitAll
@Operation(summary = "使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}
# 批量操作模板
batch_operations: |
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号列表", required = true)
@Operation(summary = "批量删除{实体名称}")
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:delete')")
public CommonResult<Boolean> deleteXxxList(@RequestParam("ids") List<Long> ids) {
xxxService.deleteXxxList(ids);
return success(true);
}
# 导入导出模板
import_export: |
@GetMapping("/get-import-template")
@Operation(summary = "获得导入{实体名称}模板")
public void importTemplate(HttpServletResponse response) throws IOException {
// 手动创建导出 demo
List<XxxImportExcelVO> list = Arrays.asList(
XxxImportExcelVO.builder().name("示例1").code("TEST1").build(),
XxxImportExcelVO.builder().name("示例2").code("TEST2").build()
);
ExcelUtils.write(response, "{实体名称}导入模板.xls", "数据", XxxImportExcelVO.class, list);
}
@PostMapping("/import")
@Operation(summary = "导入{实体名称}")
@Parameters({
@Parameter(name = "file", description = "Excel 文件", required = true),
@Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
})
@PreAuthorize("@ss.hasPermission('{模块}:{功能}:import')")
public CommonResult<XxxImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
@RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
List<XxxImportExcelVO> list = ExcelUtils.read(file, XxxImportExcelVO.class);
return success(xxxService.importXxxList(list, updateSupport));
}
# ============================================
# 第八部分:设计流程
# ============================================
design_workflow:
steps:
- step: 1
name: "实体分析"
action: "分析数据库设计或 PRD确定实体字段和业务含义"
input: ["db_design_file", "prd_file", "模块 skill 文档"]
output: "实体字段清单"
checklist:
- "确认实体所属模块"
- "阅读对应模块的 skill 文档"
- "确定需要哪些标准端点"
- step: 2
name: "端点设计"
action: "根据业务需求确定需要的 API 端点"
considerations:
- "是否需要完整 CRUDcreate/update/delete/get/page"
- "是否需要批量操作delete-list"
- "是否需要导入导出export-excel/import"
- "是否需要状态切换update-status"
- "是否有公开接口需求(使用 @PermitAll"
- step: 3
name: "请求响应设计"
action: "设计各端点的请求参数和响应格式"
templates:
- "使用 SaveReqVO 处理新增/修改"
- "使用 PageReqVO 处理分页查询(继承 PageParam"
- "使用 RespVO 处理响应"
- "使用 CommonResult 包装响应"
- step: 4
name: "权限设计"
action: "确定各端点的权限标识"
rules:
- "公开接口使用 @PermitAll"
- "权限接口使用 @PreAuthorize"
- "权限标识格式:模块:功能:操作"
- "查询类接口统一使用 query 权限"
- step: 5
name: "错误码设计"
action: "定义业务错误码"
rules:
- "格式1_模块编号_功能编号_错误序号"
- "常见错误:不存在、重复、状态错误、操作限制"
- step: 6
name: "文档生成"
action: "生成 Controller 代码和 API 文档"
output: ["Controller.java", "VO classes", "错误码定义", "权限 SQL"]
# ============================================
# 第九部分:检查清单
# ============================================
checklist:
before_design:
- "确认实体所属模块"
- "阅读对应模块的 skill 文档"
- "了解现有类似接口的设计模式"
- "准备表结构设计"
during_design:
- "URL 命名符合规范(/{模块}/{功能}/{操作}"
- "HTTP 方法与操作类型匹配"
- "请求 VO 类型选择正确"
- "响应格式使用 CommonResult"
- "权限标识命名正确"
- "注解使用完整"
after_design:
- "所有接口添加 @Operation 注解"
- "参数添加 @Parameter 或 @Parameters 注解"
- "权限接口添加 @PreAuthorize 注解"
- "公开接口添加 @PermitAll 注解"
- "导出接口添加 @ApiAccessLog 注解"
- "错误码定义完整"
# ============================================
# 第十部分:与 entity-implementation.md 协同
# ============================================
collaboration:
relationship:
- doc: "api-designer.yaml"
role: "API 设计阶段规范指导"
focus: "设计决策、端点规划、权限标识"
- doc: "entity-implementation.md"
role: "完整实现流程指导"
focus: "实现细节、模板代码、各层规范"
usage_flow:
- "1. 使用 api-designer.yaml 进行 API 设计决策"
- "2. 参考 entity-implementation.md 进行完整实现"
- "3. 两者 Controller 模板保持一致"
# ============================================
# 快速参考
# ============================================
quick_reference:
http_methods:
POST: "创建类操作create, import, send"
PUT: "更新类操作update, update-status"
DELETE: "删除类操作delete, delete-list"
GET: "查询类操作get, page, list, export-excel"
standard_endpoints:
/create: "创建 → CommonResult<Long>"
/update: "更新 → CommonResult<Boolean>"
/delete: "删除 → CommonResult<Boolean>"
/delete-list: "批量删除 → CommonResult<Boolean>"
/get: "详情 → CommonResult<XxxRespVO>"
/page: "分页 → CommonResult<PageResult<XxxRespVO>>"
/list: "列表 → CommonResult<List<XxxRespVO>>"
/export-excel: "导出 → void"
/import: "导入 → CommonResult<XxxImportRespVO>"
permission_operations:
query: "查询get/page/list"
create: "新增"
update: "修改"
delete: "删除"
export: "导出"
import: "导入"
response_types:
CommonResult<Long>: "创建类响应"
CommonResult<Boolean>: "操作类响应"
CommonResult<XxxRespVO>: "详情类响应"
CommonResult<PageResult<XxxRespVO>>: "分页类响应"
CommonResult<List<XxxRespVO>>: "列表类响应"
vo_types:
XxxSaveReqVO: "新增/修改请求"
XxxPageReqVO: "分页查询请求(继承 PageParam"
XxxRespVO: "响应对象"
XxxExcelVO: "Excel 导出对象"
XxxImportExcelVO: "Excel 导入对象"
XxxImportRespVO: "导入结果响应"
imports:
controller_common:
- "io.swagger.v3.oas.annotations.tags.Tag"
- "io.swagger.v3.oas.annotations.Operation"
- "io.swagger.v3.oas.annotations.Parameter"
- "io.swagger.v3.oas.annotations.Parameters"
- "org.springframework.security.access.prepost.PreAuthorize"
- "cn.iocoder.yudao.framework.common.pojo.CommonResult"
- "cn.iocoder.yudao.framework.common.pojo.PageResult"
- "static cn.iocoder.yudao.framework.common.pojo.CommonResult.success"

View File

@@ -0,0 +1,518 @@
# CRUD 代码生成 Skill
# 整合性入口指南 - 引用已有的模板文档
skill:
id: "crud-generator"
name: "CRUD 代码生成"
version: "1.0.0"
category: "design"
description: "根据数据库表结构自动生成符合yudao项目规范的完整CRUD功能代码"
created_at: "2026-03-31"
updated_at: "2026-03-31"
# ============================================
# 第一部分:触发条件
# ============================================
trigger:
commands:
- "/crud-gen"
- "/generate-crud"
keywords:
- "生成CRUD"
- "创建实体代码"
- "生成增删改查"
- "实体类实现"
events:
- name: "table_created"
condition: "数据库表创建完成"
# ============================================
# 第二部分:输入参数
# ============================================
input:
parameters:
- name: "module"
type: "string"
required: true
description: "模块编码system/infra/pay/mes等"
examples: ["system", "infra", "pay", "mes"]
- name: "feature"
type: "string"
required: true
description: "功能编码user/order/process等"
examples: ["user", "order", "process"]
- name: "entity_name"
type: "string"
required: true
description: "实体名称(中文)"
examples: ["用户", "订单", "工序"]
- name: "entity_class"
type: "string"
required: true
description: "实体类名(英文,首字母大写)"
examples: ["User", "Order", "Process"]
- name: "table_name"
type: "string"
required: true
description: "数据库表名"
examples: ["system_user", "pay_order", "mes_process"]
- name: "is_tenant"
type: "boolean"
required: false
default: true
description: "是否多租户实体true使用TenantBaseDOfalse使用BaseDO"
- name: "fields"
type: "array"
required: true
description: "业务字段列表"
item_schema:
name: "string"
type: "string"
required: "boolean"
description: "string"
- name: "enable_export"
type: "boolean"
required: false
default: false
description: "是否启用Excel导出功能"
- name: "enable_import"
type: "boolean"
required: false
default: false
description: "是否启用Excel导入功能"
- name: "enable_batch"
type: "boolean"
required: false
default: true
description: "是否启用批量删除功能"
# ============================================
# 第三部分执行流程5阶段
# ============================================
workflow:
description: "CRUD代码生成按以下5个阶段顺序执行"
phases:
# 阶段1结构分析
- name: "结构分析"
description: "分析表结构,确定实体属性和生成范围"
duration: "5min"
steps:
- step: "解析表名和字段定义"
action: "读取表结构信息,提取字段列表"
- step: "确定DO基类"
action: "根据is_tenant参数选择BaseDO或TenantBaseDO"
rules:
- "is_tenant=true → TenantBaseDO含tenantId"
- "is_tenant=false → BaseDO"
- step: "生成文件清单"
action: "根据参数确定需要创建的文件列表"
decision_table:
- condition: "默认"
files: ["DO", "Mapper", "Service", "ServiceImpl", "Controller", "SaveReqVO", "PageReqVO", "RespVO"]
- condition: "enable_export=true"
add_files: ["ExcelVO", "export-excel接口"]
- condition: "enable_import=true"
add_files: ["ImportExcelVO", "ImportRespVO", "import接口"]
- condition: "enable_batch=true"
add_files: ["delete-list接口"]
- step: "分配错误码编号"
action: "根据模块编号分配错误码段"
reference: "api-designer.yaml 第六部分 module_codes"
# 阶段2VO生成
- name: "VO生成"
description: "生成请求和响应VO类"
duration: "5min"
reference: "entity-implementation.md 第3.6节"
steps:
- step: "生成 SaveReqVO"
description: "新增/修改共用请求对象"
template_path: "entity-implementation.md 第621-657行"
rules:
- "包含id字段修改时必填"
- "包含所有可创建/更新字段"
- "排除creator, createTime, updater, updateTime, deleted, tenantId"
- "添加校验注解(@NotBlank/@NotNull/@Size"
- step: "生成 PageReqVO"
description: "分页查询请求对象"
template_path: "entity-implementation.md 第661-695行"
rules:
- "继承 PageParam"
- "包含常用查询条件字段"
- "时间字段使用 @DateTimeFormat"
- step: "生成 RespVO"
description: "响应对象"
template_path: "entity-implementation.md 第699-733行"
rules:
- "包含所有展示字段"
- "包含 createTime 等只读字段"
- step: "生成 ExcelVO可选"
condition: "enable_export=true 或 enable_import=true"
template_path: "entity-implementation.md 第737-773行"
# 阶段3DAL生成
- name: "DAL生成"
description: "生成数据访问层代码"
duration: "3min"
reference: "entity-implementation.md 第3.2-3.3节"
steps:
- step: "生成 DO实体类"
template_path: "entity-implementation.md 第128-180行"
rules:
- "@TableName 指定表名"
- "@TableId 标注主键"
- "继承正确基类"
- "枚举字段添加引用注释"
- step: "生成 Mapper接口"
template_path: "entity-implementation.md 第234-281行"
rules:
- "继承 BaseMapperX<EntityDO>"
- "定义 selectPage 分页查询方法"
- "使用 LambdaQueryWrapperX 构建条件"
- "使用 xxxIfPresent() 方法链"
# 阶段4Service生成
- name: "Service生成"
description: "生成业务逻辑层代码"
duration: "5min"
reference: "entity-implementation.md 第3.4节"
steps:
- step: "生成 Service接口"
template_path: "entity-implementation.md 第313-377行"
methods:
- "createXxx(SaveReqVO) → Long"
- "updateXxx(SaveReqVO) → void"
- "deleteXxx(Long id) → void"
- "getXxx(Long id) → XxxDO"
- "getXxxPage(PageReqVO) → PageResult<XxxDO>"
- step: "生成 ServiceImpl实现"
template_path: "entity-implementation.md 第382-485行"
rules:
- "@Service + @Validated 注解"
- "新增方法校验唯一性"
- "更新/删除方法校验存在性"
- "使用 exception(ERROR_CODE) 抛异常"
- "使用 BeanUtils.toBean() 转换对象"
# 阶段5Controller生成
- name: "Controller生成"
description: "生成HTTP接口层代码"
duration: "7min"
reference: "api-designer.yaml 第七部分"
steps:
- step: "生成 Controller类"
template_path: "api-designer.yaml 第420-511行"
annotations:
- "@Tag(name = '管理后台 - {实体名称}')"
- "@RestController"
- "@RequestMapping('/{module}/{feature}')"
- "@Validated"
- step: "生成标准端点"
endpoints:
- endpoint: "/create"
method: "POST"
template_path: "api-designer.yaml 第67-72行"
- endpoint: "/update"
method: "PUT"
template_path: "api-designer.yaml 第74-79行"
- endpoint: "/delete"
method: "DELETE"
template_path: "api-designer.yaml 第81-87行"
- endpoint: "/get"
method: "GET"
template_path: "api-designer.yaml 第97-103行"
- endpoint: "/page"
method: "GET"
template_path: "api-designer.yaml 第105-110行"
- step: "生成扩展端点(可选)"
conditionals:
- condition: "enable_batch=true"
endpoint: "/delete-list"
template_path: "api-designer.yaml 第523-531行"
- condition: "enable_export=true"
endpoint: "/export-excel"
template_path: "api-designer.yaml 第119-126行"
- condition: "enable_import=true"
endpoints: ["/get-import-template", "/import"]
template_path: "api-designer.yaml 第534-557行"
- step: "生成权限SQL"
template_path: "api-designer.yaml 第333-344行"
# ============================================
# 第四部分:输出产物
# ============================================
output:
base_path: "yudao-module-{module}-biz/src/main/java/cn/iocoder/yudao/module/{module}"
artifacts:
# DAL层
- name: "DO实体类"
path: "dal/dataobject/{feature}/{Entity}DO.java"
description: "数据库实体映射类"
- name: "Mapper接口"
path: "dal/mysql/{feature}/{Entity}Mapper.java"
description: "数据访问接口"
# Service层
- name: "Service接口"
path: "service/{feature}/{Entity}Service.java"
description: "业务服务接口"
- name: "Service实现"
path: "service/{feature}/{Entity}ServiceImpl.java"
description: "业务服务实现类"
# Controller层
- name: "Controller"
path: "controller/admin/{feature}/{Entity}Controller.java"
description: "HTTP API控制器"
# VO类
- name: "SaveReqVO"
path: "controller/admin/{feature}/vo/{Entity}SaveReqVO.java"
description: "新增/修改请求对象"
- name: "PageReqVO"
path: "controller/admin/{feature}/vo/{Entity}PageReqVO.java"
description: "分页查询请求对象"
- name: "RespVO"
path: "controller/admin/{feature}/vo/{Entity}RespVO.java"
description: "响应对象"
# 可选产物
- name: "ExcelVO"
path: "controller/admin/{feature}/vo/{Entity}ExcelVO.java"
condition: "enable_export=true"
description: "Excel导出对象"
- name: "ImportExcelVO"
path: "controller/admin/{feature}/vo/{Entity}ImportExcelVO.java"
condition: "enable_import=true"
description: "Excel导入对象"
- name: "ImportRespVO"
path: "controller/admin/{feature}/vo/{Entity}ImportRespVO.java"
condition: "enable_import=true"
description: "导入结果响应"
# 配置文件
- name: "错误码定义"
path: "enums/ErrorCodeConstants.java"
action: "追加"
description: "业务错误码定义"
- name: "权限配置SQL"
path: "sql/menu/{module}_{feature}_menu.sql"
description: "菜单和按钮权限SQL"
# ============================================
# 第五部分:命名规范速查
# ============================================
naming_standards:
# 文件命名
files:
do: "{Entity}DO"
mapper: "{Entity}Mapper"
service: "{Entity}Service"
service_impl: "{Entity}ServiceImpl"
controller: "{Entity}Controller"
vo_save: "{Entity}SaveReqVO"
vo_page: "{Entity}PageReqVO"
vo_resp: "{Entity}RespVO"
vo_excel: "{Entity}ExcelVO"
vo_import: "{Entity}ImportExcelVO"
# 权限标识
permission:
format: "{module}:{feature}:{operation}"
operations:
- "query"
- "create"
- "update"
- "delete"
- "export"
- "import"
example: "system:user:create"
# 错误码
error_code:
format: "1_{module_code}_{feature_code}_{seq}"
module_codes:
infra: "001"
system: "002"
member: "003"
pay: "007"
product: "008"
trade: "011"
promotion: "013"
example: "1_002_001_000"
# 数据库表名
table:
format: "{module}_{feature}"
example: "system_user"
# ============================================
# 第六部分:关联文档引用
# ============================================
references:
primary:
- path: "skills/design/api-designer.yaml"
description: "API设计规范、Controller模板、权限标识、错误码体系"
sections:
- "第二部分:接口规范(标准端点定义)"
- "第五部分:权限标识规范"
- "第六部分:错误码体系"
- "第七部分Controller模板"
- path: "skills/usage/entity-implementation.md"
description: "实体类实现完整流程、各层代码模板"
sections:
- "3.2节DO实体类模板"
- "3.3节Mapper层模板"
- "3.4节Service层模板"
- "3.5节Controller层模板"
- "3.6节VO类模板"
- "3.7节:错误码定义"
- "3.8节权限配置SQL"
- path: "skills/design/db-designer.yaml"
relationship: "前置依赖"
description: "数据库表结构设计规范"
secondary:
- path: "skills/modules/{module}/skill-{module}.yaml"
relationship: "模块参考"
description: "具体模块的技术规范和扩展指南"
example: "skills/modules/system/skill-system.yaml"
# ============================================
# 第七部分:质量检查清单
# ============================================
checklist:
before_generate:
- item: "确认模块编码正确"
check: "module参数在module_codes中有对应编号"
- item: "确认表名符合命名规范"
check: "表名格式为 {module}_{feature}"
- item: "阅读对应模块的skill文档"
reference: "skills/modules/{module}/skill-{module}.yaml"
- item: "确认字段类型映射正确"
check: "数据库类型与Java类型对应"
during_generate:
- item: "DO类继承正确的基类"
check: "TenantBaseDO多租户或 BaseDO单租户"
- item: "Mapper使用BaseMapperX和LambdaQueryWrapperX"
check: "继承BaseMapperX条件构建使用xxxIfPresent()"
- item: "Service包含业务校验方法"
check: "新增校验唯一性,更新/删除校验存在性"
- item: "Controller注解完整"
check: "@Tag/@RestController/@RequestMapping/@Validated"
- item: "接口方法注解完整"
check: "@Operation/@PreAuthorize/@Parameter"
after_generate:
- item: "错误码编号符合规范"
format: "1_{module_code}_{feature_code}_{seq}"
- item: "权限标识格式正确"
format: "{module}:{feature}:{operation}"
- item: "所有文件包路径正确"
base: "cn.iocoder.yudao.module.{module}"
- item: "VO类添加@Schema注解"
check: "所有字段有description"
- item: "导入语句无缺失"
check: "所有注解有对应import"
# ============================================
# 第八部分与原skill的差异调整
# ============================================
migration_notes:
# 命名规范调整
naming_adjustments:
- original: "CreateDTO"
target: "XxxSaveReqVO"
reason: "yudao系统新增/修改共用一个VO"
- original: "UpdateDTO"
target: "XxxSaveReqVO"
reason: "与CreateDTO合并通过id字段区分"
- original: "QueryDTO"
target: "XxxPageReqVO"
reason: "yudao系统分页查询VO继承PageParam"
- original: "VO"
target: "XxxRespVO"
reason: "响应对象命名规范"
- original: "ListVO"
target: "XxxRespVO"
reason: "分页列表使用同一响应VO"
# 技术栈调整
tech_adjustments:
- original: "BaseMapper<Entity>"
target: "BaseMapperX<XxxDO>"
reason: "yudao扩展的Mapper基类"
- original: "LambdaQueryWrapper"
target: "LambdaQueryWrapperX"
reason: "支持xxxIfPresent()简化条件构建"
- original: "Result<T>"
target: "CommonResult<T>"
reason: "yudao统一响应包装类"
- original: "Page<T>"
target: "PageResult<T>"
reason: "yudao分页结果类"
- original: "@Api (Swagger 2)"
target: "@Tag (OpenAPI 3)"
reason: "Swagger版本升级"
- original: "@ApiOperation"
target: "@Operation"
reason: "OpenAPI 3.0注解"
# 目录结构调整
path_adjustments:
- original: "controller/{Entity}Controller.java"
target: "controller/admin/{feature}/{Entity}Controller.java"
- original: "dto/{Entity}DTO.java"
target: "controller/admin/{feature}/vo/{Entity}VO.java"
- original: "mapper/{Entity}Mapper.java"
target: "dal/mysql/{feature}/{Entity}Mapper.java"
- original: "entity/{Entity}.java"
target: "dal/dataobject/{feature}/{Entity}DO.java"
# ============================================
# 快速参考
# ============================================
quick_reference:
standard_endpoints:
POST_create: "创建 → CommonResult<Long>"
PUT_update: "更新 → CommonResult<Boolean>"
DELETE_delete: "删除 → CommonResult<Boolean>"
GET_get: "详情 → CommonResult<XxxRespVO>"
GET_page: "分页 → CommonResult<PageResult<XxxRespVO>>"
DELETE_delete-list: "批量删除 → CommonResult<Boolean>"
GET_export-excel: "导出 → void"
POST_import: "导入 → CommonResult<XxxImportRespVO>"
permission_operations:
query: "查询get/page/list"
create: "新增"
update: "修改"
delete: "删除"
export: "导出"
import: "导入"
base_classes:
BaseDO: "单租户场景 - creator/createTime/updater/updateTime/deleted"
TenantBaseDO: "多租户场景 - 额外含tenantId"
mapper_methods:
selectPage: "分页查询"
selectOne: "查询单条"
selectList: "查询列表"
insert: "插入"
updateById: "更新"
deleteById: "删除"

View File

@@ -0,0 +1,654 @@
# 数据库设计 Skill
# 根据业务需求生成符合项目规范的 PostgreSQL 表结构
# 本项目默认且唯一首选数据库方言为 PostgreSQL除非用户明确指定否则禁止生成 MySQL 语法。
skill:
id: "db-designer"
name: "数据库设计 Skill"
version: "1.0.0"
category: "design"
description: "根据业务需求自动生成符合项目规范的 PostgreSQL 数据库设计包括表结构、索引、ER图等"
created_at: "2026-03-31"
updated_at: "2026-03-31"
# ============================================
# 触发条件
# ============================================
trigger:
commands:
- "/db-design"
- "/db-designer"
keywords:
- "设计数据库"
- "生成表结构"
- "创建数据模型"
- "建表"
- "DDL"
events:
- name: "prd_approved"
condition: "PRD文档审核通过"
- name: "architecture_defined"
condition: "系统架构设计完成"
# ============================================
# 输入参数
# ============================================
input:
parameters:
- name: "module_code"
type: "string"
required: true
description: "模块编码(如 mes、erp、crm、bpm 等)"
examples: ["mes", "erp", "crm", "bpm", "pay", "fz"]
- name: "table_name"
type: "string"
required: true
description: "表名(不含前缀)"
examples: ["work_order", "product", "customer", "letter"]
- name: "business_fields"
type: "array"
required: true
description: "业务字段列表"
- name: "indexes"
type: "array"
required: false
default: []
description: "额外索引定义"
- name: "create_date"
type: "string"
required: false
default: "today"
description: "创建日期(用于文件命名)"
# ============================================
# 设计理念
# ============================================
philosophy:
business_position: "数据库设计是业务建模的核心环节,将业务需求转化为可存储的数据结构"
design_principles:
- "多租户隔离:所有业务表包含 tenant_id 字段,索引必须包含 tenant_id"
- "审计追踪:标准化审计字段,支持数据变更追踪"
- "逻辑删除:使用 deleted 字段实现软删除,避免数据物理删除"
- "索引优化:根据查询场景设计索引,遵循最左匹配原则"
- "命名规范:统一的表名、字段名、索引名命名规则"
# ============================================
# 目录结构规范
# ============================================
directory_structure:
base_path: "sql/postgresql"
layout: |
sql/postgresql/
├── {module}/ # 按模块组织 PostgreSQL 迁移脚本
├── table/ # 通用表结构定义
│ ├── create/ # 建表脚本
│ └── update/ # 表结构更新脚本
├── index/ # 索引定义
├── view/ # 视图定义
├── data/ # 初始数据
└── init/ # 模块初始化脚本
directories:
- name: "table/create"
purpose: "存放建表 SQL 脚本"
note: "每个表一个文件,按日期命名"
- name: "table/update"
purpose: "存放表结构更新 SQL 脚本"
note: "ALTER TABLE、ADD COLUMN 等增量变更"
- name: "sequence"
purpose: "需要显式序列时存放序列脚本"
note: "默认优先使用 GENERATED BY DEFAULT AS IDENTITY仅兼容既有 @KeySequence 约定时显式建序列"
- name: "index"
purpose: "存放索引创建脚本"
note: "可按表名组织子目录"
- name: "view"
purpose: "存放视图创建脚本"
note: "复杂查询可封装为视图"
- name: "data"
purpose: "存放初始数据 SQL 脚本"
note: "INSERT 语句,初始化字典、菜单等"
- name: "init"
purpose: "存放模块初始化脚本"
note: "按模块组织,如 bpm.sql, quartz.sql"
# ============================================
# 文件命名规范
# ============================================
file_naming:
# 建表文件命名
create_table:
format: "YYYY_MM_DD_{表名}_create.sql"
examples:
- "2025_08_09_fz_letter_create.sql"
- "2026_03_25_mes_work_order_create.sql"
rules:
- "日期前缀使用实际创建日期"
- "表名使用小写字母和下划线"
- "统一使用 _create.sql 后缀"
# 更新文件命名
update_table:
format: "YYYY_MM_DD_{表名}_update.sql"
examples:
- "2025_08_15_fz_metaletter_update.sql"
- "2026_01_07_fz_attachment_rel_update.sql"
rules:
- "日期前缀使用实际更新日期"
- "同一表多次更新使用不同日期"
- "统一使用 _update.sql 后缀"
special_cases:
- pattern: "YYYY_MM_DD_{表名}_index_update.sql"
purpose: "专门用于索引更新"
# 序列文件命名
sequence:
format: "YYYY_MM_DD_{表名}_seq_create.sql"
examples:
- "2025_07_24_fz_metaletter_seq_create.sql"
rules:
- "与建表日期保持一致"
- "序列名通常为 表名_seq"
# 初始数据文件命名
initial_data:
format: "模块名.sql"
examples:
- "fazhi.sql"
- "bpm.sql"
- "quartz.sql"
- "ruoyi-vue-pro.sql"
rules:
- "按模块或功能命名"
- "包含该模块的初始数据"
# ============================================
# 表命名规范
# ============================================
table_naming:
# 表名前缀规范
prefixes:
- prefix: "sys_"
usage: "系统核心表"
examples: ["sys_user", "sys_role", "sys_menu", "sys_dict_data"]
- prefix: "mes_"
usage: "制造执行系统表"
examples: ["mes_work_order", "mes_product", "mes_workshop"]
- prefix: "erp_"
usage: "企业资源计划表"
examples: ["erp_purchase", "erp_sale", "erp_inventory"]
- prefix: "crm_"
usage: "客户关系管理表"
examples: ["crm_customer", "crm_contract", "crm_clue"]
- prefix: "bpm_"
usage: "工作流表"
examples: ["bpm_process_definition", "bpm_process_instance"]
- prefix: "pay_"
usage: "支付模块表"
examples: ["pay_order", "pay_refund", "pay_channel"]
- prefix: "member_"
usage: "会员模块表"
examples: ["member_user", "member_level", "member_address"]
- prefix: "infra_"
usage: "基础设施表"
examples: ["infra_file", "infra_config"]
- prefix: "fz_"
usage: "法制业务表"
examples: ["fz_letter", "fz_replay", "fz_handle_unit"]
# 表名规则
rules:
- "使用小写字母,单词间用下划线分隔"
- "使用名词,表示实体或关系"
- "避免使用 PostgreSQL 保留字"
- "长度不超过 64 个字符"
- "关联表命名主表_关联表_rel如 fz_letter_label_rel"
# 字段命名规则
column_rules:
- "使用小写字母和下划线"
- "布尔类型使用 is_ 前缀,如 is_deleted, is_enabled"
- "时间类型使用 _time 或 _date 后缀"
- "外键使用 关联表_id 格式,如 user_id, dept_id"
- "主键统一命名为 id"
# ============================================
# 必需字段规范
# ============================================
required_columns:
# 字段定义
columns:
- name: "id"
type: "BIGINT"
constraint: "GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY"
comment: "主键ID"
position: 1
- name: "tenant_id"
type: "BIGINT"
default: "0"
constraint: "NOT NULL"
comment: "租户编号"
position: 2
note: "所有业务表必须包含,位于 id 之后"
- name: "creator"
type: "VARCHAR(64)"
default: "''"
comment: "创建者"
- name: "create_time"
type: "TIMESTAMP"
default: "CURRENT_TIMESTAMP"
constraint: "NOT NULL"
comment: "创建时间"
- name: "updater"
type: "VARCHAR(64)"
default: "''"
comment: "更新者"
- name: "update_time"
type: "TIMESTAMP"
default: "CURRENT_TIMESTAMP"
constraint: "NOT NULL"
comment: "更新时间"
note: "PostgreSQL 不支持 ON UPDATE CURRENT_TIMESTAMP由应用层或触发器维护"
- name: "deleted"
type: "BOOLEAN"
default: "false"
constraint: "NOT NULL"
comment: "是否删除"
# 可选备注字段
remark:
name: "remark"
type: "VARCHAR(500)"
default: "NULL"
comment: "备注"
position: "before_audit"
# ============================================
# 索引设计规范
# ============================================
index_standards:
# 索引命名规则
naming:
primary_key: "PRIMARY KEY (id)"
unique_key: "uk_{字段名} 或 uk_{字段1_字段2}"
normal_index: "idx_{字段名} 或 idx_{字段1_字段2}"
# 索引设计原则
principles:
- "所有索引必须包含 tenant_id 作为第一列"
- "唯一索引必须包含 deleted 字段(软删除场景)"
- "外键字段必须建立索引"
- "经常用于查询条件的字段建立索引"
- "唯一约束字段建立唯一索引"
- "组合索引注意字段顺序(最左匹配)"
- "避免过多索引,影响写入性能"
# 索引模板
templates:
unique_with_tenant:
pattern: "CREATE UNIQUE INDEX uk_tenant_{col} ON {table} (tenant_id, {col}) WHERE deleted = false"
example: "CREATE UNIQUE INDEX uk_tenant_order ON mes_work_order (tenant_id, order_no) WHERE deleted = false"
normal_with_tenant:
pattern: "CREATE INDEX idx_tenant_{col} ON {table} (tenant_id, {col})"
example: "CREATE INDEX idx_tenant_status ON mes_work_order (tenant_id, status)"
foreign_key:
pattern: "CREATE INDEX idx_tenant_{fk} ON {table} (tenant_id, {fk})"
example: "CREATE INDEX idx_tenant_line ON mes_work_order (tenant_id, line_id)"
# ============================================
# 建表模板
# ============================================
create_table_template:
structure: |
-- ============================================
-- 文件名:{filename}
-- 描述:{table_comment}
-- 作者:{author}
-- 日期:{date}
-- ============================================
CREATE TABLE {table_name} (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
-- 业务字段
{business_columns}
-- 通用字段
remark VARCHAR(500) DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE {table_name} IS '{table_comment}';
{column_comments}
{indexes}
# 字段类型推荐
column_types:
string_short: "VARCHAR(64)"
string_medium: "VARCHAR(255)"
string_long: "VARCHAR(500)"
text: "TEXT"
integer: "INT"
bigint: "BIGINT"
decimal_amount: "DECIMAL(18,2)"
decimal_quantity: "DECIMAL(10,4)"
boolean: "BOOLEAN"
datetime: "TIMESTAMP"
date: "DATE"
status: "SMALLINT"
# ============================================
# 更新脚本规范
# ============================================
update_standards:
# 更新文件命名
naming: "YYYY_MM_DD_{表名}_update.sql"
# 字段操作模板
operations:
add_column: |
-- 新增字段并添加字段注释
ALTER TABLE {table} ADD COLUMN {column} {type};
COMMENT ON COLUMN {table}.{column} IS '{comment}';
-- 示例
ALTER TABLE fz_letter ADD COLUMN priority SMALLINT DEFAULT 0;
COMMENT ON COLUMN fz_letter.priority IS '优先级';
modify_column: |
-- 修改字段类型
ALTER TABLE {table} ALTER COLUMN {column} TYPE {new_type} USING {column}::{new_type};
COMMENT ON COLUMN {table}.{column} IS '{comment}';
-- 示例
ALTER TABLE fz_letter ALTER COLUMN content TYPE TEXT USING content::TEXT;
COMMENT ON COLUMN fz_letter.content IS '信件内容';
drop_column: |
-- 删除字段(确保无业务依赖)
ALTER TABLE {table} DROP COLUMN {column};
add_index: |
-- 添加普通索引
CREATE INDEX idx_tenant_{col} ON {table} (tenant_id, {col});
-- 添加软删除唯一索引
CREATE UNIQUE INDEX uk_tenant_{col} ON {table} (tenant_id, {col}) WHERE deleted = false;
# 文件头部模板
header: |
-- ============================================
-- 更新说明:{更新目的}
-- 更新日期:{YYYY-MM-DD}
-- 更新人:{姓名}
-- ============================================
# 注释规范
comment_standards:
- "文件头部说明更新目的"
- "每个变更语句添加注释"
- "危险操作(删除字段、删除数据)添加警告注释"
# ============================================
# 数据初始化规范
# ============================================
data_standards:
# INSERT 语句格式
insert_format: |
-- 插入菜单数据
INSERT INTO `sys_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES
(1001, '信件管理', '', 2, 1, 0, 'letter', 'ep:document', 'fazhi/letter/index', 0),
(1002, '信件查询', 'fz:letter:query', 3, 1, 1001, '', '', '', 0);
# 字典数据
dict_data: |
-- 字典类型
INSERT INTO `sys_dict_type` (`name`, `type`, `status`, `remark`)
VALUES ('信件状态', 'fz_letter_status', 0, '信件流转状态');
-- 字典数据
INSERT INTO `sys_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`)
VALUES
(1, '待处理', '0', 'fz_letter_status', 0),
(2, '处理中', '1', 'fz_letter_status', 0),
(3, '已完成', '2', 'fz_letter_status', 0);
# ============================================
# 编写规范
# ============================================
coding_standards:
# SQL 格式
formatting:
- "关键字使用大写SELECT, FROM, WHERE, JOIN, ORDER BY"
- "表名、字段名使用小写;仅在名称冲突或保留字场景使用双引号"
- "每个字段占一行,逗号放在行尾"
- "复杂查询使用缩进和换行提高可读性"
# 注释规范
comments:
file_header: |
-- ============================================
-- 文件名:{文件名}
-- 描述:{功能描述}
-- 作者:{作者}
-- 日期:{YYYY-MM-DD}
-- ============================================
table_comment: "CREATE TABLE 后必须使用 COMMENT ON TABLE 添加表注释"
column_comment: "每个字段必须使用 COMMENT ON COLUMN 添加字段注释"
index_comment: "复杂索引添加注释说明用途"
# 事务处理
transaction:
- "多条关联语句使用事务包裹"
- "DDL 语句(部分数据库)自动提交,注意顺序"
- "大数据量操作分批执行"
# ============================================
# 版本管理
# ============================================
version_control:
principles:
- "建表后表结构变更使用 update 脚本,不修改原文件"
- "每个变更独立文件,便于追踪和回滚"
- "日期前缀确保文件顺序和变更时间线"
- "版本升级时按日期顺序执行所有脚本"
change_log:
format: |
-- 变更记录:
-- 2025-08-09创建表
-- 2025-11-11新增 priority 字段
-- 2026-01-07新增 category 字段,添加索引
# ============================================
# 执行流程
# ============================================
workflow:
phases:
- name: "需求分析"
description: "解析业务需求,提取实体和关系"
steps:
- "识别核心业务实体"
- "分析实体间关系(一对多、多对多)"
- "确定实体属性列表"
- "确定表名前缀(模块编码)"
- name: "实体设计"
description: "设计实体属性和字段"
steps:
- "设计业务属性字段"
- "添加系统必需字段(审计字段)"
- "定义主键策略BIGINT GENERATED BY DEFAULT AS IDENTITY兼容既有代码时配合 @KeySequence"
- "设置外键关系"
- name: "多租户设计"
description: "添加租户隔离字段和索引"
steps:
- "添加 tenant_id 字段(第二位置)"
- "确保所有索引包含 tenant_id"
- "唯一索引包含 deleted 字段"
- name: "索引设计"
description: "根据查询场景设计索引"
steps:
- "分析查询场景(列表、详情、关联、搜索)"
- "设计主键索引"
- "设计唯一索引(业务唯一键)"
- "设计普通索引(查询优化)"
- "设计外键索引"
- name: "文件生成"
description: "生成符合规范的 SQL 文件"
steps:
- "确定文件命名(日期前缀)"
- "生成文件头部注释"
- "生成 CREATE TABLE 语句"
- "生成索引定义"
- "添加表注释"
# ============================================
# 质量检查清单
# ============================================
quality_check:
before_create:
- "确认表名符合命名规范"
- "确认包含所有必需字段"
- "确认字段类型和长度合理"
- "确认添加了必要的索引"
- "确认添加了表注释和字段注释"
- "确认文件命名符合规范"
after_create:
- "测试建表脚本可正常执行"
- "确认所有索引包含 tenant_id"
- "确认唯一索引包含 deleted"
- "确认外键字段有索引"
- "检查索引是否生效"
- "更新相关文档"
before_update:
- "确认变更的必要性"
- "评估对现有数据的影响"
- "准备回滚方案"
- "选择低峰期执行"
# ============================================
# 快速参考
# ============================================
quick_reference:
table_prefixes:
sys_: "系统表"
mes_: "制造执行系统表"
erp_: "企业资源计划表"
crm_: "客户关系管理表"
bpm_: "工作流表"
pay_: "支付表"
member_: "会员表"
infra_: "基础设施表"
fz_: "法制业务表"
file_suffixes:
_create.sql: "建表脚本"
_update.sql: "更新脚本"
_seq_create.sql: "序列脚本"
common_columns:
id: "主键IDBIGINT"
tenant_id: "租户编号BIGINT"
creator: "创建者VARCHAR(64)"
create_time: "创建时间TIMESTAMP"
updater: "更新者VARCHAR(64)"
update_time: "更新时间TIMESTAMP"
deleted: "逻辑删除BOOLEAN"
remark: "备注VARCHAR(500)"
# ============================================
# 示例
# ============================================
examples:
work_order:
input:
module_code: "mes"
table_name: "mes_work_order"
table_comment: "生产工单表"
business_fields:
- name: "order_no"
type: "VARCHAR(64)"
required: true
comment: "工单编号"
unique: true
- name: "product_id"
type: "BIGINT"
required: true
comment: "产品ID"
- name: "status"
type: "TINYINT"
required: true
default: "0"
comment: "状态:0-待下发,1-已下发,2-生产中,3-已完成,4-已关闭"
output_file: "2026_03_25_mes_work_order_create.sql"
output_sql: |
-- PostgreSQL 示例(字段注释通过 COMMENT ON COLUMN 补充)
CREATE TABLE mes_work_order (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
order_no VARCHAR(64) NOT NULL,
product_id BIGINT NOT NULL,
status SMALLINT NOT NULL DEFAULT 0,
plan_start_time TIMESTAMP NOT NULL,
plan_end_time TIMESTAMP NOT NULL,
remark VARCHAR(500) DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE mes_work_order IS '生产工单表';
CREATE UNIQUE INDEX uk_tenant_order
ON mes_work_order (tenant_id, order_no) WHERE deleted = false;
CREATE INDEX idx_tenant_status ON mes_work_order (tenant_id, status);
CREATE INDEX idx_tenant_time ON mes_work_order (tenant_id, plan_start_time);
# ============================================
# 错误处理
# ============================================
error_handling:
errors:
- code: "DB001"
message: "表名不符合命名规范"
solution: "检查表名前缀和格式"
- code: "DB002"
message: "缺少必需字段"
solution: "确保包含 id, tenant_id, 审计字段"
- code: "DB003"
message: "索引缺少 tenant_id"
solution: "所有索引必须包含 tenant_id 作为第一列"
- code: "DB004"
message: "文件命名不符合规范"
solution: "使用 YYYY_MM_DD_{表名}_create.sql 格式"
- code: "DB005"
message: "唯一索引缺少 deleted"
solution: "唯一索引必须包含 deleted 字段"

View File

@@ -0,0 +1,836 @@
# 实体类设计 Skill
# 根据 PostgreSQL 表结构生成符合 yudao-vue-pro 规范的实体类(DO)
# 本项目默认数据库方言为 PostgreSQL类型映射不得回退为 MySQL 默认。
skill:
id: "design-entity"
name: "实体类设计 Skill"
version: "1.0.0"
category: "design"
description: "根据 PostgreSQL 表结构自动生成符合 yudao 项目规范的实体类(DO)支持多租户、审计字段、JSONB 字段等特性"
created_at: "2026-03-31"
updated_at: "2026-03-31"
# ============================================
# 触发条件
# ============================================
trigger:
# 命令触发
commands:
- "/entity-design"
- "/design-entity"
- "/entity-generator"
# 自然语言触发
keywords:
- "生成实体类"
- "创建DO"
- "生成DO实体"
- "实体类设计"
- "数据对象"
# 事件触发
events:
- name: "db_design_completed"
condition: "数据库表结构设计完成"
- name: "sql_created"
condition: "建表 SQL 文件创建完成"
# ============================================
# 输入参数
# ============================================
input:
parameters:
- name: "module_code"
type: "string"
required: true
description: "模块编码(如 mes、erp、crm、system"
examples: ["mes", "erp", "crm", "system", "pay"]
- name: "table_name"
type: "string"
required: true
description: "表名(含前缀,如 mes_work_order"
examples: ["mes_work_order", "erp_product", "system_user"]
- name: "table_comment"
type: "string"
required: true
description: "表注释/描述"
- name: "business_fields"
type: "array"
required: true
description: "业务字段列表(不含通用字段)"
schema:
- name: "字段名"
type: "数据类型"
required: "是否必填"
comment: "字段注释"
enum_ref: "枚举引用(可选)"
json_field: "是否JSON字段可选"
java_type: "JSON字段的Java类型可选如 Set<Long>"
- name: "tenant_type"
type: "string"
required: false
default: "multi"
description: "租户类型single-单租户multi-多租户ignore-忽略租户"
examples: ["single", "multi", "ignore"]
- name: "entity_name"
type: "string"
required: false
default: "auto"
description: "实体类名称(默认根据表名自动生成)"
- name: "feature"
type: "string"
required: false
default: "auto"
description: "功能子目录名(默认根据实体名推断)"
# ============================================
# 设计理念
# ============================================
philosophy:
business_position: "实体类(DO)是数据库表与业务逻辑之间的桥梁,将关系型数据映射为对象"
design_principles:
- "继承体系:多租户继承 TenantBaseDO单租户继承 BaseDO"
- "审计字段:由基类提供,业务实体不重复定义"
- "字段注释:使用 JavaDoc 格式,不使用 Swagger 注解"
- "JSON 字段:使用 JacksonTypeHandler 处理复杂类型"
- "命名规范:实体类以 DO 结尾,与表名保持映射关系"
- "枚举引用:在注释中说明枚举类,字段使用 Integer 类型"
inheritance_model:
base_hierarchy:
BaseDO:
package: "cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO"
fields: ["createTime", "updateTime", "creator", "updater", "deleted"]
use_case: "单租户业务表"
note: "deleted 字段为 Boolean 类型"
TenantBaseDO:
package: "cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO"
parent: "BaseDO"
extra_fields: ["tenantId"]
use_case: "多租户业务表(绝大多数场景)"
special_annotation:
TenantIgnore:
package: "cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore"
use_case: "租户表、套餐表等需要忽略多租户过滤的实体"
# ============================================
# 命名转换规则
# ============================================
naming:
# 表名 -> 实体类名转换
table_to_class:
rules:
- "去除表前缀(如 mes_、erp_、system_、crm_、bpm_、pay_"
- "snake_case 转 PascalCase"
- "添加 DO 后缀"
- "特殊表名特殊处理"
examples:
- table: "mes_work_order"
class: "WorkOrderDO"
- table: "erp_product"
class: "ProductDO"
- table: "system_users"
class: "AdminUserDO"
- table: "system_role"
class: "RoleDO"
- table: "crm_customer"
class: "CustomerDO"
# 字段名 -> 属性名转换
column_to_field:
rules:
- "snake_case 转 camelCase"
- "保持与数据库字段映射"
examples:
- column: "work_order_no"
field: "workOrderNo"
- column: "create_time"
field: "createTime"
- column: "tenant_id"
field: "tenantId"
# 特殊表名映射
special_mappings:
system_users: "AdminUserDO"
system_menu: "MenuDO"
system_tenant: "TenantDO"
system_dict_data: "DictDataDO"
system_dict_type: "DictTypeDO"
# 表前缀映射(模块编码)
prefix_mapping:
sys: "system"
mes: "mes"
erp: "erp"
crm: "crm"
bpm: "bpm"
pay: "pay"
member: "member"
infra: "infra"
# ============================================
# 类型映射规则
# ============================================
type_mapping:
postgresql_to_java:
- postgresql: "BIGINT"
java: "Long"
note: "主键、外键、数量类字段"
- postgresql: "INT / INTEGER"
java: "Integer"
note: "状态、排序、计数类字段"
- postgresql: "TINYINT"
java: "Integer"
note: "枚举字段、开关字段"
- postgresql: "SMALLINT"
java: "Integer"
note: "小范围数值"
- postgresql: "DECIMAL(p,s)"
java: "BigDecimal"
note: "金额、精确数值"
import: "java.math.BigDecimal"
- postgresql: "FLOAT / DOUBLE"
java: "Double"
note: "浮点数"
- postgresql: "VARCHAR / CHAR"
java: "String"
note: "字符串"
- postgresql: "TEXT / LONGTEXT"
java: "String"
note: "大文本"
- postgresql: "TIMESTAMP / TIMESTAMPTZ"
java: "LocalDateTime"
note: "日期时间"
import: "java.time.LocalDateTime"
- postgresql: "DATE"
java: "LocalDate"
note: "日期"
import: "java.time.LocalDate"
- postgresql: "TIME"
java: "LocalTime"
note: "时间"
import: "java.time.LocalTime"
- postgresql: "BOOLEAN"
java: "Boolean"
note: "布尔值"
- postgresql: "BLOB / LONGBLOB"
java: "byte[]"
note: "二进制数据"
- postgresql: "JSON"
java: "复杂类型"
handler: "JacksonTypeHandler"
note: "JSON 数据,需特殊处理"
# JSON 字段类型映射
json_type_mapping:
- pattern: "ID数组"
java_type: "Set<Long>"
import: "java.util.Set"
- pattern: "字符串数组"
java_type: "List<String>"
import: "java.util.List"
- pattern: "键值对对象"
java_type: "Map<String, Object>"
import: "java.util.Map"
# ============================================
# 注解规范
# ============================================
annotations:
# 类级别注解
class_annotations:
required:
- annotation: "@TableName"
format: "@TableName(value = \"{table_name}\", autoResultMap = true)"
import: "com.baomidou.mybatisplus.annotation.TableName"
note: "autoResultMap = true 用于支持 TypeHandler"
- annotation: "@KeySequence"
format: "@KeySequence(\"{table_name}_seq\")"
import: "com.baomidou.mybatisplus.annotation.KeySequence"
note: "PostgreSQL 主键约定;与项目既有 @KeySequence 规范保持一致"
- annotation: "@Data"
import: "lombok.Data"
note: "Lombok getter/setter"
- annotation: "@EqualsAndHashCode"
format: "@EqualsAndHashCode(callSuper = true)"
import: "lombok.EqualsAndHashCode"
note: "必须 callSuper = true 以包含基类字段"
optional:
- annotation: "@Builder"
import: "lombok.Builder"
note: "构建器模式"
- annotation: "@NoArgsConstructor"
import: "lombok.NoArgsConstructor"
note: "无参构造(与 Builder 配合)"
- annotation: "@AllArgsConstructor"
import: "lombok.AllArgsConstructor"
note: "全参构造(与 Builder 配合)"
- annotation: "@TenantIgnore"
import: "cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore"
note: "忽略多租户过滤(仅用于租户表等特殊场景)"
# 字段级别注解
field_annotations:
primary_key:
- annotation: "@TableId"
import: "com.baomidou.mybatisplus.annotation.TableId"
note: "主键字段,默认使用雪花算法"
json_field:
- annotation: "@TableField"
format: "@TableField(typeHandler = JacksonTypeHandler.class)"
import: "com.baomidou.mybatisplus.annotation.TableField"
handler_import: "com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"
note: "JSON 类型字段,需要 @TableName 包含 autoResultMap = true"
# ============================================
# 代码模板
# ============================================
templates:
# 多租户实体模板(最常用)
tenant_entity:
description: "多租户业务表实体类"
base_class: "TenantBaseDO"
code: |
package cn.iocoder.yudao.module.{module}.dal.dataobject.{feature};
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
{additional_imports}
/**
* {table_comment} DO
*
* @author {author}
*/
@TableName(value = "{table_name}", autoResultMap = true)
@KeySequence("{table_name}_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class {EntityName}DO extends TenantBaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
{business_fields}
}
# 单租户实体模板
single_entity:
description: "单租户业务表实体类"
base_class: "BaseDO"
code: |
package cn.iocoder.yudao.module.{module}.dal.dataobject.{feature};
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
{additional_imports}
/**
* {table_comment} DO
*
* @author {author}
*/
@TableName(value = "{table_name}", autoResultMap = true)
@KeySequence("{table_name}_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class {EntityName}DO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
{business_fields}
}
# 忽略多租户实体模板
tenant_ignore_entity:
description: "需要忽略多租户过滤的实体类(如租户表、套餐表)"
base_class: "BaseDO"
extra_annotation: "@TenantIgnore"
code: |
package cn.iocoder.yudao.module.{module}.dal.dataobject.{feature};
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
{additional_imports}
/**
* {table_comment} DO
*
* @author {author}
*/
@TenantIgnore
@TableName(value = "{table_name}", autoResultMap = true)
@KeySequence("{table_name}_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class {EntityName}DO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
{business_fields}
}
# 业务字段模板
business_field:
normal: |
/**
* {字段注释}
{enum_reference}
*/
private {JavaType} {fieldName};
json_field: |
/**
* {字段注释}
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private {JavaType} {fieldName};
# ============================================
# 输出规范
# ============================================
output:
# 文件输出位置
file_location:
path_pattern: "yudao-module-{module}/src/main/java/cn/iocoder/yudao/module/{module}/dal/dataobject/{feature}/{EntityName}DO.java"
example: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/workorder/WorkOrderDO.java"
# 导入语句规范
imports:
base_imports:
- "com.baomidou.mybatisplus.annotation.TableName"
- "com.baomidou.mybatisplus.annotation.TableId"
- "com.baomidou.mybatisplus.annotation.KeySequence"
- "lombok.Data"
- "lombok.EqualsAndHashCode"
tenant_imports:
- "cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO"
single_imports:
- "cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO"
json_handler_import:
- "com.baomidou.mybatisplus.annotation.TableField"
- "com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"
builder_imports:
- "lombok.Builder"
- "lombok.NoArgsConstructor"
- "lombok.AllArgsConstructor"
# 产物清单
artifacts:
- name: "实体类文件"
type: "code"
path: "dal/dataobject/{feature}/{EntityName}DO.java"
# ============================================
# 执行流程
# ============================================
workflow:
phases:
- name: "表结构解析"
description: "解析数据库表结构,提取元数据"
steps:
- "识别表名、表前缀、表注释"
- "提取业务字段列表排除通用字段id, tenant_id, creator, create_time, updater, update_time, deleted"
- "识别主键、索引信息"
- "识别 JSON 字段、枚举字段"
- name: "命名转换"
description: "将数据库命名转换为 Java 命名"
steps:
- "表名 -> 实体类名(去除前缀 + PascalCase + DO 后缀)"
- "字段名 -> 属性名camelCase"
- "处理特殊表名映射"
- name: "类型映射"
description: "将 MySQL 类型映射为 Java 类型"
steps:
- "标准类型映射BIGINT -> Long 等)"
- "JSON 字段识别和处理"
- "枚举字段处理Integer + 注释引用)"
- name: "继承选择"
description: "确定实体类继承的基类"
steps:
- "判断租户类型multi/single/ignore"
- "选择对应基类TenantBaseDO/BaseDO"
- "判断是否需要 @TenantIgnore"
- name: "代码生成"
description: "生成实体类代码"
steps:
- "生成类注释和注解"
- "生成主键字段"
- "生成业务字段(含 JavaDoc 注释)"
- "生成导入语句(按规范分组排序)"
- "格式化代码"
- name: "质量检查"
description: "验证生成的实体类"
steps:
- "检查注解完整性"
- "检查字段注释格式"
- "检查继承正确性"
- "检查命名规范"
# ============================================
# 质量检查清单
# ============================================
quality_check:
before_generate:
- "确认表名符合命名规范"
- "确认业务字段列表完整"
- "确认租户类型选择正确"
- "确认字段类型映射正确"
after_generate:
- "实体类名称以 DO 结尾"
- "@TableName 注解包含 autoResultMap = true"
- "@EqualsAndHashCode 包含 callSuper = true"
- "继承正确的基类"
- "主键字段使用 @TableId 注解"
- "所有字段使用 JavaDoc 注释格式"
- "JSON 字段使用 JacksonTypeHandler"
- "枚举字段注释包含枚举引用"
- "不包含 Swagger @Schema 注解"
- "不重复定义审计字段(已在基类中)"
# ============================================
# 示例
# ============================================
examples:
# 多租户实体示例
work_order:
input:
module_code: "mes"
table_name: "mes_work_order"
table_comment: "生产工单"
tenant_type: "multi"
business_fields:
- name: "order_no"
type: "VARCHAR(64)"
comment: "工单编号"
unique: true
- name: "product_id"
type: "BIGINT"
comment: "产品ID"
- name: "status"
type: "TINYINT"
comment: "状态"
enum_ref: "WorkOrderStatusEnum"
- name: "plan_qty"
type: "INT"
comment: "计划数量"
output_class_name: "WorkOrderDO"
output_file: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/workorder/WorkOrderDO.java"
output_code: |
package cn.iocoder.yudao.module.mes.dal.dataobject.workorder;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.mes.enums.WorkOrderStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 生产工单 DO
*
* @author yudao
*/
@TableName(value = "mes_work_order", autoResultMap = true)
@KeySequence("mes_work_order_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WorkOrderDO extends TenantBaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 工单编号
*/
private String orderNo;
/**
* 产品ID
*/
private Long productId;
/**
* 状态
*
* 枚举 {@link WorkOrderStatusEnum}
*/
private Integer status;
/**
* 计划数量
*/
private Integer planQty;
}
# JSON 字段实体示例
role_entity:
input:
module_code: "system"
table_name: "system_role"
table_comment: "角色"
tenant_type: "multi"
business_fields:
- name: "name"
type: "VARCHAR(100)"
comment: "角色名称"
- name: "code"
type: "VARCHAR(50)"
comment: "角色标识"
- name: "data_scope_dept_ids"
type: "JSON"
comment: "数据范围部门ID数组"
json_field: true
java_type: "Set<Long>"
output_code: |
package cn.iocoder.yudao.module.system.dal.dataobject.permission;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.util.Set;
/**
* 角色 DO
*
* @author yudao
*/
@TableName(value = "system_role", autoResultMap = true)
@KeySequence("system_role_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleDO extends TenantBaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 角色名称
*/
private String name;
/**
* 角色标识
*/
private String code;
/**
* 数据范围(指定部门数组)
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Set<Long> dataScopeDeptIds;
}
# 忽略租户实体示例
tenant_entity:
input:
module_code: "system"
table_name: "system_tenant"
table_comment: "租户"
tenant_type: "ignore"
business_fields:
- name: "name"
type: "VARCHAR(100)"
comment: "租户名称"
- name: "contact_name"
type: "VARCHAR(50)"
comment: "联系人"
- name: "status"
type: "TINYINT"
comment: "状态"
enum_ref: "CommonStatusEnum"
output_code: |
package cn.iocoder.yudao.module.system.dal.dataobject.tenant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 租户 DO
*
* @author yudao
*/
@TenantIgnore
@TableName(value = "system_tenant", autoResultMap = true)
@KeySequence("system_tenant_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TenantDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 租户名称
*/
private String name;
/**
* 联系人
*/
private String contactName;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
}
# ============================================
# 与其他 Skill 的关系
# ============================================
related_skills:
upstream:
- skill: "db-designer"
relationship: "依赖"
description: "依赖数据库表结构设计,从 SQL 解析表信息"
path: "skills/design/db-designer.yaml"
downstream:
- skill: "api-designer"
relationship: "输出"
description: "实体类作为 API 设计的输入"
path: "skills/design/api-designer.yaml"
collaboration:
- path: "skills/usage/entity-implementation.md"
relationship: "协同"
description: "entity-designer 专注实体类设计规范entity-implementation 提供完整实现流程(含 Mapper、Service 等)"
# ============================================
# 错误处理
# ============================================
error_handling:
errors:
- code: "ENT001"
message: "表名不符合命名规范"
solution: "检查表名前缀和格式,确保包含模块前缀(如 mes_、erp_"
- code: "ENT002"
message: "缺少必需的表注释"
solution: "提供 table_comment 参数"
- code: "ENT003"
message: "字段类型无法映射"
solution: "使用默认 String 类型,手动调整"
- code: "ENT004"
message: "JSON 字段缺少 java_type"
solution: "为 JSON 字段指定 java_type如 Set<Long>"
- code: "ENT005"
message: "租户类型选择错误"
solution: "multi 使用 TenantBaseDOsingle 使用 BaseDOignore 使用 @TenantIgnore"
- code: "ENT006"
message: "枚举字段缺少 enum_ref"
solution: "为枚举字段提供 enum_ref 参数,在注释中引用枚举类"

View File

@@ -0,0 +1,115 @@
---
name: flyway-postgresql
description: Flyway PostgreSQL migrations for this project. Use when adding or changing database schema, indexes, constraints, required seed data, migration baselines, or Flyway configuration.
---
# Flyway PostgreSQL migrations
Use a **forward-only** migration process. Treat `flyway_schema_history` as immutable release history.
## 1. Inspect the migration state
Before editing:
1. Read `CLAUDE.md` PostgreSQL and Flyway rules.
2. Inspect `yudao-server/src/main/resources/application-{local,dev}.yaml` and `yudao-server/pom.xml` when configuration is involved.
3. List every `db/migration` directory and versioned migration across active modules.
4. Inspect the target table DO, Mapper, service use, and relevant PostgreSQL DDL.
5. Check the working tree so existing uncommitted work is preserved.
**Complete when:** the active Flyway locations, baseline, highest migration version, affected database objects, and pending user changes are known.
## 2. Choose the migration branch
### New schema change
Create a new versioned SQL migration under:
```text
<module>/src/main/resources/db/migration/<module>/
```
Use the next unused project-wide version after `V4010`. Leave gaps of 10 for normal changes when practical:
```text
V4020__add_student_progress.sql
V4030__add_practice_report_index.sql
```
### Fix an executed migration
Create a higher version that repairs or reverses the prior change. Preserve the executed file byte-for-byte.
### Existing database adoption
The current baseline is `4009`; `V4010__initialize_education_flyway.sql` is the first managed migration. Keep `baseline-on-migrate` only while existing environments are being adopted. After every existing environment has a baseline record, change it to `false` in a separate reviewed change.
### Configuration change
Flyway must target the dynamic datasource `master`, never `slave`. Keep these safeguards enabled:
```yaml
validate-on-migrate: true
clean-disabled: true
out-of-order: false
```
Allow `FLYWAY_URL`, `FLYWAY_USER`, and `FLYWAY_PASSWORD` to override master credentials.
**Complete when:** exactly one branch is selected and its version/configuration does not conflict with the current repository state.
## 3. Write PostgreSQL-native SQL
Follow these project conventions:
- Identity primary key: `BIGINT GENERATED BY DEFAULT AS IDENTITY`.
- Time: `TIMESTAMP`; use `CURRENT_TIMESTAMP` for defaults.
- Boolean: `BOOLEAN NOT NULL DEFAULT false` where appropriate.
- Idempotency/upsert: `ON CONFLICT ... DO NOTHING` or `DO UPDATE SET ... EXCLUDED.column`.
- Null fallback: `COALESCE`.
- Date formatting: `TO_CHAR`; date parts: `EXTRACT`.
- Bounded delete: delete by IDs selected in an ordered, limited subquery or CTE.
- Add comments for business tables and non-obvious columns.
- Add indexes from observed query and conflict targets, not speculation.
- Required seed data must be deterministic and idempotent.
- Put `CREATE INDEX CONCURRENTLY` in its own non-transactional migration; otherwise prefer transactional PostgreSQL DDL.
Keep verification queries in comments when useful. Put rollback notes in the change description or a separate operational document; production recovery is another forward migration.
**Complete when:** every affected object, data backfill, constraint, index, and application assumption is represented in PostgreSQL-native SQL.
## 4. Align application code
Update all affected DOs, Mappers, services, tests, and fixtures. Search Java annotations and MyBatis XML for stale column names and incompatible SQL. For a new identity/sequence-backed DO, follow the surrounding projects `@TableId` and `@KeySequence` pattern.
**Complete when:** every code reference agrees with the post-migration schema and no active runtime SQL depends on the previous shape.
## 5. Verify
Run, in order:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
Confirm each migration is present under the owning modules `target/classes/db/migration/...` after compilation. If an authorized disposable PostgreSQL database is available, run Flyway against it and inspect:
```sql
SELECT installed_rank, version, description, script, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;
```
Run focused tests for the affected module. Report any skipped database execution separately from compilation success.
**Complete when:** formatting and compilation pass, migration packaging is confirmed, focused tests pass or their exact blocker is reported, and any real-database migration status is stated truthfully.
## Release rules
- Version numbers are project-wide across every Flyway location.
- One committed migration version has one immutable meaning.
- Production migrations move forward; recovery is a higher version.
- `clean` remains disabled.
- Demo/test seed data lives outside production migrations.
- Do not copy the legacy `sql/postgresql/ruoyi-vue-pro.sql` dump into a versioned runtime migration; it contains destructive bootstrap statements and embedded transactions. Use it only to initialize a disposable empty database or to establish the pre-Flyway baseline.

152
.claude/skills/index.yaml Normal file
View File

@@ -0,0 +1,152 @@
# Skill 索引文件
# 记录所有模块的Skill文档位置和状态
skills:
# 核心模块
system:
name: "系统管理模块"
path: "skills/modules/system/skill-system.yaml"
status: "completed"
priority: 1
lines: 839
infra:
name: "基础设施模块"
path: "skills/modules/infra/skill-infra.yaml"
status: "completed"
priority: 1
lines: 700+
pay:
name: "支付模块"
path: "skills/modules/pay/skill-pay.yaml"
status: "completed"
priority: 1
lines: 654
# 业务模块
member:
name: "会员模块"
path: "skills/modules/member/skill-member.yaml"
status: "completed"
priority: 2
lines: 608
mall:
name: "商城模块"
path: "skills/modules/mall/skill-mall.yaml"
status: "completed"
priority: 2
lines: 650+
crm:
name: "CRM模块"
path: "skills/modules/crm/skill-crm.yaml"
status: "completed"
priority: 2
lines: 850+
erp:
name: "ERP模块"
path: "skills/modules/erp/skill-erp.yaml"
status: "completed"
priority: 3
lines: 900+
# 技术/平台模块
bpm:
name: "工作流模块"
path: "skills/modules/bpm/skill-bpm.yaml"
status: "completed"
priority: 2
lines: 650+
ai:
name: "AI模块"
path: "skills/modules/ai/skill-ai.yaml"
status: "completed"
priority: 3
lines: 670+
iot:
name: "物联网模块"
path: "skills/modules/iot/skill-iot.yaml"
status: "completed"
priority: 3
lines: 721
mp:
name: "微信公众号模块"
path: "skills/modules/mp/skill-mp.yaml"
status: "completed"
priority: 3
lines: 660+
report:
name: "报表模块"
path: "skills/modules/report/skill-report.yaml"
status: "completed"
priority: 3
lines: 320+
# 设计规范
design:
db:
name: "数据库设计"
path: "skills/design/db-designer.yaml"
status: "completed"
description: "数据库表结构设计规范、SQL文件规范、编写规范"
lines: 450
entity:
name: "实体类设计"
path: "skills/design/entity-designer.yaml"
status: "completed"
description: "yudao 实体类(DO)设计规范,包括继承体系、注解规范、命名转换"
lines: 520
api:
name: "API 设计"
path: "skills/design/api-designer.yaml"
status: "completed"
description: "REST API 设计规范、注解、请求响应格式、权限标识"
crud:
name: "CRUD 代码生成"
path: "skills/design/crud-designer.yaml"
status: "completed"
description: "根据数据库表结构自动生成完整CRUD功能代码"
lines: 517
# 使用样例自动引用说明
# 使用样例文档usage/目录)通过 YAML front matter 声明需要引用的规范文件
# AI 读取使用样例时会自动加载相关规范,确保生成的代码符合项目标准
usage_auto_reference:
description: "使用样例通过 YAML front matter 自动引用设计规范"
mechanism: |
每个使用样例文档头部包含 references 配置,声明该场景需要引用的规范文件:
- design: 设计规范db-designer, entity-designer, api-designer, crud-designer
- module_guide: 模块引用提示(用户指定模块后加载对应 skill
- patterns: 设计模式引用factory, strategy, template-method
- templates: 模板文件引用
example: |
---
references:
design:
- skills/design/db-designer.yaml
- skills/design/entity-designer.yaml
module_guide:
prompt: "请指定目标模块"
mapping:
mes: skills/modules/mes/skill-mes.yaml
---
# 提取进度
progress:
total: 12
completed: 12
in_progress: 0
pending: 0
# 最后更新时间
metadata:
created_at: "2026-03-18"
updated_at: "2026-03-31"
completed_at: "2026-03-18"

View File

@@ -0,0 +1,706 @@
# Skill 文件 - AI 模块
# 用于提取模块知识的标准格式
skill:
id: "skill-ai"
name: "AI Skill"
version: "1.0.0"
module_path: "yudao-module-ai"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
AI 模块是系统的智能化核心,提供大模型集成能力,支持多种 AI 能力:
1. 聊天对话:支持多模型切换、流式响应、知识库增强、联网搜索
2. 图像生成:支持 OpenAI DALL-E、Stable Diffusion、Midjourney、硅基流动等
3. 音乐生成:支持 Suno AI 音乐创作
4. 知识库RAG 检索增强生成,支持文档分片、向量存储、语义检索
5. 写作助手AI 辅助写作
6. 思维导图AI 生成思维导图
7. 工作流:可视化 AI 工作流编排
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "策略模式:通过 AiModelFactory 统一创建不同平台的 AI 模型实例"
- "工厂模式AiModelFactoryImpl 负责创建 ChatModel、ImageModel、EmbeddingModel 等"
- "单例缓存:使用 Hutool Singleton 缓存 AI 客户端实例,避免重复创建"
- "Spring AI 集成:基于 Spring AI 框架,实现多模型统一抽象"
- "开放-封闭原则:新增 AI 平台只需扩展 AiPlatformEnum 和工厂方法"
- "接口隔离Service 层接口按领域划分,职责单一"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "聊天会话"
type: "聚合根"
entities: ["AiChatMessageDO"]
description: "用户发起的对话,包含多条消息"
- name: "知识库"
type: "聚合根"
entities: ["AiKnowledgeDocumentDO", "AiKnowledgeSegmentDO"]
description: "知识库包含多个文档,文档包含多个分段"
- name: "AI 模型配置"
type: "聚合根"
entities: ["AiApiKeyDO"]
description: "模型配置关联 API 密钥"
value_objects:
- name: "AiPlatformEnum"
description: "AI 平台枚举,定义支持的 AI 服务商"
- name: "AiModelTypeEnum"
description: "模型类型枚举:对话、图像、语音、视频、向量、重排序"
services:
- name: "AiChatMessageService"
description: "聊天消息服务,处理对话生成"
- name: "AiImageService"
description: "图像生成服务"
- name: "AiKnowledgeService"
description: "知识库管理服务"
- name: "AiModelService"
description: "模型配置管理服务"
- name: "AiMusicService"
description: "音乐生成服务"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "controller"
purpose: "HTTP 接口层"
components:
- "AiChatMessageController: 聊天消息接口"
- "AiChatConversationController: 对话管理接口"
- "AiImageController: 图像生成接口"
- "AiKnowledgeController: 知识库管理接口"
- "AiModelController: 模型配置接口"
- "AiMusicController: 音乐生成接口"
- "AiWriteController: 写作助手接口"
- "AiMindMapController: 思维导图接口"
- "AiWorkflowController: 工作流接口"
- name: "service"
purpose: "业务逻辑层"
components:
- "chat/: 聊天相关服务"
- "image/: 图像生成服务"
- "knowledge/: 知识库服务"
- "model/: 模型配置服务"
- "music/: 音乐生成服务"
- "write/: 写作服务"
- "mindmap/: 思维导图服务"
- "workflow/: 工作流服务"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject/: DO 实体类"
- "mysql/: MyBatis Mapper 接口"
- name: "framework/ai"
purpose: "AI 框架集成层"
components:
- "core/model/: 模型工厂和多平台适配"
- "core/webserch/: 联网搜索客户端"
- "config/: 自动配置类"
# 设计模式应用
design_patterns:
- pattern: "工厂模式"
location: "framework/ai/core/model/AiModelFactory.java"
purpose: "统一创建各平台 AI 模型实例,屏蔽创建细节"
- pattern: "策略模式"
location: "framework/ai/core/model/AiModelFactoryImpl.java"
purpose: "根据平台类型选择不同的模型创建策略"
- pattern: "单例模式"
location: "framework/ai/core/model/AiModelFactoryImpl.java"
purpose: "使用 Hutool Singleton 缓存 AI 客户端,避免重复创建连接"
- pattern: "模板方法"
location: "service/chat/AiChatMessageServiceImpl.java"
purpose: "聊天消息处理的统一流程"
# 模块间通信
communication:
apis: []
# 暂无对外暴露的 RPC API
consumers:
- module: "system"
api: "AdminUserApi"
purpose: "获取用户信息"
mq: []
# 暂未使用消息队列
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有实体继承 BaseDO包含 id、creator、createTime、updater、updateTime、deleted 字段"
# 核心数据表
tables:
# ========== 模型配置 ==========
- name: "ai_api_key"
comment: "AI API 密钥表"
entity: "AiApiKeyDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "名称" }
- { name: "api_key", type: "String", comment: "密钥" }
- { name: "platform", type: "String", comment: "平台,枚举 AiPlatformEnum" }
- { name: "url", type: "String", comment: "API 地址" }
- { name: "status", type: "Integer", comment: "状态" }
- name: "ai_model"
comment: "AI 模型配置表"
entity: "AiModelDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "key_id", type: "Long", comment: "API 密钥编号" }
- { name: "name", type: "String", comment: "模型名称" }
- { name: "model", type: "String", comment: "模型标识" }
- { name: "platform", type: "String", comment: "平台" }
- { name: "type", type: "Integer", comment: "类型1对话 2图片 3语音 4视频 5向量 6重排序" }
- { name: "sort", type: "Integer", comment: "排序值" }
- { name: "status", type: "Integer", comment: "状态" }
- { name: "temperature", type: "Double", comment: "温度参数" }
- { name: "max_tokens", type: "Integer", comment: "最大 Token 数" }
- { name: "max_contexts", type: "Integer", comment: "最大上下文数" }
- name: "ai_chat_role"
comment: "AI 聊天角色表"
entity: "AiChatRoleDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "角色名称" }
- { name: "avatar", type: "String", comment: "角色头像" }
- { name: "category", type: "String", comment: "角色分类" }
- { name: "description", type: "String", comment: "角色描述" }
- { name: "system_message", type: "String", comment: "角色设定" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "model_id", type: "Long", comment: "模型编号" }
- { name: "knowledge_ids", type: "List<Long>", comment: "知识库编号列表" }
- { name: "tool_ids", type: "List<Long>", comment: "工具编号列表" }
- { name: "mcp_client_names", type: "List<String>", comment: "MCP Client 名字列表" }
- { name: "public_status", type: "Boolean", comment: "是否公开" }
- { name: "sort", type: "Integer", comment: "排序值" }
- { name: "status", type: "Integer", comment: "状态" }
- name: "ai_tool"
comment: "AI 工具表"
entity: "AiToolDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "工具名称Bean 名字)" }
- { name: "description", type: "String", comment: "工具描述" }
- { name: "status", type: "Integer", comment: "状态" }
# ========== 聊天对话 ==========
- name: "ai_chat_conversation"
comment: "AI 聊天对话表"
entity: "AiChatConversationDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "title", type: "String", comment: "对话标题" }
- { name: "pinned", type: "Boolean", comment: "是否置顶" }
- { name: "pinned_time", type: "LocalDateTime", comment: "置顶时间" }
- { name: "role_id", type: "Long", comment: "角色编号" }
- { name: "model_id", type: "Long", comment: "模型编号" }
- { name: "model", type: "String", comment: "模型标识" }
- { name: "system_message", type: "String", comment: "角色设定" }
- { name: "temperature", type: "Double", comment: "温度参数" }
- { name: "max_tokens", type: "Integer", comment: "最大 Token 数" }
- { name: "max_contexts", type: "Integer", comment: "最大上下文数" }
- name: "ai_chat_message"
comment: "AI 聊天消息表"
entity: "AiChatMessageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "conversation_id", type: "Long", comment: "对话编号" }
- { name: "reply_id", type: "Long", comment: "回复消息编号" }
- { name: "type", type: "String", comment: "消息类型USER/ASSISTANT/SYSTEM" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "role_id", type: "Long", comment: "角色编号" }
- { name: "model", type: "String", comment: "模型标识" }
- { name: "model_id", type: "Long", comment: "模型编号" }
- { name: "content", type: "String", comment: "聊天内容" }
- { name: "reasoning_content", type: "String", comment: "推理内容" }
- { name: "use_context", type: "Boolean", comment: "是否携带上下文" }
- { name: "segment_ids", type: "List<Long>", comment: "知识库段落编号数组" }
- { name: "web_search_pages", type: "List<WebPage>", comment: "联网搜索网页内容" }
- { name: "attachment_urls", type: "List<String>", comment: "附件 URL 数组" }
# ========== 图像生成 ==========
- name: "ai_image"
comment: "AI 绘画表"
entity: "AiImageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "prompt", type: "String", comment: "提示词" }
- { name: "platform", type: "String", comment: "平台" }
- { name: "model_id", type: "Long", comment: "模型编号" }
- { name: "model", type: "String", comment: "模型标识" }
- { name: "width", type: "Integer", comment: "图片宽度" }
- { name: "height", type: "Integer", comment: "图片高度" }
- { name: "status", type: "Integer", comment: "生成状态" }
- { name: "finish_time", type: "LocalDateTime", comment: "完成时间" }
- { name: "error_message", type: "String", comment: "错误信息" }
- { name: "pic_url", type: "String", comment: "图片地址" }
- { name: "public_status", type: "Boolean", comment: "是否公开" }
- { name: "options", type: "Map<String,Object>", comment: "绘制参数" }
- { name: "buttons", type: "List<Button>", comment: "MJ 按钮" }
- { name: "task_id", type: "String", comment: "任务编号" }
# ========== 知识库 ==========
- name: "ai_knowledge"
comment: "AI 知识库表"
entity: "AiKnowledgeDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "知识库名称" }
- { name: "description", type: "String", comment: "知识库描述" }
- { name: "embedding_model_id", type: "Long", comment: "向量模型编号" }
- { name: "embedding_model", type: "String", comment: "模型标识" }
- { name: "top_k", type: "Integer", comment: "TopK" }
- { name: "similarity_threshold", type: "Double", comment: "相似度阈值" }
- { name: "status", type: "Integer", comment: "状态" }
- name: "ai_knowledge_document"
comment: "AI 知识库文档表"
entity: "AiKnowledgeDocumentDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "knowledge_id", type: "Long", comment: "知识库编号" }
- { name: "name", type: "String", comment: "文档名称" }
- { name: "url", type: "String", comment: "文件 URL" }
- { name: "content", type: "String", comment: "内容" }
- { name: "content_length", type: "Integer", comment: "文档长度" }
- { name: "tokens", type: "Integer", comment: "Token 数量" }
- { name: "segment_max_tokens", type: "Integer", comment: "分片最大 Token 数" }
- { name: "retrieval_count", type: "Integer", comment: "召回次数" }
- { name: "status", type: "Integer", comment: "状态" }
- name: "ai_knowledge_segment"
comment: "AI 知识库分段表"
entity: "AiKnowledgeSegmentDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "knowledge_id", type: "Long", comment: "知识库编号" }
- { name: "document_id", type: "Long", comment: "文档编号" }
- { name: "content", type: "String", comment: "切片内容" }
- { name: "content_length", type: "Integer", comment: "内容长度" }
- { name: "vector_id", type: "String", comment: "向量库编号" }
- { name: "tokens", type: "Integer", comment: "Token 数量" }
- { name: "retrieval_count", type: "Integer", comment: "召回次数" }
- { name: "status", type: "Integer", comment: "状态" }
# ========== 音乐生成 ==========
- name: "ai_music"
comment: "AI 音乐表"
entity: "AiMusicDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "title", type: "String", comment: "音乐名称" }
- { name: "lyric", type: "String", comment: "歌词" }
- { name: "image_url", type: "String", comment: "图片地址" }
- { name: "audio_url", type: "String", comment: "音频地址" }
- { name: "video_url", type: "String", comment: "视频地址" }
- { name: "status", type: "Integer", comment: "音乐状态" }
- { name: "generate_mode", type: "Integer", comment: "生成模式" }
- { name: "description", type: "String", comment: "描述词" }
- { name: "platform", type: "String", comment: "平台" }
- { name: "model", type: "String", comment: "模型" }
- { name: "tags", type: "List<String>", comment: "音乐风格标签" }
- { name: "duration", type: "Double", comment: "音乐时长" }
- { name: "public_status", type: "Boolean", comment: "是否公开" }
- { name: "task_id", type: "String", comment: "任务编号" }
- { name: "error_message", type: "String", comment: "错误信息" }
# ========== 写作 ==========
- name: "ai_write"
comment: "AI 写作表"
entity: "AiWriteDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "type", type: "Integer", comment: "写作类型" }
- { name: "platform", type: "String", comment: "平台" }
- { name: "model_id", type: "Long", comment: "模型编号" }
- { name: "model", type: "String", comment: "模型" }
- { name: "prompt", type: "String", comment: "生成内容提示" }
- { name: "generated_content", type: "String", comment: "生成的内容" }
- { name: "original_content", type: "String", comment: "原文" }
- { name: "length", type: "Integer", comment: "长度提示词" }
- { name: "format", type: "Integer", comment: "格式提示词" }
- { name: "tone", type: "Integer", comment: "语气提示词" }
- { name: "language", type: "Integer", comment: "语言提示词" }
- { name: "error_message", type: "String", comment: "错误信息" }
# ========== 思维导图 ==========
- name: "ai_mind_map"
comment: "AI 思维导图表"
entity: "AiMindMapDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "platform", type: "String", comment: "平台" }
- { name: "model_id", type: "Long", comment: "模型编号" }
- { name: "model", type: "String", comment: "模型" }
- { name: "prompt", type: "String", comment: "生成内容提示" }
- { name: "generated_content", type: "String", comment: "生成的内容" }
- { name: "error_message", type: "String", comment: "错误信息" }
# ========== 工作流 ==========
- name: "ai_workflow"
comment: "AI 工作流表"
entity: "AiWorkflowDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "工作流名称" }
- { name: "code", type: "String", comment: "工作流标识" }
- { name: "graph", type: "String", comment: "工作流模型 JSON 数据" }
- { name: "remark", type: "String", comment: "备注" }
- { name: "status", type: "Integer", comment: "状态" }
# 表关系ER关系
relationships:
- from: "ai_chat_conversation"
to: "ai_chat_message"
type: "1:N"
foreign_key: "conversation_id"
- from: "ai_knowledge"
to: "ai_knowledge_document"
type: "1:N"
foreign_key: "knowledge_id"
- from: "ai_knowledge_document"
to: "ai_knowledge_segment"
type: "1:N"
foreign_key: "document_id"
- from: "ai_model"
to: "ai_api_key"
type: "N:1"
foreign_key: "key_id"
- from: "ai_chat_role"
to: "ai_model"
type: "N:1"
foreign_key: "model_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - XXX')"
- "@RestController"
- "@RequestMapping('/ai/xxx')"
- "@Operation(summary = 'xxx')"
example: |
@Tag(name = "管理后台 - 聊天消息")
@RestController
@RequestMapping("/ai/chat/message")
public class AiChatMessageController {
@Resource
private AiChatMessageService chatMessageService;
@Operation(summary = "发送消息(流式)")
@PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(
@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
}
}
# Service层规范
service:
interface_pattern: "定义接口 + Impl 实现类,接口用于解耦"
impl_pattern: "@Service 注解,使用 @Resource 注入依赖"
example: |
public interface AiChatMessageService {
// 同步发送消息
AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId);
// 流式发送消息
Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(
AiChatMessageSendReqVO sendReqVO, Long userId);
}
@Service
public class AiChatMessageServiceImpl implements AiChatMessageService {
@Resource
private AiModelService modelService;
@Override
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(
AiChatMessageSendReqVO sendReqVO, Long userId) {
// 1. 获取对话和模型配置
// 2. 构建消息上下文
// 3. 调用 ChatModel 流式生成
// 4. 保存消息记录
}
}
# 数据访问规范
dal:
mapper_pattern: "继承 BaseMapperX使用 LambdaQueryWrapper 查询"
example: |
@Mapper
public interface AiChatMessageMapper extends BaseMapperX<AiChatMessageDO> {
default List<AiChatMessageDO> selectListByConversationId(Long conversationId) {
return selectList(new LambdaQueryWrapper<AiChatMessageDO>()
.eq(AiChatMessageDO::getConversationId, conversationId)
.orderByAsc(AiChatMessageDO::getId));
}
}
# AI 模型调用规范
ai_model_usage:
chat_model: |
// 获取 ChatModel
ChatModel chatModel = modelService.getChatModel(modelId);
// 同步调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 流式调用
Flux<ChatResponse> stream = chatModel.stream(new Prompt(messages));
image_model: |
// 获取 ImageModel
ImageModel imageModel = modelService.getImageModel(modelId);
// 生成图片
ImageResponse response = imageModel.call(new ImagePrompt(prompt, options));
embedding_model: |
// 获取 EmbeddingModel
EmbeddingModel embeddingModel = modelService.getOrCreateVectorStore(modelId, metadataFields);
// 生成向量
EmbeddingResponse response = embeddingModel.embedForResponse(texts);
# 异常处理
error_handling:
code_prefix: "1-040-XXX-XXX"
examples:
- code: "1_040_000_000"
message: "API 密钥不存在"
- code: "1_040_001_000"
message: "模型不存在"
- code: "1_040_003_000"
message: "对话不存在"
- code: "1_040_004_001"
message: "对话生成异常"
- code: "1_040_009_000"
message: "知识库不存在"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "CommonResult.error(ErrorCode)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增 AI 平台提供商
new_platform:
title: "新增 AI 平台提供商"
steps:
- step: 1
action: "在 AiPlatformEnum 中添加新平台枚举值"
example: |
MY_PLATFORM("MyPlatform", "我的平台"),
- step: 2
action: "在 AiModelFactoryImpl 中添加模型创建方法"
example: |
// 在 getOrCreateChatModel 方法的 switch 中添加
case MY_PLATFORM:
return buildMyPlatformChatModel(apiKey, url);
// 新增构建方法
private static ChatModel buildMyPlatformChatModel(String apiKey, String url) {
// 参考 Spring AI 对应平台的 AutoConfiguration 实现
MyPlatformApi api = MyPlatformApi.builder()
.apiKey(apiKey)
.baseUrl(url)
.build();
return MyPlatformChatModel.builder()
.myPlatformApi(api)
.toolCallingManager(getToolCallingManager())
.build();
}
- step: 3
action: "如需自定义模型类,在 framework/ai/core/model/ 下创建"
example: |
// 参考 BaiChuanChatModel、GeminiChatModel 等
- step: 4
action: "在 YudaoAiProperties 中添加平台配置属性"
example: |
@Data
public static class MyPlatform {
private String apiKey;
private String baseUrl;
}
- step: 5
action: "在 AiAutoConfiguration 中添加自动配置"
example: |
@Bean
@ConditionalOnProperty(prefix = "yudao.ai.my-platform", name = "api-key")
public ChatModel myPlatformChatModel(YudaoAiProperties properties) {
return buildMyPlatformChatModel(properties.getMyPlatform());
}
# 新增 AI 能力类型
new_capability:
title: "新增 AI 能力类型"
steps:
- step: 1
action: "在 AiModelTypeEnum 中添加新类型"
example: |
MY_CAPABILITY(7, "新能力"),
- step: 2
action: "创建对应的 DO 实体类"
example: |
@TableName("ai_my_capability")
@Data
public class AiMyCapabilityDO extends BaseDO {
private Long id;
private Long userId;
private String prompt;
private String result;
// ...
}
- step: 3
action: "创建 Controller、Service、Mapper"
example: |
// Controller
@Tag(name = "管理后台 - 新能力")
@RestController
@RequestMapping("/ai/my-capability")
public class AiMyCapabilityController { ... }
// Service
public interface AiMyCapabilityService { ... }
// Mapper
public interface AiMyCapabilityMapper extends BaseMapperX<AiMyCapabilityDO> { ... }
- step: 4
action: "在 AiModelFactory 中添加对应的模型获取方法"
example: |
MyCapabilityModel getOrCreateMyCapabilityModel(AiPlatformEnum platform,
String apiKey, String url);
# 最佳实践
best_practices:
- "模型实例缓存:使用 Singleton.get() 缓存,避免重复创建连接"
- "流式响应优先:聊天推荐使用流式接口,用户体验更好"
- "错误处理:捕获 AI 调用异常,记录 errorMessage 字段"
- "向量存储选择:生产环境推荐 Milvus、Qdrant测试可用 SimpleVectorStore"
- "知识库分片根据文档特点选择分片策略语义分片、Markdown QA 分片)"
- "多模态支持ChatMessage 支持携带 attachmentUrls 实现多模态对话"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "system"
api: "AdminUserApi"
purpose: "获取用户信息"
# 外部依赖(第三方库)
external:
- name: "spring-ai-core"
version: "1.0.x"
purpose: "Spring AI 核心框架,提供统一的 AI 模型抽象"
- name: "spring-ai-openai"
version: "1.0.x"
purpose: "OpenAI 模型集成"
- name: "spring-ai-anthropic"
version: "1.0.x"
purpose: "Anthropic Claude 模型集成"
- name: "spring-ai-ollama"
version: "1.0.x"
purpose: "Ollama 本地模型集成"
- name: "spring-ai-zhipuai"
version: "1.0.x"
purpose: "智谱 AI 模型集成"
- name: "spring-ai-deepseek"
version: "1.0.x"
purpose: "DeepSeek 模型集成"
- name: "spring-ai-minimax"
version: "1.0.x"
purpose: "MiniMax 模型集成"
- name: "spring-ai-milvus"
version: "1.0.x"
purpose: "Milvus 向量数据库集成"
- name: "spring-ai-qdrant"
version: "1.0.x"
purpose: "Qdrant 向量数据库集成"
- name: "spring-ai-redis"
version: "1.0.x"
purpose: "Redis 向量存储集成"
- name: "hutool"
version: "5.x"
purpose: "Singleton 缓存、工具类"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java"
purpose: "AI 模型工厂接口,定义统一的模型创建方法"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java"
purpose: "AI 模型工厂实现,包含各平台模型创建逻辑"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java"
purpose: "AI 平台枚举,定义支持的 20+ AI 服务商"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java"
purpose: "AI 模型类型枚举"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java"
purpose: "模型配置服务,管理 API Key、模型配置"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java"
purpose: "聊天消息服务,核心业务逻辑"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java"
purpose: "知识库服务"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java"
purpose: "聊天消息 Controller提供 HTTP 接口"
- path: "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java"
purpose: "AI 自动配置类"

View File

@@ -0,0 +1,719 @@
# Skill 文件 - BPM 工作流模块
# 用于提取模块知识的标准格式
skill:
id: "skill-bpm"
name: "BPM 工作流 Skill"
version: "1.0.0"
module_path: "yudao-module-bpm"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
BPM (Business Process Management) 模块是工作流引擎核心模块,基于 Flowable 6 实现完整的业务流程管理功能。
主要解决企业内部审批流程、业务流程自动化等问题。
核心功能包括:
1. 流程定义管理 - 支持 BPMN 2.0 标准设计器和仿钉钉/飞书的 Simple 设计器
2. 流程表单管理 - 动态表单配置,支持与流程绑定
3. 流程实例管理 - 发起、取消、查询流程实例
4. 任务审批管理 - 待办、已办、审批通过/拒绝、退回、委派、转办、加签/减签
5. 流程抄送 - 支持流程抄送功能
6. 流程监听器 - 支持执行监听器和任务监听器扩展
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "领域驱动设计 (DDD): 按流程定义(Definition)、流程实例(Instance)、任务(Task)划分限界上下文"
- "策略模式: 审批人策略(BpmTaskCandidateStrategy)使用策略模式,支持多种审批人分配方式"
- "事件驱动: 流程状态变更通过 Spring ApplicationEvent 发布事件,实现解耦"
- "模板方法模式: BpmTaskCandidateStrategy 接口定义审批人计算模板"
- "适配器模式: 封装 Flowable API隔离底层工作流引擎实现"
- "监听器模式: 通过 Flowable Listener 扩展流程行为"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "流程定义 (ProcessDefinition)"
type: "聚合根"
entities:
- "BpmFormDO - 流程表单"
- "BpmCategoryDO - 流程分类"
- "BpmUserGroupDO - 审批用户组"
- "BpmProcessDefinitionInfoDO - 流程定义扩展信息"
- "BpmProcessListenerDO - 流程监听器"
- "BpmProcessExpressionDO - 流程表达式"
description: "流程定义是流程的模板,包含流程结构、表单配置、审批规则等元数据"
- name: "流程实例 (ProcessInstance)"
type: "聚合根"
entities:
- "BpmProcessInstanceCopyDO - 流程抄送记录"
description: "流程实例是流程定义的一次执行,由 Flowable 原生表管理,扩展信息存储在 DO 中"
- name: "流程任务 (Task)"
type: "实体"
entities: []
description: "流程任务是流程实例中的审批节点,由 Flowable 原生表管理"
value_objects:
- "BpmProcessInstanceCreateReqDTO - 流程实例创建请求"
- "BpmModelMetaInfoVO - 流程模型元信息"
- "BpmSimpleModelNodeVO - Simple 设计器节点定义"
services:
- "BpmModelService - 流程模型管理服务"
- "BpmProcessDefinitionService - 流程定义管理服务"
- "BpmProcessInstanceService - 流程实例管理服务"
- "BpmTaskService - 流程任务管理服务"
- "BpmFormService - 表单管理服务"
- "BpmMessageService - 流程消息服务"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口供其他模块调用"
components:
- "BpmProcessInstanceApi - 流程实例创建接口"
- "BpmProcessTaskApi - 流程任务操作接口"
- "BpmProcessInstanceStatusEvent - 流程状态变更事件"
- name: "controller"
purpose: "HTTP接口供前端调用"
components:
- "BpmModelController - 流程模型管理"
- "BpmProcessDefinitionController - 流程定义管理"
- "BpmProcessInstanceController - 流程实例管理"
- "BpmTaskController - 流程任务管理"
- "BpmFormController - 表单管理"
- "BpmCategoryController - 分类管理"
- "BpmUserGroupController - 用户组管理"
- "BpmOALeaveController - 请假申请示例"
- name: "service"
purpose: "业务逻辑层,封装核心业务"
components:
- "definition/ - 流程定义相关服务"
- "task/ - 流程任务相关服务"
- "message/ - 消息通知服务"
- "oa/ - OA 业务示例(请假)"
- name: "dal"
purpose: "数据访问层,与数据库交互"
components:
- "dataobject/definition/ - 定义相关 DO"
- "dataobject/task/ - 任务相关 DO"
- "dataobject/oa/ - OA 相关 DO"
- "mysql/ - MyBatis Mapper"
- "redis/ - Redis 缓存"
- name: "framework/flowable"
purpose: "Flowable 框架集成层"
components:
- "config/BpmFlowableConfiguration - Flowable 配置"
- "core/candidate/ - 审批人策略实现"
- "core/listener/ - 流程监听器"
- "core/behavior/ - 自定义行为"
- "core/util/ - 工具类"
# 设计模式应用
design_patterns:
- pattern: "策略模式 (Strategy Pattern)"
location: "framework/flowable/core/candidate/BpmTaskCandidateStrategy.java"
purpose: "审批人分配策略,支持角色、部门、用户、表达式等多种分配方式"
- pattern: "事件驱动 (Event-Driven)"
location: "api/event/BpmProcessInstanceStatusEvent.java"
purpose: "流程状态变更事件,通知其他模块流程结果"
- pattern: "监听器模式 (Listener Pattern)"
location: "framework/flowable/core/listener/BpmTaskEventListener.java"
purpose: "监听 Flowable 任务事件,执行自定义逻辑"
- pattern: "工厂模式 (Factory Pattern)"
location: "framework/flowable/core/behavior/BpmActivityBehaviorFactory.java"
purpose: "创建自定义 ActivityBehavior扩展 Flowable 行为"
- pattern: "模板方法模式 (Template Method)"
location: "service/task/trigger/BpmTrigger.java"
purpose: "流程触发器抽象,支持 HTTP 请求、表单操作等触发类型"
# 模块间通信
communication:
apis:
- name: "BpmProcessInstanceApi"
method: "createProcessInstance"
purpose: "供其他模块发起流程实例"
- name: "BpmProcessTaskApi"
method: "triggerTask"
purpose: "触发流程任务执行"
- name: "BpmProcessInstanceStatusEvent"
purpose: "流程状态变更事件,供其他模块监听"
consumers:
- module: "system"
api: "AdminUserApi"
purpose: "获取用户信息"
- module: "system"
api: "DeptApi"
purpose: "获取部门信息"
- module: "system"
api: "PermissionApi"
purpose: "获取用户角色、权限"
mq: []
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有 DO 继承 BaseDO包含 creator、createTime、updater、updateTime、deleted 字段"
# 核心数据表
tables:
- name: "bpm_category"
comment: "流程分类表"
entity: "BpmCategoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "分类编号" }
- { name: "name", type: "String", comment: "分类名" }
- { name: "code", type: "String", comment: "分类标志" }
- { name: "description", type: "String", comment: "分类描述" }
- { name: "status", type: "Integer", comment: "分类状态" }
- { name: "sort", type: "Integer", comment: "分类排序" }
- name: "bpm_form"
comment: "流程表单表"
entity: "BpmFormDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "表单名" }
- { name: "status", type: "Integer", comment: "状态" }
- { name: "conf", type: "String", comment: "表单配置 JSON" }
- { name: "fields", type: "List<String>", comment: "表单项数组" }
- { name: "remark", type: "String", comment: "备注" }
- name: "bpm_user_group"
comment: "审批用户组表"
entity: "BpmUserGroupDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "组名" }
- { name: "description", type: "String", comment: "描述" }
- { name: "status", type: "Integer", comment: "状态" }
- { name: "user_ids", type: "Set<Long>", comment: "成员用户编号数组" }
- name: "bpm_process_definition_info"
comment: "流程定义扩展信息表"
entity: "BpmProcessDefinitionInfoDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "process_definition_id", type: "String", comment: "流程定义编号" }
- { name: "model_id", type: "String", comment: "流程模型编号" }
- { name: "model_type", type: "Integer", comment: "模型类型(10:BPMN, 20:Simple)" }
- { name: "category", type: "String", comment: "流程分类编码" }
- { name: "form_type", type: "Integer", comment: "表单类型" }
- { name: "form_id", type: "Long", comment: "动态表单编号" }
- { name: "form_conf", type: "String", comment: "表单配置" }
- { name: "form_fields", type: "List<String>", comment: "表单字段" }
- { name: "start_user_ids", type: "List<Long>", comment: "可发起用户" }
- { name: "manager_user_ids", type: "List<Long>", comment: "可管理用户" }
- { name: "simple_model", type: "String", comment: "Simple设计器模型数据" }
- name: "bpm_process_listener"
comment: "流程监听器表"
entity: "BpmProcessListenerDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "name", type: "String", comment: "监听器名字" }
- { name: "status", type: "Integer", comment: "状态" }
- { name: "type", type: "String", comment: "监听类型(execution/task)" }
- { name: "event", type: "String", comment: "监听事件" }
- { name: "value_type", type: "String", comment: "值类型(class/delegateExpression/expression)" }
- { name: "value", type: "String", comment: "值" }
- name: "bpm_process_expression"
comment: "流程表达式表"
entity: "BpmProcessExpressionDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "name", type: "String", comment: "表达式名字" }
- { name: "status", type: "Integer", comment: "状态" }
- { name: "expression", type: "String", comment: "表达式" }
- name: "bpm_process_instance_copy"
comment: "流程抄送表"
entity: "BpmProcessInstanceCopyDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "编号" }
- { name: "start_user_id", type: "Long", comment: "发起人ID" }
- { name: "process_instance_id", type: "String", comment: "流程实例编号" }
- { name: "process_definition_id", type: "String", comment: "流程定义编号" }
- { name: "activity_id", type: "String", comment: "流程活动编号" }
- { name: "task_id", type: "String", comment: "任务编号" }
- { name: "user_id", type: "Long", comment: "被抄送用户编号" }
- { name: "reason", type: "String", comment: "抄送意见" }
- name: "bpm_oa_leave"
comment: "OA请假申请表示例业务表"
entity: "BpmOALeaveDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "请假表单主键" }
- { name: "user_id", type: "Long", comment: "申请人用户编号" }
- { name: "type", type: "String", comment: "请假类型" }
- { name: "reason", type: "String", comment: "原因" }
- { name: "start_time", type: "LocalDateTime", comment: "开始时间" }
- { name: "end_time", type: "LocalDateTime", comment: "结束时间" }
- { name: "day", type: "Long", comment: "请假天数" }
- { name: "status", type: "Integer", comment: "审批结果" }
- { name: "process_instance_id", type: "String", comment: "流程编号" }
# 表关系ER关系
relationships:
- from: "bpm_process_definition_info"
to: "bpm_category"
type: "N:1"
foreign_key: "category -> code"
- from: "bpm_process_definition_info"
to: "bpm_form"
type: "N:1"
foreign_key: "form_id -> id"
- from: "bpm_process_instance_copy"
to: "Flowable ProcessInstance"
type: "N:1"
foreign_key: "process_instance_id -> id"
- from: "bpm_oa_leave"
to: "Flowable ProcessInstance"
type: "1:1"
foreign_key: "process_instance_id -> id"
# Flowable 原生表说明
flowable_tables:
- name: "ACT_RE_DEPLOYMENT"
comment: "部署信息表"
- name: "ACT_RE_PROCDEF"
comment: "流程定义表"
- name: "ACT_RE_MODEL"
comment: "流程模型表"
- name: "ACT_RU_EXECUTION"
comment: "运行时执行实例表"
- name: "ACT_RU_TASK"
comment: "运行时任务表"
- name: "ACT_HI_PROCINST"
comment: "历史流程实例表"
- name: "ACT_HI_TASKINST"
comment: "历史任务实例表"
- name: "ACT_HI_VARINST"
comment: "历史变量表"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - XXX')"
- "@RestController"
- "@RequestMapping('/bpm/xxx')"
- "@Validated"
- "@PreAuthorize('@ss.hasPermission(xxx)')"
example: |
@Tag(name = "管理后台 - 流程任务实例")
@RestController
@RequestMapping("/bpm/task")
@Validated
public class BpmTaskController {
@Resource
private BpmTaskService taskService;
@PutMapping("/approve")
@Operation(summary = "通过任务")
@PreAuthorize("@ss.hasPermission('bpm:task:update')")
public CommonResult<Boolean> approveTask(@Valid @RequestBody BpmTaskApproveReqVO reqVO) {
taskService.approveTask(getLoginUserId(), reqVO);
return success(true);
}
}
# Service层规范
service:
interface_pattern: |
接口定义在 service/ 目录下命名规则Bpm{Entity}Service
核心服务接口:
- BpmModelService: 流程模型管理
- BpmProcessDefinitionService: 流程定义管理
- BpmProcessInstanceService: 流程实例管理
- BpmTaskService: 流程任务管理
方法分类:
1. Query 查询相关方法 - 查询操作
2. Update 写入相关方法 - 修改操作
3. Event 事件相关方法 - 事件处理
impl_pattern: |
实现类放在同目录下命名规则Bpm{Entity}ServiceImpl
使用 @Service 和 @Validated 注解
注意事项:
1. 循环依赖使用 @Lazy 注解
2. 事务操作使用 @Transactional
3. 数据权限使用 @DataPermission
example: |
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {
@Resource
private RuntimeService runtimeService; // Flowable 运行时服务
@Resource
private HistoryService historyService; // Flowable 历史服务
@Override
public String createProcessInstance(Long userId, BpmProcessInstanceCreateReqDTO createReqDTO) {
// 1. 获取流程定义
ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
createReqDTO.getProcessDefinitionKey());
// 2. 构建流程实例
ProcessInstanceBuilder builder = runtimeService.createProcessInstanceBuilder()
.processDefinitionKey(createReqDTO.getProcessDefinitionKey())
.businessKey(createReqDTO.getBusinessKey())
.variables(createReqDTO.getVariables());
// 3. 启动流程实例
return builder.start().getId();
}
}
# 数据访问规范
dal:
mapper_pattern: |
Mapper 继承 BaseMapperX提供基础 CRUD 操作
使用 @Mapper 注解
特殊查询:
- 使用 MyBatis-Plus 的 LambdaQueryWrapper
- 复杂查询使用自定义 XML 或 @Select 注解
example: |
@Mapper
public interface BpmFormMapper extends BaseMapperX<BpmFormDO> {
default PageResult<BpmFormDO> selectPage(BpmFormPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<BpmFormDO>()
.likeIfPresent(BpmFormDO::getName, reqVO.getName())
.orderByDesc(BpmFormDO::getId));
}
}
# 异常处理
error_handling:
code_prefix: "1-009-XXX-XXX"
examples:
- code: "1_009_002_000"
message: "已经存在流程标识为【{}】的流程"
- code: "1_009_003_002"
message: "流程定义不存在"
- code: "1_009_004_000"
message: "流程实例不存在"
- code: "1_009_005_001"
message: "操作失败,原因:该任务的审批人不是你"
- code: "1_009_005_002"
message: "流程任务不存在"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE, params...)"
# 流程发起代码示例
process_start_example: |
// 方式一:通过 API 接口发起流程(推荐)
@Resource
private BpmProcessInstanceApi processInstanceApi;
public void submitLeave(LeaveCreateReqVO createReqVO) {
// 1. 创建业务数据
BpmOALeaveDO leave = new BpmOALeaveDO();
// ... 设置属性
leaveMapper.insert(leave);
// 2. 发起流程
BpmProcessInstanceCreateReqDTO reqDTO = new BpmProcessInstanceCreateReqDTO();
reqDTO.setProcessDefinitionKey("oa_leave"); // 流程定义 Key
reqDTO.setBusinessKey(leave.getId().toString()); // 业务主键
reqDTO.setVariables(BeanUtil.beanToMap(createReqVO)); // 流程变量
String processInstanceId = processInstanceApi.createProcessInstance(userId, reqDTO);
// 3. 关联流程实例
leave.setProcessInstanceId(processInstanceId);
leaveMapper.updateById(leave);
}
// 方式二:通过 Controller 接口发起流程(前端调用)
@PostMapping("/create")
public CommonResult<String> createProcessInstance(@Valid @RequestBody BpmProcessInstanceCreateReqVO createReqVO) {
return success(processInstanceService.createProcessInstance(getLoginUserId(), createReqVO));
}
# 任务审批代码示例
task_approve_example: |
// 审批通过
@PutMapping("/approve")
public CommonResult<Boolean> approveTask(@Valid @RequestBody BpmTaskApproveReqVO reqVO) {
taskService.approveTask(getLoginUserId(), reqVO);
return success(true);
}
// 审批拒绝
@PutMapping("/reject")
public CommonResult<Boolean> rejectTask(@Valid @RequestBody BpmTaskRejectReqVO reqVO) {
taskService.rejectTask(getLoginUserId(), reqVO);
return success(true);
}
// 任务退回
@PutMapping("/return")
public CommonResult<Boolean> returnTask(@Valid @RequestBody BpmTaskReturnReqVO reqVO) {
taskService.returnTask(getLoginUserId(), reqVO);
return success(true);
}
// 任务委派
@PutMapping("/delegate")
public CommonResult<Boolean> delegateTask(@Valid @RequestBody BpmTaskDelegateReqVO reqVO) {
taskService.delegateTask(getLoginUserId(), reqVO);
return success(true);
}
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增审批类型
new_approval_type:
title: "新增审批类型(如报销、采购等)"
steps:
- step: 1
action: "创建业务 DO 实体"
description: |
在 dal/dataobject/ 下创建业务实体类,如 BpmExpenseDO
需要包含 processInstanceId 字段关联流程实例
- step: 2
action: "创建业务表"
description: |
创建对应数据库表,包含业务字段和 process_instance_id 字段
- step: 3
action: "创建业务服务"
description: |
创建 Service 接口和实现类
实现业务逻辑,调用 BpmProcessInstanceApi 发起流程
- step: 4
action: "实现流程状态监听"
description: |
实现 BpmProcessInstanceStatusEventListener
监听流程状态变更,更新业务表状态
示例:
@Component
public class BpmExpenseStatusListener extends BpmProcessInstanceStatusEventListener {
@Override
protected void onEvent(BpmProcessInstanceStatusEvent event) {
if ("expense".equals(event.getProcessDefinitionKey())) {
// 更新报销单状态
expenseMapper.updateStatus(event.getBusinessKey(), event.getStatus());
}
}
}
- step: 5
action: "配置流程定义"
description: |
在管理后台配置流程定义
1. 创建流程分类
2. 创建流程表单
3. 设计流程图BPMN 或 Simple
4. 配置审批人策略
# 自定义审批人策略
new_candidate_strategy:
title: "自定义审批人策略"
steps:
- step: 1
action: "添加策略枚举"
description: |
在 BpmTaskCandidateStrategyEnum 中添加新的策略类型
- step: 2
action: "实现策略接口"
description: |
实现 BpmTaskCandidateStrategy 接口
示例:
@Component
public class BpmTaskCandidateCustomStrategy implements BpmTaskCandidateStrategy {
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.CUSTOM;
}
@Override
public void validateParam(String param) {
// 校验参数
}
@Override
public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
// 计算审批人
return calculateUsers(param);
}
}
- step: 3
action: "注册策略 Bean"
description: |
使用 @Component 注解Spring 自动注册到策略工厂
# 自定义流程监听器
new_process_listener:
title: "自定义流程监听器"
steps:
- step: 1
action: "创建监听器类"
description: |
实现 ExecutionListener 或 TaskListener 接口
示例(执行监听器):
@Component
public class CustomExecutionListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) {
// 监听逻辑
}
}
示例(任务监听器):
@Component
public class CustomTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
// 监听逻辑
}
}
- step: 2
action: "注册监听器"
description: |
方式一:通过数据库配置(推荐)
在 bpm_process_listener 表中添加监听器配置
方式二:在 BPMN XML 中配置
<extensionElements>
<flowable:executionListener event="start" delegateExpression="${customExecutionListener}"/>
</extensionElements>
# 最佳实践
best_practices:
- "流程设计原则:流程定义和业务数据分离,通过 businessKey 关联"
- "审批人策略:优先使用已有的审批人策略,如角色、部门负责人、用户组等"
- "流程变量:使用流程变量传递表单数据,避免过度依赖业务查询"
- "事件监听:通过监听流程状态变更事件更新业务状态,实现解耦"
- "Simple 设计器:对于简单审批流程,推荐使用 Simple 设计器,降低学习成本"
- "流程版本:同一流程定义可以部署多个版本,已运行的实例使用旧版本"
- "权限控制:通过 managerUserIds 配置流程管理员,实现流程管理权限分离"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取用户信息,用于审批人计算和流程发起人信息"
- module: "yudao-module-system"
api: "DeptApi"
purpose: "获取部门信息,用于部门负责人审批策略"
- module: "yudao-module-system"
api: "PermissionApi"
purpose: "获取用户角色,用于角色审批策略"
- module: "yudao-module-system"
api: "PostApi"
purpose: "获取岗位信息,用于岗位审批策略"
# 外部依赖(第三方库)
external:
- name: "flowable-spring-boot-starter-process"
version: "6.x"
purpose: "Flowable 工作流引擎核心"
- name: "flowable-spring-boot-starter-actuator"
version: "6.x"
purpose: "Flowable 监控端点"
- name: "yudao-spring-boot-starter-mybatis"
version: "${revision}"
purpose: "MyBatis-Plus 数据访问"
- name: "yudao-spring-boot-starter-biz-tenant"
version: "${revision}"
purpose: "多租户支持"
- name: "yudao-spring-boot-starter-biz-data-permission"
version: "${revision}"
purpose: "数据权限控制"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java"
purpose: "流程实例服务接口,定义流程实例的核心操作"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java"
purpose: "流程任务服务接口,定义任务审批的核心操作"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java"
purpose: "流程模型服务接口,定义流程模型管理操作"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java"
purpose: "流程实例 API 接口,供其他模块调用发起流程"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEvent.java"
purpose: "流程状态变更事件,实现模块间解耦"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java"
purpose: "审批人策略接口,定义审批人计算的标准"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java"
purpose: "审批人策略枚举,列出所有支持的审批人分配方式"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java"
purpose: "错误码常量,定义 BPM 模块所有错误码"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java"
purpose: "流程模型类型枚举,区分 BPMN 和 Simple 设计器"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java"
purpose: "流程实例状态枚举,定义流程的所有状态"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java"
purpose: "任务管理 Controller提供任务审批相关 API"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java"
purpose: "流程实例 Controller提供流程实例管理 API"
- path: "yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListener.java"
purpose: "请假流程状态监听示例,展示如何监听流程状态变更"
- path: "yudao-module-bpm/pom.xml"
purpose: "Maven 依赖配置,包含 Flowable 等核心依赖"

View File

@@ -0,0 +1,861 @@
# Skill 文档 - yudao-module-crm 模块
# 客户关系管理模块完整技术文档
skill:
id: "skill-crm"
name: "CRM 客户关系管理 Skill"
version: "1.0.0"
module_path: "yudao-module-crm"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
CRMCustomer Relationship Management模块是客户关系管理系统的核心解决企业销售过程中的客户管理、商机跟进、合同签订、回款管理等全流程业务问题。
核心业务场景:
1. 线索管理:潜在客户信息的录入与转化
2. 客户管理:客户信息维护、公海池机制、客户分配与转移
3. 联系人管理:客户关键联系人信息管理
4. 商机管理:销售机会的全生命周期管理(销售漏斗)
5. 合同管理:合同签订与审批流程
6. 回款管理:回款计划与实际回款跟踪
7. 产品管理:产品信息与分类管理
8. 数据统计:销售漏斗、业绩排名等统计分析
在整个系统中的定位:
- 作为独立的业务模块,提供完整的 CRM 业务功能
- 依赖 System 模块(用户、部门)和 BPM 模块(审批流程)
- 不对外暴露 API通过 Controller 层提供 HTTP 接口
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- name: "领域驱动设计DDD"
description: "以客户为聚合根,商机、联系人、合同等为关联实体,形成清晰的领域模型边界"
- name: "数据权限控制"
description: "基于 CrmPermissionDO 实现细粒度的数据权限控制,支持负责人、只读、读写三级权限"
- name: "分层架构"
description: "严格的 Controller -> Service -> DAL 三层架构,职责清晰"
- name: "销售漏斗模型"
description: "商机状态组CrmBusinessStatusTypeDO+ 商机状态CrmBusinessStatusDO实现可配置的销售漏斗"
- name: "公海池机制"
description: "客户公海池配置CrmCustomerPoolConfigDO实现客户自动回收与分配机制"
- name: "AOP 权限校验"
description: "通过 @CrmPermission 注解实现声明式的数据权限校验"
- name: "操作日志记录"
description: "通过 @LogRecord 注解记录关键业务操作日志"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "客户Customer"
type: "聚合根"
description: "CRM 系统的核心聚合根,关联商机、联系人、合同、回款等所有业务对象"
entities:
- "CrmBusinessDO商机"
- "CrmContactDO联系人"
- "CrmContractDO合同"
- "CrmReceivableDO回款"
- "CrmReceivablePlanDO回款计划"
- "CrmFollowUpRecordDO跟进记录"
- name: "线索Clue"
type: "聚合根"
description: "潜在客户信息,可转化为正式客户"
entities: []
- name: "产品Product"
type: "聚合根"
description: "产品信息管理,被商机和合同引用"
entities:
- "CrmProductCategoryDO产品分类"
value_objects:
- name: "CrmPermissionDO"
description: "数据权限值对象,关联业务类型与权限级别"
- name: "CrmBusinessStatusDO"
description: "商机状态值对象,属于商机状态组"
- name: "CrmBusinessStatusTypeDO"
description: "商机状态组配置"
- name: "CrmCustomerPoolConfigDO"
description: "客户公海池配置"
- name: "CrmCustomerLimitConfigDO"
description: "客户限制配置(拥有上限、锁定上限)"
services:
- name: "CrmCustomerService"
description: "客户管理领域服务,处理客户创建、转移、公海等核心业务"
- name: "CrmBusinessService"
description: "商机管理领域服务,处理商机创建、状态变更等业务"
- name: "CrmPermissionService"
description: "数据权限领域服务,处理权限创建、转移、校验等业务"
- name: "CrmContractService"
description: "合同管理领域服务,处理合同签订、审批等业务"
- name: "CrmReceivableService"
description: "回款管理领域服务,处理回款录入、审批等业务"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口当前模块未对外暴露API"
components:
- "package-info.java"
- name: "controller"
purpose: "HTTP接口层处理请求响应"
components:
- "CrmCustomerController - 客户管理接口"
- "CrmClueController - 线索管理接口"
- "CrmContactController - 联系人管理接口"
- "CrmBusinessController - 商机管理接口"
- "CrmBusinessStatusController - 商机状态管理接口"
- "CrmContractController - 合同管理接口"
- "CrmContractConfigController - 合同配置接口"
- "CrmReceivableController - 回款管理接口"
- "CrmReceivablePlanController - 回款计划接口"
- "CrmProductController - 产品管理接口"
- "CrmProductCategoryController - 产品分类接口"
- "CrmPermissionController - 数据权限接口"
- "CrmFollowUpRecordController - 跟进记录接口"
- "CrmOperateLogController - 操作日志接口"
- "CrmCustomerLimitConfigController - 客户限制配置接口"
- "CrmCustomerPoolConfigController - 公海池配置接口"
- "CrmStatisticsCustomerController - 客户统计接口"
- "CrmStatisticsFunnelController - 漏斗统计接口"
- "CrmStatisticsPerformanceController - 业绩统计接口"
- "CrmStatisticsPortraitController - 客户画像统计接口"
- "CrmStatisticsRankController - 排行榜统计接口"
- name: "service"
purpose: "业务逻辑层,处理核心业务"
components:
- "customer/ - 客户相关服务"
- "clue/ - 线索相关服务"
- "contact/ - 联系人相关服务"
- "business/ - 商机相关服务"
- "contract/ - 合同相关服务"
- "receivable/ - 回款相关服务"
- "product/ - 产品相关服务"
- "permission/ - 权限相关服务"
- "followup/ - 跟进记录服务"
- "statistics/ - 统计服务"
- name: "dal"
purpose: "数据访问层,处理数据库操作"
components:
- "dataobject/ - DO 实体类"
- "mysql/ - MyBatis Mapper 接口"
# 设计模式应用
design_patterns:
- pattern: "AOP 权限校验"
location: "CrmPermission 注解 + CrmPermissionAspect"
purpose: "通过注解声明数据权限要求AOP 自动校验"
- pattern: "策略模式"
location: "CrmBizTypeEnum 业务类型枚举"
purpose: "统一处理不同业务类型的权限校验逻辑"
- pattern: "模板方法"
location: "各 Service 实现类的校验方法"
purpose: "统一的业务校验流程"
- pattern: "观察者模式"
location: "CrmContractStatusListener、CrmReceivableStatusListener"
purpose: "监听 BPM 审批状态变更,更新业务状态"
- pattern: "Builder 模式"
location: "所有 DO 实体类使用 @Builder"
purpose: "简化复杂对象的构建"
- pattern: "代理模式"
location: "CrmCustomerServiceImpl.getSelf()"
purpose: "解决 AOP 事务代理问题"
# 模块间通信
communication:
apis: [] # 当前模块未对外暴露 API
consumers:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取用户信息、校验用户"
- module: "yudao-module-system"
api: "DeptApi"
purpose: "获取部门信息"
- module: "yudao-module-bpm"
api: "流程审批"
purpose: "合同、回款的审批流程"
mq: [] # 当前模块未使用消息队列
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有 CRM 实体继承 BaseDO包含 id、creator、create_time、updater、update_time、deleted 字段。注意CRM 模块未使用 TenantBaseDO不涉及多租户"
# 核心数据表
tables:
# ==================== 客户相关 ====================
- name: "crm_customer"
comment: "客户表"
entity: "CrmCustomerDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "客户名称" }
- { name: "follow_up_status", type: "TINYINT", comment: "跟进状态" }
- { name: "contact_last_time", type: "DATETIME", comment: "最后跟进时间" }
- { name: "contact_last_content", type: "VARCHAR", comment: "最后跟进内容" }
- { name: "contact_next_time", type: "DATETIME", comment: "下次联系时间" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "owner_time", type: "DATETIME", comment: "成为负责人时间" }
- { name: "lock_status", type: "TINYINT", comment: "锁定状态" }
- { name: "deal_status", type: "TINYINT", comment: "成交状态" }
- { name: "mobile", type: "VARCHAR", comment: "手机号" }
- { name: "telephone", type: "VARCHAR", comment: "电话" }
- { name: "qq", type: "VARCHAR", comment: "QQ" }
- { name: "wechat", type: "VARCHAR", comment: "微信" }
- { name: "email", type: "VARCHAR", comment: "邮箱" }
- { name: "area_id", type: "INT", comment: "地区ID" }
- { name: "detail_address", type: "VARCHAR", comment: "详细地址" }
- { name: "industry_id", type: "INT", comment: "所属行业" }
- { name: "level", type: "INT", comment: "客户等级" }
- { name: "source", type: "INT", comment: "客户来源" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_owner_user_id", columns: ["owner_user_id"] }
- { name: "idx_name", columns: ["name"] }
- name: "crm_customer_pool_config"
comment: "客户公海池配置表"
entity: "CrmCustomerPoolConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "enabled", type: "TINYINT", comment: "是否启用客户公海" }
- { name: "contact_expire_days", type: "INT", comment: "未跟进放入公海天数" }
- { name: "deal_expire_days", type: "INT", comment: "未成交放入公海天数" }
- { name: "notify_enabled", type: "TINYINT", comment: "是否开启提前提醒" }
- { name: "notify_days", type: "INT", comment: "提前提醒天数" }
- name: "crm_customer_limit_config"
comment: "客户限制配置表"
entity: "CrmCustomerLimitConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "type", type: "INT", comment: "限制类型(拥有上限/锁定上限)" }
- { name: "max_count", type: "INT", comment: "最大数量" }
- { name: "deal_count_enabled", type: "TINYINT", comment: "是否计入成交客户" }
- { name: "user_ids", type: "VARCHAR", comment: "适用用户ID列表" }
- { name: "dept_ids", type: "VARCHAR", comment: "适用部门ID列表" }
# ==================== 线索相关 ====================
- name: "crm_clue"
comment: "线索表"
entity: "CrmClueDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "线索名称" }
- { name: "follow_up_status", type: "TINYINT", comment: "跟进状态" }
- { name: "contact_last_time", type: "DATETIME", comment: "最后跟进时间" }
- { name: "contact_last_content", type: "VARCHAR", comment: "最后跟进内容" }
- { name: "contact_next_time", type: "DATETIME", comment: "下次联系时间" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "transform_status", type: "TINYINT", comment: "转化状态" }
- { name: "customer_id", type: "BIGINT", comment: "转化后的客户ID" }
- { name: "mobile", type: "VARCHAR", comment: "手机号" }
- { name: "telephone", type: "VARCHAR", comment: "电话" }
- { name: "qq", type: "VARCHAR", comment: "QQ" }
- { name: "wechat", type: "VARCHAR", comment: "微信" }
- { name: "email", type: "VARCHAR", comment: "邮箱" }
- { name: "area_id", type: "INT", comment: "地区ID" }
- { name: "detail_address", type: "VARCHAR", comment: "详细地址" }
- { name: "industry_id", type: "INT", comment: "所属行业" }
- { name: "level", type: "INT", comment: "客户等级" }
- { name: "source", type: "INT", comment: "客户来源" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
# ==================== 联系人相关 ====================
- name: "crm_contact"
comment: "联系人表"
entity: "CrmContactDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "联系人姓名" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "contact_last_time", type: "DATETIME", comment: "最后跟进时间" }
- { name: "contact_last_content", type: "VARCHAR", comment: "最后跟进内容" }
- { name: "contact_next_time", type: "DATETIME", comment: "下次联系时间" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "mobile", type: "VARCHAR", comment: "手机号" }
- { name: "telephone", type: "VARCHAR", comment: "电话" }
- { name: "email", type: "VARCHAR", comment: "邮箱" }
- { name: "qq", type: "BIGINT", comment: "QQ" }
- { name: "wechat", type: "VARCHAR", comment: "微信" }
- { name: "area_id", type: "INT", comment: "地区ID" }
- { name: "detail_address", type: "VARCHAR", comment: "详细地址" }
- { name: "sex", type: "INT", comment: "性别" }
- { name: "master", type: "TINYINT", comment: "是否关键决策人" }
- { name: "post", type: "VARCHAR", comment: "职位" }
- { name: "parent_id", type: "BIGINT", comment: "直属上级ID" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- name: "crm_contact_business"
comment: "联系人商机关联表"
entity: "CrmContactBusinessDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "contact_id", type: "BIGINT", comment: "联系人ID" }
- { name: "business_id", type: "BIGINT", comment: "商机ID" }
# ==================== 商机相关 ====================
- name: "crm_business"
comment: "商机表"
entity: "CrmBusinessDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "商机名称" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "follow_up_status", type: "TINYINT", comment: "跟进状态" }
- { name: "contact_last_time", type: "DATETIME", comment: "最后跟进时间" }
- { name: "contact_next_time", type: "DATETIME", comment: "下次联系时间" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "status_type_id", type: "BIGINT", comment: "商机状态组ID" }
- { name: "status_id", type: "BIGINT", comment: "商机状态ID" }
- { name: "end_status", type: "INT", comment: "结束状态(赢单/输单/无效)" }
- { name: "end_remark", type: "VARCHAR", comment: "结束备注" }
- { name: "deal_time", type: "DATETIME", comment: "预计成交日期" }
- { name: "total_product_price", type: "DECIMAL", comment: "产品总金额" }
- { name: "discount_percent", type: "DECIMAL", comment: "整单折扣" }
- { name: "total_price", type: "DECIMAL", comment: "商机总金额" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- name: "crm_business_status_type"
comment: "商机状态组表(配置表)"
entity: "CrmBusinessStatusTypeDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "状态组名称" }
- { name: "dept_ids", type: "VARCHAR", comment: "使用的部门ID列表" }
- name: "crm_business_status"
comment: "商机状态表(配置表)"
entity: "CrmBusinessStatusDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "type_id", type: "BIGINT", comment: "状态组ID" }
- { name: "name", type: "VARCHAR", comment: "状态名称" }
- { name: "percent", type: "INT", comment: "赢单率百分比" }
- { name: "sort", type: "INT", comment: "排序" }
- name: "crm_business_product"
comment: "商机产品关联表"
entity: "CrmBusinessProductDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "business_id", type: "BIGINT", comment: "商机ID" }
- { name: "product_id", type: "BIGINT", comment: "产品ID" }
- { name: "count", type: "DECIMAL", comment: "数量" }
- { name: "price", type: "DECIMAL", comment: "单价" }
# ==================== 合同相关 ====================
- name: "crm_contract"
comment: "合同表"
entity: "CrmContractDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "合同名称" }
- { name: "no", type: "VARCHAR", comment: "合同编号" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "business_id", type: "BIGINT", comment: "商机ID" }
- { name: "contact_last_time", type: "DATETIME", comment: "最后跟进时间" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "process_instance_id", type: "VARCHAR", comment: "工作流实例ID" }
- { name: "audit_status", type: "INT", comment: "审批状态" }
- { name: "order_date", type: "DATETIME", comment: "下单日期" }
- { name: "start_time", type: "DATETIME", comment: "开始时间" }
- { name: "end_time", type: "DATETIME", comment: "结束时间" }
- { name: "total_product_price", type: "DECIMAL", comment: "产品总金额" }
- { name: "discount_percent", type: "DECIMAL", comment: "整单折扣" }
- { name: "total_price", type: "DECIMAL", comment: "合同总金额" }
- { name: "sign_contact_id", type: "BIGINT", comment: "客户签约人ID" }
- { name: "sign_user_id", type: "BIGINT", comment: "公司签约人ID" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- name: "crm_contract_config"
comment: "合同配置表"
entity: "CrmContractConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "deposit_enabled", type: "TINYINT", comment: "是否开启押金" }
- { name: "deposit_percent", type: "DECIMAL", comment: "押金比例" }
- name: "crm_contract_product"
comment: "合同产品关联表"
entity: "CrmContractProductDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "contract_id", type: "BIGINT", comment: "合同ID" }
- { name: "product_id", type: "BIGINT", comment: "产品ID" }
- { name: "count", type: "DECIMAL", comment: "数量" }
- { name: "price", type: "DECIMAL", comment: "单价" }
# ==================== 回款相关 ====================
- name: "crm_receivable"
comment: "回款表"
entity: "CrmReceivableDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "no", type: "VARCHAR", comment: "回款编号" }
- { name: "plan_id", type: "BIGINT", comment: "回款计划ID" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "contract_id", type: "BIGINT", comment: "合同ID" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "return_time", type: "DATETIME", comment: "回款日期" }
- { name: "return_type", type: "INT", comment: "回款方式" }
- { name: "price", type: "DECIMAL", comment: "回款金额" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- { name: "process_instance_id", type: "VARCHAR", comment: "工作流实例ID" }
- { name: "audit_status", type: "INT", comment: "审批状态" }
- name: "crm_receivable_plan"
comment: "回款计划表"
entity: "CrmReceivablePlanDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "period", type: "INT", comment: "期数" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "contract_id", type: "BIGINT", comment: "合同ID" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- { name: "return_time", type: "DATETIME", comment: "计划回款日期" }
- { name: "return_type", type: "INT", comment: "计划回款类型" }
- { name: "price", type: "DECIMAL", comment: "计划回款金额" }
- { name: "receivable_id", type: "BIGINT", comment: "实际回款ID" }
- { name: "remind_days", type: "INT", comment: "提前提醒天数" }
- { name: "remind_time", type: "DATETIME", comment: "提醒日期" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
# ==================== 产品相关 ====================
- name: "crm_product"
comment: "产品表"
entity: "CrmProductDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "产品名称" }
- { name: "no", type: "VARCHAR", comment: "产品编码" }
- { name: "unit", type: "INT", comment: "单位" }
- { name: "price", type: "DECIMAL", comment: "价格" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "category_id", type: "BIGINT", comment: "分类ID" }
- { name: "description", type: "VARCHAR", comment: "产品描述" }
- { name: "owner_user_id", type: "BIGINT", comment: "负责人用户编号" }
- name: "crm_product_category"
comment: "产品分类表"
entity: "CrmProductCategoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "name", type: "VARCHAR", comment: "分类名称" }
- { name: "parent_id", type: "BIGINT", comment: "父分类ID" }
- { name: "sort", type: "INT", comment: "排序" }
# ==================== 权限与跟进 ====================
- name: "crm_permission"
comment: "数据权限表"
entity: "CrmPermissionDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "biz_type", type: "INT", comment: "业务类型" }
- { name: "biz_id", type: "BIGINT", comment: "业务ID" }
- { name: "user_id", type: "BIGINT", comment: "用户ID" }
- { name: "level", type: "INT", comment: "权限级别" }
indexes:
- { name: "idx_biz", columns: ["biz_type", "biz_id"] }
- { name: "idx_user", columns: ["user_id"] }
- name: "crm_follow_up_record"
comment: "跟进记录表"
entity: "CrmFollowUpRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID" }
- { name: "biz_type", type: "INT", comment: "业务类型" }
- { name: "biz_id", type: "BIGINT", comment: "业务ID" }
- { name: "type", type: "INT", comment: "跟进类型" }
- { name: "content", type: "VARCHAR", comment: "跟进内容" }
- { name: "next_time", type: "DATETIME", comment: "下次联系时间" }
- { name: "pic_urls", type: "VARCHAR", comment: "图片URL列表" }
- { name: "file_urls", type: "VARCHAR", comment: "附件URL列表" }
- { name: "business_ids", type: "VARCHAR", comment: "关联商机ID列表" }
- { name: "contact_ids", type: "VARCHAR", comment: "关联联系人ID列表" }
# 表关系ER关系
relationships:
- from: "crm_customer"
to: "crm_business"
type: "1:N"
foreign_key: "customer_id"
description: "一个客户可以有多个商机"
- from: "crm_customer"
to: "crm_contact"
type: "1:N"
foreign_key: "customer_id"
description: "一个客户可以有多个联系人"
- from: "crm_customer"
to: "crm_contract"
type: "1:N"
foreign_key: "customer_id"
description: "一个客户可以签订多个合同"
- from: "crm_customer"
to: "crm_receivable"
type: "1:N"
foreign_key: "customer_id"
description: "一个客户可以有多个回款"
- from: "crm_business"
to: "crm_contract"
type: "1:N"
foreign_key: "business_id"
description: "一个商机可以签订多个合同"
- from: "crm_contract"
to: "crm_receivable"
type: "1:N"
foreign_key: "contract_id"
description: "一个合同可以有多个回款"
- from: "crm_contract"
to: "crm_receivable_plan"
type: "1:N"
foreign_key: "contract_id"
description: "一个合同可以有多个回款计划"
- from: "crm_receivable_plan"
to: "crm_receivable"
type: "1:1"
foreign_key: "receivable_id"
description: "一个回款计划对应一个实际回款"
- from: "crm_business_status_type"
to: "crm_business_status"
type: "1:N"
foreign_key: "type_id"
description: "一个状态组包含多个状态"
- from: "crm_product_category"
to: "crm_product"
type: "1:N"
foreign_key: "category_id"
description: "一个分类下有多个产品"
- from: "crm_clue"
to: "crm_customer"
type: "N:1"
foreign_key: "customer_id"
description: "线索转化为客户"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - CRM xxx')"
- "@RestController"
- "@RequestMapping('/crm/xxx')"
- "@Validated"
- "@PreAuthorize('@ss.hasPermission('crm:xxx:action')')"
- "@Operation(summary = 'xxx')"
- "@Parameter(name = 'id', description = '编号', required = true)"
example: |
@Tag(name = "管理后台 - CRM 客户")
@RestController
@RequestMapping("/crm/customer")
@Validated
public class CrmCustomerController {
@PostMapping("/create")
@Operation(summary = "创建客户")
@PreAuthorize("@ss.hasPermission('crm:customer:create')")
public CommonResult<Long> createCustomer(@Valid @RequestBody CrmCustomerSaveReqVO createReqVO) {
return success(customerService.createCustomer(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新客户")
@PreAuthorize("@ss.hasPermission('crm:customer:update')")
public CommonResult<Boolean> updateCustomer(@Valid @RequestBody CrmCustomerSaveReqVO updateReqVO) {
customerService.updateCustomer(updateReqVO);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得客户分页")
@PreAuthorize("@ss.hasPermission('crm:customer:query')")
public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal()));
}
}
# Service层规范
service:
interface_pattern: "接口定义业务方法,使用 @Valid 注解校验参数"
impl_pattern: |
- @Service 注解标记服务类
- @Validated 启用方法参数校验
- @Transactional(rollbackFor = Exception.class) 声明事务
- @LogRecord 记录操作日志
- @CrmPermission 声明数据权限要求
example: |
@Service
@Validated
public class CrmCustomerServiceImpl implements CrmCustomerService {
@Override
@Transactional(rollbackFor = Exception.class)
@LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_CREATE_SUB_TYPE,
bizNo = "{{#customer.id}}", success = CRM_CUSTOMER_CREATE_SUCCESS)
public Long createCustomer(CrmCustomerSaveReqVO createReqVO, Long userId) {
// 1. 校验拥有客户是否到达上限
validateCustomerExceedOwnerLimit(createReqVO.getOwnerUserId(), 1);
// 2. 插入客户
CrmCustomerDO customer = initCustomer(createReqVO, userId);
customerMapper.insert(customer);
// 3. 创建数据权限
permissionService.createPermission(new CrmPermissionCreateReqBO()
.setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
.setBizId(customer.getId())
.setUserId(userId)
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
// 4. 记录操作日志上下文
LogRecordContext.putVariable("customer", customer);
return customer.getId();
}
@Override
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id",
level = CrmPermissionLevelEnum.READ)
public CrmCustomerDO getCustomer(Long id) {
return customerMapper.selectById(id);
}
}
# 数据访问规范
dal:
mapper_pattern: |
- 继承 BaseMapperX<T> 获得通用 CRUD 方法
- 使用 @Mapper 注解标记
- 复杂查询使用 LambdaQueryWrapperX
example: |
@Mapper
public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Long userId) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
.likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName())
.eqIfPresent(CrmCustomerDO::getOwnerUserId, pageReqVO.getOwnerUserId())
.orderByDesc(CrmCustomerDO::getId));
}
}
# 数据权限控制
permission:
annotation_pattern: "@CrmPermission(bizType = CrmBizTypeEnum.XXX, bizId = '#id', level = CrmPermissionLevelEnum.XXX)"
levels:
- level: "OWNER"
code: 1
description: "负责人权限,拥有所有操作权限,可以删除、转移"
- level: "READ"
code: 2
description: "只读权限,只能查看数据"
- level: "WRITE"
code: 3
description: "读写权限,可以编辑数据但不能删除"
example: |
// 查询需要 READ 权限
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.READ)
public CrmCustomerDO getCustomer(Long id)
// 更新需要 WRITE 权限
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
public void updateCustomer(CrmCustomerSaveReqVO updateReqVO)
// 删除需要 OWNER 权限
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
public void deleteCustomer(Long id)
# 异常处理
error_handling:
code_prefix: "1-020-xxx-xxx"
examples:
- code: "1_020_006_000"
message: "客户不存在"
constant: "CUSTOMER_NOT_EXISTS"
- code: "1_020_002_000"
message: "商机不存在"
constant: "BUSINESS_NOT_EXISTS"
- code: "1_020_007_001"
message: "{}操作失败,原因:没有权限"
constant: "CRM_PERMISSION_DENIED"
usage: |
// 抛出业务异常
throw exception(CUSTOMER_NOT_EXISTS);
throw exception(CUSTOMER_OWNER_EXISTS, customer.getName());
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "CommonResult.error(errorCode)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增 CRM 业务功能"
steps:
- step: 1
action: "创建 DO 实体类"
description: "在 dal/dataobject 包下创建实体类,继承 BaseDO添加 @TableName 注解"
- step: 2
action: "创建 Mapper 接口"
description: "在 dal/mysql 包下创建 Mapper 接口,继承 BaseMapperX"
- step: 3
action: "创建 Service 接口和实现类"
description: "在 service 包下创建接口和实现类,添加 @CrmPermission 注解控制数据权限"
- step: 4
action: "创建 Controller 和 VO"
description: "在 controller 包下创建 Controller 和 VO 类,添加权限注解和 Swagger 注解"
- step: 5
action: "添加业务类型枚举"
description: "在 CrmBizTypeEnum 中添加新的业务类型,确保权限系统正确识别"
- step: 6
action: "添加错误码"
description: "在 ErrorCodeConstants 中添加相关错误码"
# 新增商机阶段示例
new_business_stage:
title: "自定义商机阶段"
steps:
- step: 1
action: "创建商机状态组"
description: "通过 CrmBusinessStatusService.createBusinessStatusType() 创建新的状态组"
- step: 2
action: "添加状态阶段"
description: "为状态组添加多个状态,设置赢单率和排序"
- step: 3
action: "关联部门"
description: "设置状态组适用的部门列表"
- step: 4
action: "使用状态组"
description: "创建商机时选择对应的状态组"
# 新增客户类型示例
new_customer_type:
title: "新增客户类型/行业"
steps:
- step: 1
action: "修改字典数据"
description: "在 CRM_CUSTOMER_INDUSTRY 字典中添加新的行业类型"
- step: 2
action: "前端适配"
description: "前端页面会自动读取字典数据显示下拉选项"
# 最佳实践
best_practices:
- practice: "数据权限校验"
description: "所有涉及数据操作的方法都必须添加 @CrmPermission 注解,确保数据安全"
- practice: "操作日志记录"
description: "关键业务操作(创建、更新、删除、转移)使用 @LogRecord 记录操作日志"
- practice: "事务管理"
description: "涉及多表操作的方法添加 @Transactional 注解,确保数据一致性"
- practice: "参数校验"
description: "VO 类使用 @Valid 注解进行参数校验Service 层使用 @Validated"
- practice: "公海池机制"
description: "客户放入公海时,需要同时更新联系人的负责人为空"
- practice: "客户转移"
description: "客户转移时,可选择同时转移联系人、商机、合同"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取用户信息、校验用户存在性"
- module: "yudao-module-system"
api: "DeptApi"
purpose: "获取部门信息,用于商机状态组关联"
- module: "yudao-module-bpm"
api: "流程审批"
purpose: "合同、回款的审批流程管理"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
version: "3.x"
purpose: "ORM 框架,提供 CRUD 操作"
- name: "Spring Security"
version: "5.x"
purpose: "权限校验"
- name: "Swagger/OpenAPI"
version: "3.x"
purpose: "API 文档生成"
- name: "EasyExcel"
version: "3.x"
purpose: "Excel 导入导出"
- name: "LogRecord"
version: "mzt-biz-log"
purpose: "操作日志记录"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java"
purpose: "客户实体类CRM 核心聚合根"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java"
purpose: "商机实体类,销售漏斗核心"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java"
purpose: "数据权限实体,权限控制核心"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java"
purpose: "客户服务接口,定义客户核心业务方法"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java"
purpose: "客户服务实现,包含公海池、转移等复杂业务逻辑"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java"
purpose: "数据权限服务接口"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java"
purpose: "客户控制器HTTP 接口入口"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java"
purpose: "业务类型枚举,权限控制的基础"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionLevelEnum.java"
purpose: "权限级别枚举"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义"
- path: "yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/annotations/CrmPermission.java"
purpose: "数据权限注解定义"

View File

@@ -0,0 +1,968 @@
# ERP 模块 Skill 文档
# 企业资源计划模块,涵盖采购、销售、库存、财务等核心业务
skill:
id: "skill-erp"
name: "ERP Skill"
version: "1.0.0"
module_path: "yudao-module-erp"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
ERP企业资源计划模块是企业管理核心系统实现进销存一体化管理。
主要业务场景:
1. 采购管理:采购订单 -> 采购入库 -> 采购退货 -> 付款
2. 销售管理:销售订单 -> 销售出库 -> 销售退货 -> 收款
3. 库存管理:其它入库/出库、库存调拨、库存盘点、库存查询
4. 财务管理:结算账户、付款单、收款单
5. 基础资料:产品、产品分类、产品单位、供应商、客户、仓库
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "单据状态机:所有业务单据采用 PROCESS(未审核) -> APPROVE(已审核) 状态流转"
- "主子表设计:订单类单据采用 Order + OrderItem 主子表结构,支持明细项增删改差量更新"
- "库存增量更新:库存变动采用 updateCountIncrement 增量更新,避免并发问题"
- "单据号生成器:基于 Redis 的分布式单据号生成,格式:前缀 + yyyyMMdd + 6位序号"
- "审核权限控制:已审核单据不可修改/删除,需反审核后才能操作"
- "业务关联校验:单据之间存在关联关系,删除/反审核时需检查关联单据"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
# 采购聚合
- name: "采购订单 (ErpPurchaseOrderDO)"
type: "聚合根"
entities:
- "采购订单项 (ErpPurchaseOrderItemDO)"
- "供应商 (ErpSupplierDO)"
description: "采购域核心聚合,管理采购订单及其明细项"
- name: "采购入库 (ErpPurchaseInDO)"
type: "聚合根"
entities:
- "采购入库项 (ErpPurchaseInItemDO)"
description: "关联采购订单,记录采购入库明细"
- name: "采购退货 (ErpPurchaseReturnDO)"
type: "聚合根"
entities:
- "采购退货项 (ErpPurchaseReturnItemDO)"
description: "关联采购订单,记录采购退货明细"
# 销售聚合
- name: "销售订单 (ErpSaleOrderDO)"
type: "聚合根"
entities:
- "销售订单项 (ErpSaleOrderItemDO)"
- "客户 (ErpCustomerDO)"
description: "销售域核心聚合,管理销售订单及其明细项"
- name: "销售出库 (ErpSaleOutDO)"
type: "聚合根"
entities:
- "销售出库项 (ErpSaleOutItemDO)"
description: "关联销售订单,记录销售出库明细"
- name: "销售退货 (ErpSaleReturnDO)"
type: "聚合根"
entities:
- "销售退货项 (ErpSaleReturnItemDO)"
description: "关联销售订单,记录销售退货明细"
# 库存聚合
- name: "库存 (ErpStockDO)"
type: "聚合根"
entities:
- "仓库 (ErpWarehouseDO)"
- "产品 (ErpProductDO)"
description: "库存域核心,记录产品在各仓库的实时库存"
- name: "库存记录 (ErpStockRecordDO)"
type: "实体"
description: "记录所有库存变动明细,支持追溯"
# 财务聚合
- name: "付款单 (ErpFinancePaymentDO)"
type: "聚合根"
entities:
- "付款单项 (ErpFinancePaymentItemDO)"
- "结算账户 (ErpAccountDO)"
description: "财务域付款管理"
- name: "收款单 (ErpFinanceReceiptDO)"
type: "聚合根"
entities:
- "收款单项 (ErpFinanceReceiptItemDO)"
- "结算账户 (ErpAccountDO)"
description: "财务域收款管理"
value_objects:
- name: "ErpStockRecordCreateReqBO"
type: "值对象"
description: "库存记录创建请求BO封装库存变动所需的业务信息"
services:
- name: "ErpPurchaseOrderService"
type: "领域服务"
description: "采购订单领域服务,管理采购订单生命周期"
- name: "ErpSaleOrderService"
type: "领域服务"
description: "销售订单领域服务,管理销售订单生命周期"
- name: "ErpStockService"
type: "领域服务"
description: "库存领域服务,管理库存变动"
- name: "ErpStockRecordService"
type: "领域服务"
description: "库存记录领域服务,记录库存变动明细"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "controller"
purpose: "HTTP接口层处理请求响应、参数校验、权限控制"
components:
- "ErpPurchaseOrderController - 采购订单接口"
- "ErpPurchaseInController - 采购入库接口"
- "ErpPurchaseReturnController - 采购退货接口"
- "ErpSaleOrderController - 销售订单接口"
- "ErpSaleOutController - 销售出库接口"
- "ErpSaleReturnController - 销售退货接口"
- "ErpStockController - 库存查询接口"
- "ErpStockInController - 其它入库接口"
- "ErpStockOutController - 其它出库接口"
- "ErpStockMoveController - 库存调拨接口"
- "ErpStockCheckController - 库存盘点接口"
- "ErpFinancePaymentController - 付款单接口"
- "ErpFinanceReceiptController - 收款单接口"
- "ErpProductController - 产品接口"
- "ErpSupplierController - 供应商接口"
- "ErpCustomerController - 客户接口"
- "ErpWarehouseController - 仓库接口"
- name: "service"
purpose: "业务逻辑层,实现领域服务、事务管理、业务规则校验"
components:
- "purchase/ - 采购域服务(订单、入库、退货、供应商)"
- "sale/ - 销售域服务(订单、出库、退货、客户)"
- "stock/ - 库存域服务(库存、仓库、入库、出库、调拨、盘点、记录)"
- "finance/ - 财务域服务(账户、付款、收款)"
- "product/ - 产品域服务(产品、分类、单位)"
- "statistics/ - 统计服务(采购统计、销售统计)"
- name: "dal"
purpose: "数据访问层封装数据库操作、Redis操作"
components:
- "dataobject/ - DO实体类按业务域分包"
- "mysql/ - MyBatis Mapper接口"
- "redis/ - Redis操作单据号生成"
# 设计模式应用
design_patterns:
- pattern: "状态机模式 (State Machine)"
location: "cn.iocoder.yudao.module.erp.enums.ErpAuditStatus"
purpose: "定义单据审核状态 PROCESS(10-未审核) -> APPROVE(20-已审核),控制单据生命周期"
- pattern: "主子表模式 (Master-Detail)"
location: "purchase/ErpPurchaseOrderDO + ErpPurchaseOrderItemDO"
purpose: "订单类单据采用主子表设计,支持明细项差量更新"
- pattern: "策略模式 (Strategy)"
location: "cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum"
purpose: "库存记录业务类型枚举,定义不同业务场景的库存变动类型"
- pattern: "分布式ID生成"
location: "cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO"
purpose: "基于Redis的分布式单据号生成保证唯一性和有序性"
- pattern: "增量更新模式"
location: "ErpStockMapper.updateCountIncrement"
purpose: "库存增量更新,使用乐观锁避免并发问题"
# 模块间通信
communication:
apis: [] # ERP模块不对外暴露API接口
consumers:
- module: "system"
api: "AdminUserApi"
purpose: "获取用户信息,用于显示操作人姓名"
mq: [] # 暂无消息队列使用
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有DO继承BaseDO包含id、creator、create_time、updater、update_time、deleted字段"
# 核心数据表
tables:
# ========== 产品相关表 ==========
- name: "erp_product"
comment: "产品表"
entity: "ErpProductDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "产品编号" }
- { name: "name", type: "VARCHAR", comment: "产品名称" }
- { name: "bar_code", type: "VARCHAR", comment: "产品条码" }
- { name: "category_id", type: "BIGINT", comment: "产品分类ID" }
- { name: "unit_id", type: "BIGINT", comment: "单位ID" }
- { name: "status", type: "INT", comment: "状态(0正常 1停用)" }
- { name: "standard", type: "VARCHAR", comment: "规格" }
- { name: "purchase_price", type: "DECIMAL", comment: "采购价" }
- { name: "sale_price", type: "DECIMAL", comment: "销售价" }
- { name: "min_price", type: "DECIMAL", comment: "最低价" }
- name: "erp_product_category"
comment: "产品分类表"
entity: "ErpProductCategoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "分类编号" }
- { name: "parent_id", type: "BIGINT", comment: "父分类ID" }
- { name: "name", type: "VARCHAR", comment: "分类名称" }
- { name: "status", type: "INT", comment: "状态" }
- name: "erp_product_unit"
comment: "产品单位表"
entity: "ErpProductUnitDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "单位编号" }
- { name: "name", type: "VARCHAR", comment: "单位名称" }
- { name: "status", type: "INT", comment: "状态" }
# ========== 采购相关表 ==========
- name: "erp_supplier"
comment: "供应商表"
entity: "ErpSupplierDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "供应商编号" }
- { name: "name", type: "VARCHAR", comment: "供应商名称" }
- { name: "contact", type: "VARCHAR", comment: "联系人" }
- { name: "mobile", type: "VARCHAR", comment: "手机号" }
- { name: "status", type: "INT", comment: "状态" }
- name: "erp_purchase_order"
comment: "采购订单表"
entity: "ErpPurchaseOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "采购订单号(CGDD+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态(10未审核 20已审核)" }
- { name: "supplier_id", type: "BIGINT", comment: "供应商ID" }
- { name: "account_id", type: "BIGINT", comment: "结算账户ID" }
- { name: "order_time", type: "DATETIME", comment: "下单时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "最终合计价格" }
- { name: "total_product_price", type: "DECIMAL", comment: "合计产品价格" }
- { name: "total_tax_price", type: "DECIMAL", comment: "合计税额" }
- { name: "discount_percent", type: "DECIMAL", comment: "优惠率(%)" }
- { name: "discount_price", type: "DECIMAL", comment: "优惠金额" }
- { name: "in_count", type: "DECIMAL", comment: "已入库数量" }
- { name: "return_count", type: "DECIMAL", comment: "已退货数量" }
- name: "erp_purchase_order_item"
comment: "采购订单项表"
entity: "ErpPurchaseOrderItemDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "order_id", type: "BIGINT", comment: "采购订单ID" }
- { name: "product_id", type: "BIGINT", comment: "产品ID" }
- { name: "product_unit_id", type: "BIGINT", comment: "产品单位ID" }
- { name: "count", type: "DECIMAL", comment: "数量" }
- { name: "product_price", type: "DECIMAL", comment: "产品单价" }
- { name: "tax_percent", type: "DECIMAL", comment: "税率(%)" }
- { name: "in_count", type: "DECIMAL", comment: "已入库数量" }
- { name: "return_count", type: "DECIMAL", comment: "已退货数量" }
- name: "erp_purchase_in"
comment: "采购入库表"
entity: "ErpPurchaseInDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "入库单号" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "supplier_id", type: "BIGINT", comment: "供应商ID" }
- { name: "order_id", type: "BIGINT", comment: "采购订单ID" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "in_time", type: "DATETIME", comment: "入库时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "合计价格" }
- { name: "payment_price", type: "DECIMAL", comment: "已付款金额" }
- name: "erp_purchase_return"
comment: "采购退货表"
entity: "ErpPurchaseReturnDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "退货单号" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "supplier_id", type: "BIGINT", comment: "供应商ID" }
- { name: "order_id", type: "BIGINT", comment: "采购订单ID" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "return_time", type: "DATETIME", comment: "退货时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "合计价格" }
- { name: "refund_price", type: "DECIMAL", comment: "已退款金额" }
# ========== 销售相关表 ==========
- name: "erp_customer"
comment: "客户表"
entity: "ErpCustomerDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "客户编号" }
- { name: "name", type: "VARCHAR", comment: "客户名称" }
- { name: "contact", type: "VARCHAR", comment: "联系人" }
- { name: "mobile", type: "VARCHAR", comment: "手机号" }
- { name: "status", type: "INT", comment: "状态" }
- name: "erp_sale_order"
comment: "销售订单表"
entity: "ErpSaleOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "销售订单号(XSDD+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态(10未审核 20已审核)" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "account_id", type: "BIGINT", comment: "结算账户ID" }
- { name: "sale_user_id", type: "BIGINT", comment: "销售员ID" }
- { name: "order_time", type: "DATETIME", comment: "下单时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "最终合计价格" }
- { name: "out_count", type: "DECIMAL", comment: "已出库数量" }
- { name: "return_count", type: "DECIMAL", comment: "已退货数量" }
- name: "erp_sale_order_item"
comment: "销售订单项表"
entity: "ErpSaleOrderItemDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "order_id", type: "BIGINT", comment: "销售订单ID" }
- { name: "product_id", type: "BIGINT", comment: "产品ID" }
- { name: "count", type: "DECIMAL", comment: "数量" }
- { name: "product_price", type: "DECIMAL", comment: "产品单价" }
- { name: "out_count", type: "DECIMAL", comment: "已出库数量" }
- { name: "return_count", type: "DECIMAL", comment: "已退货数量" }
- name: "erp_sale_out"
comment: "销售出库表"
entity: "ErpSaleOutDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "出库单号" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "order_id", type: "BIGINT", comment: "销售订单ID" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "out_time", type: "DATETIME", comment: "出库时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "合计价格" }
- { name: "receipt_price", type: "DECIMAL", comment: "已收款金额" }
- name: "erp_sale_return"
comment: "销售退货表"
entity: "ErpSaleReturnDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "退货单号" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "order_id", type: "BIGINT", comment: "销售订单ID" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "return_time", type: "DATETIME", comment: "退货时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "合计价格" }
- { name: "refund_price", type: "DECIMAL", comment: "已退款金额" }
# ========== 库存相关表 ==========
- name: "erp_warehouse"
comment: "仓库表"
entity: "ErpWarehouseDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "仓库编号" }
- { name: "name", type: "VARCHAR", comment: "仓库名称" }
- { name: "address", type: "VARCHAR", comment: "仓库地址" }
- { name: "status", type: "INT", comment: "状态" }
- name: "erp_stock"
comment: "产品库存表"
entity: "ErpStockDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "product_id", type: "BIGINT", comment: "产品ID" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "count", type: "DECIMAL", comment: "库存数量" }
- name: "erp_stock_record"
comment: "库存明细表"
entity: "ErpStockRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "product_id", type: "BIGINT", comment: "产品ID" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "count", type: "DECIMAL", comment: "出入库数量(正数入库/负数出库)" }
- { name: "total_count", type: "DECIMAL", comment: "变动后库存量" }
- { name: "biz_type", type: "INT", comment: "业务类型" }
- { name: "biz_id", type: "BIGINT", comment: "业务编号" }
- { name: "biz_item_id", type: "BIGINT", comment: "业务项编号" }
- { name: "biz_no", type: "VARCHAR", comment: "业务单号" }
- name: "erp_stock_in"
comment: "其它入库表"
entity: "ErpStockInDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "入库单号(QTRK+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "in_time", type: "DATETIME", comment: "入库时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "合计价格" }
- name: "erp_stock_out"
comment: "其它出库表"
entity: "ErpStockOutDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "出库单号(QCKD+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "out_time", type: "DATETIME", comment: "出库时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- { name: "total_price", type: "DECIMAL", comment: "合计价格" }
- name: "erp_stock_move"
comment: "库存调拨表"
entity: "ErpStockMoveDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "调拨单号(QCDB+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "from_warehouse_id", type: "BIGINT", comment: "调出仓库ID" }
- { name: "to_warehouse_id", type: "BIGINT", comment: "调入仓库ID" }
- { name: "move_time", type: "DATETIME", comment: "调拨时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
- name: "erp_stock_check"
comment: "库存盘点表"
entity: "ErpStockCheckDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "盘点单号(QCPD+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库ID" }
- { name: "check_time", type: "DATETIME", comment: "盘点时间" }
- { name: "total_count", type: "DECIMAL", comment: "合计数量" }
# ========== 财务相关表 ==========
- name: "erp_account"
comment: "结算账户表"
entity: "ErpAccountDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "账户编号" }
- { name: "name", type: "VARCHAR", comment: "账户名称" }
- { name: "no", type: "VARCHAR", comment: "账户编号" }
- { name: "status", type: "INT", comment: "状态" }
- name: "erp_finance_payment"
comment: "付款单表"
entity: "ErpFinancePaymentDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "付款单号(FKD+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "supplier_id", type: "BIGINT", comment: "供应商ID" }
- { name: "account_id", type: "BIGINT", comment: "付款账户ID" }
- { name: "payment_time", type: "DATETIME", comment: "付款时间" }
- { name: "total_price", type: "DECIMAL", comment: "合计金额" }
- { name: "discount_price", type: "DECIMAL", comment: "优惠金额" }
- { name: "payment_price", type: "DECIMAL", comment: "实付金额" }
- name: "erp_finance_receipt"
comment: "收款单表"
entity: "ErpFinanceReceiptDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "no", type: "VARCHAR", comment: "收款单号(SKD+yyyyMMdd+6位序号)" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "customer_id", type: "BIGINT", comment: "客户ID" }
- { name: "account_id", type: "BIGINT", comment: "收款账户ID" }
- { name: "receipt_time", type: "DATETIME", comment: "收款时间" }
- { name: "total_price", type: "DECIMAL", comment: "合计金额" }
- { name: "discount_price", type: "DECIMAL", comment: "优惠金额" }
- { name: "receipt_price", type: "DECIMAL", comment: "实收金额" }
# 表关系ER关系
relationships:
# 产品相关
- from: "erp_product"
to: "erp_product_category"
type: "N:1"
foreign_key: "category_id"
- from: "erp_product"
to: "erp_product_unit"
type: "N:1"
foreign_key: "unit_id"
# 采购相关
- from: "erp_purchase_order"
to: "erp_supplier"
type: "N:1"
foreign_key: "supplier_id"
- from: "erp_purchase_order_item"
to: "erp_purchase_order"
type: "N:1"
foreign_key: "order_id"
- from: "erp_purchase_in"
to: "erp_purchase_order"
type: "N:1"
foreign_key: "order_id"
- from: "erp_purchase_return"
to: "erp_purchase_order"
type: "N:1"
foreign_key: "order_id"
# 销售相关
- from: "erp_sale_order"
to: "erp_customer"
type: "N:1"
foreign_key: "customer_id"
- from: "erp_sale_order_item"
to: "erp_sale_order"
type: "N:1"
foreign_key: "order_id"
- from: "erp_sale_out"
to: "erp_sale_order"
type: "N:1"
foreign_key: "order_id"
- from: "erp_sale_return"
to: "erp_sale_order"
type: "N:1"
foreign_key: "order_id"
# 库存相关
- from: "erp_stock"
to: "erp_product"
type: "N:1"
foreign_key: "product_id"
- from: "erp_stock"
to: "erp_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
- from: "erp_stock_record"
to: "erp_product"
type: "N:1"
foreign_key: "product_id"
- from: "erp_stock_record"
to: "erp_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - ERP XXX') - Swagger接口文档标签"
- "@RestController - RESTful控制器"
- "@RequestMapping('/erp/xxx') - 请求路径前缀"
- "@PreAuthorize('@ss.hasPermission('erp:xxx:create')') - 权限控制"
- "@Validated - 参数校验"
example: |
@Tag(name = "管理后台 - ERP 采购订单")
@RestController
@RequestMapping("/erp/purchase-order")
@Validated
public class ErpPurchaseOrderController {
@PostMapping("/create")
@Operation(summary = "创建采购订单")
@PreAuthorize("@ss.hasPermission('erp:purchase-order:create')")
public CommonResult<Long> createPurchaseOrder(@Valid @RequestBody ErpPurchaseOrderSaveReqVO createReqVO) {
return success(purchaseOrderService.createPurchaseOrder(createReqVO));
}
@PutMapping("/update-status")
@Operation(summary = "更新采购订单的状态")
@PreAuthorize("@ss.hasPermission('erp:purchase-order:update-status')")
public CommonResult<Boolean> updatePurchaseOrderStatus(@RequestParam("id") Long id,
@RequestParam("status") Integer status) {
purchaseOrderService.updatePurchaseOrderStatus(id, status);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得采购订单分页")
@PreAuthorize("@ss.hasPermission('erp:purchase-order:query')")
public CommonResult<PageResult<ErpPurchaseOrderRespVO>> getPurchaseOrderPage(@Valid ErpPurchaseOrderPageReqVO pageReqVO) {
PageResult<ErpPurchaseOrderDO> pageResult = purchaseOrderService.getPurchaseOrderPage(pageReqVO);
return success(buildPurchaseOrderVOPageResult(pageResult));
}
}
# Service层规范
service:
interface_pattern: |
public interface ErpPurchaseOrderService {
// 基础CRUD
Long createPurchaseOrder(ErpPurchaseOrderSaveReqVO createReqVO);
void updatePurchaseOrder(ErpPurchaseOrderSaveReqVO updateReqVO);
void deletePurchaseOrder(List<Long> ids);
ErpPurchaseOrderDO getPurchaseOrder(Long id);
PageResult<ErpPurchaseOrderDO> getPurchaseOrderPage(ErpPurchaseOrderPageReqVO pageReqVO);
// 状态更新
void updatePurchaseOrderStatus(Long id, Integer status);
// 关联更新
void updatePurchaseOrderInCount(Long id, Map<Long, BigDecimal> inCountMap);
void updatePurchaseOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap);
// 校验方法
ErpPurchaseOrderDO validatePurchaseOrder(Long id);
// 订单项查询
List<ErpPurchaseOrderItemDO> getPurchaseOrderItemListByOrderId(Long orderId);
}
impl_pattern: |
@Service
@Validated
public class ErpPurchaseOrderServiceImpl implements ErpPurchaseOrderService {
@Resource
private ErpPurchaseOrderMapper purchaseOrderMapper;
@Resource
private ErpPurchaseOrderItemMapper purchaseOrderItemMapper;
@Resource
private ErpNoRedisDAO noRedisDAO;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createPurchaseOrder(ErpPurchaseOrderSaveReqVO createReqVO) {
// 1. 校验业务数据
List<ErpPurchaseOrderItemDO> items = validatePurchaseOrderItems(createReqVO.getItems());
supplierService.validateSupplier(createReqVO.getSupplierId());
// 2. 生成单据号
String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
// 3. 计算价格
ErpPurchaseOrderDO order = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class);
order.setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus());
calculateTotalPrice(order, items);
// 4. 保存主表和子表
purchaseOrderMapper.insert(order);
items.forEach(item -> item.setOrderId(order.getId()));
purchaseOrderItemMapper.insertBatch(items);
return order.getId();
}
}
example: |
// 采购入库审核流程
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePurchaseInStatus(Long id, Integer status) {
boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
// 1. 校验存在
ErpPurchaseInDO purchaseIn = validatePurchaseInExists(id);
// 2. 校验状态
if (purchaseIn.getStatus().equals(status)) {
throw exception(approve ? PURCHASE_IN_APPROVE_FAIL : PURCHASE_IN_PROCESS_FAIL);
}
// 3. 更新库存
if (approve) {
List<ErpPurchaseInItemDO> items = purchaseInItemMapper.selectListByInId(id);
items.forEach(item -> {
// 创建库存记录
ErpStockRecordCreateReqBO reqBO = new ErpStockRecordCreateReqBO();
reqBO.setProductId(item.getProductId());
reqBO.setWarehouseId(purchaseIn.getWarehouseId());
reqBO.setCount(item.getCount());
reqBO.setBizType(ErpStockRecordBizTypeEnum.PURCHASE_IN.getType());
reqBO.setBizId(id);
reqBO.setBizItemId(item.getId());
reqBO.setBizNo(purchaseIn.getNo());
// 更新库存
stockRecordService.createStockRecord(reqBO);
});
} else {
// 反审核,扣减库存
stockRecordService.deleteStockRecord(id, ErpStockRecordBizTypeEnum.PURCHASE_IN_CANCEL.getType());
}
// 4. 更新状态
purchaseInMapper.updateById(new ErpPurchaseInDO().setId(id).setStatus(status));
}
# 数据访问规范
dal:
mapper_pattern: |
@Mapper
public interface ErpPurchaseOrderMapper extends BaseMapperX<ErpPurchaseOrderDO> {
default ErpPurchaseOrderDO selectByNo(String no) {
return selectOne(ErpPurchaseOrderDO::getNo, no);
}
default PageResult<ErpPurchaseOrderDO> selectPage(ErpPurchaseOrderPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ErpPurchaseOrderDO>()
.likeIfPresent(ErpPurchaseOrderDO::getNo, reqVO.getNo())
.eqIfPresent(ErpPurchaseOrderDO::getStatus, reqVO.getStatus())
.eqIfPresent(ErpPurchaseOrderDO::getSupplierId, reqVO.getSupplierId())
.betweenIfPresent(ErpPurchaseOrderDO::getOrderTime, reqVO.getOrderTime())
.orderByDesc(ErpPurchaseOrderDO::getId));
}
default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseOrderDO update) {
return update(update, new LambdaQueryWrapperX<ErpPurchaseOrderDO>()
.eq(ErpPurchaseOrderDO::getId, id)
.eq(ErpPurchaseOrderDO::getStatus, status));
}
}
example: |
// 库存增量更新 - 使用乐观锁避免并发问题
@Mapper
public interface ErpStockMapper extends BaseMapperX<ErpStockDO> {
default int updateCountIncrement(Long id, BigDecimal count, Boolean negativeEnable) {
// 如果不允许负库存,需要检查库存是否充足
if (!negativeEnable) {
return update(null, new LambdaUpdateWrapper<ErpStockDO>()
.setSql("count = count + " + count)
.eq(ErpStockDO::getId, id)
.ge(count.compareTo(BigDecimal.ZERO) < 0, ErpStockDO::getCount, count.abs()));
}
return update(null, new LambdaUpdateWrapper<ErpStockDO>()
.setSql("count = count + " + count)
.eq(ErpStockDO::getId, id));
}
}
# 异常处理
error_handling:
code_prefix: "1-030-xxx-xxx"
examples:
- code: "1_030_101_000"
message: "采购订单不存在"
constant: "PURCHASE_ORDER_NOT_EXISTS"
- code: "1_030_101_001"
message: "采购订单({})已审核,无法删除"
constant: "PURCHASE_ORDER_DELETE_FAIL_APPROVE"
- code: "1_030_101_003"
message: "审核失败,只有未审核的采购订单才能审核"
constant: "PURCHASE_ORDER_APPROVE_FAIL"
- code: "1_030_404_000"
message: "操作失败,产品({})所在仓库({})的库存:{},小于变更数量:{}"
constant: "STOCK_COUNT_NEGATIVE"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE, args...)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增业务单据类型"
steps:
- step: "1. 创建DO实体类"
detail: |
在 dal/dataobject/{domain}/ 下创建主表DO和子表DO
继承 BaseDO使用 @TableName、@KeySequence 注解
定义字段并使用 @TableId 标注主键
- step: "2. 创建Mapper接口"
detail: |
在 dal/mysql/{domain}/ 下创建Mapper接口
继承 BaseMapperX<DO>,添加自定义查询方法
实现分页查询、按ID查询等方法
- step: "3. 创建Service层"
detail: |
在 service/{domain}/ 下创建Service接口和实现类
实现CRUD、状态更新、业务校验等方法
使用 @Transactional 注解保证事务
- step: "4. 创建Controller层"
detail: |
在 controller/admin/{domain}/ 下创建Controller
添加 @Tag、@RestController、@PreAuthorize 注解
实现创建、更新、删除、查询、导出等接口
- step: "5. 创建VO类"
detail: |
在 controller/admin/{domain}/vo/ 下创建:
- SaveReqVO: 创建/更新请求VO
- PageReqVO: 分页查询请求VO
- RespVO: 响应VO
- step: "6. 添加单据号前缀"
detail: |
在 ErpNoRedisDAO 中添加单据号前缀常量
格式X + X + XXCGDD-采购订单)
- step: "7. 添加错误码"
detail: |
在 ErrorCodeConstants 中添加错误码
格式1_030_xxx_xxx
# 新增渠道/类型示例
new_channel:
title: "新增库存业务类型"
steps:
- step: "1. 添加业务类型枚举"
detail: |
在 ErpStockRecordBizTypeEnum 中添加新类型:
NEW_TYPE(90, "新业务类型"),
NEW_TYPE_CANCEL(91, "新业务类型(作废)")
- step: "2. 创建业务单据"
detail: "参考采购入库/销售出库创建对应的业务单据"
- step: "3. 在业务审核时创建库存记录"
detail: |
审核通过时调用 stockRecordService.createStockRecord()
反审核时调用 stockRecordService.deleteStockRecord()
# 最佳实践
best_practices:
- practice: "单据状态管理"
description: |
所有业务单据使用 ErpAuditStatus 枚举管理状态
PROCESS(10) - 未审核/审核中
APPROVE(20) - 已审核
状态变更需要校验前置状态和关联数据
- practice: "主子表更新"
description: |
使用 diffList 方法对比新旧数据
分别处理新增、修改、删除的子表记录
批量操作使用 insertBatch、updateBatch、deleteByIds
- practice: "库存操作"
description: |
使用 updateCountIncrement 增量更新库存
通过乐观锁避免并发问题
每次库存变动都创建 stock_record 记录
- practice: "单据号生成"
description: |
使用 ErpNoRedisDAO.generate() 生成唯一单据号
格式:前缀 + yyyyMMdd + 6位自增序号
基于Redis保证分布式唯一性
- practice: "数据关联查询"
description: |
分页查询时批量查询关联数据
使用 convertMap、convertMultiMap 构建映射
避免N+1查询问题
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "system"
api: "AdminUserApi"
purpose: "获取用户信息,显示操作人姓名"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
version: "3.x"
purpose: "ORM框架简化数据库操作"
- name: "Hutool"
version: "5.x"
purpose: "Java工具类库日期、集合等工具"
- name: "Spring Redis"
version: "2.x"
purpose: "Redis操作单据号生成"
# ============================================
# 关键文件清单
# ============================================
key_files:
# 枚举和常量
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java"
purpose: "审核状态枚举,定义单据状态流转"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java"
purpose: "错误码常量,定义所有业务异常"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/stock/ErpStockRecordBizTypeEnum.java"
purpose: "库存记录业务类型枚举"
# 核心服务
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java"
purpose: "采购订单服务实现,展示标准业务流程"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockServiceImpl.java"
purpose: "库存服务实现,展示库存增量更新"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordServiceImpl.java"
purpose: "库存记录服务,管理库存变动明细"
# 数据访问
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java"
purpose: "单据号生成器分布式唯一ID"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java"
purpose: "采购订单DO展示主表设计"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockDO.java"
purpose: "库存DO展示库存表设计"
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockRecordDO.java"
purpose: "库存记录DO展示库存明细设计"
# 业务对象
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/stock/bo/ErpStockRecordCreateReqBO.java"
purpose: "库存记录创建请求BO展示值对象设计"
# 控制器示例
- path: "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java"
purpose: "采购订单控制器展示标准Controller设计"

View File

@@ -0,0 +1,748 @@
# Skill 文件 - yudao-module-im 模块
# 即时通讯模块知识提取
skill:
id: "skill-im"
name: "Instant Messaging Skill"
version: "1.0.0"
module_path: "yudao-module-im"
created_at: "2026-06-08"
updated_at: "2026-06-08"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
im 模块是系统的"即时通讯层",提供全链路即时通讯能力:
1. 好友关系管理 - 双向好友关系,支持拉黑、免打扰、置顶、备注名
2. 群组管理 - 创建/解散群组,三级角色体系(群主/管理员/普通成员),审批入群
3. 私聊消息 - 一对一聊天支持多种消息类型clientMessageId 幂等发送
4. 群聊消息 - 群组聊天,支持 @提醒、已读回执统计、消息置顶
5. 频道广播 - 单向广播频道,管理员发布素材推送给指定用户
6. RTC 实时通话 - 音视频通话,集成 LiveKit支持呼叫/接听/拒绝/取消/挂断
7. 表情系统 - 系统表情包 + 用户私人表情
8. 敏感词过滤 - 基于 Trie 树的敏感词检测和替换
定位:面向终端用户的即时通讯模块,支持单聊、群聊、频道广播及 RTC 音视频通话。
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- principle: "双层 Controller 模式"
description: "用户端 Controller无 RBAC通过登录态鉴权+ Manager 端 ControllerRBAC 权限控制),分离普通用户操作和管理后台操作"
- principle: "双向好友模型"
description: "每对好友在 im_friend 表中存储 2 行记录A->B 和 B->A各自拥有独立的 silent/pinned/blocked/displayName 属性"
- principle: "幂等消息发送"
description: "通过 clientMessageId 字段实现客户端消息去重,避免网络重试导致重复消息"
- principle: "事务感知异步推送"
description: "消息推送使用 afterCommit 回调,确保数据库事务提交后再推送,避免读到未提交数据"
- principle: "Redis 已读游标"
description: "使用 Redis 存储每个用户在每个会话中的最大已读消息 ID避免频繁更新数据库"
- principle: "LiveKit Webhook 安全"
description: "LiveKit Webhook 接口使用 JWT + SHA256 签名验证,同时标记 @PermitAll + @TenantIgnore"
- principle: "Trie 树敏感词过滤"
description: "使用 sensitive-word 库实现基于 Trie 树的高效敏感词检测和替换"
- principle: "OpenIM 兼容消息类型"
description: "消息类型编号兼容 OpenIM 协议,便于与 OpenIM 生态对接"
- principle: "配置驱动"
description: "模块配置统一在 yudao.im.* 命名空间下,通过 ImProperties 类管理"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "频道聚合"
type: "聚合根"
entities: ["ChannelDO", "ChannelMaterialDO", "ChannelMessageDO"]
description: "ChannelDO 是频道聚合根ChannelMaterialDO 是频道素材ChannelMessageDO 是频道广播消息"
- name: "表情聚合"
type: "聚合根"
entities: ["FacePackDO", "FacePackItemDO", "FaceUserItemDO"]
description: "FacePackDO 是表情包聚合根FacePackItemDO 是表情项FaceUserItemDO 是用户私人表情"
- name: "好友聚合"
type: "聚合根"
entities: ["FriendDO", "FriendRequestDO"]
description: "FriendDO 是好友关系记录(双向 2 行FriendRequestDO 是好友请求记录"
- name: "群组聚合"
type: "聚合根"
entities: ["GroupDO", "GroupMemberDO", "GroupRequestDO"]
description: "GroupDO 是群组聚合根GroupMemberDO 是群成员GroupRequestDO 是入群请求"
- name: "消息聚合"
type: "聚合根"
entities: ["PrivateMessageDO", "GroupMessageDO"]
description: "PrivateMessageDO 是私聊消息GroupMessageDO 是群聊消息"
- name: "RTC 通话聚合"
type: "聚合根"
entities: ["RtcCallDO", "RtcParticipantDO"]
description: "RtcCallDO 是通话会话聚合根RtcParticipantDO 是通话参与者"
value_objects:
- name: "ImProperties"
description: "IM 模块配置属性yudao.im.* 命名空间下的配置项"
- name: "GroupMemberRoleEnum"
description: "群成员角色枚举OWNER/ADMIN/NORMAL"
- name: "MessageTypeEnum"
description: "消息类型枚举,兼容 OpenIM 协议编号"
services:
- name: "FriendService"
description: "好友关系服务,管理好友的增删改查、拉黑/取消拉黑"
- name: "FriendRequestService"
description: "好友请求服务,管理申请、同意、拒绝流程"
- name: "GroupService"
description: "群组核心服务,管理群组生命周期"
- name: "GroupMemberService"
description: "群成员服务,管理成员的加入/退出/角色变更"
- name: "GroupRequestService"
description: "入群请求服务,管理申请和审批流程"
- name: "PrivateMessageService"
description: "私聊消息服务,管理消息的发送、拉取、已读、撤回"
- name: "GroupMessageService"
description: "群聊消息服务,管理群消息的发送、拉取、已读、撤回"
- name: "ChannelService"
description: "频道管理服务,管理频道的 CRUD 和状态"
- name: "ChannelMaterialService"
description: "素材管理服务,管理素材的创建、编辑、查询"
- name: "ChannelMessageService"
description: "频道消息服务,管理广播消息的发送和拉取"
- name: "RtcService"
description: "RTC 通话核心服务,管理通话生命周期"
- name: "LiveKitService"
description: "LiveKit 集成服务,管理房间和 Token 生成"
- name: "FacePackService"
description: "表情包服务,管理表情包的 CRUD 和状态"
- name: "FaceUserItemService"
description: "用户表情服务,管理用户的私人表情"
- name: "SensitiveWordService"
description: "敏感词服务,管理敏感词库的 CRUD"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间 API 接口,供其他模块 RPC 调用"
components: []
- name: "controller"
purpose: "HTTP 接口层,提供 RESTful API"
components:
- "admin/friend - 好友管理接口(用户端)"
- "admin/friend-request - 好友请求接口(用户端)"
- "admin/group - 群组管理接口(用户端)"
- "admin/group-member - 群成员管理接口(用户端)"
- "admin/group-request - 群组请求接口(用户端)"
- "admin/message/private - 私聊消息接口(用户端)"
- "admin/message/group - 群聊消息接口(用户端)"
- "admin/message/channel - 频道消息接口(用户端)"
- "admin/channel/material - 频道素材接口(用户端)"
- "admin/face-pack - 表情包接口(用户端)"
- "admin/face-user-item - 用户表情接口(用户端)"
- "admin/rtc - RTC 通话接口(用户端)"
- "admin/livekit - LiveKit Webhook 接口"
- "admin/manager/* - 管理后台接口RBAC 保护)"
- name: "service"
purpose: "业务逻辑层"
components:
- "friend - 好友服务FriendService, FriendRequestService"
- "group - 群组服务GroupService, GroupMemberService, GroupRequestService"
- "message - 消息服务PrivateMessageService, GroupMessageService, ChannelMessageService"
- "channel - 频道服务ChannelService, ChannelMaterialService"
- "face - 表情服务FacePackService, FacePackItemService, FaceUserItemService"
- "rtc - RTC 服务RtcService, LiveKitService"
- "sensitive - 敏感词服务SensitiveWordService"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject - DO 实体类定义"
- "mysql - MyBatis Mapper 接口"
- name: "framework"
purpose: "框架层,模块内部基础设施"
components:
- "config - 模块配置ImProperties"
- "redis - Redis 已读游标操作"
# 设计模式应用
design_patterns:
- pattern: "双层 Controller 模式 (Two-Tier Controller)"
location: "controller/admin/ + controller/admin/manager/"
purpose: "用户端 Controller 通过登录态鉴权,管理端 Controller 通过 @PreAuthorize 进行 RBAC 权限校验"
- pattern: "幂等模式 (Idempotency Pattern)"
location: "service/message/"
purpose: "通过 clientMessageId 字段和数据库唯一索引实现消息幂等发送"
- pattern: "事务感知异步推送"
location: "service/message/"
purpose: "使用 TransactionSynchronizationManager.registerSynchronization(afterCommit) 确保事务提交后再推送"
- pattern: "游标模式 (Cursor Pattern)"
location: "service/message/ + framework/redis/"
purpose: "Redis 存储每个用户在每个会话中的最大已读消息 ID支持增量拉取"
- pattern: "安全验证模式"
location: "controller/admin/livekit/"
purpose: "LiveKit Webhook 使用 JWT + SHA256 签名验证,@PermitAll + @TenantIgnore 组合"
- pattern: "Trie 树过滤模式"
location: "service/sensitive/"
purpose: "基于 sensitive-word 库的 Trie 树实现高效敏感词检测"
# 模块间通信
communication:
apis:
- name: "AdminUserApi"
method: "getUser()"
description: "查询用户基本信息,用于好友信息、群成员信息展示"
- name: "AdminUserApi"
method: "getUsers()"
description: "批量查询用户信息,用于消息列表中展示发送者信息"
- name: "FileApi"
method: "createFile()"
description: "上传文件(头像、图片消息、文件消息)"
consumers:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "查询用户信息(好友、群成员、消息发送者)"
- module: "yudao-module-infra"
api: "FileApi"
purpose: "上传头像、图片消息、文件消息"
mq: []
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有 DO 继承 BaseDO包含 creator, createTime, updater, updateTime, deleted 字段。"
# 核心数据表
tables:
# ========== 频道相关 ==========
- name: "im_channel"
comment: "频道表"
entity: "ChannelDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "频道编号,主键" }
- { name: "code", type: "VARCHAR", comment: "频道编码,唯一" }
- { name: "name", type: "VARCHAR", comment: "频道名称" }
- { name: "avatar", type: "VARCHAR", comment: "频道头像 URL" }
- { name: "sort", type: "INT", comment: "排序值" }
- { name: "status", type: "TINYINT", comment: "频道状态(启用/禁用)" }
indexes:
- { name: "uk_code", columns: ["code"], unique: true }
- name: "im_channel_material"
comment: "频道素材表"
entity: "ChannelMaterialDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "素材编号,主键" }
- { name: "title", type: "VARCHAR", comment: "素材标题" }
- { name: "cover_url", type: "VARCHAR", comment: "封面图片 URL" }
- { name: "summary", type: "VARCHAR", comment: "内容摘要" }
- { name: "content", type: "TEXT", comment: "内容 HTML" }
- { name: "url", type: "VARCHAR", comment: "原文链接" }
- name: "im_channel_message"
comment: "频道消息表"
entity: "ChannelMessageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "消息编号,主键" }
- { name: "channel_id", type: "BIGINT", comment: "频道编号" }
- { name: "material_id", type: "BIGINT", comment: "关联素材编号" }
- { name: "receiver_user_ids", type: "VARCHAR", comment: "接收用户 ID 列表JSON 数组)" }
- { name: "send_time", type: "DATETIME", comment: "发送时间" }
indexes:
- { name: "idx_channel_id", columns: ["channel_id"] }
# ========== 表情相关 ==========
- name: "im_face_pack"
comment: "表情包表"
entity: "FacePackDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "表情包编号,主键" }
- { name: "name", type: "VARCHAR", comment: "表情包名称" }
- { name: "icon", type: "VARCHAR", comment: "表情包图标 URL" }
- { name: "sort", type: "INT", comment: "排序值" }
- { name: "status", type: "TINYINT", comment: "启用状态" }
- name: "im_face_pack_item"
comment: "表情项表"
entity: "FacePackItemDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "表情项编号,主键" }
- { name: "pack_id", type: "BIGINT", comment: "所属表情包编号" }
- { name: "url", type: "VARCHAR", comment: "表情图片 URL" }
- { name: "name", type: "VARCHAR", comment: "表情名称" }
- { name: "width", type: "INT", comment: "图片宽度" }
- { name: "height", type: "INT", comment: "图片高度" }
indexes:
- { name: "idx_pack_id", columns: ["pack_id"] }
- name: "im_face_user_item"
comment: "用户表情表"
entity: "FaceUserItemDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "表情编号,主键" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "url", type: "VARCHAR", comment: "表情图片 URL" }
- { name: "name", type: "VARCHAR", comment: "表情名称" }
- { name: "width", type: "INT", comment: "图片宽度" }
- { name: "height", type: "INT", comment: "图片高度" }
indexes:
- { name: "idx_user_id", columns: ["user_id"] }
# ========== 好友相关 ==========
- name: "im_friend"
comment: "好友关系表"
entity: "FriendDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "关系编号,主键" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "friend_id", type: "BIGINT", comment: "好友用户编号" }
- { name: "silent", type: "BIT", comment: "是否免打扰" }
- { name: "pinned", type: "BIT", comment: "是否置顶" }
- { name: "blocked", type: "BIT", comment: "是否拉黑" }
- { name: "display_name", type: "VARCHAR", comment: "好友备注名" }
indexes:
- { name: "uk_user_friend", columns: ["user_id", "friend_id"], unique: true }
- name: "im_friend_request"
comment: "好友请求表"
entity: "FriendRequestDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "请求编号,主键" }
- { name: "user_id", type: "BIGINT", comment: "申请人用户编号" }
- { name: "friend_id", type: "BIGINT", comment: "目标用户编号" }
- { name: "apply_content", type: "VARCHAR", comment: "申请附言" }
- { name: "display_name", type: "VARCHAR", comment: "对好友的备注名" }
- { name: "add_source", type: "TINYINT", comment: "添加来源" }
- { name: "handle_result", type: "TINYINT", comment: "处理结果" }
- { name: "handle_time", type: "DATETIME", comment: "处理时间" }
- { name: "handle_user_id", type: "BIGINT", comment: "处理人用户编号" }
indexes:
- { name: "idx_user_id", columns: ["user_id"] }
- { name: "idx_friend_id", columns: ["friend_id"] }
# ========== 群组相关 ==========
- name: "im_group"
comment: "群组表"
entity: "GroupDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "群组编号,主键" }
- { name: "name", type: "VARCHAR", comment: "群组名称" }
- { name: "owner_id", type: "BIGINT", comment: "群主用户编号" }
- { name: "avatar", type: "VARCHAR", comment: "群组头像 URL" }
- { name: "notice", type: "VARCHAR", comment: "群公告" }
- { name: "join_approval", type: "BIT", comment: "是否开启入群审批" }
- { name: "banned", type: "BIT", comment: "是否被封禁" }
- { name: "muted_all", type: "BIT", comment: "是否全员禁言" }
- { name: "pinned_message_ids", type: "VARCHAR", comment: "置顶消息 ID 列表JSON 数组)" }
indexes:
- { name: "idx_owner_id", columns: ["owner_id"] }
- name: "im_group_member"
comment: "群成员表"
entity: "GroupMemberDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "成员编号,主键" }
- { name: "group_id", type: "BIGINT", comment: "群组编号" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "role", type: "TINYINT", comment: "角色OWNER/ADMIN/NORMAL" }
- { name: "nickname", type: "VARCHAR", comment: "群内昵称" }
- { name: "mute_end_time", type: "DATETIME", comment: "禁言结束时间" }
- { name: "add_source", type: "TINYINT", comment: "加入来源" }
- { name: "inviter", type: "BIGINT", comment: "邀请人用户编号" }
indexes:
- { name: "uk_group_user", columns: ["group_id", "user_id"], unique: true }
- name: "im_group_request"
comment: "群组请求表"
entity: "GroupRequestDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "请求编号,主键" }
- { name: "group_id", type: "BIGINT", comment: "群组编号" }
- { name: "user_id", type: "BIGINT", comment: "申请人/被邀请人用户编号" }
- { name: "type", type: "TINYINT", comment: "请求类型(用户申请/成员邀请)" }
- { name: "apply_content", type: "VARCHAR", comment: "申请附言" }
- { name: "handle_result", type: "TINYINT", comment: "处理结果" }
- { name: "handle_time", type: "DATETIME", comment: "处理时间" }
- { name: "handle_user_id", type: "BIGINT", comment: "处理人用户编号" }
indexes:
- { name: "idx_group_id", columns: ["group_id"] }
- { name: "idx_user_id", columns: ["user_id"] }
# ========== 消息相关 ==========
- name: "im_private_message"
comment: "私聊消息表"
entity: "PrivateMessageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "消息编号,主键" }
- { name: "client_message_id", type: "VARCHAR", comment: "客户端消息 ID幂等键" }
- { name: "sender_id", type: "BIGINT", comment: "发送者用户编号" }
- { name: "receiver_id", type: "BIGINT", comment: "接收者用户编号" }
- { name: "type", type: "TINYINT", comment: "消息类型" }
- { name: "content", type: "VARCHAR", comment: "消息内容JSON 格式)" }
- { name: "status", type: "TINYINT", comment: "消息状态(正常/已撤回)" }
indexes:
- { name: "uk_client_message_id", columns: ["client_message_id"], unique: true }
- { name: "idx_sender_id", columns: ["sender_id"] }
- { name: "idx_receiver_id", columns: ["receiver_id"] }
- name: "im_group_message"
comment: "群聊消息表"
entity: "GroupMessageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "消息编号,主键" }
- { name: "client_message_id", type: "VARCHAR", comment: "客户端消息 ID幂等键" }
- { name: "sender_id", type: "BIGINT", comment: "发送者用户编号" }
- { name: "group_id", type: "BIGINT", comment: "群组编号" }
- { name: "type", type: "TINYINT", comment: "消息类型" }
- { name: "content", type: "VARCHAR", comment: "消息内容JSON 格式)" }
- { name: "at_user_ids", type: "VARCHAR", comment: "@的用户 ID 列表JSON 数组)" }
- { name: "status", type: "TINYINT", comment: "消息状态(正常/已撤回)" }
- { name: "receipt_status", type: "TINYINT", comment: "已读回执状态" }
- { name: "read_count", type: "INT", comment: "已读人数" }
indexes:
- { name: "uk_client_message_id", columns: ["client_message_id"], unique: true }
- { name: "idx_group_id", columns: ["group_id"] }
- { name: "idx_sender_id", columns: ["sender_id"] }
# ========== RTC 相关 ==========
- name: "im_rtc_call"
comment: "RTC 通话表"
entity: "RtcCallDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "通话编号,主键" }
- { name: "room_id", type: "VARCHAR", comment: "LiveKit 房间 UUID" }
- { name: "conversation_type", type: "TINYINT", comment: "会话类型(私聊/群聊)" }
- { name: "media_type", type: "TINYINT", comment: "媒体类型(音频/视频)" }
- { name: "inviter_uid", type: "BIGINT", comment: "发起者用户编号" }
- { name: "status", type: "TINYINT", comment: "通话状态" }
- { name: "start_time", type: "DATETIME", comment: "开始时间" }
- { name: "accept_time", type: "DATETIME", comment: "接听时间" }
- { name: "end_time", type: "DATETIME", comment: "结束时间" }
indexes:
- { name: "uk_room_id", columns: ["room_id"], unique: true }
- { name: "idx_inviter_uid", columns: ["inviter_uid"] }
- name: "im_rtc_participant"
comment: "RTC 参与者表"
entity: "RtcParticipantDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "参与者编号,主键" }
- { name: "call_id", type: "BIGINT", comment: "通话编号" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "role", type: "TINYINT", comment: "角色(发起者/参与者)" }
- { name: "status", type: "TINYINT", comment: "状态INVITING/JOINED/LEFT/REJECTED/NO_ANSWER" }
- { name: "join_time", type: "DATETIME", comment: "加入时间" }
- { name: "leave_time", type: "DATETIME", comment: "离开时间" }
indexes:
- { name: "idx_call_id", columns: ["call_id"] }
- { name: "idx_user_id", columns: ["user_id"] }
# ========== 敏感词相关 ==========
- name: "im_sensitive_word"
comment: "敏感词表"
entity: "SensitiveWordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "敏感词编号,主键" }
- { name: "word", type: "VARCHAR", comment: "敏感词内容" }
- { name: "status", type: "TINYINT", comment: "启用状态" }
indexes:
- { name: "uk_word", columns: ["word"], unique: true }
# 表关系ER关系
relationships:
- from: "im_channel_message"
to: "im_channel"
type: "N:1"
foreign_key: "channel_id"
- from: "im_channel_message"
to: "im_channel_material"
type: "N:1"
foreign_key: "material_id"
- from: "im_face_pack_item"
to: "im_face_pack"
type: "N:1"
foreign_key: "pack_id"
- from: "im_group_member"
to: "im_group"
type: "N:1"
foreign_key: "group_id"
- from: "im_group_request"
to: "im_group"
type: "N:1"
foreign_key: "group_id"
- from: "im_group_message"
to: "im_group"
type: "N:1"
foreign_key: "group_id"
- from: "im_rtc_participant"
to: "im_rtc_call"
type: "N:1"
foreign_key: "call_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = 'xxx') - Swagger 文档标签"
- "@RestController - REST 控制器"
- "@RequestMapping('/im/xxx') - 请求路径前缀"
- "@Validated - 参数校验"
- "@PreAuthorize('@ss.hasPermission('im:xxx:action')') - 权限控制(仅管理端)"
example: |
@Tag(name = "用户端 - 好友管理")
@RestController
@RequestMapping("/im/friend")
@Validated
public class ImFriendController {
@Resource
private FriendService friendService;
@GetMapping("/list")
@Operation(summary = "获取好友列表")
public CommonResult<List<FriendDO>> getFriendList() {
return success(friendService.getFriendList(LoginContextHolder.getLoginUserId()));
}
}
# Service层规范
service:
interface_pattern: "XxxService 接口定义业务方法XxxServiceImpl 实现类"
impl_pattern: |
@Service
@Validated
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
// 使用 static final 定义常量
// 使用 @Transactional 注解控制事务
// 使用 validateXxxExists 方法校验存在性
// 使用 exception(ErrorCode) 抛出业务异常
}
example: |
@Service
public class PrivateMessageServiceImpl implements PrivateMessageService {
@Resource
private ImPrivateMessageMapper privateMessageMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public PrivateMessageDO sendPrivateMessage(PrivateMessageSendReqDTO reqDTO) {
// 1. 校验接收者存在
// 2. 检查 clientMessageId 幂等
// 3. 敏感词过滤
// 4. 保存消息
PrivateMessageDO message = ...;
privateMessageMapper.insert(message);
// 5. 事务提交后异步推送
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 推送消息给接收者
}
});
return message;
}
}
# 数据访问规范
dal:
mapper_pattern: |
public interface XxxMapper extends BaseMapperX<XxxDO> {
// 继承 BaseMapperX 获得通用 CRUD 方法
// 自定义查询方法使用 @Select 注解或 XML
// 分页查询返回 PageResult<XxxDO>
}
example: |
public interface ImPrivateMessageMapper extends BaseMapperX<PrivateMessageDO> {
default PageResult<PrivateMessageDO> selectPage(PrivateMessagePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PrivateMessageDO>()
.eqIfPresent(PrivateMessageDO::getSenderId, reqVO.getSenderId())
.eqIfPresent(PrivateMessageDO::getReceiverId, reqVO.getReceiverId())
.betweenIfPresent(PrivateMessageDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PrivateMessageDO::getId));
}
}
# 异常处理
error_handling:
code_prefix: "1-009-xxx-xxx"
examples:
- code: "好友已存在"
- code: "不是好友关系"
- code: "群组不存在"
- code: "不是群成员"
- code: "已被禁言"
- code: "消息已撤回"
- code: "撤回超时"
- code: "通话不存在"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ErrorCode)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增消息类型
new_channel:
title: "新增消息类型"
steps:
- step: "1. 定义消息类型枚举"
description: "在 MessageTypeEnum 中添加新的消息类型编号,兼容 OpenIM 协议"
- step: "2. 定义消息内容 DTO"
description: "创建 XxxContentDTO 类定义该消息类型的 content JSON 结构"
code: |
@Data
public class VideoContentDTO {
private String url;
private Integer duration;
private Integer width;
private Integer height;
private String thumbUrl;
}
- step: "3. 实现消息渲染"
description: "在客户端实现该消息类型的渲染逻辑"
- step: "4. 敏感词处理"
description: "如果消息类型包含文本内容,需要在发送时进行敏感词过滤"
# 新增管理接口
new_feature:
title: "新增管理后台接口"
steps:
- step: "1. 创建 Manager Controller"
description: "在 controller/admin/manager/ 下创建新的 Controller 类"
code: |
@Tag(name = "管理后台 - XXX 管理")
@RestController
@RequestMapping("/im/manager/xxx")
@Validated
@PreAuthorize("@ss.hasPermission('im:xxx:query')")
public class ImXxxManagerController {
// 注入 Service
// 定义 CRUD 接口
}
- step: "2. 定义权限标识"
description: "在权限系统中注册 im:xxx:query/create/update/delete 权限"
- step: "3. 实现 Service 方法"
description: "在对应的 Service 中实现管理端的业务逻辑"
- step: "4. 添加数据权限"
description: "根据需要添加数据权限控制"
# 最佳实践
best_practices:
- practice: "clientMessageId 幂等发送"
description: "客户端生成 UUID 作为 clientMessageId服务端通过唯一索引保证幂等避免网络重试导致重复消息"
- practice: "事务感知异步推送"
description: "使用 afterCommit 回调确保事务提交后再推送消息,避免接收方读到未提交数据"
- practice: "Redis 已读游标"
description: "使用 Redis 存储已读游标,避免频繁更新数据库,提高已读状态查询性能"
- practice: "双向好友独立属性"
description: "每对好友的 2 行记录拥有独立属性,更新时只修改当前用户方向的记录"
- practice: "群组角色权限校验"
description: "通过 GroupMemberDO.role 字段判断操作权限,注意权限边界(管理员不可踢管理员)"
- practice: "LiveKit Webhook 安全"
description: "使用 JWT + SHA256 签名验证 Webhook 请求,不要在未验证签名的情况下处理事件"
- practice: "敏感词过滤时机"
description: "敏感词过滤在消息发送时执行,过滤后直接存储,已发送消息不受后续词库更新影响"
- practice: "消息增量拉取"
description: "使用 maxMessageId 作为游标进行增量拉取,避免使用 offset 分页的性能问题"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
purpose: "AdminUserApi - 查询用户信息、校验用户存在性"
- module: "yudao-module-infra"
purpose: "文件存储(头像、图片、文件消息等)"
- module: "yudao-framework-common"
purpose: "通用工具类、CommonResult、PageResult 等"
- module: "yudao-framework-mybatis"
purpose: "MyBatis-Plus 封装、BaseMapperX、BaseDO"
- module: "yudao-framework-redis"
purpose: "Redis 操作封装,用于已读游标存储"
- module: "yudao-framework-tenant"
purpose: "租户支持"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
purpose: "ORM 框架,提供 CRUD 封装"
- name: "sensitive-word"
purpose: "基于 Trie 树的敏感词过滤库"
- name: "pinyin4j"
purpose: "汉字转拼音,用于敏感词拼音匹配"
- name: "LiveKit"
purpose: "开源实时音视频通信框架RTC 能力支撑"
- name: "Swagger/OpenAPI"
purpose: "API 文档注解"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java"
purpose: "好友管理用户端 Controller"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java"
purpose: "群组管理用户端 Controller"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java"
purpose: "私聊消息用户端 Controller"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java"
purpose: "群聊消息用户端 Controller"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/rtc/ImRtcController.java"
purpose: "RTC 通话用户端 Controller"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/livekit/ImLivekitController.java"
purpose: "LiveKit Webhook Controller"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/FriendService.java"
purpose: "好友关系服务接口"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/GroupService.java"
purpose: "群组核心服务接口"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/PrivateMessageService.java"
purpose: "私聊消息服务接口"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/GroupMessageService.java"
purpose: "群聊消息服务接口"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/RtcService.java"
purpose: "RTC 通话服务接口"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitive/SensitiveWordService.java"
purpose: "敏感词服务接口"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/GroupDO.java"
purpose: "群组实体类"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/PrivateMessageDO.java"
purpose: "私聊消息实体类"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/rtc/RtcCallDO.java"
purpose: "RTC 通话实体类"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义"
- path: "yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/config/ImProperties.java"
purpose: "IM 模块配置属性类"

View File

@@ -0,0 +1,662 @@
# Skill 文件 - yudao-module-infra 模块
# 基础设施模块知识提取
skill:
id: "skill-infra"
name: "Infrastructure Skill"
version: "1.0.0"
module_path: "yudao-module-infra"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
infra 模块是整个系统的"基础设施层",提供跨业务模块的通用能力支撑:
1. 文件存储服务 - 统一的文件上传、下载、管理能力,支持多存储渠道(本地/S3/OSS/FTP等
2. 代码生成器 - 基于数据库表结构自动生成 CRUD 代码,提升开发效率
3. 定时任务管理 - 基于 Quartz 的任务调度管理,支持动态配置和监控
4. API 日志审计 - 记录 API 访问日志和异常日志,支持问题追踪和分析
5. 系统配置管理 - 键值对形式的系统参数配置
6. 数据源管理 - 多数据源配置和管理,支持代码生成器连接不同数据库
定位:作为底层基础设施模块,不直接面向终端用户业务,而是为其他业务模块提供技术能力支撑。
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- principle: "策略模式 + 工厂模式"
description: "文件存储采用策略模式,通过 FileClientFactory 创建不同的 FileClient 实现支持本地、S3、FTP、SFTP、数据库等多种存储方式"
- principle: "模板方法模式"
description: "AbstractFileClient 抽象类定义文件客户端的通用流程,子类实现具体的初始化和操作逻辑"
- principle: "多态配置"
description: "FileClientConfig 使用 Jackson @JsonTypeInfo 实现多态序列化,配置类与存储类型一一对应"
- principle: "Builder 模式"
description: "CodegenBuilder 负责构建代码生成的表和列定义对象CodegenEngine 负责执行代码生成"
- principle: "模板引擎"
description: "代码生成使用 Velocity 模板引擎支持多前端框架Vue2/Vue3/Vben/UniApp"
- principle: "缓存优化"
description: "FileClient 使用 Guava LoadingCache 实现10秒异步刷新避免频繁查询数据库"
- principle: "租户隔离忽略"
description: "infra 模块大部分表使用 @TenantIgnore 注解,基础设施不参与租户隔离"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "文件存储聚合"
type: "聚合根"
entities: ["FileDO", "FileConfigDO", "FileContentDO"]
description: "FileConfigDO 是配置聚合根FileDO 是文件记录FileContentDO 是数据库存储的文件内容"
- name: "代码生成聚合"
type: "聚合根"
entities: ["CodegenTableDO", "CodegenColumnDO"]
description: "CodegenTableDO 是表定义聚合根CodegenColumnDO 是列定义,两者是一对多关系"
- name: "定时任务聚合"
type: "聚合根"
entities: ["JobDO", "JobLogDO"]
description: "JobDO 是任务定义聚合根JobLogDO 是执行日志,两者是一对多关系"
- name: "API日志聚合"
type: "聚合根"
entities: ["ApiAccessLogDO", "ApiErrorLogDO"]
description: "独立的日志实体,无关联关系"
value_objects:
- name: "FileClientConfig"
description: "文件客户端配置接口有多种实现LocalFileClientConfig, S3FileClientConfig 等)"
- name: "FileStorageEnum"
description: "文件存储类型枚举,定义存储类型与配置类、客户端类的映射关系"
services:
- name: "FileService"
description: "文件管理服务,提供文件上传、下载、删除等功能"
- name: "FileConfigService"
description: "文件配置服务,管理存储渠道配置"
- name: "CodegenService"
description: "代码生成服务,提供表导入、代码生成等功能"
- name: "JobService"
description: "定时任务服务,与 Quartz 集成管理任务调度"
- name: "ConfigService"
description: "系统配置服务,管理键值对配置"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间 API 接口,供其他模块 RPC 调用"
components:
- "FileApi - 文件上传接口"
- "ConfigApi - 配置获取接口"
- "WebSocketSenderApi - WebSocket 消息发送接口"
- name: "controller"
purpose: "HTTP 接口层,提供 RESTful API"
components:
- "admin/codegen - 代码生成器接口"
- "admin/file - 文件管理接口"
- "admin/job - 定时任务管理接口"
- "admin/logger - API 日志管理接口"
- "admin/config - 系统配置接口"
- "admin/db - 数据源管理接口"
- "admin/redis - Redis 监控接口"
- "app/file - App 端文件上传接口"
- "admin/demo - 代码生成示例Demo01/Demo02/Demo03"
- name: "service"
purpose: "业务逻辑层"
components:
- "codegen - 代码生成服务CodegenService, CodegenBuilder, CodegenEngine"
- "file - 文件服务FileService, FileConfigService"
- "job - 定时任务服务JobService, JobLogService"
- "logger - 日志服务ApiAccessLogService, ApiErrorLogService"
- "config - 配置服务ConfigService"
- "db - 数据源服务DataSourceConfigService, DatabaseTableService"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject - DO 实体类定义"
- "mysql - MyBatis Mapper 接口"
- name: "framework"
purpose: "框架层,模块内部基础设施"
components:
- "file/core/client - 文件客户端抽象和实现"
- "file/core/enums - 文件存储枚举"
- "codegen/config - 代码生成配置"
# 设计模式应用
design_patterns:
- pattern: "策略模式 (Strategy Pattern)"
location: "framework/file/core/client/"
purpose: "FileClient 接口定义统一操作不同存储实现LocalFileClient, S3FileClient, FtpFileClient 等)"
- pattern: "工厂模式 (Factory Pattern)"
location: "framework/file/core/client/FileClientFactoryImpl.java"
purpose: "根据存储类型动态创建对应的 FileClient 实例"
- pattern: "模板方法模式 (Template Method Pattern)"
location: "framework/file/core/client/AbstractFileClient.java"
purpose: "定义文件客户端的初始化和刷新模板流程,子类实现 doInit()"
- pattern: "Builder 模式"
location: "service/codegen/inner/CodegenBuilder.java"
purpose: "构建 CodegenTableDO 和 CodegenColumnDO 对象"
- pattern: "模板引擎模式"
location: "service/codegen/inner/CodegenEngine.java"
purpose: "使用 Velocity 模板生成代码,支持多前端框架"
- pattern: "缓存模式"
location: "service/file/FileConfigServiceImpl.java"
purpose: "使用 Guava LoadingCache 缓存 FileClient10秒异步刷新"
# 模块间通信
communication:
apis:
- name: "FileApi"
method: "createFile()"
description: "其他模块调用上传文件,返回文件 URL"
- name: "FileApi"
method: "presignGetUrl()"
description: "获取文件预签名访问地址"
- name: "ConfigApi"
method: "getConfigValueByKey()"
description: "其他模块获取系统配置值"
- name: "WebSocketSenderApi"
method: "send()"
description: "发送 WebSocket 消息"
consumers:
- module: "yudao-module-system"
api: "FileApi"
purpose: "用户头像上传"
- module: "yudao-module-member"
api: "FileApi"
purpose: "会员头像上传"
- module: "所有模块"
api: "ConfigApi"
purpose: "获取系统配置"
mq: []
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有 DO 继承 BaseDO包含 creator, createTime, updater, updateTime, deleted 字段。大部分表使用 @TenantIgnore 忽略租户隔离。"
# 核心数据表
tables:
# ========== 文件存储相关 ==========
- name: "infra_file_config"
comment: "文件配置表"
entity: "FileConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "配置编号,主键" }
- { name: "name", type: "VARCHAR", comment: "配置名称" }
- { name: "storage", type: "TINYINT", comment: "存储器类型,枚举 FileStorageEnum" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- { name: "master", type: "BIT", comment: "是否主配置" }
- { name: "config", type: "JSON", comment: "存储配置JSON 格式" }
indexes:
- { name: "idx_master", columns: ["master"] }
- name: "infra_file"
comment: "文件表"
entity: "FileDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "文件编号,主键" }
- { name: "config_id", type: "BIGINT", comment: "配置编号,关联 FileConfigDO" }
- { name: "name", type: "VARCHAR", comment: "原文件名" }
- { name: "path", type: "VARCHAR", comment: "文件路径" }
- { name: "url", type: "VARCHAR", comment: "访问地址" }
- { name: "type", type: "VARCHAR", comment: "MIME 类型" }
- { name: "size", type: "BIGINT", comment: "文件大小(字节)" }
indexes:
- { name: "idx_config_id", columns: ["config_id"] }
- name: "infra_file_content"
comment: "文件内容表(数据库存储方式)"
entity: "FileContentDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号,主键" }
- { name: "config_id", type: "BIGINT", comment: "配置编号" }
- { name: "path", type: "VARCHAR", comment: "文件路径" }
- { name: "content", type: "LONGBLOB", comment: "文件内容" }
# ========== 代码生成相关 ==========
- name: "infra_codegen_table"
comment: "代码生成表定义"
entity: "CodegenTableDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号,主键" }
- { name: "data_source_config_id", type: "BIGINT", comment: "数据源编号" }
- { name: "scene", type: "TINYINT", comment: "生成场景" }
- { name: "table_name", type: "VARCHAR", comment: "表名称" }
- { name: "table_comment", type: "VARCHAR", comment: "表描述" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- { name: "module_name", type: "VARCHAR", comment: "模块名" }
- { name: "business_name", type: "VARCHAR", comment: "业务名" }
- { name: "class_name", type: "VARCHAR", comment: "类名称" }
- { name: "class_comment", type: "VARCHAR", comment: "类描述" }
- { name: "author", type: "VARCHAR", comment: "作者" }
- { name: "template_type", type: "TINYINT", comment: "模板类型" }
- { name: "front_type", type: "TINYINT", comment: "前端类型" }
- { name: "parent_menu_id", type: "BIGINT", comment: "父菜单编号" }
- { name: "master_table_id", type: "BIGINT", comment: "主表编号(子表时使用)" }
- { name: "sub_join_column_id", type: "BIGINT", comment: "子表关联字段编号" }
- { name: "sub_join_many", type: "BIT", comment: "是否一对多" }
- { name: "tree_parent_column_id", type: "BIGINT", comment: "树表父字段编号" }
- { name: "tree_name_column_id", type: "BIGINT", comment: "树表名称字段编号" }
- name: "infra_codegen_column"
comment: "代码生成列定义"
entity: "CodegenColumnDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号,主键" }
- { name: "table_id", type: "BIGINT", comment: "表编号,关联 CodegenTableDO" }
- { name: "column_name", type: "VARCHAR", comment: "字段名" }
- { name: "data_type", type: "VARCHAR", comment: "数据库类型" }
- { name: "column_comment", type: "VARCHAR", comment: "字段描述" }
- { name: "nullable", type: "BIT", comment: "是否允许空" }
- { name: "primary_key", type: "BIT", comment: "是否主键" }
- { name: "ordinal_position", type: "INT", comment: "排序" }
- { name: "java_type", type: "VARCHAR", comment: "Java 类型" }
- { name: "java_field", type: "VARCHAR", comment: "Java 属性名" }
- { name: "dict_type", type: "VARCHAR", comment: "字典类型" }
- { name: "example", type: "VARCHAR", comment: "数据示例" }
- { name: "create_operation", type: "BIT", comment: "是否 Create 操作字段" }
- { name: "update_operation", type: "BIT", comment: "是否 Update 操作字段" }
- { name: "list_operation", type: "BIT", comment: "是否 List 查询字段" }
- { name: "list_operation_condition", type: "VARCHAR", comment: "List 查询条件" }
- { name: "list_operation_result", type: "BIT", comment: "是否 List 返回字段" }
- { name: "html_type", type: "VARCHAR", comment: "显示类型" }
# ========== 定时任务相关 ==========
- name: "infra_job"
comment: "定时任务表"
entity: "JobDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "任务编号,主键" }
- { name: "name", type: "VARCHAR", comment: "任务名称" }
- { name: "status", type: "TINYINT", comment: "任务状态" }
- { name: "handler_name", type: "VARCHAR", comment: "处理器名称" }
- { name: "handler_param", type: "VARCHAR", comment: "处理器参数" }
- { name: "cron_expression", type: "VARCHAR", comment: "CRON 表达式" }
- { name: "retry_count", type: "INT", comment: "重试次数" }
- { name: "retry_interval", type: "INT", comment: "重试间隔(毫秒)" }
- { name: "monitor_timeout", type: "INT", comment: "监控超时时间(毫秒)" }
- name: "infra_job_log"
comment: "定时任务日志表"
entity: "JobLogDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "日志编号,主键" }
- { name: "job_id", type: "BIGINT", comment: "任务编号,关联 JobDO" }
- { name: "handler_name", type: "VARCHAR", comment: "处理器名称" }
- { name: "handler_param", type: "VARCHAR", comment: "处理器参数" }
- { name: "execute_index", type: "INT", comment: "第几次执行" }
- { name: "begin_time", type: "DATETIME", comment: "开始时间" }
- { name: "end_time", type: "DATETIME", comment: "结束时间" }
- { name: "duration", type: "INT", comment: "执行时长(毫秒)" }
- { name: "status", type: "TINYINT", comment: "执行状态" }
- { name: "result", type: "VARCHAR", comment: "执行结果" }
# ========== API 日志相关 ==========
- name: "infra_api_access_log"
comment: "API 访问日志表"
entity: "ApiAccessLogDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号,主键" }
- { name: "trace_id", type: "VARCHAR", comment: "链路追踪编号" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "user_type", type: "TINYINT", comment: "用户类型" }
- { name: "application_name", type: "VARCHAR", comment: "应用名" }
- { name: "request_method", type: "VARCHAR", comment: "请求方法" }
- { name: "request_url", type: "VARCHAR", comment: "请求地址" }
- { name: "request_params", type: "VARCHAR", comment: "请求参数" }
- { name: "response_body", type: "VARCHAR", comment: "响应结果" }
- { name: "user_ip", type: "VARCHAR", comment: "用户 IP" }
- { name: "user_agent", type: "VARCHAR", comment: "浏览器 UA" }
- { name: "operate_module", type: "VARCHAR", comment: "操作模块" }
- { name: "operate_name", type: "VARCHAR", comment: "操作名" }
- { name: "operate_type", type: "TINYINT", comment: "操作分类" }
- { name: "begin_time", type: "DATETIME", comment: "开始时间" }
- { name: "end_time", type: "DATETIME", comment: "结束时间" }
- { name: "duration", type: "INT", comment: "执行时长(毫秒)" }
- { name: "result_code", type: "INT", comment: "结果码" }
- { name: "result_msg", type: "VARCHAR", comment: "结果提示" }
- name: "infra_api_error_log"
comment: "API 异常日志表"
entity: "ApiErrorLogDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号,主键" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "trace_id", type: "VARCHAR", comment: "链路追踪编号" }
- { name: "user_type", type: "TINYINT", comment: "用户类型" }
- { name: "application_name", type: "VARCHAR", comment: "应用名" }
- { name: "request_method", type: "VARCHAR", comment: "请求方法" }
- { name: "request_url", type: "VARCHAR", comment: "请求地址" }
- { name: "request_params", type: "VARCHAR", comment: "请求参数" }
- { name: "user_ip", type: "VARCHAR", comment: "用户 IP" }
- { name: "user_agent", type: "VARCHAR", comment: "浏览器 UA" }
- { name: "exception_time", type: "DATETIME", comment: "异常时间" }
- { name: "exception_name", type: "VARCHAR", comment: "异常名" }
- { name: "exception_message", type: "VARCHAR", comment: "异常消息" }
- { name: "exception_root_cause_message", type: "VARCHAR", comment: "异常根消息" }
- { name: "exception_stack_trace", type: "VARCHAR", comment: "异常栈轨迹" }
- { name: "exception_class_name", type: "VARCHAR", comment: "异常类名" }
- { name: "exception_file_name", type: "VARCHAR", comment: "异常文件名" }
- { name: "exception_method_name", type: "VARCHAR", comment: "异常方法名" }
- { name: "exception_line_number", type: "INT", comment: "异常行号" }
- { name: "process_status", type: "TINYINT", comment: "处理状态" }
- { name: "process_time", type: "DATETIME", comment: "处理时间" }
- { name: "process_user_id", type: "BIGINT", comment: "处理用户编号" }
# ========== 系统配置相关 ==========
- name: "infra_config"
comment: "参数配置表"
entity: "ConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "参数主键" }
- { name: "category", type: "VARCHAR", comment: "参数分类" }
- { name: "name", type: "VARCHAR", comment: "参数名称" }
- { name: "config_key", type: "VARCHAR", comment: "参数键名" }
- { name: "value", type: "VARCHAR", comment: "参数键值" }
- { name: "type", type: "TINYINT", comment: "参数类型" }
- { name: "visible", type: "BIT", comment: "是否可见" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
# ========== 数据源配置相关 ==========
- name: "infra_data_source_config"
comment: "数据源配置表"
entity: "DataSourceConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键编号0 表示 Master 数据源" }
- { name: "name", type: "VARCHAR", comment: "连接名" }
- { name: "url", type: "VARCHAR", comment: "数据源连接" }
- { name: "username", type: "VARCHAR", comment: "用户名" }
- { name: "password", type: "VARCHAR", comment: "密码(加密存储)" }
# 表关系ER关系
relationships:
- from: "infra_file"
to: "infra_file_config"
type: "N:1"
foreign_key: "config_id"
- from: "infra_codegen_column"
to: "infra_codegen_table"
type: "N:1"
foreign_key: "table_id"
- from: "infra_job_log"
to: "infra_job"
type: "N:1"
foreign_key: "job_id"
- from: "infra_codegen_table"
to: "infra_codegen_table"
type: "N:1"
foreign_key: "master_table_id"
comment: "主子表关联"
- from: "infra_codegen_table"
to: "infra_data_source_config"
type: "N:1"
foreign_key: "data_source_config_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - xxx') - Swagger 文档标签"
- "@RestController - REST 控制器"
- "@RequestMapping('/infra/xxx') - 请求路径前缀"
- "@Validated - 参数校验"
- "@PreAuthorize('@ss.hasPermission('infra:xxx:action')') - 权限控制"
example: |
@Tag(name = "管理后台 - 文件存储")
@RestController
@RequestMapping("/infra/file")
@Validated
public class FileController {
@Resource
private FileService fileService;
@PostMapping("/upload")
@Operation(summary = "上传文件")
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
byte[] content = IoUtil.readBytes(file.getInputStream());
return success(fileService.createFile(content, file.getOriginalFilename(),
uploadReqVO.getDirectory(), file.getContentType()));
}
}
# Service层规范
service:
interface_pattern: "XxxService 接口定义业务方法XxxServiceImpl 实现类"
impl_pattern: |
@Service
@Validated
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
// 使用 static final 定义常量
// 使用 @Transactional 注解控制事务
// 使用 validateXxxExists 方法校验存在性
// 使用 exception(ErrorCode) 抛出业务异常
}
example: |
@Service
public class FileServiceImpl implements FileService {
@Resource
private FileConfigService fileConfigService;
@Resource
private FileMapper fileMapper;
@Override
public String createFile(byte[] content, String name, String directory, String type) {
// 1. 处理参数
// 2. 生成上传路径
String path = generateUploadPath(name, directory);
// 3. 上传到文件存储器
FileClient client = fileConfigService.getMasterFileClient();
String url = client.upload(content, path, type);
// 4. 保存到数据库
fileMapper.insert(new FileDO()...);
return url;
}
}
# 数据访问规范
dal:
mapper_pattern: |
public interface XxxMapper extends BaseMapperX<XxxDO> {
// 继承 BaseMapperX 获得通用 CRUD 方法
// 自定义查询方法使用 @Select 注解或 XML
// 分页查询返回 PageResult<XxxDO>
}
example: |
public interface FileMapper extends BaseMapperX<FileDO> {
default PageResult<FileDO> selectPage(FilePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<FileDO>()
.likeIfPresent(FileDO::getPath, reqVO.getPath())
.likeIfPresent(FileDO::getName, reqVO.getName())
.betweenIfPresent(FileDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(FileDO::getId));
}
}
# 异常处理
error_handling:
code_prefix: "1-001-xxx-xxx"
examples:
- code: "1_001_000_001"
message: "参数配置不存在"
- code: "1_001_001_000"
message: "定时任务不存在"
- code: "1_001_003_001"
message: "文件不存在"
- code: "1_001_004_002"
message: "表定义已经存在"
- code: "1_001_006_000"
message: "文件配置不存在"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ErrorCode)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增文件存储类型
new_channel:
title: "新增文件存储类型"
steps:
- step: "1. 创建配置类"
description: "在 framework/file/core/client/ 下创建 XxxFileClientConfig 实现 FileClientConfig"
code: |
@Data
public class XxxFileClientConfig implements FileClientConfig {
private String endpoint;
private String accessKey;
private String secretKey;
}
- step: "2. 创建客户端类"
description: "创建 XxxFileClient 继承 AbstractFileClient<XxxFileClientConfig>"
code: |
public class XxxFileClient extends AbstractFileClient<XxxFileClientConfig> {
public XxxFileClient(Long id, XxxFileClientConfig config) {
super(id, config);
}
@Override
protected void doInit() {
// 初始化客户端
}
@Override
public String upload(byte[] content, String path, String type) {
// 实现上传逻辑
}
@Override
public void delete(String path) {
// 实现删除逻辑
}
@Override
public byte[] getContent(String path) {
// 实现获取内容逻辑
}
}
- step: "3. 注册枚举"
description: "在 FileStorageEnum 中添加新存储类型"
code: |
XXX(30, XxxFileClientConfig.class, XxxFileClient.class);
- step: "4. 测试验证"
description: "编写单元测试验证功能正确性"
# 新增代码生成模板
new_feature:
title: "自定义代码生成模板"
steps:
- step: "1. 创建模板文件"
description: "在 resources/codegen/ 下创建 .vm 模板文件"
- step: "2. 注册模板路径"
description: "在 CodegenEngine 的 SERVER_TEMPLATES 或 FRONT_TEMPLATES 中添加映射"
code: |
.put(javaTemplatePath("custom/template"), javaModuleImplMainFilePath("custom/${table.className}Custom"))
- step: "3. 添加模板变量"
description: "在 initBindingMap() 方法中添加模板需要的全局变量"
- step: "4. 测试生成结果"
description: "使用预览接口验证生成代码"
# 最佳实践
best_practices:
- practice: "文件上传路径规范"
description: "使用 generateUploadPath() 方法生成唯一路径,包含日期前缀和时间戳后缀,避免文件名冲突"
- practice: "文件配置缓存"
description: "使用 Guava LoadingCache 缓存 FileClient避免频繁查询数据库支持异步刷新"
- practice: "代码生成同步"
description: "使用 syncCodegenFromDB() 同步数据库表结构变更,保持代码生成定义与数据库一致"
- practice: "定时任务幂等"
description: "JobHandler.execute() 应实现幂等性,避免重复执行产生副作用"
- practice: "错误日志追踪"
description: "通过 traceId 关联 API 访问日志和错误日志,便于问题排查"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-framework-common"
purpose: "通用工具类、CommonResult、PageResult 等"
- module: "yudao-framework-mybatis"
purpose: "MyBatis-Plus 封装、BaseMapperX、BaseDO"
- module: "yudao-framework-quartz"
purpose: "Quartz 封装、JobHandler、SchedulerManager"
- module: "yudao-framework-tenant"
purpose: "租户支持、@TenantIgnore"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
purpose: "ORM 框架,提供 CRUD 封装"
- name: "Quartz"
purpose: "定时任务调度框架"
- name: "Velocity"
purpose: "模板引擎,用于代码生成"
- name: "Hutool"
purpose: "Java 工具类库"
- name: "Guava"
purpose: "Google 工具库,使用 Cache、Table 等"
- name: "MinIO/S3 SDK"
purpose: "对象存储客户端"
- name: "Swagger/OpenAPI"
purpose: "API 文档注解"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java"
purpose: "文件服务实现,核心上传下载逻辑"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java"
purpose: "文件配置服务FileClient 缓存管理"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java"
purpose: "文件客户端接口定义"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/AbstractFileClient.java"
purpose: "文件客户端抽象类,模板方法模式"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/enums/FileStorageEnum.java"
purpose: "文件存储类型枚举,配置类与客户端类映射"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java"
purpose: "代码生成服务实现"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java"
purpose: "代码生成引擎,模板渲染核心"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java"
purpose: "代码生成对象构建器"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java"
purpose: "定时任务服务实现Quartz 集成"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java"
purpose: "文件 API 接口,供其他模块调用"
- path: "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义"

View File

@@ -0,0 +1,722 @@
# Skill 文档 - yudao-module-iot 模块
# 物联网平台模块知识提取
skill:
id: "skill-iot"
name: "IoT Skill"
version: "1.0.0"
module_path: "yudao-module-iot"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位物联网平台核心模块提供设备接入、管理、数据采集、规则引擎、OTA升级等能力
business_position: |
IoT 物联网平台模块,作为整个系统的设备接入和管理中心:
1. 设备管理:设备注册、认证、状态管理、分组管理
2. 产品管理:产品定义、物模型配置、协议设置
3. 协议适配:支持 MQTT、CoAP、HTTP、TCP、UDP、WebSocket、Modbus、EMQX 等多种协议
4. 规则引擎:场景联动、数据流转、告警触发
5. OTA升级固件管理、升级任务、升级记录
6. 数据处理:设备属性存储、消息记录、统计分析
# 设计原则:协议抽象、设备模型化、规则驱动
design_principles:
- "协议适配器模式:通过 IotProtocol 接口抽象不同传输协议,支持灵活扩展"
- "产品-设备模型:产品定义设备模板,设备继承产品配置,支持一型一密动态注册"
- "物模型驱动:通过 ThingModel 定义设备属性、事件、服务,实现标准化数据交互"
- "规则引擎:基于触发器-条件-动作模型,实现场景联动自动化"
- "多租户设计:设备、产品、规则等都支持租户隔离"
- "分层架构gateway 网关层处理协议biz 业务层处理逻辑core 核心层定义模型"
# 领域模型:核心领域对象及其关系
domain_model:
aggregates:
- name: "设备聚合"
type: "聚合根"
entities: ["IotDeviceDO", "IotDeviceGroupDO", "IotDevicePropertyDO", "IotDeviceMessageDO"]
description: "设备是 IoT 平台的核心实体,包含设备基本信息、状态、属性、消息等"
- name: "产品聚合"
type: "聚合根"
entities: ["IotProductDO", "IotProductCategoryDO", "IotThingModelDO"]
description: "产品是设备的模板,定义物模型(属性、事件、服务)和协议配置"
- name: "规则引擎聚合"
type: "聚合根"
entities: ["IotSceneRuleDO", "IotDataRuleDO", "IotDataSinkDO", "IotAlertConfigDO", "IotAlertRecordDO"]
description: "规则引擎处理设备数据流转、场景联动、告警触发"
- name: "OTA升级聚合"
type: "聚合根"
entities: ["IotOtaFirmwareDO", "IotOtaTaskDO", "IotOtaTaskRecordDO"]
description: "OTA 升级管理固件版本、升级任务和升级记录"
value_objects:
- name: "ThingModelProperty"
description: "物模型属性定义,包含数据类型、单位、范围等"
- name: "ThingModelEvent"
description: "物模型事件定义,包含事件类型、参数等"
- name: "ThingModelService"
description: "物模型服务定义,包含输入输出参数"
- name: "IotDeviceIdentity"
description: "设备身份标识,包含 productKey、deviceName"
services:
- name: "IotDeviceService"
description: "设备管理服务,处理设备 CRUD、认证、状态更新"
- name: "IotDeviceMessageService"
description: "设备消息服务,处理消息收发、记录存储"
- name: "IotSceneRuleService"
description: "场景规则服务,执行场景联动逻辑"
- name: "IotProtocol"
description: "协议接口,定义协议生命周期(启动、停止、运行状态)"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "yudao-module-iot-gateway"
purpose: "协议网关层,处理设备连接、协议解析"
components:
- "protocol/mqtt/IotMqttProtocol - MQTT 协议实现"
- "protocol/coap/IotCoapProtocol - CoAP 协议实现"
- "protocol/http/IotHttpProtocol - HTTP 协议实现"
- "protocol/tcp/IotTcpProtocol - TCP 协议实现"
- "protocol/udp/IotUdpProtocol - UDP 协议实现"
- "protocol/websocket/IotWebSocketProtocol - WebSocket 协议实现"
- "protocol/modbus/IotModbusTcpClientProtocol - Modbus TCP 客户端"
- "protocol/modbus/IotModbusTcpServerProtocol - Modbus TCP 服务端"
- "protocol/emqx/IotEmqxProtocol - EMQX 协议桥接"
- "handler/upstream - 上行消息处理器"
- "handler/downstream - 下行消息处理器"
- name: "yudao-module-iot-biz"
purpose: "业务逻辑层,处理设备、产品、规则等核心业务"
components:
- "service/device - 设备管理服务"
- "service/product - 产品管理服务"
- "service/thingmodel - 物模型管理服务"
- "service/rule - 规则引擎服务"
- "service/ota - OTA 升级服务"
- "service/alert - 告警服务"
- "controller/admin - 管理后台接口"
- name: "yudao-module-iot-core"
purpose: "核心层,定义公共模型、枚举、工具类"
components:
- "enums - 协议类型、设备状态、消息方法等枚举"
- "mq/message - 设备消息模型"
- "biz/dto - 业务 DTO"
- "topic - Topic 定义和工具"
# 设计模式应用
design_patterns:
- pattern: "策略模式 / 适配器模式"
location: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java"
purpose: "定义协议接口,不同协议实现统一接口,支持灵活扩展新协议"
- pattern: "模板方法模式"
location: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java"
purpose: "抽象下行消息订阅者,子类实现具体的消息处理逻辑"
- pattern: "观察者模式"
location: "yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/"
purpose: "场景规则引擎,设备状态变化触发规则执行"
- pattern: "责任链模式"
location: "yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/"
purpose: "规则匹配器链,依次判断触发条件、执行条件是否满足"
- pattern: "工厂模式"
location: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java"
purpose: "消息序列化器工厂,根据序列化类型获取对应的序列化器"
# 模块间通信
communication:
apis:
- name: "IotDeviceCommonApi"
path: "yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java"
description: "设备通用 API提供设备认证、查询、注册等接口"
methods:
- "authDevice - 设备认证"
- "getDevice - 获取设备信息"
- "getModbusDeviceConfigList - 获取 Modbus 设备配置"
- "registerDevice - 设备动态注册"
- "registerSubDevices - 子设备动态注册"
consumers:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取告警接收用户信息"
mq:
- type: "IotMessageBus"
description: "内部消息总线,用于设备消息在 gateway 和 biz 之间传递"
topics:
- "device:message:upstream - 设备上行消息"
- "device:message:downstream - 设备下行消息"
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "TenantBaseDO | BaseDO"
description: |
- TenantBaseDO: 设备、产品、场景规则等需要租户隔离的实体
- BaseDO: 设备分组、固件、告警记录等不需要租户隔离的实体
# 核心数据表
tables:
# 设备相关表
- name: "iot_device"
comment: "IoT 设备表"
entity: "IotDeviceDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "设备 ID主键" }
- { name: "device_name", type: "VARCHAR(64)", comment: "设备名称,产品内唯一" }
- { name: "nickname", type: "VARCHAR(64)", comment: "设备备注名称" }
- { name: "serial_number", type: "VARCHAR(64)", comment: "设备序列号" }
- { name: "product_id", type: "BIGINT", comment: "产品编号" }
- { name: "product_key", type: "VARCHAR(32)", comment: "产品标识(冗余)" }
- { name: "device_type", type: "INT", comment: "设备类型(冗余)" }
- { name: "gateway_id", type: "BIGINT", comment: "网关设备编号(子设备)" }
- { name: "state", type: "INT", comment: "设备状态0未激活、1在线、2离线" }
- { name: "device_secret", type: "VARCHAR(64)", comment: "设备密钥" }
- { name: "group_ids", type: "VARCHAR(255)", comment: "设备分组编号集合JSON" }
- { name: "firmware_id", type: "BIGINT", comment: "固件编号" }
- { name: "latitude", type: "DECIMAL(10,6)", comment: "纬度" }
- { name: "longitude", type: "DECIMAL(10,6)", comment: "经度" }
- { name: "online_time", type: "DATETIME", comment: "最后上线时间" }
- { name: "offline_time", type: "DATETIME", comment: "最后离线时间" }
- { name: "active_time", type: "DATETIME", comment: "激活时间" }
indexes:
- { name: "idx_product_id", columns: ["product_id"] }
- { name: "idx_product_key_device_name", columns: ["product_key", "device_name"], unique: true }
- name: "iot_device_group"
comment: "IoT 设备分组表"
entity: "IotDeviceGroupDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "分组 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "分组名称" }
- { name: "status", type: "INT", comment: "分组状态" }
- { name: "description", type: "VARCHAR(255)", comment: "分组描述" }
- name: "iot_device_message"
comment: "IoT 设备消息表"
entity: "IotDeviceMessageDO"
extends: "BaseDO"
storage: "TDengine 时序数据库"
columns:
- { name: "device_id", type: "BIGINT", comment: "设备编号" }
- { name: "product_key", type: "VARCHAR(32)", comment: "产品标识" }
- { name: "device_name", type: "VARCHAR(64)", comment: "设备名称" }
- { name: "method", type: "VARCHAR(64)", comment: "消息方法" }
- { name: "request_id", type: "VARCHAR(64)", comment: "请求 ID" }
- { name: "payload", type: "TEXT", comment: "消息内容" }
# 产品相关表
- name: "iot_product"
comment: "IoT 产品表"
entity: "IotProductDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "产品 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "产品名称" }
- { name: "product_key", type: "VARCHAR(32)", comment: "产品标识" }
- { name: "product_secret", type: "VARCHAR(64)", comment: "产品密钥" }
- { name: "category_id", type: "BIGINT", comment: "产品分类编号" }
- { name: "device_type", type: "INT", comment: "设备类型" }
- { name: "net_type", type: "INT", comment: "联网方式" }
- { name: "protocol_type", type: "VARCHAR(32)", comment: "协议类型" }
- { name: "serialize_type", type: "VARCHAR(32)", comment: "序列化类型" }
- { name: "status", type: "INT", comment: "产品状态" }
- { name: "register_enabled", type: "BOOLEAN", comment: "是否开启动态注册" }
- name: "iot_product_category"
comment: "IoT 产品分类表"
entity: "IotProductCategoryDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "分类 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "分类名称" }
- { name: "parent_id", type: "BIGINT", comment: "父分类 ID" }
- { name: "sort", type: "INT", comment: "排序" }
- name: "iot_thing_model"
comment: "IoT 产品物模型功能表"
entity: "IotThingModelDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "物模型功能 ID" }
- { name: "product_id", type: "BIGINT", comment: "产品编号" }
- { name: "product_key", type: "VARCHAR(32)", comment: "产品标识" }
- { name: "identifier", type: "VARCHAR(64)", comment: "功能标识符" }
- { name: "name", type: "VARCHAR(64)", comment: "功能名称" }
- { name: "type", type: "INT", comment: "功能类型1属性、2事件、3服务" }
- { name: "property", type: "JSON", comment: "属性定义" }
- { name: "event", type: "JSON", comment: "事件定义" }
- { name: "service", type: "JSON", comment: "服务定义" }
# 规则引擎相关表
- name: "iot_scene_rule"
comment: "IoT 场景联动规则表"
entity: "IotSceneRuleDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "场景联动 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "场景名称" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "triggers", type: "JSON", comment: "触发器配置" }
- { name: "actions", type: "JSON", comment: "动作配置" }
- { name: "last_trigger_time", type: "DATETIME", comment: "最后触发时间" }
- name: "iot_data_sink"
comment: "IoT 数据流转目的地表"
entity: "IotDataSinkDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "数据目的地 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "名称" }
- { name: "type", type: "VARCHAR(32)", comment: "类型HTTP/MQTT/Kafka/RabbitMQ/Redis/RocketMQ/TCP/WebSocket" }
- { name: "config", type: "JSON", comment: "配置信息" }
- name: "iot_alert_config"
comment: "IoT 告警配置表"
entity: "IotAlertConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "告警配置 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "告警名称" }
- { name: "level", type: "INT", comment: "告警级别" }
- { name: "status", type: "INT", comment: "状态" }
- { name: "scene_rule_ids", type: "VARCHAR(255)", comment: "关联的场景规则 ID 列表" }
- { name: "receive_user_ids", type: "VARCHAR(255)", comment: "接收用户 ID 列表" }
# OTA 升级相关表
- name: "iot_ota_firmware"
comment: "IoT OTA 固件表"
entity: "IotOtaFirmwareDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "固件 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "固件名称" }
- { name: "version", type: "VARCHAR(32)", comment: "版本号" }
- { name: "product_id", type: "BIGINT", comment: "产品编号" }
- { name: "file_url", type: "VARCHAR(512)", comment: "固件文件 URL" }
- { name: "file_size", type: "BIGINT", comment: "文件大小" }
- { name: "file_digest_algorithm", type: "VARCHAR(32)", comment: "签名算法" }
- { name: "file_digest_value", type: "VARCHAR(128)", comment: "签名值" }
- name: "iot_ota_task"
comment: "IoT OTA 升级任务表"
entity: "IotOtaTaskDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "任务 ID" }
- { name: "name", type: "VARCHAR(64)", comment: "任务名称" }
- { name: "firmware_id", type: "BIGINT", comment: "固件编号" }
- { name: "product_id", type: "BIGINT", comment: "产品编号" }
- { name: "status", type: "INT", comment: "任务状态" }
- name: "iot_ota_task_record"
comment: "IoT OTA 升级任务记录表"
entity: "IotOtaTaskRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "记录 ID" }
- { name: "task_id", type: "BIGINT", comment: "任务 ID" }
- { name: "device_id", type: "BIGINT", comment: "设备 ID" }
- { name: "status", type: "INT", comment: "升级状态" }
# 表关系ER关系
relationships:
- from: "iot_device"
to: "iot_product"
type: "N:1"
foreign_key: "product_id"
description: "设备属于某个产品"
- from: "iot_device"
to: "iot_device"
type: "N:1"
foreign_key: "gateway_id"
description: "子设备关联网关设备"
- from: "iot_product"
to: "iot_product_category"
type: "N:1"
foreign_key: "category_id"
description: "产品属于某个分类"
- from: "iot_thing_model"
to: "iot_product"
type: "N:1"
foreign_key: "product_id"
description: "物模型属于某个产品"
- from: "iot_ota_firmware"
to: "iot_product"
type: "N:1"
foreign_key: "product_id"
description: "固件属于某个产品"
- from: "iot_ota_task"
to: "iot_ota_firmware"
type: "N:1"
foreign_key: "firmware_id"
description: "升级任务使用某个固件"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = \"管理后台 - IoT 设备\")"
- "@RestController"
- "@RequestMapping(\"/iot/device\")"
- "@PreAuthorize(\"@ss.hasPermission('iot:device:create')\")"
example: |
@Tag(name = "管理后台 - IoT 设备")
@RestController
@RequestMapping("/iot/device")
@Validated
public class IotDeviceController {
@Resource
private IotDeviceService deviceService;
@PostMapping("/create")
@Operation(summary = "创建设备")
@PreAuthorize("@ss.hasPermission('iot:device:create')")
public CommonResult<Long> createDevice(@Valid @RequestBody IotDeviceSaveReqVO createReqVO) {
return success(deviceService.createDevice(createReqVO));
}
@GetMapping("/page")
@Operation(summary = "获得设备分页")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<PageResult<IotDeviceRespVO>> getDevicePage(@Valid IotDevicePageReqVO pageReqVO) {
PageResult<IotDeviceDO> pageResult = deviceService.getDevicePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, IotDeviceRespVO.class));
}
}
# Service层规范
service:
interface_pattern: |
public interface IotDeviceService {
// CRUD 操作
Long createDevice(@Valid IotDeviceSaveReqVO createReqVO);
void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
void deleteDevice(Long id);
IotDeviceDO getDevice(Long id);
PageResult<IotDeviceDO> getDevicePage(IotDevicePageReqVO pageReqVO);
// 业务操作
boolean authDevice(@Valid IotDeviceAuthReqDTO authReqDTO);
void updateDeviceState(Long id, Integer state);
IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
// 缓存操作
IotDeviceDO getDeviceFromCache(Long id);
IotDeviceDO getDeviceFromCache(String productKey, String deviceName);
}
impl_pattern: |
@Service
@Validated
public class IotDeviceServiceImpl implements IotDeviceService {
@Resource
private IotDeviceMapper deviceMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createDevice(IotDeviceSaveReqVO createReqVO) {
// 1. 校验产品存在
IotProductDO product = productService.validateProductExists(createReqVO.getProductId());
// 2. 生成设备密钥
String deviceSecret = generateDeviceSecret();
// 3. 插入设备
IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class)
.setProductKey(product.getProductKey())
.setDeviceType(product.getDeviceType())
.setDeviceSecret(deviceSecret)
.setState(IotDeviceStateEnum.INACTIVE.getState());
deviceMapper.insert(device);
return device.getId();
}
}
example: |
// 设备认证示例
@Override
public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) {
// 1. 获取设备信息
IotDeviceDO device = getDeviceFromCache(authReqDTO.getProductKey(), authReqDTO.getDeviceName());
if (device == null) {
return false;
}
// 2. 校验设备密钥
if (!StrUtil.equals(device.getDeviceSecret(), authReqDTO.getDeviceSecret())) {
return false;
}
// 3. 更新设备状态为在线
updateDeviceState(device, IotDeviceStateEnum.ONLINE.getState());
return true;
}
# 数据访问规范
dal:
mapper_pattern: |
@Mapper
public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
default PageResult<IotDeviceDO> selectPage(IotDevicePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceDO>()
.likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName())
.eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId())
.eqIfPresent(IotDeviceDO::getState, reqVO.getState())
.orderByDesc(IotDeviceDO::getId));
}
}
example: |
// 设备属性存储在 Redis 中
@Resource
private DevicePropertyRedisDAO devicePropertyRedisDAO;
public void updateDeviceProperty(String productKey, String deviceName, String identifier, Object value) {
String key = formatDevicePropertyKey(productKey, deviceName, identifier);
IotDevicePropertyDO property = IotDevicePropertyDO.builder()
.value(value)
.updateTime(LocalDateTime.now())
.build();
devicePropertyRedisDAO.set(key, property);
}
# 协议扩展示例
protocol:
interface_definition: |
public interface IotProtocol {
String getId(); // 协议实例 ID
String getServerId(); // 服务器 ID消息追踪
IotProtocolTypeEnum getType(); // 协议类型
void start(); // 启动协议服务
void stop(); // 停止协议服务
boolean isRunning(); // 检查运行状态
}
example: |
// MQTT 协议实现示例
public class IotMqttProtocol implements IotProtocol {
private MqttServer mqttServer;
private IotMqttConnectionManager connectionManager;
@Override
public void start() {
// 1. 创建 MQTT 服务器
mqttServer = MqttServer.create(vertx, options);
mqttServer.endpointHandler(this::handleEndpoint);
// 2. 启动服务器
mqttServer.listen();
running = true;
// 3. 启动下行消息订阅者
downstreamSubscriber.start();
}
private void handleEndpoint(MqttEndpoint endpoint) {
// 1. 处理认证
if (!authHandler.handleAuthenticationRequest(endpoint)) {
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
return;
}
// 2. 设置消息处理器
endpoint.publishHandler(message -> processMessage(endpoint, message));
// 3. 接受连接
endpoint.accept(false);
}
}
# 异常处理
error_handling:
code_prefix: "1-011"
examples:
- code: "1-011-000-000"
message: "设备不存在"
- code: "1-011-000-001"
message: "设备认证失败"
- code: "1-011-000-002"
message: "设备已存在"
- code: "1-011-001-000"
message: "产品不存在"
- code: "1-011-002-000"
message: "物模型不存在"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "CommonResult.error(ErrorCodeConstants.DEVICE_NOT_EXISTS)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增协议类型
new_protocol:
title: "新增协议类型"
steps:
- step: 1
action: "添加协议类型枚举"
file: "yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java"
description: "在枚举中添加新的协议类型,如 GOPHER(\"gopher\")"
- step: 2
action: "创建协议配置类"
file: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{protocol}/Iot{Protocol}Config.java"
description: "定义协议特有的配置参数如端口、超时时间、SSL 配置等"
- step: 3
action: "实现协议接口"
file: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{protocol}/Iot{Protocol}Protocol.java"
description: |
实现 IotProtocol 接口:
- start(): 启动协议服务,创建服务器/客户端
- stop(): 停止协议服务,释放资源
- handleUpstream(): 处理上行消息
- handleDownstream(): 处理下行消息
- step: 4
action: "实现上行消息处理器"
file: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{protocol}/handler/upstream/Iot{Protocol}UpstreamHandler.java"
description: "处理设备上报的消息,解析后发送到消息总线"
- step: 5
action: "实现下行消息订阅者"
file: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{protocol}/handler/downstream/Iot{Protocol}DownstreamSubscriber.java"
description: "订阅下行消息,下发给设备"
- step: 6
action: "注册协议到管理器"
file: "yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java"
description: "在配置中添加新协议的实例化逻辑"
# 新增设备类型
new_device_type:
title: "新增设备类型"
steps:
- step: 1
action: "添加设备类型枚举"
file: "yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java"
description: "在枚举中添加新的设备类型"
- step: 2
action: "定义物模型"
file: "通过管理后台配置"
description: "为新产品定义物模型(属性、事件、服务)"
- step: 3
action: "配置协议"
file: "产品管理页面"
description: "设置产品使用的协议类型和序列化方式"
# 新增规则动作
new_rule_action:
title: "新增场景联动动作类型"
steps:
- step: 1
action: "添加动作类型枚举"
file: "yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java"
description: "添加新的动作类型枚举值"
- step: 2
action: "实现动作执行器"
file: "yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/Iot{Action}SceneRuleAction.java"
description: "实现 IotSceneRuleAction 接口,定义动作执行逻辑"
- step: 3
action: "注册动作执行器"
description: "在 Spring 容器中注册为 Bean自动被规则引擎发现和调用"
# 最佳实践
best_practices:
- "设备密钥安全性:设备密钥应使用强随机数生成,避免硬编码或简单规则"
- "协议扩展:新协议应继承 AbstractIotProtocolDownstreamSubscriber复用下行消息处理逻辑"
- "消息存储:设备消息使用 TDengine 时序数据库存储,支持高效查询和聚合"
- "设备缓存:频繁访问的设备信息使用 Redis 缓存,减少数据库压力"
- "规则引擎:场景规则应设置合理的触发条件,避免频繁触发导致性能问题"
- "OTA升级大规模升级应分批进行避免网络拥塞和服务器压力"
- "租户隔离:所有设备操作都应考虑租户隔离,防止越权访问"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取告警接收用户信息"
# 外部依赖(第三方库)
external:
- name: "vertx-mqtt"
version: "4.x"
purpose: "MQTT 服务器实现"
- name: "californium"
version: "3.x"
purpose: "CoAP 服务器实现"
- name: "jmodbus"
version: "1.x"
purpose: "Modbus 协议实现"
- name: "netty"
version: "4.x"
purpose: "TCP/UDP/WebSocket 网络框架"
- name: "tdengine-jdbc"
version: "3.x"
purpose: "TDengine 时序数据库驱动"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java"
purpose: "协议类型枚举定义"
- path: "yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java"
purpose: "协议接口定义,所有协议必须实现此接口"
- path: "yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java"
purpose: "MQTT 协议实现,最常用的设备接入协议"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java"
purpose: "设备管理服务接口"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java"
purpose: "设备消息服务接口"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleService.java"
purpose: "场景联动规则服务"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java"
purpose: "设备实体类定义"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java"
purpose: "产品实体类定义"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java"
purpose: "物模型实体类定义"
- path: "yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java"
purpose: "设备管理 Controller"
- path: "yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java"
purpose: "设备通用 API 接口"

View File

@@ -0,0 +1,654 @@
# Skill 模板文件
# 用于提取模块知识的标准格式
skill:
id: "skill-mall"
name: "Mall Skill"
version: "1.0.0"
module_path: "yudao-module-mall"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
商城系统Mall是 ruoyi-vue-pro 项目的电商核心模块,提供完整的电商解决方案。
包含商品管理、促销活动、交易订单、数据统计四大核心能力:
- 商品管理SPU/SKU 商品模型、商品分类、品牌、属性规格管理
- 促销活动:优惠券、秒杀、拼团、砍价、满减送、积分商城等
- 交易订单:购物车、订单创建/支付/发货/收货、售后退款
- 数据统计:交易统计、商品统计、会员统计等
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "SPU-SKU 模型标准电商商品模型SPU 为商品标准化单元SKU 为库存单位"
- "订单状态机:订单状态流转严格遵循状态机模式,确保状态变更可追溯"
- "领域驱动设计按业务领域划分子模块product/promotion/trade/statistics"
- "分层架构Controller -> Service -> DAL 三层架构,职责清晰"
- "聚合根设计订单TradeOrder和商品ProductSpu作为聚合根"
- "API 模块分离trade-api 独立模块,提供跨模块调用接口"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "ProductSpu商品SPU"
type: "聚合根"
entities: ["ProductSku商品SKU", "ProductCategory分类", "ProductBrand品牌", "ProductProperty属性"]
description: "商品SPU作为聚合根管理SKU、分类、品牌、属性等关联实体"
- name: "TradeOrder交易订单"
type: "聚合根"
entities: ["TradeOrderItem订单项", "AfterSale售后单", "Cart购物车"]
description: "订单作为交易核心聚合根,管理订单项、售后、购物车等"
- name: "CouponTemplate优惠券模板"
type: "聚合根"
entities: ["Coupon优惠券"]
description: "优惠券模板管理优惠券的发放和使用"
- name: "CombinationActivity拼团活动"
type: "聚合根"
entities: ["CombinationProduct拼团商品", "CombinationRecord拼团记录"]
description: "拼团活动管理拼团商品和拼团参与记录"
value_objects:
- "ProductSkuDO.PropertySKU属性值对象"
- "TradeOrderDO.Address收货地址值对象"
- "DeliveryExpressTemplateCharge运费模板计费规则"
services:
- "ProductSpuService商品SPU业务逻辑"
- "TradeOrderUpdateService订单创建和状态更新"
- "TradePriceService价格计算服务"
- "AfterSaleService售后服务"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口提供RPC调用能力"
components:
- "ProductSpuApi/ProductSkuApi商品信息查询、库存更新"
- "TradeOrderApi订单信息查询"
- "CouponApi优惠券使用"
- "SeckillActivityApi/CombinationRecordApi营销活动验证"
- name: "controller"
purpose: "HTTP接口分为管理后台(admin)和用户端(app)"
components:
- "admin/spu/ProductSpuController商品SPU管理"
- "admin/order/TradeOrderController订单管理"
- "app/spu/AppProductSpuController商品展示"
- "app/order/AppTradeOrderController用户下单"
- name: "service"
purpose: "业务逻辑层,核心业务处理"
components:
- "ProductSpuService/ProductSkuService商品业务"
- "TradeOrderUpdateService/TradeOrderQueryService订单业务"
- "AfterSaleService售后业务"
- "CartService购物车业务"
- "CouponService优惠券业务"
- name: "dal"
purpose: "数据访问层MyBatis-Plus Mapper"
components:
- "ProductSpuMapper/ProductSkuMapper商品数据访问"
- "TradeOrderMapper/TradeOrderItemMapper订单数据访问"
- "CouponMapper/CouponTemplateMapper优惠券数据访问"
# 设计模式应用
design_patterns:
- pattern: "状态模式"
location: "TradeOrderStatusEnum, ProductSpuStatusEnum"
purpose: "订单状态和商品状态的状态机管理,通过枚举定义状态流转"
- pattern: "策略模式"
location: "TradePriceService"
purpose: "价格计算策略,支持不同促销活动的价格计算"
- pattern: "模板方法模式"
location: "BaseDO, TenantBaseDO"
purpose: "数据实体基类,统一处理创建时间、更新时间、删除标记等"
- pattern: "工厂模式"
location: "ProductSpuConvert, TradeOrderConvert"
purpose: "MapStruct转换器VO与DO之间的对象转换"
- pattern: "观察者模式"
location: "TradeOrderLogService"
purpose: "订单操作日志记录,订单状态变更时自动记录日志"
# 模块间通信
communication:
apis:
- name: "ProductSpuApi"
module: "yudao-module-product"
purpose: "查询商品信息、更新库存"
- name: "ProductSkuApi"
module: "yudao-module-product"
purpose: "查询SKU信息、更新库存"
- name: "CouponApi"
module: "yudao-module-promotion"
purpose: "优惠券使用、回收"
- name: "SeckillActivityApi"
module: "yudao-module-promotion"
purpose: "秒杀活动校验、库存扣减"
consumers:
- module: "yudao-module-trade"
api: "ProductSpuApi"
purpose: "下单时查询商品信息、扣减库存"
- module: "yudao-module-trade"
api: "CouponApi"
purpose: "下单时使用优惠券"
- module: "yudao-module-statistics"
api: "TradeOrderApi"
purpose: "统计交易数据"
mq:
- topic: "order.paid"
purpose: "订单支付成功消息,触发积分发放、优惠券赠送等"
- topic: "order.delivered"
purpose: "订单发货消息,通知用户"
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有DO实体继承BaseDO包含id、creator、createTime、updater、updateTime、deleted字段"
# 核心数据表
tables:
# ========== 商品模块 ==========
- name: "product_spu"
comment: "商品SPU表"
entity: "ProductSpuDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "商品SPU编号" }
- { name: "name", type: "String", comment: "商品名称" }
- { name: "keyword", type: "String", comment: "关键字" }
- { name: "introduction", type: "String", comment: "商品简介" }
- { name: "description", type: "String", comment: "商品详情" }
- { name: "category_id", type: "Long", comment: "分类编号" }
- { name: "brand_id", type: "Long", comment: "品牌编号" }
- { name: "pic_url", type: "String", comment: "商品封面图" }
- { name: "slider_pic_urls", type: "List<String>", comment: "轮播图" }
- { name: "status", type: "Integer", comment: "商品状态:-1回收站 0下架 1上架" }
- { name: "spec_type", type: "Boolean", comment: "规格类型false单规格 true多规格" }
- { name: "price", type: "Integer", comment: "最低价格(分)" }
- { name: "market_price", type: "Integer", comment: "市场价(分)" }
- { name: "cost_price", type: "Integer", comment: "成本价(分)" }
- { name: "stock", type: "Integer", comment: "库存总量" }
- { name: "sales_count", type: "Integer", comment: "销量" }
- { name: "delivery_types", type: "List<Integer>", comment: "配送方式" }
- { name: "delivery_template_id", type: "Long", comment: "运费模板编号" }
- name: "product_sku"
comment: "商品SKU表"
entity: "ProductSkuDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "SKU编号" }
- { name: "spu_id", type: "Long", comment: "SPU编号" }
- { name: "properties", type: "List<Property>", comment: "属性数组JSON" }
- { name: "price", type: "Integer", comment: "商品价格(分)" }
- { name: "market_price", type: "Integer", comment: "市场价(分)" }
- { name: "cost_price", type: "Integer", comment: "成本价(分)" }
- { name: "bar_code", type: "String", comment: "商品条码" }
- { name: "pic_url", type: "String", comment: "图片地址" }
- { name: "stock", type: "Integer", comment: "库存" }
- { name: "weight", type: "Double", comment: "重量kg" }
- { name: "volume", type: "Double", comment: "体积m^3" }
- { name: "first_brokerage_price", type: "Integer", comment: "一级分销佣金" }
- { name: "second_brokerage_price", type: "Integer", comment: "二级分销佣金" }
- name: "product_category"
comment: "商品分类表"
entity: "ProductCategoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "分类编号" }
- { name: "parent_id", type: "Long", comment: "父分类编号" }
- { name: "name", type: "String", comment: "分类名称" }
- { name: "pic_url", type: "String", comment: "分类图片" }
- { name: "sort", type: "Integer", comment: "排序" }
- { name: "status", type: "Integer", comment: "状态0启用 1禁用" }
- name: "product_brand"
comment: "商品品牌表"
entity: "ProductBrandDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "品牌编号" }
- { name: "name", type: "String", comment: "品牌名称" }
- { name: "pic_url", type: "String", comment: "品牌图片" }
- { name: "sort", type: "Integer", comment: "排序" }
- { name: "status", type: "Integer", comment: "状态" }
# ========== 交易模块 ==========
- name: "trade_order"
comment: "交易订单表"
entity: "TradeOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "订单编号" }
- { name: "no", type: "String", comment: "订单流水号" }
- { name: "type", type: "Integer", comment: "订单类型" }
- { name: "terminal", type: "Integer", comment: "订单来源终端" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "status", type: "Integer", comment: "订单状态" }
- { name: "product_count", type: "Integer", comment: "商品数量" }
- { name: "total_price", type: "Integer", comment: "商品原价(分)" }
- { name: "discount_price", type: "Integer", comment: "优惠金额(分)" }
- { name: "delivery_price", type: "Integer", comment: "运费金额(分)" }
- { name: "pay_price", type: "Integer", comment: "应付金额(分)" }
- { name: "pay_order_id", type: "Long", comment: "支付订单编号" }
- { name: "pay_status", type: "Boolean", comment: "是否已支付" }
- { name: "pay_time", type: "LocalDateTime", comment: "付款时间" }
- { name: "delivery_type", type: "Integer", comment: "配送方式" }
- { name: "logistics_id", type: "Long", comment: "物流公司编号" }
- { name: "logistics_no", type: "String", comment: "物流单号" }
- { name: "receiver_name", type: "String", comment: "收件人名称" }
- { name: "receiver_mobile", type: "String", comment: "收件人手机" }
- { name: "receiver_detail_address", type: "String", comment: "收件人详细地址" }
- { name: "refund_status", type: "Integer", comment: "售后状态" }
- { name: "coupon_id", type: "Long", comment: "优惠券编号" }
- { name: "coupon_price", type: "Integer", comment: "优惠券减免金额" }
- name: "trade_order_item"
comment: "交易订单项表"
entity: "TradeOrderItemDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "订单项编号" }
- { name: "order_id", type: "Long", comment: "订单编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "spu_id", type: "Long", comment: "SPU编号" }
- { name: "sku_id", type: "Long", comment: "SKU编号" }
- { name: "spu_name", type: "String", comment: "商品名称" }
- { name: "sku_name", type: "String", comment: "SKU名称" }
- { name: "price", type: "Integer", comment: "商品原价(分)" }
- { name: "count", type: "Integer", comment: "购买数量" }
- { name: "pay_price", type: "Integer", comment: "子订单实付金额" }
- { name: "after_sale_status", type: "Integer", comment: "售后状态" }
- name: "trade_after_sale"
comment: "售后订单表"
entity: "AfterSaleDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "售后编号" }
- { name: "no", type: "String", comment: "售后流水号" }
- { name: "order_id", type: "Long", comment: "订单编号" }
- { name: "order_item_id", type: "Long", comment: "订单项编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "type", type: "Integer", comment: "售后类型0退款1退货退款" }
- { name: "way", type: "Integer", comment: "退款方式" }
- { name: "status", type: "Integer", comment: "售后状态" }
- { name: "refund_price", type: "Integer", comment: "退款金额" }
- { name: "apply_reason", type: "String", comment: "申请原因" }
# ========== 促销模块 ==========
- name: "promotion_coupon_template"
comment: "优惠券模板表"
entity: "CouponTemplateDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "模板编号" }
- { name: "name", type: "String", comment: "优惠券名称" }
- { name: "status", type: "Integer", comment: "状态" }
- { name: "total_count", type: "Integer", comment: "发放数量" }
- { name: "take_limit_count", type: "Integer", comment: "每人限领数量" }
- { name: "take_type", type: "Integer", comment: "领取方式" }
- { name: "product_scope", type: "Integer", comment: "商品范围" }
- { name: "discount_type", type: "Integer", comment: "折扣类型" }
- name: "promotion_coupon"
comment: "优惠券表"
entity: "CouponDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "优惠券编号" }
- { name: "template_id", type: "Long", comment: "模板编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "status", type: "Integer", comment: "状态1未使用2已使用3已过期" }
- name: "promotion_seckill_activity"
comment: "秒杀活动表"
entity: "SeckillActivityDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "活动编号" }
- { name: "name", type: "String", comment: "活动名称" }
- { name: "spu_id", type: "Long", comment: "SPU编号" }
- { name: "status", type: "Integer", comment: "活动状态" }
- { name: "start_time", type: "LocalDateTime", comment: "开始时间" }
- { name: "end_time", type: "LocalDateTime", comment: "结束时间" }
- { name: "total_limit_count", type: "Integer", comment: "总限购数量" }
- { name: "single_limit_count", type: "Integer", comment: "单次限购数量" }
- name: "promotion_combination_activity"
comment: "拼团活动表"
entity: "CombinationActivityDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "活动编号" }
- { name: "name", type: "String", comment: "活动名称" }
- { name: "spu_id", type: "Long", comment: "SPU编号" }
- { name: "status", type: "Integer", comment: "活动状态" }
- { name: "user_size", type: "Integer", comment: "成团人数" }
- { name: "limit_duration", type: "Integer", comment: "拼团时长(小时)" }
- name: "promotion_bargain_activity"
comment: "砍价活动表"
entity: "BargainActivityDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "活动编号" }
- { name: "name", type: "String", comment: "活动名称" }
- { name: "spu_id", type: "Long", comment: "SPU编号" }
- { name: "status", type: "Integer", comment: "活动状态" }
- { name: "bargain_first_price", type: "Integer", comment: "砍价起始价格" }
- { name: "bargain_min_price", type: "Integer", comment: "砍价最低价格" }
# 表关系ER关系
relationships:
- from: "product_spu"
to: "product_sku"
type: "1:N"
foreign_key: "spu_id"
comment: "一个SPU对应多个SKU"
- from: "product_spu"
to: "product_category"
type: "N:1"
foreign_key: "category_id"
comment: "多个SPU属于一个分类"
- from: "product_spu"
to: "product_brand"
type: "N:1"
foreign_key: "brand_id"
comment: "多个SPU属于一个品牌"
- from: "trade_order"
to: "trade_order_item"
type: "1:N"
foreign_key: "order_id"
comment: "一个订单包含多个订单项"
- from: "trade_order_item"
to: "product_sku"
type: "N:1"
foreign_key: "sku_id"
comment: "订单项关联SKU"
- from: "trade_order"
to: "trade_after_sale"
type: "1:N"
foreign_key: "order_id"
comment: "一个订单可能有多个售后单"
- from: "promotion_coupon_template"
to: "promotion_coupon"
type: "1:N"
foreign_key: "template_id"
comment: "一个模板发放多张优惠券"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@RestController"
- "@RequestMapping"
- "@Tag(name = \"模块名称\")"
- "@Operation(summary = \"接口描述\")"
- "@PreAuthorize(\"@ss.hasPermission('权限标识')\")"
example: |
@Tag(name = "管理后台 - 商品 SPU")
@RestController
@RequestMapping("/product/spu")
@Validated
public class ProductSpuController {
@Resource
private ProductSpuService productSpuService;
@PostMapping("/create")
@Operation(summary = "创建商品 SPU")
@PreAuthorize("@ss.hasPermission('product:spu:create')")
public CommonResult<Long> createProductSpu(@Valid @RequestBody ProductSpuSaveReqVO createReqVO) {
return success(productSpuService.createSpu(createReqVO));
}
@GetMapping("/page")
@Operation(summary = "获得商品 SPU 分页")
@PreAuthorize("@ss.hasPermission('product:spu:query')")
public CommonResult<PageResult<ProductSpuRespVO>> getSpuPage(@Valid ProductSpuPageReqVO pageVO) {
PageResult<ProductSpuDO> pageResult = productSpuService.getSpuPage(pageVO);
return success(BeanUtils.toBean(pageResult, ProductSpuRespVO.class));
}
}
# Service层规范
service:
interface_pattern: |
public interface ProductSpuService {
Long createSpu(@Valid ProductSpuSaveReqVO createReqVO);
void updateSpu(@Valid ProductSpuSaveReqVO updateReqVO);
void deleteSpu(Long id);
ProductSpuDO getSpu(Long id);
PageResult<ProductSpuDO> getSpuPage(ProductSpuPageReqVO pageReqVO);
}
impl_pattern: |
@Service
@Validated
public class ProductSpuServiceImpl implements ProductSpuService {
@Resource
private ProductSpuMapper productSpuMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createSpu(ProductSpuSaveReqVO createReqVO) {
// 1. 校验分类、品牌
validateCategory(createReqVO.getCategoryId());
brandService.validateProductBrand(createReqVO.getBrandId());
// 2. 校验 SKU
productSkuService.validateSkuList(skuSaveReqList, createReqVO.getSpecType());
// 3. 插入 SPU
ProductSpuDO spu = BeanUtils.toBean(createReqVO, ProductSpuDO.class);
productSpuMapper.insert(spu);
// 4. 插入 SKU
productSkuService.createSkuList(spu.getId(), skuSaveReqList);
return spu.getId();
}
}
example: |
// 商品创建流程
@Transactional(rollbackFor = Exception.class)
public Long createSpu(ProductSpuSaveReqVO createReqVO) {
// 1. 校验分类、品牌
validateCategory(createReqVO.getCategoryId());
brandService.validateProductBrand(createReqVO.getBrandId());
// 2. 校验 SKU
List<ProductSkuSaveReqVO> skuSaveReqList = createReqVO.getSkus();
productSkuService.validateSkuList(skuSaveReqList, createReqVO.getSpecType());
// 3. 初始化 SPU 信息
ProductSpuDO spu = BeanUtils.toBean(createReqVO, ProductSpuDO.class);
initSpuFromSkus(spu, skuSaveReqList);
// 4. 插入 SPU
productSpuMapper.insert(spu);
// 5. 插入 SKU
productSkuService.createSkuList(spu.getId(), skuSaveReqList);
return spu.getId();
}
# 数据访问规范
dal:
mapper_pattern: |
@Mapper
public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
default PageResult<ProductSpuDO> selectPage(ProductSpuPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ProductSpuDO>()
.likeIfPresent(ProductSpuDO::getName, reqVO.getName())
.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId())
.eqIfPresent(ProductSpuDO::getStatus, reqVO.getStatus())
.orderByDesc(ProductSpuDO::getSort));
}
}
example: |
// 使用 MyBatis-Plus 查询
List<ProductSpuDO> list = productSpuMapper.selectList(
new LambdaQueryWrapper<ProductSpuDO>()
.eq(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus())
.orderByDesc(ProductSpuDO::getSort)
);
# 异常处理
error_handling:
code_prefix:
product: "1-008-xxx-xxx"
trade: "1-011-xxx-xxx"
promotion: "1-013-xxx-xxx"
examples:
- code: "1_008_005_000"
message: "商品 SPU 不存在"
module: "product"
- code: "1_008_006_004"
message: "商品 SKU 库存不足"
module: "product"
- code: "1_011_000_011"
message: "交易订单不存在"
module: "trade"
- code: "1_011_000_017"
message: "交易订单发货失败,订单不是【待发货】状态"
module: "trade"
- code: "1_013_004_000"
message: "优惠劵模板不存在"
module: "promotion"
- code: "1_013_008_006"
message: "秒杀失败,原因:秒杀库存不足"
module: "promotion"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE, args)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增业务功能(以商品为例)"
steps:
- "1. 创建 DO 实体类:在 dal/dataobject 目录下创建实体类,继承 BaseDO"
- "2. 创建 Mapper 接口:在 dal/mysql 目录下创建 Mapper继承 BaseMapperX"
- "3. 创建 Service 接口和实现:在 service 目录下创建接口和实现类"
- "4. 创建 VO 类:在 controller/vo 目录下创建请求和响应 VO"
- "5. 创建 Controller在 controller 目录下创建控制器"
- "6. 创建错误码:在 enums/ErrorCodeConstants.java 中定义错误码"
- "7. 编写单元测试:验证业务逻辑正确性"
# 新增渠道/类型示例
new_channel:
title: "新增促销活动类型"
steps:
- "1. 创建活动 DO 实体:如 XxxActivityDO关联商品和规则"
- "2. 创建 API 接口:提供活动校验、库存扣减等跨模块调用能力"
- "3. 创建 Service 层:处理活动创建、状态流转、库存管理"
- "4. 集成到订单流程:在 TradePriceService 中增加价格计算逻辑"
- "5. 集成到下单流程:在 TradeOrderUpdateService 中处理活动商品下单"
- "6. 创建管理后台接口:活动创建、编辑、上下架等"
- "7. 创建用户端接口:活动列表、详情、参与等"
# 最佳实践
best_practices:
- "商品状态管理:使用 ProductSpuStatusEnum 枚举,状态变更通过 updateSpuStatus 方法"
- "订单状态流转:严格遵循状态机模式,状态变更记录日志"
- "库存扣减:使用乐观锁或 Redis 原子操作,防止超卖"
- "价格计算:统一使用分为单位,避免浮点精度问题"
- "事务管理:涉及多表操作时使用 @Transactional 注解"
- "异常处理:使用框架提供的 exception() 方法抛出业务异常"
- "API 设计:跨模块调用通过 API 接口,避免直接依赖实现类"
- "并发控制:使用分布式锁处理秒杀、拼团等高并发场景"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-member"
api: "MemberUserApi"
purpose: "获取会员信息,用于订单关联用户"
- module: "yudao-module-pay"
api: "PayOrderApi"
purpose: "创建支付订单,处理支付回调"
- module: "yudao-module-system"
api: "AreaApi"
purpose: "获取地区信息,用于收货地址"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
version: "3.x"
purpose: "ORM 框架,简化数据库操作"
- name: "MapStruct"
version: "1.x"
purpose: "对象映射VO 与 DO 转换"
- name: "Spring Validation"
version: "2.x"
purpose: "参数校验"
- name: "Swagger/OpenAPI"
version: "3.x"
purpose: "API 文档生成"
- name: "Redis"
version: "6.x"
purpose: "缓存、分布式锁、库存扣减"
# ============================================
# 关键文件清单
# ============================================
key_files:
# 商品模块
- path: "yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/spu/ProductSpuDO.java"
purpose: "商品SPU实体类电商核心领域模型"
- path: "yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java"
purpose: "商品SKU实体类库存管理单元"
- path: "yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java"
purpose: "商品SPU服务接口定义商品CRUD操作"
- path: "yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.java"
purpose: "商品SPU管理接口后台商品管理入口"
- path: "yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java"
purpose: "商品模块错误码定义"
# 交易模块
- path: "yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java"
purpose: "交易订单实体类,订单聚合根"
- path: "yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java"
purpose: "订单更新服务,订单创建和状态流转"
- path: "yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceService.java"
purpose: "价格计算服务,订单金额计算核心"
- path: "yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleService.java"
purpose: "售后服务,退款退货处理"
- path: "yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java"
purpose: "交易模块错误码定义"
# 促销模块
- path: "yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java"
purpose: "优惠券模板实体类"
- path: "yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/seckill/SeckillActivityDO.java"
purpose: "秒杀活动实体类"
- path: "yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/combination/CombinationActivityDO.java"
purpose: "拼团活动实体类"
- path: "yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java"
purpose: "促销模块错误码定义"
# 统计模块
- path: "yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/service/trade/TradeStatisticsService.java"
purpose: "交易统计服务,订单数据汇总"

View File

@@ -0,0 +1,609 @@
# Skill 文件 - yudao-module-member 会员模块
# 用于提取模块知识的标准格式
skill:
id: "skill-member"
name: "Member Skill"
version: "1.0.0"
module_path: "yudao-module-member"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
会员中心模块负责管理C端用户的会员体系包括
1. 会员用户管理:注册、登录、个人信息管理
2. 会员等级体系:等级配置、经验累积、自动升降级
3. 积分系统:积分获取、消费、记录追踪
4. 签到功能:每日签到、连续签到奖励
5. 会员标签与分组:用户分类管理
6. 收货地址管理:多地址支持、默认地址
在整个系统中的定位:
- 作为C端用户的核心数据层与system模块的AdminUser区分
- 为mall模块提供会员信息、积分抵扣、等级折扣等支持
- 通过API接口对外暴露会员服务支持跨模块调用
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "领域驱动设计(DDD):会员(MemberUser)作为聚合根,关联等级、积分、标签等实体"
- "分层架构Controller -> Service -> DAL 清晰分层API层提供跨模块调用"
- "单一职责每个Service专注一个业务领域如MemberUserService、MemberLevelService"
- "开闭原则:通过枚举定义业务类型(MemberPointBizTypeEnum),便于扩展新的积分/经验来源"
- "事务一致性:积分变更、等级变更使用@Transactional保证数据一致性"
- "TenantBaseDO继承会员数据支持多租户隔离"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "MemberUser"
type: "聚合根"
description: "会员用户,包含账号信息、基础信息、积分、等级、标签等"
entities:
- "MemberLevelDO (会员等级)"
- "MemberGroupDO (会员分组)"
- "MemberTagDO (会员标签)"
- "MemberAddressDO (收货地址)"
value_objects:
- name: "MemberPointRecord"
type: "值对象"
description: "积分变更记录,不可变的历史记录"
- name: "MemberExperienceRecord"
type: "值对象"
description: "经验变更记录"
- name: "MemberLevelRecord"
type: "值对象"
description: "等级变更记录"
- name: "MemberSignInRecord"
type: "值对象"
description: "签到记录"
services:
- name: "MemberUserService"
description: "会员用户管理服务"
- name: "MemberAuthService"
description: "会员认证服务登录、登出、token刷新"
- name: "MemberLevelService"
description: "会员等级服务(等级配置、经验管理、自动升级)"
- name: "MemberPointRecordService"
description: "积分记录服务"
- name: "MemberSignInRecordService"
description: "签到服务"
- name: "AddressService"
description: "收货地址服务"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口供其他模块调用"
components:
- "MemberUserApi - 会员用户查询接口"
- "MemberPointApi - 积分增减接口"
- "MemberLevelApi - 等级查询、经验增减接口"
- "MemberAddressApi - 收货地址查询接口"
- "MemberConfigApi - 会员配置查询接口"
- name: "controller"
purpose: "HTTP接口分为admin(管理后台)和app(C端应用)"
components:
- "admin/user/MemberUserController - 会员用户管理"
- "admin/level/MemberLevelController - 等级配置管理"
- "admin/point/MemberPointRecordController - 积分记录查询"
- "admin/signin/MemberSignInConfigController - 签到规则配置"
- "admin/tag/MemberTagController - 标签管理"
- "admin/group/MemberGroupController - 分组管理"
- "app/auth/AppAuthController - C端认证登录、注册、登出"
- "app/address/AppAddressController - C端收货地址"
- "app/signin/AppMemberSignInRecordController - C端签到"
- name: "service"
purpose: "业务逻辑层"
components:
- "user/MemberUserService - 会员用户核心服务"
- "auth/MemberAuthService - 认证服务"
- "level/MemberLevelService - 等级服务"
- "level/MemberLevelRecordService - 等级变更记录"
- "level/MemberExperienceRecordService - 经验记录"
- "point/MemberPointRecordService - 积分记录"
- "signin/MemberSignInRecordService - 签到服务"
- "signin/MemberSignInConfigService - 签到配置"
- "tag/MemberTagService - 标签服务"
- "group/MemberGroupService - 分组服务"
- "address/AddressService - 地址服务"
- "config/MemberConfigService - 配置服务"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject/user/MemberUserDO - 会员用户DO"
- "dataobject/level/MemberLevelDO - 等级DO"
- "dataobject/level/MemberLevelRecordDO - 等级记录DO"
- "dataobject/level/MemberExperienceRecordDO - 经验记录DO"
- "dataobject/point/MemberPointRecordDO - 积分记录DO"
- "dataobject/signin/MemberSignInConfigDO - 签到配置DO"
- "dataobject/signin/MemberSignInRecordDO - 签到记录DO"
- "dataobject/tag/MemberTagDO - 标签DO"
- "dataobject/group/MemberGroupDO - 分组DO"
- "dataobject/address/MemberAddressDO - 地址DO"
- "dataobject/config/MemberConfigDO - 配置DO"
# 设计模式应用
design_patterns:
- pattern: "DTO转换模式"
location: "api/*/dto/*.java"
purpose: "API层使用DTO隔离内部DO保护领域模型"
- pattern: "策略模式(枚举实现)"
location: "enums/point/MemberPointBizTypeEnum.java"
purpose: "定义积分业务类型,支持多种积分来源"
- pattern: "模板方法"
location: "service/level/MemberLevelServiceImpl.java"
purpose: "等级变更流程:记录变更 -> 更新经验 -> 计算新等级 -> 通知用户"
- pattern: "Facade模式"
location: "api/*ApiImpl.java"
purpose: "API实现类封装Service调用简化跨模块交互"
- pattern: "观察者模式(MQ)"
location: "mq/producer/user/MemberUserProducer.java"
purpose: "用户创建后发送消息,解耦后续处理逻辑"
# 模块间通信
communication:
apis:
- name: "MemberUserApi"
methods: ["getUser", "getUserList", "getUserByMobile", "validateUser"]
consumers: ["pay", "mall-trade", "mall-promotion"]
- name: "MemberPointApi"
methods: ["addPoint", "reducePoint"]
consumers: ["mall-trade", "mall-promotion"]
- name: "MemberLevelApi"
methods: ["getMemberLevel", "addExperience", "reduceExperience"]
consumers: ["mall-trade"]
- name: "MemberAddressApi"
methods: ["getAddress", "getDefaultAddress"]
consumers: ["mall-trade"]
- name: "MemberConfigApi"
methods: ["getConfig"]
consumers: ["mall-trade"]
consumers:
- module: "system"
api: "SmsCodeApi"
purpose: "短信验证码校验"
- module: "system"
api: "SocialClientApi"
purpose: "社交登录、微信小程序登录"
- module: "infra"
api: "文件存储"
purpose: "头像上传"
mq:
- name: "MemberUserCreateMessage"
direction: "producer"
purpose: "用户创建后发送消息,通知其他模块"
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "TenantBaseDO"
description: "会员核心表(MemberUserDO)继承TenantBaseDO支持多租户其他表继承BaseDO"
# 核心数据表
tables:
- name: "member_user"
comment: "会员用户表"
entity: "MemberUserDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "Long", comment: "用户ID" }
- { name: "mobile", type: "String", comment: "手机号(唯一索引)" }
- { name: "password", type: "String", comment: "加密密码" }
- { name: "status", type: "Integer", comment: "状态(0启用1禁用)" }
- { name: "register_ip", type: "String", comment: "注册IP" }
- { name: "register_terminal", type: "Integer", comment: "注册终端" }
- { name: "login_ip", type: "String", comment: "最后登录IP" }
- { name: "login_date", type: "LocalDateTime", comment: "最后登录时间" }
- { name: "nickname", type: "String", comment: "昵称" }
- { name: "avatar", type: "String", comment: "头像" }
- { name: "name", type: "String", comment: "真实姓名" }
- { name: "sex", type: "Integer", comment: "性别" }
- { name: "birthday", type: "LocalDateTime", comment: "生日" }
- { name: "area_id", type: "Integer", comment: "地区ID" }
- { name: "mark", type: "String", comment: "用户备注" }
- { name: "point", type: "Integer", comment: "当前积分" }
- { name: "tag_ids", type: "List<Long>", comment: "标签ID列表(JSON存储)" }
- { name: "level_id", type: "Long", comment: "等级ID" }
- { name: "experience", type: "Integer", comment: "当前经验" }
- { name: "group_id", type: "Long", comment: "分组ID" }
indexes:
- { name: "uk_mobile", columns: ["mobile"] }
- name: "member_level"
comment: "会员等级表"
entity: "MemberLevelDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "等级ID" }
- { name: "name", type: "String", comment: "等级名称" }
- { name: "level", type: "Integer", comment: "等级值(用于排序)" }
- { name: "experience", type: "Integer", comment: "升级所需经验" }
- { name: "discount_percent", type: "Integer", comment: "享受折扣百分比" }
- { name: "icon", type: "String", comment: "等级图标" }
- { name: "background_url", type: "String", comment: "等级背景图" }
- { name: "status", type: "Integer", comment: "状态(0启用1禁用)" }
- name: "member_point_record"
comment: "积分记录表"
entity: "MemberPointRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "记录ID" }
- { name: "user_id", type: "Long", comment: "用户ID" }
- { name: "biz_id", type: "String", comment: "业务编码" }
- { name: "biz_type", type: "Integer", comment: "业务类型(枚举)" }
- { name: "title", type: "String", comment: "积分标题" }
- { name: "description", type: "String", comment: "积分描述" }
- { name: "point", type: "Integer", comment: "变动积分(正负)" }
- { name: "total_point", type: "Integer", comment: "变动后积分" }
- name: "member_experience_record"
comment: "经验记录表"
entity: "MemberExperienceRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "记录ID" }
- { name: "user_id", type: "Long", comment: "用户ID" }
- { name: "biz_type", type: "Integer", comment: "业务类型(枚举)" }
- { name: "biz_id", type: "String", comment: "业务编号" }
- { name: "title", type: "String", comment: "标题" }
- { name: "description", type: "String", comment: "描述" }
- { name: "experience", type: "Integer", comment: "变动经验(正负)" }
- { name: "total_experience", type: "Integer", comment: "变动后经验" }
- name: "member_level_record"
comment: "等级变更记录表"
entity: "MemberLevelRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "记录ID" }
- { name: "user_id", type: "Long", comment: "用户ID" }
- { name: "level_id", type: "Long", comment: "等级ID" }
- { name: "level", type: "Integer", comment: "等级值(冗余)" }
- { name: "discount_percent", type: "Integer", comment: "折扣(冗余)" }
- { name: "experience", type: "Integer", comment: "升级经验(冗余)" }
- { name: "user_experience", type: "Integer", comment: "用户变更后经验" }
- { name: "remark", type: "String", comment: "备注" }
- { name: "description", type: "String", comment: "描述" }
- name: "member_sign_in_config"
comment: "签到配置表"
entity: "MemberSignInConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "配置ID" }
- { name: "day", type: "Integer", comment: "签到第N天" }
- { name: "point", type: "Integer", comment: "奖励积分" }
- { name: "experience", type: "Integer", comment: "奖励经验" }
- { name: "status", type: "Integer", comment: "状态" }
- name: "member_sign_in_record"
comment: "签到记录表"
entity: "MemberSignInRecordDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "记录ID" }
- { name: "user_id", type: "Long", comment: "用户ID" }
- { name: "day", type: "Integer", comment: "第几天签到" }
- { name: "point", type: "Integer", comment: "签到获得积分" }
- { name: "experience", type: "Integer", comment: "签到获得经验" }
- name: "member_tag"
comment: "会员标签表"
entity: "MemberTagDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "标签ID" }
- { name: "name", type: "String", comment: "标签名称" }
- name: "member_group"
comment: "会员分组表"
entity: "MemberGroupDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "分组ID" }
- { name: "name", type: "String", comment: "分组名称" }
- { name: "remark", type: "String", comment: "备注" }
- { name: "status", type: "Integer", comment: "状态" }
- name: "member_address"
comment: "收货地址表"
entity: "MemberAddressDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "地址ID" }
- { name: "user_id", type: "Long", comment: "用户ID" }
- { name: "name", type: "String", comment: "收件人名称" }
- { name: "mobile", type: "String", comment: "手机号" }
- { name: "area_id", type: "Long", comment: "地区ID" }
- { name: "detail_address", type: "String", comment: "详细地址" }
- { name: "default_status", type: "Boolean", comment: "是否默认" }
- name: "member_config"
comment: "会员配置表"
entity: "MemberConfigDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "配置ID" }
- { name: "point_trade_deduct_enable", type: "Boolean", comment: "积分抵扣开关" }
- { name: "point_trade_deduct_unit_price", type: "Integer", comment: "积分抵扣单位(1积分=多少分)" }
- { name: "point_trade_deduct_max_price", type: "Integer", comment: "积分抵扣上限(分)" }
- { name: "point_trade_give_point", type: "Integer", comment: "1元赠送积分" }
# 表关系ER关系
relationships:
- from: "member_user"
to: "member_level"
type: "N:1"
foreign_key: "level_id"
- from: "member_user"
to: "member_group"
type: "N:1"
foreign_key: "group_id"
- from: "member_user"
to: "member_tag"
type: "N:N"
foreign_key: "tag_ids(JSON数组)"
- from: "member_point_record"
to: "member_user"
type: "N:1"
foreign_key: "user_id"
- from: "member_experience_record"
to: "member_user"
type: "N:1"
foreign_key: "user_id"
- from: "member_level_record"
to: "member_user"
type: "N:1"
foreign_key: "user_id"
- from: "member_level_record"
to: "member_level"
type: "N:1"
foreign_key: "level_id"
- from: "member_sign_in_record"
to: "member_user"
type: "N:1"
foreign_key: "user_id"
- from: "member_address"
to: "member_user"
type: "N:1"
foreign_key: "user_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - XXX') - Swagger分组"
- "@RestController @RequestMapping('/member/xxx')"
- "@PreAuthorize('@ss.hasPermission(\\'member:xxx:action\\')') - 权限控制"
- "@PermitAll - 无需登录(如登录接口)"
example: |
@Tag(name = "管理后台 - 会员用户")
@RestController
@RequestMapping("/member/user")
@Validated
public class MemberUserController {
@Resource
private MemberUserService memberUserService;
@PutMapping("/update-point")
@Operation(summary = "更新会员用户积分")
@PreAuthorize("@ss.hasPermission('member:user:update-point')")
public CommonResult<Boolean> updateUserPoint(@Valid @RequestBody MemberUserUpdatePointReqVO updateReqVO) {
memberPointRecordService.createPointRecord(updateReqVO.getId(), updateReqVO.getPoint(),
MemberPointBizTypeEnum.ADMIN, String.valueOf(getLoginUserId()));
return success(true);
}
}
# Service层规范
service:
interface_pattern: "定义在service包下以Service结尾"
impl_pattern: "实现类以ServiceImpl结尾使用@Service注解"
example: |
// Service接口
public interface MemberPointRecordService {
void createPointRecord(Long userId, Integer point, MemberPointBizTypeEnum bizType, String bizId);
}
// Service实现
@Service
@Validated
public class MemberPointRecordServiceImpl implements MemberPointRecordService {
@Resource
private MemberPointRecordMapper memberPointRecordMapper;
@Resource
private MemberUserService memberUserService;
@Override
@Transactional(rollbackFor = Exception.class)
public void createPointRecord(Long userId, Integer point, MemberPointBizTypeEnum bizType, String bizId) {
// 1. 校验用户积分余额
MemberUserDO user = memberUserService.getUser(userId);
int totalPoint = ObjectUtil.defaultIfNull(user.getPoint(), 0) + point;
if (totalPoint < 0) {
throw exception(USER_POINT_NOT_ENOUGH);
}
// 2. 更新用户积分
memberUserService.updateUserPoint(userId, point);
// 3. 增加积分记录
MemberPointRecordDO record = new MemberPointRecordDO()
.setUserId(userId).setBizId(bizId).setBizType(bizType.getType())
.setPoint(point).setTotalPoint(totalPoint);
memberPointRecordMapper.insert(record);
}
}
# 数据访问规范
dal:
mapper_pattern: "继承BaseMapperX在mysql包下"
example: |
@Mapper
public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
default MemberUserDO selectByMobile(String mobile) {
return selectOne(MemberUserDO::getMobile, mobile);
}
default int updatePointIncr(Long id, Integer point) {
return update(new MemberUserDO().setPoint(point),
new LambdaQueryWrapper<MemberUserDO>().eq(MemberUserDO::getId, id));
}
}
# 异常处理
error_handling:
code_prefix: "1_004_XXX_XXX"
examples:
- code: "1_004_001_000"
message: "用户不存在"
- code: "1_004_001_003"
message: "用户积分余额不足"
- code: "1_004_003_000"
message: "登录失败,账号密码不正确"
- code: "1_004_006_000"
message: "用户标签不存在"
- code: "1_004_008_000"
message: "用户积分记录业务类型不支持"
- code: "1_004_010_000"
message: "今日已签到,请勿重复签到"
- code: "1_004_011_000"
message: "用户等级不存在"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE, args...)"
# 枚举定义
enums:
- name: "MemberPointBizTypeEnum"
description: "积分业务类型"
values:
- { type: 1, name: "SIGN", desc: "签到", add: true }
- { type: 2, name: "ADMIN", desc: "管理员修改", add: true }
- { type: 11, name: "ORDER_USE", desc: "订单积分抵扣", add: false }
- { type: 21, name: "ORDER_GIVE", desc: "订单积分奖励", add: true }
- name: "MemberExperienceBizTypeEnum"
description: "经验业务类型"
values:
- { type: 0, name: "ADMIN", desc: "管理员调整", add: true }
- { type: 1, name: "INVITE_REGISTER", desc: "邀新奖励", add: true }
- { type: 4, name: "SIGN_IN", desc: "签到奖励", add: true }
- { type: 11, name: "ORDER_GIVE", desc: "下单奖励", add: true }
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增会员权益功能"
steps:
- "1. 在dal/dataobject下创建MemberBenefitDO实体类"
- "2. 创建MemberBenefitMapper接口"
- "3. 创建MemberBenefitService接口和实现类"
- "4. 创建admin和app的Controller"
- "5. 在ErrorCodeConstants中添加错误码"
- "6. 如需对外暴露API在api包下创建MemberBenefitApi"
# 新增会员等级步骤
new_channel:
title: "新增会员等级"
steps:
- "1. 通过管理后台或直接插入member_level表"
- "2. 设置等级名称、等级值、升级所需经验"
- "3. 配置享受折扣百分比"
- "4. 上传等级图标和背景图"
- "5. 确保升级经验值在相邻等级之间"
# 新增积分来源
new_point_source:
title: "新增积分来源类型"
steps:
- "1. 在MemberPointBizTypeEnum中添加新的枚举值"
- "2. 设置type(唯一)、name、description、add(是否增加)"
- "3. 在对应业务场景调用MemberPointApi.addPoint或reducePoint"
- "4. 示例MemberPointApi.addPoint(userId, 10, MemberPointBizTypeEnum.NEW_TYPE.getType(), bizId)"
# 最佳实践
best_practices:
- "积分变更使用MemberPointApi保证记录完整性"
- "经验变更使用MemberLevelApi自动触发等级计算"
- "用户创建后发送MQ消息便于其他模块处理"
- "等级经验配置需确保递增顺序,系统会自动校验"
- "积分抵扣配置通过MemberConfigApi获取支持动态调整"
- "签到规则配置支持连续签到奖励递增"
- "地址管理支持多地址,需处理默认地址切换逻辑"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "system"
api: "SmsCodeApi"
purpose: "短信验证码校验(登录、修改手机、重置密码)"
- module: "system"
api: "SocialClientApi"
purpose: "社交登录(微信、QQ等)、微信小程序登录"
- module: "infra"
api: "文件存储"
purpose: "头像上传存储"
# 外部依赖(第三方库)
external:
- name: "spring-security-crypto"
version: "Spring Boot自带"
purpose: "密码加密(BCryptPasswordEncoder)"
- name: "mybatis-plus"
version: "3.x"
purpose: "ORM框架数据访问增强"
- name: "hutool"
version: "5.x"
purpose: "工具类库"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java"
purpose: "会员用户实体,聚合根"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java"
purpose: "会员用户服务接口"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelService.java"
purpose: "会员等级服务,含经验管理和自动升级"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/api/point/MemberPointApi.java"
purpose: "积分API接口供其他模块调用"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/enums/point/MemberPointBizTypeEnum.java"
purpose: "积分业务类型枚举"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java"
purpose: "C端认证控制器"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/signin/MemberSignInRecordService.java"
purpose: "签到服务"
- path: "yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/config/MemberConfigDO.java"
purpose: "会员配置(积分抵扣设置)"

View File

@@ -0,0 +1,964 @@
# Skill 文件 - yudao-module-mes 模块
# 制造执行系统模块知识提取
skill:
id: "skill-mes"
name: "Manufacturing Execution System Skill"
version: "1.0.0"
module_path: "yudao-module-mes"
created_at: "2026-06-08"
updated_at: "2026-06-08"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
mes 模块是制造执行系统MES连接 ERP 计划层与车间执行层,实现生产过程的数字化管理:
1. 主数据管理 - 物料、BOM、工艺路线、SOP/SIP、客户/供应商、车间/工位、自动编码规则
2. 生产管理 - 工单(草稿->确认->完工/取消)、工序任务(甘特图)、流转卡、报工反馈、安灯呼叫
3. 质量管理 - 检验模板、检验指标、缺陷管理、IQC/IPQC/OQC/RQC 四大检验
4. 设备管理 - 设备台账、点检计划/记录、保养记录、维修管理
5. 工具管理 - 工具台账、工具类型
6. 仓库管理 - 仓库/库区/库位、物料库存、批次、条码/SN、各类出入库、调拨、盘点
7. 排班日历 - 工厂日历、节假日、排班计划、班组管理
8. 首页看板 - 订单汇总、工单状态、生产趋势统计
定位:面向离散制造和流程制造,提供从生产计划下达到成品入库的全流程管理。
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- principle: "状态机模式"
description: "工单/流转卡/报工/维修/调拨/排班计划等核心实体使用状态机管理状态流转,变更前校验前置状态,使用 CAS 防止并发覆盖"
- principle: "策略模式"
description: "自动编码规则支持日期、固定字符、输入字符、序列号等零件组合,不同零件类型有不同的生成策略"
- principle: "VO Assembly 模式"
description: "查询接口批量获取关联实体后组装 VO避免 N+1 查询,提升查询性能"
- principle: "标准 CRUD 模式"
description: "全部 Controller/Service/Mapper 遵循统一的增删改查规范,配合 @PreAuthorize 权限控制"
- principle: "主子表结构"
description: "BOM、检验单、出入库单、维修单等使用主子表结构子表通过外键关联主表"
- principle: "Oracle/PG 兼容"
description: "使用 @KeySequence 注解兼容 Oracle 和 PostgreSQL 的主键生成方式"
- principle: "跨模块 API 集成"
description: "通过 AdminUserApi、RoleApi 等接口与 system 模块集成,不直接依赖实现类"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "物料聚合"
type: "聚合根"
entities: ["ItemDO", "ItemBatchConfigDO"]
description: "ItemDO 是物料聚合根BatchConfigDO 定义物料的批次生成规则"
- name: "BOM 聚合"
type: "聚合根"
entities: ["ProductBomDO", "Bom子表"]
description: "ProductBomDO 是 BOM 聚合根,定义产品由哪些子物料组成及用量"
- name: "工艺路线聚合"
type: "聚合根"
entities: ["RouteDO", "RouteProcessDO", "RouteProductDO"]
description: "RouteDO 是工艺路线聚合根,包含工序列表和适用产品"
- name: "工单聚合"
type: "聚合根"
entities: ["WorkOrderDO", "WorkOrderBomDO"]
description: "WorkOrderDO 是工单聚合根,关联 BOM 和工艺路线"
- name: "任务聚合"
type: "聚合根"
entities: ["TaskDO", "TaskIssueDO"]
description: "TaskDO 是生产任务聚合根,关联工序、工位、设备、操作员"
- name: "流转卡聚合"
type: "聚合根"
entities: ["CardDO", "CardProcessDO"]
description: "CardDO 是流转卡聚合根,跟踪各工序的执行情况"
- name: "检验单聚合"
type: "聚合根"
entities: ["QcIqcDO/QcIpqcDO/QcOqcDO/QcRqcDO", "检验行"]
description: "四大检验单各自为聚合根,主子表结构"
- name: "设备聚合"
type: "聚合根"
entities: ["MachineryDO", "CheckPlanDO", "CheckRecordDO", "MaintenRecordDO", "RepairDO"]
description: "MachineryDO 是设备聚合根,关联点检、保养、维修"
- name: "仓库聚合"
type: "聚合根"
entities: ["WarehouseDO", "WarehouseAreaDO", "WarehouseLocationDO"]
description: "WarehouseDO 是仓库聚合根,包含库区和库位的层级结构"
- name: "库存聚合"
type: "聚合根"
entities: ["MaterialStockDO", "BatchDO", "BarcodeDO", "SnDO"]
description: "MaterialStockDO 是库存聚合根,按物料/仓库/库位/批次维度管理"
- name: "排班聚合"
type: "聚合根"
entities: ["CalPlanDO", "CalPlanShiftDO", "CalPlanTeamDO", "CalTeamDO"]
description: "CalPlanDO 是排班计划聚合根,关联班次和班组"
- name: "自动编码聚合"
type: "聚合根"
entities: ["AutoCodeRuleDO", "AutoCodePartDO", "AutoCodeRecordDO"]
description: "AutoCodeRuleDO 是编码规则聚合根,由零件组合定义编码格式"
services:
- name: "ItemService"
description: "物料管理服务,提供物料的增删改查"
- name: "ProductBomService"
description: "BOM 管理服务,管理产品物料清单"
- name: "RouteService"
description: "工艺路线服务,管理生产工序流程"
- name: "WorkOrderService"
description: "工单管理服务,工单状态机流转"
- name: "TaskService"
description: "生产任务服务,支持甘特图排产"
- name: "CardService"
description: "流转卡服务,跟踪生产工序执行"
- name: "FeedbackService"
description: "报工反馈服务,提交/审批报工"
- name: "QcIqcService/QcIpqcService/QcOqcService/QcRqcService"
description: "四大检验服务,管理来料/过程/出货/退货检验"
- name: "MachineryService"
description: "设备管理服务,设备台账维护"
- name: "RepairService"
description: "维修管理服务,维修单状态流转"
- name: "WarehouseService"
description: "仓库管理服务,仓库/库区/库位层级管理"
- name: "MaterialStockService"
description: "库存管理服务,出入库、调拨、盘点"
- name: "AutoCodeService"
description: "自动编码服务,根据规则生成业务单号"
- name: "CalPlanService"
description: "排班计划服务,工厂日历和班组排班"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "controller"
purpose: "HTTP 接口层,提供 RESTful API"
components:
- "admin/home - 首页看板接口"
- "admin/cal - 排班日历接口cal/holiday, cal/plan, cal/team 等)"
- "admin/md - 主数据接口item, bom, sop, sip, client, vendor, workshop, workstation, auto-code"
- "admin/pro - 生产管理接口work-order, task, route, process, card, feedback, work-record, andon"
- "admin/qc - 质量管理接口template, indicator, defect, iqc, ipqc, oqc, rqc, pending-inspect"
- "admin/dv - 设备管理接口machinery, machinery-type, check-plan, check-record, mainten-record, repair, subject"
- "admin/tm - 工具管理接口tool, tool-type"
- "admin/wm - 仓库管理接口warehouse, area, location, material-stock, batch, barcode, sn, arrival-notice, item-receipt, product-receipt, product-issue, return-issue, misc-issue, misc-receipt, product-sales, return-sales, outsource-issue, outsource-receipt, return-vendor, transfer, package, stock-taking, sales-notice"
- name: "service"
purpose: "业务逻辑层"
components:
- "md - 主数据服务ItemService, ProductBomService, RouteService, WorkshopService, WorkstationService, AutoCodeService"
- "pro - 生产服务WorkOrderService, TaskService, CardService, FeedbackService, WorkRecordService, AndonService"
- "qc - 质量服务QcTemplateService, QcIndicatorService, QcDefectService, QcIqcService, QcIpqcService, QcOqcService, QcRqcService"
- "dv - 设备服务MachineryService, CheckPlanService, CheckRecordService, MaintenRecordService, RepairService"
- "tm - 工具服务ToolService"
- "wm - 仓库服务WarehouseService, MaterialStockService, BatchService, BarcodeService, 各出入库Service, TransferService, PackageService, StockTakingService"
- "cal - 日历服务CalPlanService, CalHolidayService, CalTeamService"
- "home - 首页服务HomeService"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject - DO 实体类定义按域组织md/pro/qc/dv/tm/wm/cal"
- "mysql - MyBatis Mapper 接口,继承 BaseMapperX"
- name: "enums"
purpose: "枚举和错误码"
components:
- "MesBizTypeConstants - 集中管理业务类型常量"
- "各状态枚举 - WorkOrderStatusEnum, CardStatusEnum, FeedbackStatusEnum, RepairStatusEnum 等"
- "ErrorCodeConstants - 错误码常量定义"
# 设计模式应用
design_patterns:
- pattern: "状态机模式 (State Machine Pattern)"
location: "service 层各核心实体的状态管理"
purpose: "工单/流转卡/报工/维修/调拨/排班计划的状态流转管理,变更前校验前置状态"
- pattern: "策略模式 (Strategy Pattern)"
location: "service/md/AutoCodeService"
purpose: "自动编码规则支持日期、固定字符、输入字符、序列号等零件组合,不同零件类型有不同的生成策略"
- pattern: "VO Assembly 模式"
location: "controller 层的 get/page 接口"
purpose: "批量获取关联实体后组装 VO避免 N+1 查询"
- pattern: "标准 CRUD 模式"
location: "全部 Controller/Service/Mapper"
purpose: "统一的增删改查 + @PreAuthorize 权限控制"
- pattern: "主子表模式"
location: "BOM、检验单、出入库单、维修单等"
purpose: "主表记录基本信息,子表记录明细,通过外键关联"
- pattern: "CAS 更新模式"
location: "状态变更、库存扣减"
purpose: "使用 WHERE status = #{oldStatus} 防止并发覆盖"
# 模块间通信
communication:
apis:
- name: "AdminUserApi"
method: "getUser()"
description: "获取用户信息,用于工位操作员、报工人等关联"
- name: "RoleApi"
method: "hasAnyRole()"
description: "角色权限校验"
consumers:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取用户信息(工位操作员名称、报工人名称等)"
- module: "yudao-module-system"
api: "RoleApi"
purpose: "角色权限校验"
mq: []
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有 DO 继承 BaseDO包含 id, creator, createTime, updater, updateTime, deleted 字段。部分表使用 @KeySequence 注解兼容 Oracle/PostgreSQL。"
# 核心数据表(按域组织)
tables:
# ========== 主数据域 (MD) - 17 张表 ==========
- name: "mes_md_item"
comment: "物料表"
entity: "MesItemDO"
domain: "MD"
- name: "mes_md_item_type"
comment: "物料分类表"
entity: "MesItemTypeDO"
domain: "MD"
- name: "mes_md_item_batch_config"
comment: "物料批次配置表"
entity: "MesItemBatchConfigDO"
domain: "MD"
- name: "mes_md_product_bom"
comment: "产品 BOM 表"
entity: "MesProductBomDO"
domain: "MD"
- name: "mes_md_product_sip"
comment: "产品 SIP 表(标准检验程序)"
entity: "MesProductSipDO"
domain: "MD"
- name: "mes_md_product_sop"
comment: "产品 SOP 表(标准作业指导书)"
entity: "MesProductSopDO"
domain: "MD"
- name: "mes_md_client"
comment: "客户表"
entity: "MesClientDO"
domain: "MD"
- name: "mes_md_vendor"
comment: "供应商表"
entity: "MesVendorDO"
domain: "MD"
- name: "mes_md_unit_measure"
comment: "计量单位表"
entity: "MesUnitMeasureDO"
domain: "MD"
- name: "mes_md_workshop"
comment: "车间表"
entity: "MesWorkshopDO"
domain: "MD"
- name: "mes_md_workstation"
comment: "工位表"
entity: "MesWorkstationDO"
domain: "MD"
- name: "mes_md_workstation_machine"
comment: "工位机器关联表"
entity: "MesWorkstationMachineDO"
domain: "MD"
- name: "mes_md_workstation_tool"
comment: "工位工具关联表"
entity: "MesWorkstationToolDO"
domain: "MD"
- name: "mes_md_workstation_worker"
comment: "工位工人关联表"
entity: "MesWorkstationWorkerDO"
domain: "MD"
- name: "mes_md_auto_code_rule"
comment: "自动编码规则表"
entity: "MesAutoCodeRuleDO"
domain: "MD"
- name: "mes_md_auto_code_part"
comment: "编码规则零件表"
entity: "MesAutoCodePartDO"
domain: "MD"
- name: "mes_md_auto_code_record"
comment: "编码记录表"
entity: "MesAutoCodeRecordDO"
domain: "MD"
# ========== 生产域 (PRO) - 17 张表 ==========
- name: "mes_pro_work_order"
comment: "生产工单表"
entity: "MesWorkOrderDO"
domain: "PRO"
- name: "mes_pro_work_order_bom"
comment: "工单 BOM 表"
entity: "MesWorkOrderBomDO"
domain: "PRO"
- name: "mes_pro_task"
comment: "生产任务表"
entity: "MesTaskDO"
domain: "PRO"
- name: "mes_pro_task_issue"
comment: "任务领料表"
entity: "MesTaskIssueDO"
domain: "PRO"
- name: "mes_pro_route"
comment: "工艺路线表"
entity: "MesRouteDO"
domain: "PRO"
- name: "mes_pro_route_process"
comment: "工艺路线工序表"
entity: "MesRouteProcessDO"
domain: "PRO"
- name: "mes_pro_route_product"
comment: "工艺路线产品关联表"
entity: "MesRouteProductDO"
domain: "PRO"
- name: "mes_pro_route_product_bom"
comment: "工艺路线产品 BOM 关联表"
entity: "MesRouteProductBomDO"
domain: "PRO"
- name: "mes_pro_process"
comment: "工序表"
entity: "MesProcessDO"
domain: "PRO"
- name: "mes_pro_process_content"
comment: "工序内容表"
entity: "MesProcessContentDO"
domain: "PRO"
- name: "mes_pro_card"
comment: "流转卡表"
entity: "MesCardDO"
domain: "PRO"
- name: "mes_pro_card_process"
comment: "流转卡工序表"
entity: "MesCardProcessDO"
domain: "PRO"
- name: "mes_pro_feedback"
comment: "报工反馈表"
entity: "MesFeedbackDO"
domain: "PRO"
- name: "mes_pro_work_record"
comment: "报工记录表"
entity: "MesWorkRecordDO"
domain: "PRO"
- name: "mes_pro_work_record_log"
comment: "报工日志表"
entity: "MesWorkRecordLogDO"
domain: "PRO"
- name: "mes_pro_andon_config"
comment: "安灯配置表"
entity: "MesAndonConfigDO"
domain: "PRO"
- name: "mes_pro_andon_record"
comment: "安灯记录表"
entity: "MesAndonRecordDO"
domain: "PRO"
# ========== 质量域 (QC) - 17 张表 ==========
- name: "mes_qc_template"
comment: "检验模板表"
entity: "MesQcTemplateDO"
domain: "QC"
- name: "mes_qc_template_item"
comment: "检验模板检验项表"
entity: "MesQcTemplateItemDO"
domain: "QC"
- name: "mes_qc_template_indicator"
comment: "检验模板指标表"
entity: "MesQcTemplateIndicatorDO"
domain: "QC"
- name: "mes_qc_indicator"
comment: "检验指标表"
entity: "MesQcIndicatorDO"
domain: "QC"
- name: "mes_qc_indicator_result"
comment: "指标结果表"
entity: "MesQcIndicatorResultDO"
domain: "QC"
- name: "mes_qc_indicator_result_detail"
comment: "指标结果明细表"
entity: "MesQcIndicatorResultDetailDO"
domain: "QC"
- name: "mes_qc_defect"
comment: "缺陷类型表"
entity: "MesQcDefectDO"
domain: "QC"
- name: "mes_qc_defect_record"
comment: "缺陷记录表"
entity: "MesQcDefectRecordDO"
domain: "QC"
- name: "mes_qc_iqc"
comment: "来料检验单表"
entity: "MesQcIqcDO"
domain: "QC"
- name: "mes_qc_iqc_line"
comment: "来料检验行表"
entity: "MesQcIqcLineDO"
domain: "QC"
- name: "mes_qc_ipqc"
comment: "过程检验单表"
entity: "MesQcIpqcDO"
domain: "QC"
- name: "mes_qc_ipqc_line"
comment: "过程检验行表"
entity: "MesQcIpqcLineDO"
domain: "QC"
- name: "mes_qc_oqc"
comment: "出货检验单表"
entity: "MesQcOqcDO"
domain: "QC"
- name: "mes_qc_oqc_line"
comment: "出货检验行表"
entity: "MesQcOqcLineDO"
domain: "QC"
- name: "mes_qc_rqc"
comment: "退货检验单表"
entity: "MesQcRqcDO"
domain: "QC"
- name: "mes_qc_rqc_line"
comment: "退货检验行表"
entity: "MesQcRqcLineDO"
domain: "QC"
# ========== 设备域 (DV) - 12 张表 ==========
- name: "mes_dv_machinery"
comment: "设备台账表"
entity: "MesMachineryDO"
domain: "DV"
- name: "mes_dv_machinery_type"
comment: "设备类型表"
entity: "MesMachineryTypeDO"
domain: "DV"
- name: "mes_dv_check_plan"
comment: "点检计划表"
entity: "MesCheckPlanDO"
domain: "DV"
- name: "mes_dv_check_plan_machinery"
comment: "点检计划设备关联表"
entity: "MesCheckPlanMachineryDO"
domain: "DV"
- name: "mes_dv_check_plan_subject"
comment: "点检计划检查项关联表"
entity: "MesCheckPlanSubjectDO"
domain: "DV"
- name: "mes_dv_check_record"
comment: "点检记录表"
entity: "MesCheckRecordDO"
domain: "DV"
- name: "mes_dv_check_record_line"
comment: "点检记录行表"
entity: "MesCheckRecordLineDO"
domain: "DV"
- name: "mes_dv_mainten_record"
comment: "保养记录表"
entity: "MesMaintenRecordDO"
domain: "DV"
- name: "mes_dv_mainten_record_line"
comment: "保养记录行表"
entity: "MesMaintenRecordLineDO"
domain: "DV"
- name: "mes_dv_repair"
comment: "维修单表"
entity: "MesRepairDO"
domain: "DV"
- name: "mes_dv_repair_line"
comment: "维修行表"
entity: "MesRepairLineDO"
domain: "DV"
- name: "mes_dv_subject"
comment: "检查项目表"
entity: "MesSubjectDO"
domain: "DV"
# ========== 工具域 (TM) - 2 张表 ==========
- name: "mes_tm_tool"
comment: "工具台账表"
entity: "MesToolDO"
domain: "TM"
- name: "mes_tm_tool_type"
comment: "工具类型表"
entity: "MesToolTypeDO"
domain: "TM"
# ========== 仓库域 (WM) - 40+ 张表 ==========
- name: "mes_wm_warehouse"
comment: "仓库表"
entity: "MesWarehouseDO"
domain: "WM"
- name: "mes_wm_warehouse_area"
comment: "库区表"
entity: "MesWarehouseAreaDO"
domain: "WM"
- name: "mes_wm_warehouse_location"
comment: "库位表"
entity: "MesWarehouseLocationDO"
domain: "WM"
- name: "mes_wm_material_stock"
comment: "物料库存表"
entity: "MesMaterialStockDO"
domain: "WM"
- name: "mes_wm_batch"
comment: "批次表"
entity: "MesBatchDO"
domain: "WM"
- name: "mes_wm_barcode"
comment: "条码表"
entity: "MesBarcodeDO"
domain: "WM"
- name: "mes_wm_barcode_config"
comment: "条码配置表"
entity: "MesBarcodeConfigDO"
domain: "WM"
- name: "mes_wm_sn"
comment: "SN 序列号表"
entity: "MesSnDO"
domain: "WM"
- name: "mes_wm_transaction"
comment: "库存事务表"
entity: "MesTransactionDO"
domain: "WM"
- name: "mes_wm_arrival_notice"
comment: "到货通知表"
entity: "MesArrivalNoticeDO"
domain: "WM"
- name: "mes_wm_arrival_notice_line"
comment: "到货通知行表"
entity: "MesArrivalNoticeLineDO"
domain: "WM"
- name: "mes_wm_item_receipt"
comment: "采购入库表"
entity: "MesItemReceiptDO"
domain: "WM"
- name: "mes_wm_item_receipt_line"
comment: "采购入库行表"
entity: "MesItemReceiptLineDO"
domain: "WM"
- name: "mes_wm_item_receipt_detail"
comment: "采购入库详情表"
entity: "MesItemReceiptDetailDO"
domain: "WM"
- name: "mes_wm_product_receipt"
comment: "产品入库表"
entity: "MesProductReceiptDO"
domain: "WM"
- name: "mes_wm_product_issue"
comment: "生产领料表"
entity: "MesProductIssueDO"
domain: "WM"
- name: "mes_wm_return_issue"
comment: "生产退料表"
entity: "MesReturnIssueDO"
domain: "WM"
- name: "mes_wm_misc_issue"
comment: "其他出库表"
entity: "MesMiscIssueDO"
domain: "WM"
- name: "mes_wm_misc_receipt"
comment: "其他入库表"
entity: "MesMiscReceiptDO"
domain: "WM"
- name: "mes_wm_product_sales"
comment: "销售出库表"
entity: "MesProductSalesDO"
domain: "WM"
- name: "mes_wm_return_sales"
comment: "销售退货表"
entity: "MesReturnSalesDO"
domain: "WM"
- name: "mes_wm_outsource_issue"
comment: "委外发料表"
entity: "MesOutsourceIssueDO"
domain: "WM"
- name: "mes_wm_outsource_receipt"
comment: "委外收料表"
entity: "MesOutsourceReceiptDO"
domain: "WM"
- name: "mes_wm_return_vendor"
comment: "供应商退货表"
entity: "MesReturnVendorDO"
domain: "WM"
- name: "mes_wm_transfer"
comment: "库存调拨表"
entity: "MesTransferDO"
domain: "WM"
- name: "mes_wm_package"
comment: "包装表"
entity: "MesPackageDO"
domain: "WM"
- name: "mes_wm_package_line"
comment: "包装行表"
entity: "MesPackageLineDO"
domain: "WM"
- name: "mes_wm_stock_taking_plan"
comment: "盘点计划表"
entity: "MesStockTakingPlanDO"
domain: "WM"
- name: "mes_wm_stock_taking_task"
comment: "盘点任务表"
entity: "MesStockTakingTaskDO"
domain: "WM"
- name: "mes_wm_stock_taking_task_result"
comment: "盘点结果表"
entity: "MesStockTakingTaskResultDO"
domain: "WM"
- name: "mes_wm_item_consume"
comment: "物料消耗表"
entity: "MesItemConsumeDO"
domain: "WM"
- name: "mes_wm_product_produce"
comment: "产品产出表"
entity: "MesProductProduceDO"
domain: "WM"
- name: "mes_wm_sales_notice"
comment: "销售通知表"
entity: "MesSalesNoticeDO"
domain: "WM"
# ========== 日历域 (CAL) - 7 张表 ==========
- name: "mes_cal_holiday"
comment: "节假日表"
entity: "MesCalHolidayDO"
domain: "CAL"
- name: "mes_cal_plan"
comment: "排班计划表"
entity: "MesCalPlanDO"
domain: "CAL"
- name: "mes_cal_plan_shift"
comment: "排班计划班次表"
entity: "MesCalPlanShiftDO"
domain: "CAL"
- name: "mes_cal_plan_team"
comment: "排班计划班组表"
entity: "MesCalPlanTeamDO"
domain: "CAL"
- name: "mes_cal_team"
comment: "班组表"
entity: "MesCalTeamDO"
domain: "CAL"
- name: "mes_cal_team_member"
comment: "班组成员表"
entity: "MesCalTeamMemberDO"
domain: "CAL"
- name: "mes_cal_team_shift"
comment: "班组班次表"
entity: "MesCalTeamShiftDO"
domain: "CAL"
# 表关系ER关系
relationships:
# 主数据域
- { from: "mes_md_item", to: "mes_md_item_type", type: "N:1", foreign_key: "item_type_id" }
- { from: "mes_md_workstation", to: "mes_md_workshop", type: "N:1", foreign_key: "workshop_id" }
- { from: "mes_md_workstation_machine", to: "mes_md_workstation", type: "N:1", foreign_key: "workstation_id" }
- { from: "mes_md_workstation_tool", to: "mes_md_workstation", type: "N:1", foreign_key: "workstation_id" }
- { from: "mes_md_workstation_worker", to: "mes_md_workstation", type: "N:1", foreign_key: "workstation_id" }
- { from: "mes_md_auto_code_part", to: "mes_md_auto_code_rule", type: "N:1", foreign_key: "rule_id" }
- { from: "mes_md_auto_code_record", to: "mes_md_auto_code_rule", type: "N:1", foreign_key: "rule_id" }
# 生产域
- { from: "mes_pro_work_order_bom", to: "mes_pro_work_order", type: "N:1", foreign_key: "work_order_id" }
- { from: "mes_pro_task", to: "mes_pro_work_order", type: "N:1", foreign_key: "work_order_id" }
- { from: "mes_pro_task", to: "mes_pro_route_process", type: "N:1", foreign_key: "route_process_id" }
- { from: "mes_pro_task_issue", to: "mes_pro_task", type: "N:1", foreign_key: "task_id" }
- { from: "mes_pro_route_process", to: "mes_pro_route", type: "N:1", foreign_key: "route_id" }
- { from: "mes_pro_route_process", to: "mes_pro_process", type: "N:1", foreign_key: "process_id" }
- { from: "mes_pro_card_process", to: "mes_pro_card", type: "N:1", foreign_key: "card_id" }
- { from: "mes_pro_feedback", to: "mes_pro_work_order", type: "N:1", foreign_key: "work_order_id" }
- { from: "mes_pro_andon_record", to: "mes_pro_andon_config", type: "N:1", foreign_key: "config_id" }
# 质量域
- { from: "mes_qc_template_item", to: "mes_qc_template", type: "N:1", foreign_key: "template_id" }
- { from: "mes_qc_template_indicator", to: "mes_qc_template_item", type: "N:1", foreign_key: "template_item_id" }
- { from: "mes_qc_iqc_line", to: "mes_qc_iqc", type: "N:1", foreign_key: "iqc_id" }
- { from: "mes_qc_ipqc_line", to: "mes_qc_ipqc", type: "N:1", foreign_key: "ipqc_id" }
- { from: "mes_qc_oqc_line", to: "mes_qc_oqc", type: "N:1", foreign_key: "oqc_id" }
- { from: "mes_qc_rqc_line", to: "mes_qc_rqc", type: "N:1", foreign_key: "rqc_id" }
- { from: "mes_qc_indicator_result", to: "mes_qc_indicator", type: "N:1", foreign_key: "indicator_id" }
- { from: "mes_qc_indicator_result_detail", to: "mes_qc_indicator_result", type: "N:1", foreign_key: "result_id" }
- { from: "mes_qc_defect_record", to: "mes_qc_defect", type: "N:1", foreign_key: "defect_id" }
# 设备域
- { from: "mes_dv_machinery", to: "mes_dv_machinery_type", type: "N:1", foreign_key: "machinery_type_id" }
- { from: "mes_dv_check_plan_machinery", to: "mes_dv_check_plan", type: "N:1", foreign_key: "plan_id" }
- { from: "mes_dv_check_plan_subject", to: "mes_dv_check_plan", type: "N:1", foreign_key: "plan_id" }
- { from: "mes_dv_check_record", to: "mes_dv_check_plan", type: "N:1", foreign_key: "plan_id" }
- { from: "mes_dv_check_record_line", to: "mes_dv_check_record", type: "N:1", foreign_key: "record_id" }
- { from: "mes_dv_mainten_record_line", to: "mes_dv_mainten_record", type: "N:1", foreign_key: "record_id" }
- { from: "mes_dv_repair_line", to: "mes_dv_repair", type: "N:1", foreign_key: "repair_id" }
# 仓库域
- { from: "mes_wm_warehouse_area", to: "mes_wm_warehouse", type: "N:1", foreign_key: "warehouse_id" }
- { from: "mes_wm_warehouse_location", to: "mes_wm_warehouse_area", type: "N:1", foreign_key: "area_id" }
- { from: "mes_wm_material_stock", to: "mes_md_item", type: "N:1", foreign_key: "item_id" }
- { from: "mes_wm_arrival_notice_line", to: "mes_wm_arrival_notice", type: "N:1", foreign_key: "notice_id" }
- { from: "mes_wm_item_receipt_line", to: "mes_wm_item_receipt", type: "N:1", foreign_key: "receipt_id" }
- { from: "mes_wm_item_receipt_detail", to: "mes_wm_item_receipt_line", type: "N:1", foreign_key: "line_id" }
- { from: "mes_wm_package_line", to: "mes_wm_package", type: "N:1", foreign_key: "package_id" }
- { from: "mes_wm_stock_taking_task", to: "mes_wm_stock_taking_plan", type: "N:1", foreign_key: "plan_id" }
- { from: "mes_wm_stock_taking_task_result", to: "mes_wm_stock_taking_task", type: "N:1", foreign_key: "task_id" }
# 日历域
- { from: "mes_cal_plan_shift", to: "mes_cal_plan", type: "N:1", foreign_key: "plan_id" }
- { from: "mes_cal_plan_team", to: "mes_cal_plan", type: "N:1", foreign_key: "plan_id" }
- { from: "mes_cal_plan_team", to: "mes_cal_team", type: "N:1", foreign_key: "team_id" }
- { from: "mes_cal_team_member", to: "mes_cal_team", type: "N:1", foreign_key: "team_id" }
- { from: "mes_cal_team_shift", to: "mes_cal_team", type: "N:1", foreign_key: "team_id" }
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - xxx') - Swagger 文档标签"
- "@RestController - REST 控制器"
- "@RequestMapping('/admin-api/mes/xxx') - 请求路径前缀"
- "@Validated - 参数校验"
- "@PreAuthorize('@ss.hasPermission('mes:xxx:action')') - 权限控制"
example: |
@Tag(name = "管理后台 - 物料管理")
@RestController
@RequestMapping("/admin-api/mes/item")
@Validated
public class MesItemController {
@Resource
private MesItemService itemService;
@PostMapping("/create")
@Operation(summary = "创建物料")
@PreAuthorize("@ss.hasPermission('mes:item:create')")
public CommonResult<Long> create(@Valid @RequestBody MesItemSaveReqVO reqVO) {
return success(itemService.create(reqVO));
}
@PutMapping("/update")
@Operation(summary = "更新物料")
@PreAuthorize("@ss.hasPermission('mes:item:update')")
public CommonResult<Boolean> update(@Valid @RequestBody MesItemSaveReqVO reqVO) {
itemService.update(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除物料")
@PreAuthorize("@ss.hasPermission('mes:item:delete')")
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
itemService.delete(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获取物料详情")
@PreAuthorize("@ss.hasPermission('mes:item:query')")
public CommonResult<MesItemRespVO> get(@RequestParam("id") Long id) {
return success(itemService.get(id));
}
@GetMapping("/page")
@Operation(summary = "物料分页查询")
@PreAuthorize("@ss.hasPermission('mes:item:query')")
public CommonResult<PageResult<MesItemRespVO>> page(@Valid MesItemPageReqVO reqVO) {
return success(itemService.page(reqVO));
}
}
# Service层规范
service:
interface_pattern: "XxxService 接口定义业务方法XxxServiceImpl 实现类"
impl_pattern: |
@Service
@Validated
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
// 使用 static final 定义常量
// 使用 @Transactional 注解控制事务
// 使用 validateXxxExists 方法校验存在性
// 使用 exception(ErrorCode) 抛出业务异常
}
state_machine_example: |
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStatus(Long id, Integer status) {
MesWorkOrderDO order = validateWorkOrderExists(id);
// 校验状态流转
if (!MesWorkOrderStatusEnum.canTransit(order.getStatus(), status)) {
throw exception(WORK_ORDER_STATUS_TRANSIT_FAIL);
}
// CAS 更新
workOrderMapper.updateStatus(id, order.getStatus(), status);
}
# 数据访问规范
dal:
mapper_pattern: |
public interface XxxMapper extends BaseMapperX<XxxDO> {
default PageResult<XxxDO> selectPage(XxxPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<XxxDO>()
.likeIfPresent(XxxDO::getName, reqVO.getName())
.eqIfPresent(XxxDO::getStatus, reqVO.getStatus())
.orderByDesc(XxxDO::getId));
}
}
example: |
public interface MesItemMapper extends BaseMapperX<MesItemDO> {
default PageResult<MesItemDO> selectPage(MesItemPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<MesItemDO>()
.likeIfPresent(MesItemDO::getName, reqVO.getName())
.likeIfPresent(MesItemDO::getCode, reqVO.getCode())
.eqIfPresent(MesItemDO::getItemTypeId, reqVO.getItemTypeId())
.eqIfPresent(MesItemDO::getStatus, reqVO.getStatus())
.orderByDesc(MesItemDO::getId));
}
}
# VO Assembly 模式
vo_assembly: |
// 查询时批量获取关联数据,避免 N+1
PageResult<MesWorkOrderDO> pageResult = workOrderMapper.selectPage(reqVO);
Set<Long> itemIds = pageResult.getList().stream()
.map(MesWorkOrderDO::getItemId).collect(Collectors.toSet());
Map<Long, MesItemDO> itemMap = itemService.getItemMap(itemIds);
// 组装 VO
return new PageResult<>(pageResult.getList().stream().map(order -> {
MesWorkOrderRespVO vo = BeanUtils.toBean(order, MesWorkOrderRespVO.class);
MesItemDO item = itemMap.get(order.getItemId());
if (item != null) {
vo.setItemName(item.getName());
}
return vo;
}).collect(Collectors.toList()), pageResult.getTotal());
# 异常处理
error_handling:
code_prefix: "1-xxx-xxx-xxx"
note: "具体错误码前缀需查阅 ErrorCodeConstants.java 确认"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ErrorCode)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增物料属性
new_feature:
title: "新增物料属性"
steps:
- step: "1. 修改数据库表"
description: "在 mes_md_item 表中添加新字段"
- step: "2. 更新 DO 实体"
description: "在 MesItemDO 中添加对应属性"
- step: "3. 更新 VO"
description: "在 MesItemSaveReqVO 和 MesItemRespVO 中添加字段"
- step: "4. 更新 Controller"
description: "确保新字段在创建/更新/查询接口中正确处理"
# 新增出入库类型
new_channel:
title: "新增出入库类型"
steps:
- step: "1. 创建出入库表"
description: "参考 mes_wm_misc_issue 创建新的出入库主子表"
- step: "2. 创建 DO 实体"
description: "创建 XxxDO 继承 BaseDO"
- step: "3. 创建 Mapper/Service/Controller"
description: "遵循标准 CRUD 模式创建各层代码"
- step: "4. 集成库存更新"
description: "在 Service 中调用 MaterialStockService 更新库存"
- step: "5. 添加权限配置"
description: "在系统权限中添加 mes:xxx:create/update/delete/query 权限"
# 新增检验类型
new_inspection:
title: "新增检验类型"
steps:
- step: "1. 创建检验单表"
description: "参考 mes_qc_iqc 创建新的检验主子表"
- step: "2. 创建 DO/VO/Service/Controller"
description: "遵循标准 CRUD + 主子表模式"
- step: "3. 集成检验模板"
description: "关联 QcTemplateDO 的检验项和指标"
- step: "4. 集成缺陷管理"
description: "检验不合格时关联 QcDefectRecordDO"
- step: "5. 联动仓库操作"
description: "检验结果触发对应的出入库操作"
# 最佳实践
best_practices:
- practice: "状态机校验"
description: "所有状态变更前校验前置状态,使用 CAS 方式更新防止并发覆盖"
- practice: "VO Assembly"
description: "查询接口批量获取关联实体后组装 VO避免 N+1 查询"
- practice: "库存乐观锁"
description: "出库操作使用 quantity >= #{outQuantity} 条件防止库存变为负数"
- practice: "自动编码防重"
description: "使用 SELECT FOR UPDATE 锁定编码记录,防止并发生成重复编码"
- practice: "检验与入库联动"
description: "IQC 合格后才能办理采购入库,不合格走供应商退货"
- practice: "设备状态联动"
description: "维修/保养完成后自动恢复设备状态为正常"
- practice: "事务范围控制"
description: "只包含写操作,查询移到事务外,避免长事务和锁竞争"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "获取用户信息(工位操作员、报工人等)"
- module: "yudao-module-system"
api: "RoleApi"
purpose: "角色权限校验"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
purpose: "ORM 框架,提供 CRUD 封装BaseMapperX"
- name: "Swagger/OpenAPI"
purpose: "API 文档注解"
- name: "MapStruct"
purpose: "VO/DO 对象映射转换"
- name: "Hutool"
purpose: "Java 工具类库"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/ItemService.java"
purpose: "物料管理服务接口"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/WorkOrderService.java"
purpose: "工单管理服务接口,核心状态机"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/TaskService.java"
purpose: "生产任务服务,甘特图支持"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/CardService.java"
purpose: "流转卡服务,工序执行跟踪"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/FeedbackService.java"
purpose: "报工反馈服务,审批流程"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/QcIqcService.java"
purpose: "来料检验服务"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/MachineryService.java"
purpose: "设备管理服务"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/RepairService.java"
purpose: "维修管理服务,状态机"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/MaterialStockService.java"
purpose: "库存管理服务,出入库核心"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/TransferService.java"
purpose: "调拨服务,双向库存更新"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/AutoCodeService.java"
purpose: "自动编码服务,策略模式"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/CalPlanService.java"
purpose: "排班计划服务"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java"
purpose: "业务类型常量定义"
- path: "yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义"

View File

@@ -0,0 +1,638 @@
# Skill 文档 - 微信公众号模块 (yudao-module-mp)
# 提取时间: 2026-03-18
skill:
id: "skill-mp"
name: "WeChat MP (Public Account) Skill"
version: "1.0.0"
module_path: "yudao-module-mp"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:微信公众号全生命周期管理平台
# 解决问题:统一管理公众号账号、粉丝、消息、素材、菜单、自动回复、模板消息等核心功能
# 系统定位:作为企业与微信用户交互的关键渠道,支撑营销推送、客户服务等业务场景
business_position: |
微信公众号管理模块,提供完整的公众号运营能力:
- 多公众号管理:支持接入多个公众号账号,实现统一管理
- 粉丝管理:粉丝信息同步、标签分组、用户画像
- 消息管理:消息接收、客服消息、模板消息推送
- 素材管理:临时素材、永久素材、图文消息
- 菜单管理:自定义菜单创建与响应
- 自动回复:关注回复、关键词回复、消息回复
- 数据统计:用户分析、消息分析、接口分析
# 设计原则
design_principles:
- name: "消息路由模式"
description: "采用 WxMpMessageRouter 实现消息分发,通过责任链模式处理不同类型消息和事件"
- name: "多账号隔离"
description: "通过 MpServiceFactory 工厂模式管理多公众号 WxMpService 实例,实现账号级别的隔离"
- name: "事件驱动架构"
description: "微信回调事件通过 Handler 处理器链式处理,支持关注、取消关注、菜单点击、扫码等事件"
- name: "素材统一管理"
description: "临时素材和永久素材统一抽象,自动下载并存储到本地文件服务"
- name: "多租户支持"
description: "账号层支持租户隔离,通过 TenantBaseDO 实现数据隔离"
# 领域模型
domain_model:
aggregates:
- name: "MpAccount"
type: "聚合根"
description: "公众号账号聚合根,管理公众号配置信息"
entities: ["MpMenu", "MpTag", "MpStatistics"]
- name: "MpUser"
type: "聚合根"
description: "公众号粉丝聚合根,关联粉丝标签和消息记录"
entities: ["MpTag"]
- name: "MpMessage"
type: "聚合根"
description: "消息聚合根,包含自动回复配置和消息模板"
entities: ["MpAutoReply", "MpMessageTemplate"]
- name: "MpMaterial"
type: "聚合根"
description: "素材聚合根,管理临时素材和永久素材"
entities: []
value_objects:
- name: "MpMessageDO.Article"
description: "图文消息值对象,包含标题、描述、图片链接、跳转链接"
- name: "MpAutoReplyTypeEnum"
description: "自动回复类型枚举:关注回复、消息回复、关键词回复"
- name: "MpAutoReplyMatchEnum"
description: "关键词匹配模式枚举:完全匹配、半匹配"
- name: "MpMessageSendFromEnum"
description: "消息发送方向枚举:粉丝发给公众号、公众号发给粉丝"
services:
- name: "MpAccountService"
description: "公众号账号管理服务提供账号CRUD和缓存能力"
- name: "MpUserService"
description: "粉丝管理服务,提供粉丝同步、更新、查询能力"
- name: "MpMessageService"
description: "消息服务,处理消息接收、发送、自动回复"
- name: "MpMaterialService"
description: "素材服务,处理素材上传、下载、管理"
- name: "MpMenuService"
description: "菜单服务,管理自定义菜单"
- name: "MpAutoReplyService"
description: "自动回复服务,处理关注回复、关键词回复、消息回复"
- name: "MpMessageTemplateService"
description: "模板消息服务,管理模板同步和发送"
- name: "MpStatisticsService"
description: "统计数据服务,获取用户分析、消息分析数据"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "controller"
purpose: "HTTP接口层提供管理后台和微信回调接口"
components:
- "MpAccountController: 公众号账号管理接口"
- "MpUserController: 粉丝管理接口"
- "MpMessageController: 消息管理接口"
- "MpMessageTemplateController: 模板消息接口"
- "MpAutoReplyController: 自动回复管理接口"
- "MpMaterialController: 素材管理接口"
- "MpMenuController: 菜单管理接口"
- "MpTagController: 标签管理接口"
- "MpOpenController: 微信回调入口(签名校验、消息处理)"
- "MpStatisticsController: 数据统计接口"
- "MpDraftController: 草稿管理接口"
- "MpFreePublishController: 发布能力接口"
- name: "service"
purpose: "业务逻辑层"
components:
- "MpAccountService: 账号管理核心服务"
- "MpUserService: 粉丝管理服务"
- "MpMessageService: 消息处理服务"
- "MpMaterialService: 素材管理服务"
- "MpMenuService: 菜单管理服务"
- "MpAutoReplyService: 自动回复服务"
- "MpMessageTemplateService: 模板消息服务"
- "MpTagService: 标签管理服务"
- "MpStatisticsService: 统计服务"
- name: "handler"
purpose: "微信消息事件处理器"
components:
- "SubscribeHandler: 关注事件处理器"
- "UnsubscribeHandler: 取消关注事件处理器"
- "MenuHandler: 菜单点击事件处理器"
- "MessageReceiveHandler: 消息接收处理器"
- "MessageAutoReplyHandler: 自动回复处理器"
- "LocationHandler: 地理位置处理器"
- "ScanHandler: 扫码事件处理器"
- "KfSessionHandler: 客服会话处理器"
- name: "dal"
purpose: "数据访问层"
components:
- "MpAccountMapper: 账号数据访问"
- "MpUserMapper: 粉丝数据访问"
- "MpMessageMapper: 消息数据访问"
- "MpMaterialMapper: 素材数据访问"
- "MpMenuMapper: 菜单数据访问"
- "MpAutoReplyMapper: 自动回复数据访问"
- "MpMessageTemplateMapper: 模板消息数据访问"
- "MpTagMapper: 标签数据访问"
- name: "framework/mp"
purpose: "微信公众号框架核心"
components:
- "MpServiceFactory: WxMpService 工厂接口"
- "DefaultMpServiceFactory: 默认工厂实现,管理多账号 WxMpService"
- "MpContextHolder: 公众号上下文持有者"
# 设计模式应用
design_patterns:
- pattern: "Factory Pattern"
location: "framework/mp/core/MpServiceFactory.java"
purpose: "创建和管理 WxMpService 实例,支持多公众号场景"
- pattern: "Chain of Responsibility"
location: "DefaultMpServiceFactory.buildMpMessageRouter()"
purpose: "WxMpMessageRouter 实现消息处理链,按规则路由到不同 Handler"
- pattern: "Strategy Pattern"
location: "service/handler/*"
purpose: "不同类型消息/事件由不同 Handler 实现 WxMpMessageHandler 接口处理"
- pattern: "Convert Pattern"
location: "convert/*"
purpose: "MapStruct 转换器VO/DO/微信对象之间转换"
- pattern: "Template Method"
location: "MpOpenController.handleMessage()"
purpose: "统一的微信回调处理流程:签名校验 -> 消息解析 -> 路由处理 -> 响应构建"
# 模块间通信
communication:
apis: [] # 无对外暴露的 API 模块
consumers:
- module: "yudao-module-system"
purpose: "依赖用户体系、权限体系"
- module: "yudao-module-infra"
purpose: "依赖文件服务存储素材"
mq: [] # 未使用消息队列
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "TenantBaseDO | BaseDO"
description: "MpAccountDO 使用 TenantBaseDO 支持多租户,其他实体使用 BaseDO"
# 核心数据表
tables:
- name: "mp_account"
comment: "公众号账号表"
entity: "MpAccountDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "name", type: "String", comment: "公众号名称" }
- { name: "account", type: "String", comment: "公众号账号" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
- { name: "app_secret", type: "String", comment: "公众号密钥" }
- { name: "token", type: "String", comment: "公众号Token" }
- { name: "aes_key", type: "String", comment: "消息加解密密钥" }
- { name: "qr_code_url", type: "String", comment: "二维码图片URL" }
- { name: "remark", type: "String", comment: "备注" }
indexes:
- { name: "uk_app_id", columns: ["app_id"] }
- name: "mp_user"
comment: "公众号粉丝表"
entity: "MpUserDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "openid", type: "String", comment: "粉丝标识OpenId" }
- { name: "union_id", type: "String", comment: "微信生态唯一标识" }
- { name: "subscribe_status", type: "Integer", comment: "关注状态(1已关注/2取消关注)" }
- { name: "subscribe_time", type: "LocalDateTime", comment: "关注时间" }
- { name: "unsubscribe_time", type: "LocalDateTime", comment: "取消关注时间" }
- { name: "nickname", type: "String", comment: "昵称" }
- { name: "head_image_url", type: "String", comment: "头像地址" }
- { name: "language", type: "String", comment: "语言" }
- { name: "country", type: "String", comment: "国家" }
- { name: "province", type: "String", comment: "省份" }
- { name: "city", type: "String", comment: "城市" }
- { name: "remark", type: "String", comment: "备注" }
- { name: "tag_ids", type: "List<Long>", comment: "标签ID数组" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId(冗余)" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
- { name: "uk_appid_openid", columns: ["app_id", "openid"] }
- name: "mp_message"
comment: "公众号消息表"
entity: "MpMessageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "msg_id", type: "Long", comment: "微信消息ID" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
- { name: "user_id", type: "Long", comment: "粉丝编号" }
- { name: "openid", type: "String", comment: "粉丝OpenId" }
- { name: "type", type: "String", comment: "消息类型(text/image/voice/video等)" }
- { name: "send_from", type: "Integer", comment: "消息来源(1粉丝发给公众号/2公众号发给粉丝)" }
- { name: "content", type: "String", comment: "文本消息内容" }
- { name: "media_id", type: "String", comment: "媒体文件ID" }
- { name: "media_url", type: "String", comment: "媒体文件URL" }
- { name: "recognition", type: "String", comment: "语音识别文本" }
- { name: "format", type: "String", comment: "语音格式" }
- { name: "title", type: "String", comment: "标题" }
- { name: "description", type: "String", comment: "描述" }
- { name: "thumb_media_id", type: "String", comment: "缩略图媒体ID" }
- { name: "thumb_media_url", type: "String", comment: "缩略图URL" }
- { name: "url", type: "String", comment: "链接地址" }
- { name: "location_x", type: "Double", comment: "地理位置纬度" }
- { name: "location_y", type: "Double", comment: "地理位置经度" }
- { name: "scale", type: "Double", comment: "地图缩放级别" }
- { name: "label", type: "String", comment: "地理位置信息" }
- { name: "articles", type: "List<Article>", comment: "图文消息数组(JSON)" }
- { name: "music_url", type: "String", comment: "音乐链接" }
- { name: "hq_music_url", type: "String", comment: "高质量音乐链接" }
- { name: "event", type: "String", comment: "事件类型" }
- { name: "event_key", type: "String", comment: "事件Key值" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
- { name: "idx_user_id", columns: ["user_id"] }
- name: "mp_material"
comment: "公众号素材表"
entity: "MpMaterialDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
- { name: "media_id", type: "String", comment: "微信素材ID" }
- { name: "type", type: "String", comment: "文件类型(image/voice/video/thumb)" }
- { name: "permanent", type: "Boolean", comment: "是否永久素材" }
- { name: "url", type: "String", comment: "文件服务器URL" }
- { name: "name", type: "String", comment: "文件名称" }
- { name: "mp_url", type: "String", comment: "公众号文件URL(永久素材)" }
- { name: "title", type: "String", comment: "视频素材标题" }
- { name: "introduction", type: "String", comment: "视频素材描述" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
- { name: "idx_media_id", columns: ["media_id"] }
- name: "mp_menu"
comment: "公众号菜单表"
entity: "MpMenuDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
- { name: "name", type: "String", comment: "菜单名称" }
- { name: "menu_key", type: "String", comment: "菜单标识" }
- { name: "parent_id", type: "Long", comment: "父菜单ID" }
- { name: "type", type: "String", comment: "按钮类型(click/view/miniprogram等)" }
- { name: "url", type: "String", comment: "网页链接" }
- { name: "mini_program_app_id", type: "String", comment: "小程序AppId" }
- { name: "mini_program_page_path", type: "String", comment: "小程序页面路径" }
- { name: "article_id", type: "String", comment: "跳转图文媒体ID" }
- { name: "reply_message_type", type: "String", comment: "回复消息类型" }
- { name: "reply_content", type: "String", comment: "回复文本内容" }
- { name: "reply_media_id", type: "String", comment: "回复媒体ID" }
- { name: "reply_media_url", type: "String", comment: "回复媒体URL" }
- { name: "reply_articles", type: "List<Article>", comment: "回复图文消息(JSON)" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
- name: "mp_auto_reply"
comment: "公众号自动回复表"
entity: "MpAutoReplyDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
- { name: "type", type: "Integer", comment: "回复类型(1关注回复/2消息回复/3关键词回复)" }
- { name: "request_keyword", type: "String", comment: "请求关键词" }
- { name: "request_match", type: "Integer", comment: "关键词匹配模式(1完全匹配/2半匹配)" }
- { name: "request_message_type", type: "String", comment: "请求消息类型" }
- { name: "response_message_type", type: "String", comment: "响应消息类型" }
- { name: "response_content", type: "String", comment: "响应文本内容" }
- { name: "response_media_id", type: "String", comment: "响应媒体ID" }
- { name: "response_media_url", type: "String", comment: "响应媒体URL" }
- { name: "response_articles", type: "List<Article>", comment: "响应图文消息(JSON)" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
- name: "mp_message_template"
comment: "公众号模板消息表"
entity: "MpMessageTemplateDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
- { name: "template_id", type: "String", comment: "模板ID" }
- { name: "title", type: "String", comment: "模板标题" }
- { name: "content", type: "String", comment: "模板内容" }
- { name: "example", type: "String", comment: "模板示例" }
- { name: "primary_industry", type: "String", comment: "一级行业" }
- { name: "deputy_industry", type: "String", comment: "二级行业" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
- { name: "idx_template_id", columns: ["template_id"] }
- name: "mp_tag"
comment: "公众号标签表"
entity: "MpTagDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "主键ID" }
- { name: "tag_id", type: "Long", comment: "微信标签ID" }
- { name: "name", type: "String", comment: "标签名称" }
- { name: "count", type: "Integer", comment: "标签下粉丝数" }
- { name: "account_id", type: "Long", comment: "公众号账号ID" }
- { name: "app_id", type: "String", comment: "公众号AppId" }
indexes:
- { name: "idx_account_id", columns: ["account_id"] }
# 表关系ER关系
relationships:
- from: "mp_user"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
- from: "mp_message"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
- from: "mp_message"
to: "mp_user"
type: "N:1"
foreign_key: "user_id"
- from: "mp_material"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
- from: "mp_menu"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
- from: "mp_auto_reply"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
- from: "mp_message_template"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
- from: "mp_tag"
to: "mp_account"
type: "N:1"
foreign_key: "account_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - XXX')"
- "@RestController"
- "@RequestMapping('/mp/xxx')"
- "@Validated"
- "@PreAuthorize('@ss.hasPermission(\"mp:xxx:action\")')"
example: |
@Tag(name = "管理后台 - 公众号模版消息")
@RestController
@RequestMapping("/mp/message-template")
@Validated
public class MpMessageTemplateController {
@Resource
private MpMessageTemplateService messageTemplateService;
@PostMapping("/send")
@Operation(summary = "给粉丝发送模版消息")
@PreAuthorize("@ss.hasPermission('mp:message-template:send')")
public CommonResult<Boolean> sendMessageTemplate(@Valid @RequestBody MpMessageTemplateSendReqVO sendReqVO) {
messageTemplateService.sendMessageTempalte(sendReqVO);
return success(true);
}
}
# Service层规范
service:
interface_pattern: "接口定义业务方法参数使用VO或BO对象"
impl_pattern: "实现类注入MpServiceFactory获取WxMpService处理微信API调用"
example: |
@Service
@Validated
@Slf4j
public class MpMessageTemplateServiceImpl implements MpMessageTemplateService {
@Resource
@Lazy // 延迟加载,解决循环依赖
private MpServiceFactory mpServiceFactory;
@Override
public void sendMessageTempalte(MpMessageTemplateSendReqVO sendReqVO) {
// 1. 获得关联信息
MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId());
MpMessageTemplateDO template = validateMsgTemplateExists(sendReqVO.getId());
// 2. 构建模板消息并发送
WxMpTemplateMessage templateMessage = buildTemplateMessage(template, user, sendReqVO);
try {
mpServiceFactory.getRequiredMpService(template.getAppId())
.getTemplateMsgService().sendTemplateMsg(templateMessage);
} catch (WxErrorException e) {
throw exception(MESSAGE_TEMPLATE_SEND_FAIL, e.getError().getErrorMsg());
}
}
}
# 消息处理器规范
handler:
pattern: "实现 WxMpMessageHandler 接口,通过 @Component 注册为 Spring Bean"
example: |
@Component
@Slf4j
public class SubscribeHandler implements WxMpMessageHandler {
@Resource
private MpUserService mpUserService;
@Resource
private MpAutoReplyService mpAutoReplyService;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
WxMpService weixinService, WxSessionManager sessionManager) {
// 1. 获取粉丝信息
WxMpUser wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser());
// 2. 保存粉丝信息
mpUserService.saveUser(MpContextHolder.getAppId(), wxMpUser);
// 3. 回复关注欢迎语
return mpAutoReplyService.replyForSubscribe(MpContextHolder.getAppId(), wxMessage);
}
}
# 数据访问规范
dal:
mapper_pattern: "继承 BaseMapperX使用 LambdaQueryWrapperX 构建查询条件"
example: |
@Mapper
public interface MpMessageMapper extends BaseMapperX<MpMessageDO> {
default PageResult<MpMessageDO> selectPage(MpMessagePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<MpMessageDO>()
.eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId())
.eqIfPresent(MpMessageDO::getUserId, reqVO.getUserId())
.orderByDesc(MpMessageDO::getId));
}
}
# 异常处理
error_handling:
code_prefix: "1-006-XXX-XXX"
examples:
- code: "1_006_000_000"
message: "公众号账号不存在"
- code: "1_006_003_000"
message: "粉丝不存在"
- code: "1_006_005_000"
message: "发送消息失败,原因:{}"
- code: "1_006_009_000"
message: "自动回复不存在"
- code: "1_006_010_004"
message: "发送模版消息失败,原因:{}"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE, args...)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增消息类型处理
new_feature:
title: "新增消息类型处理器"
steps:
- step: 1
action: "创建Handler类"
description: "在 service/handler/ 目录下创建新的处理器类,实现 WxMpMessageHandler 接口"
- step: 2
action: "注册到消息路由"
description: "在 DefaultMpServiceFactory.buildMpMessageRouter() 中添加路由规则"
code: |
// 示例:添加新消息类型处理
router.rule().async(false)
.msgType(WxConsts.XmlMsgType.XXX)
.handler(newHandler).end();
- step: 3
action: "实现业务逻辑"
description: "在 handle() 方法中实现消息处理逻辑,返回 WxMpXmlOutMessage 响应"
# 新增自动回复类型
new_channel:
title: "新增自动回复类型"
steps:
- step: 1
action: "扩展枚举"
description: "在 MpAutoReplyTypeEnum 中添加新的类型"
- step: 2
action: "创建自动回复配置"
description: "通过 MpAutoReplyController 创建自动回复规则"
- step: 3
action: "实现回复逻辑"
description: "在 MpAutoReplyServiceImpl 中实现对应的回复逻辑"
# 最佳实践
best_practices:
- title: "多账号管理"
description: "使用 MpServiceFactory 获取指定账号的 WxMpService避免账号混淆"
example: "WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);"
- title: "消息上下文"
description: "使用 MpContextHolder 在处理链中传递 appId 上下文"
example: "String appId = MpContextHolder.getAppId();"
- title: "异步处理"
description: "消息接收使用异步处理,避免阻塞微信回调响应"
example: "router.rule().handler(messageReceiveHandler).next(); // next() 表示继续执行后续规则"
- title: "素材下载"
description: "收到粉丝发送的媒体消息时,自动下载并存储到本地文件服务"
example: "mpMaterialService.downloadMaterialUrl(accountId, mediaId, type);"
- title: "错误处理"
description: "捕获 WxErrorException 并转换为业务异常"
example: |
try {
wxMpService.someOperation();
} catch (WxErrorException e) {
throw exception(ERROR_CODE, e.getError().getErrorMsg());
}
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
purpose: "依赖用户体系、权限验证"
- module: "yudao-module-infra"
purpose: "依赖文件服务存储素材"
# 外部依赖(第三方库)
external:
- name: "wx-java-mp-spring-boot-starter"
version: "继承自父POM"
purpose: "微信公众号Java SDK提供WxMpService等核心API"
- name: "yudao-spring-boot-starter-mybatis"
version: "继承自父POM"
purpose: "MyBatis-Plus封装提供BaseMapperX等"
- name: "yudao-spring-boot-starter-redis"
version: "继承自父POM"
purpose: "Redis封装用于WxMpService配置存储"
- name: "yudao-spring-boot-starter-biz-tenant"
version: "继承自父POM"
purpose: "多租户支持"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java"
purpose: "WxMpService工厂接口管理多公众号实例"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java"
purpose: "默认工厂实现构建WxMpService和WxMpMessageRouter"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java"
purpose: "微信回调入口,处理签名校验和消息路由"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java"
purpose: "关注事件处理器,保存粉丝信息并回复欢迎语"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java"
purpose: "自动回复处理器,处理关键词和消息类型回复"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java"
purpose: "消息服务实现,处理消息接收和发送"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageTemplateServiceImpl.java"
purpose: "模板消息服务实现,同步和发送模板消息"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java"
purpose: "公众号账号实体,支持多租户"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java"
purpose: "消息实体,包含多种消息类型字段"
- path: "yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java"
purpose: "错误码定义"

View File

@@ -0,0 +1,654 @@
# Skill 文档 - 支付模块 (yudao-module-pay)
# 支付中台核心模块,支持多渠道支付、退款、转账等功能
skill:
id: "skill-pay"
name: "Pay Module Skill"
version: "1.0.0"
module_path: "yudao-module-pay"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
支付中台模块,为整个系统提供统一的支付能力支持。
核心功能包括:
1. 多渠道支付支持微信支付JSAPI/小程序/App/Native/H5/付款码、支付宝PC/WAP/App/扫码/条码)、钱包支付、模拟支付
2. 统一退款:支持全额退款和部分退款,自动处理退款状态同步
3. 转账功能:支持企业付款到零钱、转账到银行卡
4. 钱包系统:用户余额充值、消费、提现
5. 异步通知:统一处理各渠道的支付/退款回调
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "策略模式:各支付渠道实现 PayClient 接口,统一调用方式,隔离渠道差异"
- "模板方法模式AbstractPayClient 定义统一流程,子类实现具体细节"
- "工厂模式PayClientFactory 管理客户端创建和缓存,支持动态刷新配置"
- "开闭原则:新增支付渠道只需创建新的 PayClient 实现类,无需修改现有代码"
- "依赖倒置Service 层依赖 PayClient 接口而非具体实现"
- "单一职责:每个 PayClient 只负责对应渠道的支付逻辑"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "PayOrder"
type: "聚合根"
description: "支付订单聚合根,管理订单生命周期"
entities: ["PayOrderExtension"]
invariants:
- "订单状态WAITING -> SUCCESS/REFUND/CLOSED"
- "退款金额不能超过支付金额"
- name: "PayRefund"
type: "聚合根"
description: "退款单聚合根,管理退款流程"
entities: []
invariants:
- "退款状态WAITING -> SUCCESS/FAILURE"
- "一个支付订单可以有多个退款单"
- name: "PayChannel"
type: "实体"
description: "支付渠道配置,关联支付客户端"
entities: []
- name: "PayApp"
type: "实体"
description: "支付应用,业务系统的支付入口"
entities: []
value_objects:
- name: "PayClientConfig"
description: "支付客户端配置,不同渠道有不同的配置类型"
implementations: ["AlipayPayClientConfig", "WxPayClientConfig"]
services:
- name: "PayOrderService"
description: "支付订单领域服务,负责订单创建、提交、回调处理"
- name: "PayRefundService"
description: "退款领域服务,负责退款发起、回调处理"
- name: "PayChannelService"
description: "渠道管理服务,负责渠道配置和客户端获取"
- name: "PayNotifyService"
description: "通知服务,负责异步通知的处理和重试"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口供其他模块调用"
components:
- "PayOrderApi: 支付订单API"
- "PayRefundApi: 退款API"
- "PayTransferApi: 转账API"
- "PayWalletApi: 钱包API"
- name: "controller"
purpose: "HTTP接口暴露给前端调用"
components:
- "PayOrderController: 支付订单管理"
- "PayRefundController: 退款管理"
- "PayChannelController: 渠道配置管理"
- "PayAppController: 应用管理"
- "PayNotifyController: 通知回调"
- name: "service"
purpose: "业务逻辑层,核心领域服务"
components:
- "PayOrderServiceImpl: 订单创建、提交、回调"
- "PayRefundServiceImpl: 退款创建、回调"
- "PayChannelServiceImpl: 渠道管理"
- "PayNotifyServiceImpl: 通知处理"
- name: "framework/pay/core/client"
purpose: "支付客户端抽象层对接各支付渠道SDK"
components:
- "PayClient: 客户端接口"
- "AbstractPayClient: 抽象基类"
- "PayClientFactory: 客户端工厂"
- name: "dal"
purpose: "数据访问层"
components:
- "PayOrderMapper: 订单数据访问"
- "PayRefundMapper: 退款数据访问"
- "PayChannelMapper: 渠道数据访问"
# 设计模式应用
design_patterns:
- pattern: "Factory Pattern"
location: "yudao-module-pay/framework/pay/core/client/impl/PayClientFactoryImpl.java"
purpose: "根据渠道编码创建对应的支付客户端,缓存已创建的客户端,支持配置热刷新"
implementation: |
// 注册渠道与客户端类的映射
clientClass.put(WX_PUB, WxPubPayClient.class);
clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class);
// 反射创建客户端实例
ReflectUtil.newInstance(payClientClass, channelId, config);
- pattern: "Template Method Pattern"
location: "yudao-module-pay/framework/pay/core/client/impl/AbstractPayClient.java"
purpose: "定义支付/退款/转账的统一流程,包括参数校验、异常处理、日志记录"
implementation: |
// 模板方法:统一下单
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO); // 参数校验
try {
return doUnifiedOrder(reqDTO); // 子类实现
} catch (Throwable ex) {
throw buildPayException(ex); // 统一异常处理
}
}
- pattern: "Strategy Pattern"
location: "yudao-module-pay/framework/pay/core/client/impl/*"
purpose: "不同支付渠道实现不同的支付策略,但暴露统一接口"
implementations:
- "AbstractAlipayPayClient: 支付宝支付策略"
- "AbstractWxPayClient: 微信支付策略"
- "WalletPayClient: 钱包支付策略"
- "MockPayClient: 模拟支付策略"
# 模块间通信
communication:
apis:
- name: "PayOrderApi"
methods: ["createOrder", "getOrder"]
consumers: ["订单模块", "会员模块"]
- name: "PayRefundApi"
methods: ["createRefund", "getRefund"]
consumers: ["订单模块"]
- name: "PayWalletApi"
methods: ["addBalance", "getWallet"]
consumers: ["会员模块", "订单模块"]
consumers: []
mq:
- type: "Job"
name: "PayOrderSyncJob"
purpose: "定时同步待支付订单状态"
- type: "Job"
name: "PayOrderExpireJob"
purpose: "定时关闭过期订单"
- type: "Job"
name: "PayNotifyJob"
purpose: "异步通知重试"
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO | TenantBaseDO"
description: "支付渠道 PayChannelDO 继承 TenantBaseDO 支持多租户,其他实体继承 BaseDO"
# 核心数据表
tables:
- name: "pay_app"
comment: "支付应用表"
entity: "PayAppDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "Long", comment: "应用编号" }
- { name: "name", type: "String", comment: "应用名称" }
- { name: "status", type: "Integer", comment: "开启状态" }
- { name: "order_notify_url", type: "String", comment: "订单回调地址" }
- { name: "refund_notify_url", type: "String", comment: "退款回调地址" }
- name: "pay_channel"
comment: "支付渠道表"
entity: "PayChannelDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "Long", comment: "渠道编号" }
- { name: "code", type: "String", comment: "渠道编码,如 wx_pub, alipay_wap" }
- { name: "status", type: "Integer", comment: "开启状态" }
- { name: "fee_rate", type: "Double", comment: "渠道费率" }
- { name: "app_id", type: "Long", comment: "应用编号" }
- { name: "config", type: "PayClientConfig", comment: "支付配置JSON" }
- name: "pay_order"
comment: "支付订单表"
entity: "PayOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "订单编号" }
- { name: "app_id", type: "Long", comment: "应用编号" }
- { name: "channel_id", type: "Long", comment: "渠道编号" }
- { name: "channel_code", type: "String", comment: "渠道编码" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "merchant_order_id", type: "String", comment: "商户订单号" }
- { name: "subject", type: "String", comment: "商品标题" }
- { name: "body", type: "String", comment: "商品描述" }
- { name: "price", type: "Integer", comment: "支付金额(分)" }
- { name: "status", type: "Integer", comment: "支付状态: 0待支付/10成功/20退款/30关闭" }
- { name: "refund_price", type: "Integer", comment: "已退款金额(分)" }
- { name: "channel_order_no", type: "String", comment: "渠道订单号" }
- { name: "expire_time", type: "LocalDateTime", comment: "过期时间" }
- { name: "success_time", type: "LocalDateTime", comment: "支付成功时间" }
indexes:
- { name: "idx_app_merchant", columns: ["app_id", "merchant_order_id"] }
- name: "pay_order_extension"
comment: "支付订单扩展表"
entity: "PayOrderExtensionDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "扩展编号" }
- { name: "order_id", type: "Long", comment: "订单编号" }
- { name: "no", type: "String", comment: "外部订单号" }
- { name: "channel_id", type: "Long", comment: "渠道编号" }
- { name: "channel_code", type: "String", comment: "渠道编码" }
- { name: "status", type: "Integer", comment: "支付状态" }
- { name: "channel_notify_data", type: "String", comment: "渠道回调数据" }
- name: "pay_refund"
comment: "退款订单表"
entity: "PayRefundDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "退款编号" }
- { name: "no", type: "String", comment: "外部退款号" }
- { name: "app_id", type: "Long", comment: "应用编号" }
- { name: "channel_id", type: "Long", comment: "渠道编号" }
- { name: "order_id", type: "Long", comment: "支付订单编号" }
- { name: "order_no", type: "String", comment: "支付订单号" }
- { name: "merchant_order_id", type: "String", comment: "商户订单号" }
- { name: "merchant_refund_id", type: "String", comment: "商户退款号" }
- { name: "pay_price", type: "Integer", comment: "原支付金额(分)" }
- { name: "refund_price", type: "Integer", comment: "退款金额(分)" }
- { name: "status", type: "Integer", comment: "退款状态: 0待退款/1成功/2失败" }
- { name: "reason", type: "String", comment: "退款原因" }
- { name: "channel_refund_no", type: "String", comment: "渠道退款单号" }
- { name: "success_time", type: "LocalDateTime", comment: "退款成功时间" }
- name: "pay_wallet"
comment: "钱包表"
entity: "PayWalletDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "钱包编号" }
- { name: "user_id", type: "Long", comment: "用户编号" }
- { name: "balance", type: "Integer", comment: "余额(分)" }
- { name: "freeze_price", type: "Integer", comment: "冻结金额(分)" }
- name: "pay_wallet_transaction"
comment: "钱包交易表"
entity: "PayWalletTransactionDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "交易编号" }
- { name: "wallet_id", type: "Long", comment: "钱包编号" }
- { name: "biz_type", type: "Integer", comment: "业务类型" }
- { name: "biz_id", type: "String", comment: "业务编号" }
- { name: "amount", type: "Integer", comment: "交易金额(分)" }
- { name: "balance", type: "Integer", comment: "交易后余额(分)" }
- name: "pay_transfer"
comment: "转账表"
entity: "PayTransferDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "转账编号" }
- { name: "no", type: "String", comment: "外部转账号" }
- { name: "app_id", type: "Long", comment: "应用编号" }
- { name: "channel_id", type: "Long", comment: "渠道编号" }
- { name: "type", type: "Integer", comment: "转账类型" }
- { name: "price", type: "Integer", comment: "转账金额(分)" }
- { name: "status", type: "Integer", comment: "转账状态" }
- { name: "user_name", type: "String", comment: "收款人姓名" }
- { name: "user_account", type: "String", comment: "收款人账号" }
- name: "pay_notify_task"
comment: "通知任务表"
entity: "PayNotifyTaskDO"
extends: "BaseDO"
columns:
- { name: "id", type: "Long", comment: "任务编号" }
- { name: "app_id", type: "Long", comment: "应用编号" }
- { name: "type", type: "Integer", comment: "通知类型: 1订单/2退款" }
- { name: "data_id", type: "Long", comment: "数据编号" }
- { name: "status", type: "Integer", comment: "通知状态" }
- { name: "notify_times", type: "Integer", comment: "通知次数" }
- { name: "max_notify_times", type: "Integer", comment: "最大通知次数" }
# 表关系ER关系
relationships:
- from: "pay_app"
to: "pay_channel"
type: "1:N"
foreign_key: "app_id"
description: "一个应用可以有多个支付渠道"
- from: "pay_app"
to: "pay_order"
type: "1:N"
foreign_key: "app_id"
description: "一个应用可以有多个支付订单"
- from: "pay_order"
to: "pay_order_extension"
type: "1:N"
foreign_key: "order_id"
description: "一个订单可以有多个扩展单(多次支付尝试)"
- from: "pay_order"
to: "pay_refund"
type: "1:N"
foreign_key: "order_id"
description: "一个订单可以有多个退款单"
- from: "pay_channel"
to: "pay_order"
type: "1:N"
foreign_key: "channel_id"
description: "一个渠道可以有多个订单"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@RestController"
- "@RequestMapping"
- "@PreAuthorize('@ss.hasPermission')")
- "@Operation(summary = ...)"
example: |
@RestController
@RequestMapping("/pay/order")
@Tag(name = "管理后台 - 支付订单")
public class PayOrderController {
@PostMapping("/submit")
@Operation(summary = "提交支付订单")
@PermitAll // 支付提交允许匿名访问
public CommonResult<PayOrderSubmitRespVO> submitOrder(
@RequestBody @Valid PayOrderSubmitReqVO reqVO,
HttpServletRequest request) {
return success(orderService.submitOrder(reqVO, getClientIP(request)));
}
}
# Service层规范
service:
interface_pattern: "接口定义业务方法,使用 @Valid 注解校验参数"
impl_pattern: "实现类使用 @Service 和 @Validated 注解,事务使用 @Transactional"
example: |
@Service
@Validated
@Slf4j
public class PayOrderServiceImpl implements PayOrderService {
@Override
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
// 1. 校验 App
PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
// 2. 查询是否已存在
PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
app.getId(), reqDTO.getMerchantOrderId());
if (order != null) {
return order.getId();
}
// 3. 创建订单
order = PayOrderConvert.INSTANCE.convert(reqDTO)
.setAppId(app.getId())
.setStatus(PayOrderStatusEnum.WAITING.getStatus());
orderMapper.insert(order);
return order.getId();
}
}
# 数据访问规范
dal:
mapper_pattern: "继承 BaseMapperX使用 MyBatis-Plus 的 LambdaQueryWrapper"
example: |
@Mapper
public interface PayOrderMapper extends BaseMapperX<PayOrderDO> {
default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
return selectOne(PayOrderDO::getAppId, appId,
PayOrderDO::getMerchantOrderId, merchantOrderId);
}
default int updateByIdAndStatus(Long id, Integer status, PayOrderDO updateObj) {
return update(updateObj, new LambdaUpdateWrapper<PayOrderDO>()
.eq(PayOrderDO::getId, id)
.eq(PayOrderDO::getStatus, status));
}
}
# 异常处理
error_handling:
code_prefix: "1-007-XXX-XXX"
examples:
- code: "1_007_000_000"
message: "App 不存在"
- code: "1_007_001_000"
message: "支付渠道的配置不存在"
- code: "1_007_002_000"
message: "支付订单不存在"
- code: "1_007_002_003"
message: "支付订单已经过期"
- code: "1_007_006_000"
message: "退款金额超过订单可退款金额"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE, args...)"
# 支付客户端使用示例
pay_client_usage:
create_order: |
// 1. 获取支付客户端
PayClient client = channelService.getPayClient(channelId);
// 2. 构建统一下单请求
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO()
.setOutTradeNo(orderExtension.getNo())
.setSubject(order.getSubject())
.setPrice(order.getPrice())
.setExpireTime(order.getExpireTime())
.setNotifyUrl(notifyUrl);
// 3. 调用支付渠道
PayOrderRespDTO respDTO = client.unifiedOrder(reqDTO);
create_refund: |
// 1. 获取支付客户端
PayClient client = channelService.getPayClient(channelId);
// 2. 构建退款请求
PayRefundUnifiedReqDTO reqDTO = new PayRefundUnifiedReqDTO()
.setOutTradeNo(order.getNo())
.setOutRefundNo(refund.getNo())
.setRefundPrice(refund.getRefundPrice())
.setReason(refund.getReason());
// 3. 调用退款接口
PayRefundRespDTO respDTO = client.unifiedRefund(reqDTO);
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增支付渠道步骤
new_channel:
title: "新增支付渠道(如添加银联支付)"
steps:
- step: 1
title: "创建支付渠道枚举"
file: "PayChannelEnum.java"
action: "添加新的渠道编码,如 UNION_PAY(\"union_pay\", \"银联支付\")"
- step: 2
title: "创建配置类"
file: "UnionPayClientConfig.java"
action: |
实现 PayClientConfig 接口,定义银联支付所需的配置字段:
@Data
public class UnionPayClientConfig implements PayClientConfig {
private String merchantId; // 商户号
private String apiKey; // API密钥
private String certPath; // 证书路径
// ...
}
- step: 3
title: "创建抽象客户端类(可选)"
file: "AbstractUnionPayClient.java"
action: |
如果有多种支付方式如扫码、APP可以创建抽象类
public abstract class AbstractUnionPayClient
extends AbstractPayClient<UnionPayClientConfig> {
// 实现公共逻辑
}
- step: 4
title: "创建具体客户端实现类"
file: "UnionPayQrPayClient.java"
action: |
继承 AbstractPayClient 或抽象类,实现具体支付逻辑:
public class UnionPayQrPayClient extends AbstractPayClient<UnionPayClientConfig> {
public UnionPayQrPayClient(Long channelId, UnionPayClientConfig config) {
super(channelId, PayChannelEnum.UNION_PAY.getCode(), config);
}
@Override
protected void doInit() {
// 初始化银联SDK
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 调用银联下单接口
}
@Override
protected PayOrderRespDTO doParseOrderNotify(...) {
// 解析银联回调
}
// 实现其他抽象方法...
}
- step: 5
title: "注册到工厂"
file: "PayClientFactoryImpl.java"
action: |
在构造函数中注册新的客户端:
public PayClientFactoryImpl() {
// ...
clientClass.put(UNION_PAY, UnionPayQrPayClient.class);
}
- step: 6
title: "添加单元测试"
file: "UnionPayQrPayClientTest.java"
action: "编写测试用例验证支付流程"
# 新增功能步骤
new_feature:
title: "新增业务功能"
steps:
- step: 1
title: "创建DO实体"
action: "在 dal/dataobject 下创建实体类"
- step: 2
title: "创建Mapper"
action: "在 dal/mysql 下创建Mapper接口"
- step: 3
title: "创建Service接口和实现"
action: "在 service 下创建服务层"
- step: 4
title: "创建Controller"
action: "在 controller 下创建HTTP接口"
- step: 5
title: "创建API如需模块间调用"
action: "在 api 下创建对外API接口"
# 最佳实践
best_practices:
- "支付金额统一使用 Integer 类型,单位为分,避免浮点精度问题"
- "订单状态变更使用乐观锁,通过 updateByIdAndStatus 方法保证并发安全"
- "支付回调需要验签,使用渠道提供的验签方法"
- "异步通知需要支持重试机制,使用 PayNotifyTask 记录通知状态"
- "支付订单需要设置过期时间,定时任务关闭过期订单"
- "退款金额累计不能超过支付金额,在 Service 层进行校验"
- "客户端配置支持热刷新,通过 refresh 方法更新配置"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
api: "TenantApi"
purpose: "获取租户信息"
- module: "yudao-module-member"
api: "MemberUserApi"
purpose: "获取用户信息(钱包支付)"
# 外部依赖(第三方库)
external:
- name: "alipay-sdk-java"
version: "4.x"
purpose: "支付宝支付SDK"
- name: "weixin-java-pay"
version: "4.x"
purpose: "微信支付SDKWxJava"
- name: "hutool-all"
version: "5.x"
purpose: "Java工具库"
- name: "mybatis-plus"
version: "3.x"
purpose: "ORM框架"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/PayClient.java"
purpose: "支付客户端接口,定义统一的支付/退款/转账方法"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/AbstractPayClient.java"
purpose: "支付客户端抽象基类,实现模板方法模式"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java"
purpose: "支付客户端工厂,管理客户端创建和缓存"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java"
purpose: "支付宝支付抽象类,实现支付宝统一的接口"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java"
purpose: "微信支付抽象类,实现微信统一的接口"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java"
purpose: "支付订单服务实现,核心业务逻辑"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java"
purpose: "退款服务实现"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java"
purpose: "支付订单实体"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java"
purpose: "支付渠道枚举"
- path: "yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java"
purpose: "错误码定义"

View File

@@ -0,0 +1,345 @@
# Skill 文档 - yudao-module-report
# 数据报表模块知识提取
skill:
id: "skill-report"
name: "Report Skill"
version: "1.0.0"
module_path: "yudao-module-report"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
report 模块主要实现数据可视化报表功能,为系统提供报表设计、大屏设计、数据分析等能力。
核心功能包括:
1. 积木报表集成:基于 JimuReport 实现打印设计、报表设计、图形设计、大屏设计
2. GoView 大屏设计器:提供可视化大屏报表设计能力,支持 SQL 和 HTTP 数据源
3. 数据可视化:支持多种图表类型,满足企业数据展示需求
模块定位:作为企业级应用的可视化报表解决方案,与业务模块解耦,提供独立的数据展示能力。
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- "单一职责原则GoViewProject 负责 project 管理GoViewData 负责数据查询"
- "开放封闭原则:通过 JmReportTokenServiceI 接口扩展积木报表认证机制"
- "依赖倒置原则Service 层依赖接口而非实现,便于测试和扩展"
- "策略模式支持多种数据源查询策略SQL、HTTP可扩展新数据源"
- "适配器模式JmReportTokenServiceImpl 适配积木报表与 yudao 认证体系"
- "数据源抽象:默认使用 JdbcTemplate支持切换至 ClickHouse 等大数据引擎"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "GoViewProject"
type: "聚合根"
description: "大屏报表项目,包含项目配置、发布状态、报表内容等"
entities: ["GoViewProjectDO"]
value_objects: ["GoViewDataRespVO"]
value_objects:
- name: "GoViewDataRespVO"
type: "值对象"
description: "数据响应结构,包含维度(dimensions)和数据明细(source)"
- name: "GoViewProjectCreateReqVO"
type: "值对象"
description: "项目创建请求参数"
- name: "GoViewProjectUpdateReqVO"
type: "值对象"
description: "项目更新请求参数"
services:
- name: "GoViewProjectService"
type: "领域服务"
description: "管理大屏项目的创建、更新、删除、查询"
- name: "GoViewDataService"
type: "领域服务"
description: "提供数据查询能力,支持 SQL/HTTP 等数据源"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "controller"
purpose: "HTTP 接口层,提供 REST API"
components:
- "GoViewProjectController: 大屏项目管理接口"
- "GoViewDataController: 数据查询接口SQL/HTTP"
- name: "service"
purpose: "业务逻辑层"
components:
- "GoViewProjectService/GoViewProjectServiceImpl: 项目管理业务逻辑"
- "GoViewDataService/GoViewDataServiceImpl: 数据查询业务逻辑"
- name: "dal"
purpose: "数据访问层"
components:
- "GoViewProjectMapper: 项目数据访问"
- "GoViewProjectDO: 项目实体"
- name: "convert"
purpose: "对象转换层"
components:
- "GoViewProjectConvert: DO/VO 对象转换MapStruct"
- name: "framework"
purpose: "框架集成层"
components:
- "JmReportConfiguration: 积木报表配置"
- "JmReportTokenServiceImpl: 积木报表 Token 认证适配"
- "JmOnlDragExternalServiceImpl: 积木仪表盘扩展服务"
- "SecurityConfiguration: 安全配置"
# 设计模式应用
design_patterns:
- pattern: "适配器模式"
location: "framework/jmreport/core/service/JmReportTokenServiceImpl.java"
purpose: "将 yudao 的 OAuth2 认证体系适配到积木报表的 JmReportTokenServiceI 接口"
- pattern: "策略模式"
location: "controller/admin/goview/GoViewDataController.java"
purpose: "支持多种数据查询策略getDataBySQL、getDataByHttp"
- pattern: "转换器模式"
location: "convert/goview/GoViewProjectConvert.java"
purpose: "使用 MapStruct 实现 DO 与 VO 之间的转换"
# 模块间通信
communication:
apis: [] # 对外暴露的API通过 HTTP 接口暴露)
consumers:
- module: "system"
api: "OAuth2TokenCommonApi"
purpose: "验证 Token 获取用户信息"
- module: "system"
api: "PermissionCommonApi"
purpose: "权限校验,判断用户角色"
mq: [] # 无消息队列使用
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "report 模块实体继承 BaseDO包含 id、creator、create_time、updater、update_time、deleted 字段"
# 核心数据表
tables:
- name: "report_go_view_project"
comment: "GoView 大屏项目表"
entity: "GoViewProjectDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键ID自增" }
- { name: "name", type: "VARCHAR(255)", comment: "项目名称" }
- { name: "pic_url", type: "VARCHAR(1024)", comment: "预览图片URL" }
- { name: "content", type: "LONGTEXT", comment: "报表内容JSON格式配置" }
- { name: "status", type: "TINYINT", comment: "发布状态0-已发布1-未发布" }
- { name: "remark", type: "VARCHAR(255)", comment: "项目备注" }
- { name: "creator", type: "VARCHAR(64)", comment: "创建人" }
- { name: "create_time", type: "DATETIME", comment: "创建时间" }
- { name: "updater", type: "VARCHAR(64)", comment: "更新人" }
- { name: "update_time", type: "DATETIME", comment: "更新时间" }
- { name: "deleted", type: "BIT", comment: "是否删除" }
indexes:
- { name: "idx_creator", columns: ["creator"] }
# 表关系ER关系
relationships:
- from: "report_go_view_project"
to: "system_users"
type: "N:1"
foreign_key: "creator"
comment: "项目创建人关联用户表"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - XXX')"
- "@RestController"
- "@RequestMapping('/report/xxx')"
- "@Validated"
- "@PreAuthorize('@ss.hasPermission(...)')"
example: |
@Tag(name = "管理后台 - GoView 项目")
@RestController
@RequestMapping("/report/go-view/project")
@Validated
public class GoViewProjectController {
@Resource
private GoViewProjectService goViewProjectService;
@PostMapping("/create")
@Operation(summary = "创建项目")
@PreAuthorize("@ss.hasPermission('report:go-view-project:create')")
public CommonResult<Long> createProject(@Valid @RequestBody GoViewProjectCreateReqVO createReqVO) {
return success(goViewProjectService.createProject(createReqVO));
}
}
# Service层规范
service:
interface_pattern: "定义接口 + 使用 @Service 注解的实现类"
impl_pattern: "使用 @Validated 注解支持参数校验,依赖注入使用 @Resource"
example: |
public interface GoViewProjectService {
Long createProject(@Valid GoViewProjectCreateReqVO createReqVO);
void updateProject(@Valid GoViewProjectUpdateReqVO updateReqVO);
void deleteProject(Long id);
GoViewProjectDO getProject(Long id);
PageResult<GoViewProjectDO> getMyProjectPage(PageParam pageReqVO, Long userId);
}
@Service
@Validated
public class GoViewProjectServiceImpl implements GoViewProjectService {
@Resource
private GoViewProjectMapper goViewProjectMapper;
@Override
public Long createProject(GoViewProjectCreateReqVO createReqVO) {
GoViewProjectDO goViewProject = GoViewProjectConvert.INSTANCE.convert(createReqVO)
.setStatus(CommonStatusEnum.DISABLE.getStatus());
goViewProjectMapper.insert(goViewProject);
return goViewProject.getId();
}
}
# 数据访问规范
dal:
mapper_pattern: "继承 BaseMapperX<DO>,使用 default 方法实现自定义查询"
example: |
@Mapper
public interface GoViewProjectMapper extends BaseMapperX<GoViewProjectDO> {
default PageResult<GoViewProjectDO> selectPage(PageParam reqVO, Long userId) {
return selectPage(reqVO, new LambdaQueryWrapperX<GoViewProjectDO>()
.eq(GoViewProjectDO::getCreator, userId)
.orderByDesc(GoViewProjectDO::getId));
}
}
# 异常处理
error_handling:
code_prefix: "1_003_000_000"
examples:
- code: "1_003_000_000"
message: "GoView 项目不存在"
constant: "GO_VIEW_PROJECT_NOT_EXISTS"
usage: |
// 使用方式
if (goViewProjectMapper.selectById(id) == null) {
throw exception(GO_VIEW_PROJECT_NOT_EXISTS);
}
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ERROR_CODE)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增报表功能"
steps:
- "1. 在 dal/dataobject 下创建 DO 实体类,继承 BaseDO"
- "2. 在 dal/mysql 下创建 Mapper 接口,继承 BaseMapperX"
- "3. 在 controller/admin/vo 下创建请求/响应 VO"
- "4. 在 convert 下创建 Convert 接口(使用 MapStruct"
- "5. 在 service 下创建 Service 接口和实现类"
- "6. 在 controller/admin 下创建 Controller"
- "7. 在 enums/ErrorCodeConstants 添加错误码"
# 新增数据源类型
new_channel:
title: "新增数据源类型"
steps:
- "1. 在 GoViewDataController 中新增查询方法,如 getDataByElasticsearch"
- "2. 在 GoViewDataService 接口中定义新方法"
- "3. 在 GoViewDataServiceImpl 中实现查询逻辑"
- "4. 创建对应的请求 VO如 GoViewDataGetByEsReqVO"
- "5. 添加权限注解 @PreAuthorize('@ss.hasPermission(...)')"
example: |
// 新增 Elasticsearch 数据源查询
@RequestMapping("/get-by-es")
@Operation(summary = "使用 Elasticsearch 查询数据")
@PreAuthorize("@ss.hasPermission('report:go-view-data:get-by-es')")
public CommonResult<GoViewDataRespVO> getDataByEs(@Valid @RequestBody GoViewDataGetByEsReqVO reqVO) {
return success(goViewDataService.getDataByEs(reqVO));
}
# 最佳实践
best_practices:
- "数据查询时考虑性能,大数据量场景建议使用 ClickHouse 等列式数据库"
- "报表内容使用 JSON 格式存储,前端负责解析渲染"
- "项目创建时默认状态为未发布(status=1),需手动发布"
- "使用 creator 字段实现数据隔离,用户只能看到自己创建的项目"
- "积木报表集成时需注意 Token 传递,通过 customApiHeader 方法处理"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "system"
api: "OAuth2TokenCommonApi"
purpose: "积木报表 Token 验证"
- module: "system"
api: "PermissionCommonApi"
purpose: "积木报表权限校验"
# 外部依赖(第三方库)
external:
- name: "jimureport-spring-boot-starter"
version: "管理在父 POM"
purpose: "积木报表核心依赖,提供报表设计器"
- name: "jimubi-spring-boot-starter"
version: "管理在父 POM"
purpose: "积木仪表盘/大屏设计器"
- name: "spring-boot-starter-jdbc"
version: "Spring Boot 管理"
purpose: "JdbcTemplate 数据源查询"
- name: "mapstruct"
version: "管理在父 POM"
purpose: "对象转换"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/dal/dataobject/goview/GoViewProjectDO.java"
purpose: "GoView 项目实体类,对应 report_go_view_project 表"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/service/goview/GoViewProjectService.java"
purpose: "项目管理服务接口"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/service/goview/GoViewDataServiceImpl.java"
purpose: "数据查询服务实现,核心 SQL 查询逻辑"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/controller/admin/goview/GoViewProjectController.java"
purpose: "项目管理 HTTP 接口"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/controller/admin/goview/GoViewDataController.java"
purpose: "数据查询 HTTP 接口"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/config/JmReportConfiguration.java"
purpose: "积木报表配置类"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java"
purpose: "积木报表 Token 认证适配器"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/enums/ErrorCodeConstants.java"
purpose: "错误码定义"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/dal/mysql/goview/GoViewProjectMapper.java"
purpose: "项目数据访问 Mapper"
- path: "yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/convert/goview/GoViewProjectConvert.java"
purpose: "对象转换器MapStruct"

View File

@@ -0,0 +1,840 @@
# Skill 文档 - yudao-module-system 模块
# 系统管理模块完整知识提取
skill:
id: "skill-system"
name: "System Module Skill"
version: "1.0.0"
module_path: "yudao-module-system"
created_at: "2026-03-18"
updated_at: "2026-03-18"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
yudao-module-system 是整个平台的核心基础设施模块,负责系统级的管理功能:
- 用户管理:管理后台用户的增删改查、密码管理、状态控制
- 角色管理基于RBAC模型的角色定义与权限分配
- 权限管理:菜单权限、按钮权限、数据权限的统一控制
- 部门管理:组织架构的树形结构管理
- 岗位管理:岗位信息的维护
- 租户管理:多租户架构的核心支撑,租户创建、套餐绑定、过期控制
- 字典管理:系统字典类型与字典数据的维护
- 通知公告:系统公告的发布与管理
- 短信/邮件/站内信:消息通知的三驾马车
- OAuth2开放授权协议支持
- 社交登录:第三方登录集成(微信、钉钉等)
该模块是其他所有业务模块的基础,提供统一的用户认证、权限校验、租户隔离能力。
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- principle: "RBAC权限模型"
description: "采用基于角色的访问控制模型,用户-角色-菜单三层关联,支持细粒度的按钮级权限控制"
- principle: "多租户架构"
description: "通过TenantBaseDO基类实现租户数据隔离所有业务表自动携带tenant_id字段"
- principle: "数据权限"
description: "支持5种数据权限范围全部、自定义部门、本部门、本部门及子部门、仅本人"
- principle: "分层架构"
description: "严格遵循Controller-Service-DAL三层架构API层提供跨模块调用能力"
- principle: "接口隔离"
description: "Service接口与实现分离便于测试和扩展"
- principle: "缓存优先"
description: "权限数据使用Spring Cache + Redis缓存减少数据库查询压力"
- principle: "统一异常处理"
description: "通过ErrorCodeConstants定义统一错误码格式为1-002-XXX-XXX"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "User用户聚合"
type: "聚合根"
entities: ["AdminUserDO", "UserRoleDO", "UserPostDO"]
description: "用户是系统的核心实体,关联角色和岗位,属于租户"
- name: "Role角色聚合"
type: "聚合根"
entities: ["RoleDO", "RoleMenuDO"]
description: "角色是权限的载体,关联菜单实现功能权限控制"
- name: "Menu菜单聚合"
type: "聚合根"
entities: ["MenuDO"]
description: "菜单是权限的最小单元,支持目录、菜单、按钮三种类型"
- name: "Dept部门聚合"
type: "聚合根"
entities: ["DeptDO"]
description: "部门是组织架构的核心,树形结构,支持数据权限"
- name: "Tenant租户聚合"
type: "聚合根"
entities: ["TenantDO", "TenantPackageDO"]
description: "租户是SaaS架构的核心绑定套餐控制功能范围"
value_objects:
- name: "Post岗位"
description: "岗位是用户的职位标识,不涉及复杂业务逻辑"
- name: "DictType/DictData字典"
description: "字典是系统配置的枚举值,全局共享不区分租户"
- name: "Notice通知公告"
description: "通知公告是系统级消息简单CRUD"
services:
- name: "PermissionService"
description: "权限领域服务,处理用户-角色-菜单的关联关系"
- name: "AdminAuthService"
description: "认证领域服务,处理登录、登出、令牌刷新"
- name: "TenantService"
description: "租户领域服务,处理租户生命周期管理"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口供其他模块RPC调用"
components:
- "PermissionApi: 权限校验接口"
- "AdminUserApi: 用户信息查询接口"
- "DeptApi: 部门信息查询接口"
- "PostApi: 岗位信息查询接口"
- "DictDataApi: 字典数据查询接口"
- "RoleApi: 角色信息查询接口"
- "SmsSendApi: 短信发送接口"
- "MailSendApi: 邮件发送接口"
- "NotifyMessageSendApi: 站内信发送接口"
- "SocialUserApi: 社交用户接口"
- "LoginLogApi: 登录日志接口"
- "OperateLogApi: 操作日志接口"
- name: "controller"
purpose: "HTTP接口供前端调用"
components:
- "AuthController: 认证相关(登录、登出、刷新令牌)"
- "UserController: 用户管理"
- "RoleController: 角色管理"
- "MenuController: 菜单管理"
- "DeptController: 部门管理"
- "PostController: 岗位管理"
- "DictTypeController/DictDataController: 字典管理"
- "TenantController: 租户管理"
- "TenantPackageController: 租户套餐管理"
- "SmsChannelController/SmsTemplateController: 短信管理"
- "MailAccountController/MailTemplateController: 邮件管理"
- "NotifyTemplateController/NotifyMessageController: 站内信管理"
- "OAuth2ClientController: OAuth2客户端管理"
- "SocialClientController: 社交客户端管理"
- name: "service"
purpose: "业务逻辑层"
components:
- "auth: AdminAuthService认证服务"
- "permission: PermissionService, RoleService, MenuService权限服务"
- "user: AdminUserService用户服务"
- "dept: DeptService, PostService组织服务"
- "dict: DictTypeService, DictDataService字典服务"
- "tenant: TenantService, TenantPackageService租户服务"
- "sms: SmsChannelService, SmsTemplateService, SmsSendService, SmsCodeService短信服务"
- "mail: MailAccountService, MailTemplateService, MailSendService邮件服务"
- "notify: NotifyTemplateService, NotifyMessageService, NotifySendService站内信服务"
- "oauth2: OAuth2ClientService, OAuth2TokenService, OAuth2CodeServiceOAuth2服务"
- "social: SocialClientService, SocialUserService社交服务"
- "logger: LoginLogService, OperateLogService日志服务"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject: 32个DO实体类"
- "mysql: MyBatis Mapper接口"
- "redis: Redis缓存操作"
# 设计模式应用
design_patterns:
- pattern: "策略模式"
location: "SmsChannelService, MailSendService"
purpose: "不同短信/邮件渠道的发送策略切换"
- pattern: "模板方法模式"
location: "NotifySendService, SmsSendService"
purpose: "消息发送的统一流程,子类实现具体发送逻辑"
- pattern: "工厂模式"
location: "SmsClientFactory, MailClientFactory"
purpose: "根据渠道类型创建对应的客户端实例"
- pattern: "代理模式"
location: "PermissionServiceImpl.getSelf()"
purpose: "解决Spring AOP缓存注解生效问题"
- pattern: "DTO模式"
location: "api/dto/*"
purpose: "跨模块数据传输对象,隔离内部实体"
- pattern: "转换器模式"
location: "convert/*"
purpose: "DO与VO之间的对象转换使用MapStruct"
# 模块间通信
communication:
apis: # 对外暴露的API
- api: "PermissionApi"
methods: ["hasAnyPermissions", "hasAnyRoles", "getUserRoleIdListByRoleIds"]
consumers: ["yudao-module-infra", "yudao-module-member"]
- api: "AdminUserApi"
methods: ["getUser", "getUserList"]
consumers: ["yudao-module-infra", "yudao-module-pay"]
- api: "DeptApi"
methods: ["getDept", "getDeptList"]
consumers: ["yudao-module-infra"]
- api: "DictDataApi"
methods: ["parseDictData", "getDictDataList"]
consumers: ["所有模块"]
- api: "SmsSendApi"
methods: ["sendSingleSms"]
consumers: ["yudao-module-member"]
- api: "MailSendApi"
methods: ["sendSingleMail"]
consumers: ["yudao-module-member"]
consumers: # 消费的其他模块API
- api: "PermissionCommonApi (framework-common)"
purpose: "基础权限校验能力"
- api: "FileApi (yudao-module-infra)"
purpose: "文件上传(用户头像等)"
mq: [] # 暂未使用消息队列
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO | TenantBaseDO"
description: |
- BaseDO: 包含id, creator, createTime, updater, updateTime, deleted字段
- TenantBaseDO: 继承BaseDO额外包含tenantId字段实现多租户隔离
- 使用@TenantIgnore注解标记不需要租户隔离的表如system_menu, system_dict_data
# 核心数据表
tables:
# ========== 用户相关 ==========
- name: "system_users"
comment: "用户表"
entity: "AdminUserDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "用户ID主键" }
- { name: "username", type: "VARCHAR(30)", comment: "用户账号" }
- { name: "password", type: "VARCHAR(100)", comment: "加密密码" }
- { name: "nickname", type: "VARCHAR(30)", comment: "用户昵称" }
- { name: "dept_id", type: "BIGINT", comment: "部门ID" }
- { name: "post_ids", type: "VARCHAR(255)", comment: "岗位编号数组JSON格式" }
- { name: "email", type: "VARCHAR(50)", comment: "用户邮箱" }
- { name: "mobile", type: "VARCHAR(20)", comment: "手机号码" }
- { name: "sex", type: "TINYINT", comment: "用户性别" }
- { name: "avatar", type: "VARCHAR(100)", comment: "用户头像" }
- { name: "status", type: "TINYINT", comment: "帐号状态0正常 1停用" }
- { name: "login_ip", type: "VARCHAR(50)", comment: "最后登录IP" }
- { name: "login_date", type: "DATETIME", comment: "最后登录时间" }
indexes:
- { name: "uk_username", columns: ["username", "tenant_id"] }
- { name: "uk_mobile", columns: ["mobile", "tenant_id"] }
- name: "system_user_role"
comment: "用户-角色关联表"
entity: "UserRoleDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键" }
- { name: "user_id", type: "BIGINT", comment: "用户ID" }
- { name: "role_id", type: "BIGINT", comment: "角色ID" }
indexes:
- { name: "idx_user_id", columns: ["user_id"] }
- { name: "idx_role_id", columns: ["role_id"] }
# ========== 角色相关 ==========
- name: "system_role"
comment: "角色表"
entity: "RoleDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "角色ID" }
- { name: "name", type: "VARCHAR(30)", comment: "角色名称" }
- { name: "code", type: "VARCHAR(100)", comment: "角色标识" }
- { name: "sort", type: "INT", comment: "角色排序" }
- { name: "status", type: "TINYINT", comment: "角色状态" }
- { name: "type", type: "TINYINT", comment: "角色类型1系统内置 2自定义" }
- { name: "data_scope", type: "TINYINT", comment: "数据范围" }
- { name: "data_scope_dept_ids", type: "VARCHAR(500)", comment: "数据范围部门ID数组" }
indexes:
- { name: "uk_code", columns: ["code", "tenant_id"] }
- name: "system_role_menu"
comment: "角色-菜单关联表"
entity: "RoleMenuDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键" }
- { name: "role_id", type: "BIGINT", comment: "角色ID" }
- { name: "menu_id", type: "BIGINT", comment: "菜单ID" }
indexes:
- { name: "idx_role_id", columns: ["role_id"] }
- { name: "idx_menu_id", columns: ["menu_id"] }
# ========== 菜单相关 ==========
- name: "system_menu"
comment: "菜单表"
entity: "MenuDO"
extends: "BaseDO"
annotation: "@TenantIgnore"
columns:
- { name: "id", type: "BIGINT", comment: "菜单ID" }
- { name: "name", type: "VARCHAR(50)", comment: "菜单名称" }
- { name: "permission", type: "VARCHAR(100)", comment: "权限标识" }
- { name: "type", type: "TINYINT", comment: "菜单类型1目录 2菜单 3按钮" }
- { name: "sort", type: "INT", comment: "显示顺序" }
- { name: "parent_id", type: "BIGINT", comment: "父菜单ID" }
- { name: "path", type: "VARCHAR(200)", comment: "路由地址" }
- { name: "icon", type: "VARCHAR(100)", comment: "菜单图标" }
- { name: "component", type: "VARCHAR(255)", comment: "组件路径" }
- { name: "component_name", type: "VARCHAR(100)", comment: "组件名" }
- { name: "status", type: "TINYINT", comment: "菜单状态" }
- { name: "visible", type: "BIT", comment: "是否可见" }
- { name: "keep_alive", type: "BIT", comment: "是否缓存" }
- { name: "always_show", type: "BIT", comment: "是否总是显示" }
# ========== 部门相关 ==========
- name: "system_dept"
comment: "部门表"
entity: "DeptDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "部门ID" }
- { name: "name", type: "VARCHAR(30)", comment: "部门名称" }
- { name: "parent_id", type: "BIGINT", comment: "父部门ID" }
- { name: "sort", type: "INT", comment: "显示顺序" }
- { name: "leader_user_id", type: "BIGINT", comment: "负责人" }
- { name: "phone", type: "VARCHAR(20)", comment: "联系电话" }
- { name: "email", type: "VARCHAR(50)", comment: "邮箱" }
- { name: "status", type: "TINYINT", comment: "部门状态" }
- name: "system_post"
comment: "岗位表"
entity: "PostDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "岗位ID" }
- { name: "name", type: "VARCHAR(50)", comment: "岗位名称" }
- { name: "code", type: "VARCHAR(64)", comment: "岗位编码" }
- { name: "sort", type: "INT", comment: "显示顺序" }
- { name: "status", type: "TINYINT", comment: "状态" }
# ========== 租户相关 ==========
- name: "system_tenant"
comment: "租户表"
entity: "TenantDO"
extends: "BaseDO"
annotation: "@TenantIgnore"
columns:
- { name: "id", type: "BIGINT", comment: "租户编号" }
- { name: "name", type: "VARCHAR(100)", comment: "租户名" }
- { name: "contact_user_id", type: "BIGINT", comment: "联系人的用户编号" }
- { name: "contact_name", type: "VARCHAR(100)", comment: "联系人" }
- { name: "contact_mobile", type: "VARCHAR(20)", comment: "联系手机" }
- { name: "status", type: "TINYINT", comment: "租户状态" }
- { name: "websites", type: "VARCHAR(500)", comment: "绑定域名列表" }
- { name: "package_id", type: "BIGINT", comment: "租户套餐编号" }
- { name: "expire_time", type: "DATETIME", comment: "过期时间" }
- { name: "account_count", type: "INT", comment: "账号数量" }
- name: "system_tenant_package"
comment: "租户套餐表"
entity: "TenantPackageDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "套餐编号" }
- { name: "name", type: "VARCHAR(100)", comment: "套餐名" }
- { name: "status", type: "TINYINT", comment: "套餐状态" }
- { name: "menu_ids", type: "VARCHAR(2000)", comment: "关联菜单编号数组" }
- { name: "remark", type: "VARCHAR(255)", comment: "备注" }
# ========== 字典相关 ==========
- name: "system_dict_type"
comment: "字典类型表"
entity: "DictTypeDO"
extends: "BaseDO"
annotation: "@TenantIgnore"
columns:
- { name: "id", type: "BIGINT", comment: "字典主键" }
- { name: "name", type: "VARCHAR(100)", comment: "字典名称" }
- { name: "type", type: "VARCHAR(100)", comment: "字典类型" }
- { name: "status", type: "TINYINT", comment: "字典状态" }
- name: "system_dict_data"
comment: "字典数据表"
entity: "DictDataDO"
extends: "BaseDO"
annotation: "@TenantIgnore"
columns:
- { name: "id", type: "BIGINT", comment: "字典主键" }
- { name: "sort", type: "INT", comment: "字典排序" }
- { name: "label", type: "VARCHAR(100)", comment: "字典标签" }
- { name: "value", type: "VARCHAR(100)", comment: "字典值" }
- { name: "dict_type", type: "VARCHAR(100)", comment: "字典类型" }
- { name: "status", type: "TINYINT", comment: "状态" }
- { name: "color_type", type: "VARCHAR(20)", comment: "颜色类型" }
# ========== 短信相关 ==========
- name: "system_sms_channel"
comment: "短信渠道表"
entity: "SmsChannelDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "渠道编号" }
- { name: "signature", type: "VARCHAR(12)", comment: "短信签名" }
- { name: "code", type: "VARCHAR(63)", comment: "渠道编码" }
- { name: "status", type: "TINYINT", comment: "开启状态" }
- { name: "api_key", type: "VARCHAR(128)", comment: "短信API密钥" }
- { name: "callback_url", type: "VARCHAR(255)", comment: "短信回调URL" }
- name: "system_sms_template"
comment: "短信模板表"
entity: "SmsTemplateDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "模板编号" }
- { name: "name", type: "VARCHAR(63)", comment: "模板名称" }
- { name: "code", type: "VARCHAR(63)", comment: "模板编码" }
- { name: "content", type: "VARCHAR(255)", comment: "模板内容" }
- { name: "channel_id", type: "BIGINT", comment: "短信渠道编号" }
- { name: "channel_code", type: "VARCHAR(63)", comment: "短信渠道编码" }
- { name: "status", type: "TINYINT", comment: "开启状态" }
- name: "system_sms_log"
comment: "短信日志表"
entity: "SmsLogDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "日志编号" }
- { name: "channel_id", type: "BIGINT", comment: "短信渠道编号" }
- { name: "template_id", type: "BIGINT", comment: "模板编号" }
- { name: "template_code", type: "VARCHAR(63)", comment: "模板编码" }
- { name: "template_content", type: "VARCHAR(255)", comment: "模板内容" }
- { name: "template_params", type: "VARCHAR(255)", comment: "模板参数" }
- { name: "mobile", type: "VARCHAR(11)", comment: "手机号" }
- { name: "send_status", type: "TINYINT", comment: "发送状态" }
- { name: "send_time", type: "DATETIME", comment: "发送时间" }
- { name: "receive_status", type: "TINYINT", comment: "接收状态" }
- { name: "receive_time", type: "DATETIME", comment: "接收时间" }
# ========== 邮件相关 ==========
- name: "system_mail_account"
comment: "邮箱账号表"
entity: "MailAccountDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键" }
- { name: "mail", type: "VARCHAR(100)", comment: "邮箱" }
- { name: "username", type: "VARCHAR(100)", comment: "用户名" }
- { name: "password", type: "VARCHAR(100)", comment: "密码" }
- { name: "host", type: "VARCHAR(100)", comment: "SMTP服务器域名" }
- { name: "port", type: "INT", comment: "SMTP服务器端口" }
- { name: "ssl_enable", type: "BIT", comment: "是否开启SSL" }
- name: "system_mail_template"
comment: "邮件模板表"
entity: "MailTemplateDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "主键" }
- { name: "name", type: "VARCHAR(63)", comment: "模板名称" }
- { name: "code", type: "VARCHAR(63)", comment: "模板编码" }
- { name: "account_id", type: "BIGINT", comment: "发送的邮箱账号编号" }
- { name: "nickname", type: "VARCHAR(255)", comment: "发送人名称" }
- { name: "title", type: "VARCHAR(255)", comment: "邮件标题" }
- { name: "content", type: "VARCHAR(10240)", comment: "邮件内容" }
- { name: "status", type: "TINYINT", comment: "开启状态" }
# ========== OAuth2相关 ==========
- name: "system_oauth2_client"
comment: "OAuth2客户端表"
entity: "OAuth2ClientDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "client_id", type: "VARCHAR(255)", comment: "客户端编号" }
- { name: "secret", type: "VARCHAR(255)", comment: "客户端密钥" }
- { name: "name", type: "VARCHAR(255)", comment: "应用名" }
- { name: "logo", type: "VARCHAR(255)", comment: "应用图标" }
- { name: "authorized_grant_types", type: "VARCHAR(255)", comment: "授权类型" }
- { name: "scopes", type: "VARCHAR(255)", comment: "授权范围" }
- { name: "redirect_uris", type: "VARCHAR(255)", comment: "回调地址" }
- { name: "access_token_validity_seconds", type: "INT", comment: "访问令牌有效期" }
- { name: "refresh_token_validity_seconds", type: "INT", comment: "刷新令牌有效期" }
- name: "system_oauth2_access_token"
comment: "OAuth2访问令牌表"
entity: "OAuth2AccessTokenDO"
extends: "TenantBaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "编号" }
- { name: "access_token", type: "VARCHAR(255)", comment: "访问令牌" }
- { name: "user_id", type: "BIGINT", comment: "用户编号" }
- { name: "user_type", type: "TINYINT", comment: "用户类型" }
- { name: "client_id", type: "VARCHAR(255)", comment: "客户端编号" }
- { name: "expires_time", type: "DATETIME", comment: "过期时间" }
# 表关系ER关系
relationships:
- from: "system_users"
to: "system_dept"
type: "N:1"
foreign_key: "dept_id"
description: "用户属于一个部门"
- from: "system_users"
to: "system_post"
type: "N:N"
foreign_key: "post_ids (JSON数组)"
description: "用户可关联多个岗位"
- from: "system_user_role"
to: "system_users"
type: "N:1"
foreign_key: "user_id"
- from: "system_user_role"
to: "system_role"
type: "N:1"
foreign_key: "role_id"
- from: "system_role_menu"
to: "system_role"
type: "N:1"
foreign_key: "role_id"
- from: "system_role_menu"
to: "system_menu"
type: "N:1"
foreign_key: "menu_id"
- from: "system_menu"
to: "system_menu"
type: "N:1"
foreign_key: "parent_id"
description: "菜单树形结构"
- from: "system_dept"
to: "system_dept"
type: "N:1"
foreign_key: "parent_id"
description: "部门树形结构"
- from: "system_tenant"
to: "system_tenant_package"
type: "N:1"
foreign_key: "package_id"
- from: "system_dict_data"
to: "system_dict_type"
type: "N:1"
foreign_key: "dict_type"
- from: "system_sms_template"
to: "system_sms_channel"
type: "N:1"
foreign_key: "channel_id"
- from: "system_mail_template"
to: "system_mail_account"
type: "N:1"
foreign_key: "account_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - XXX'): OpenAPI 3.0 文档标签"
- "@RestController: RESTful控制器"
- "@RequestMapping('/system/xxx'): 路由前缀"
- "@Validated: 参数校验"
- "@Operation(summary = 'XXX'): 接口文档说明"
- "@PreAuthorize('@ss.hasPermission('system:xxx:add')'): 权限校验"
- "@PermitAll: 允许匿名访问(如登录接口)"
example: |
@Tag(name = "管理后台 - 用户")
@RestController
@RequestMapping("/system/user")
@Validated
public class UserController {
@PostMapping("/create")
@Operation(summary = "创建用户")
@PreAuthorize("@ss.hasPermission('system:user:create')")
public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO createReqVO) {
return success(userService.createUser(createReqVO));
}
}
# Service层规范
service:
interface_pattern: |
- 接口定义在 service 包下
- 方法参数使用 @Valid 注解进行校验
- 返回值使用具体类型或 void
impl_pattern: |
- 实现类放在 service 包下,命名为 XxxServiceImpl
- 使用 @Service 注解
- 使用 @Resource 注入 Mapper 和其他 Service
- 使用 @Transactional(rollbackFor = Exception.class) 标记事务方法
- 使用 @Cacheable/@CacheEvict 进行缓存操作
example: |
@Service
@Slf4j
public class PermissionServiceImpl implements PermissionService {
@Resource
private RoleMenuMapper roleMenuMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Override
@DSTransactional // 多数据源事务
@Caching(evict = {
@CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true),
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true)
})
public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
// 业务逻辑
}
}
# 数据访问规范
dal:
mapper_pattern: |
- Mapper 接口继承 BaseMapperX<DO类>
- 使用 @Mapper 注解
- 复杂查询使用 MPJLambdaWrapperWrapper 或 XML
example: |
@Mapper
public interface UserMapper extends BaseMapperX<AdminUserDO> {
default AdminUserDO selectByUsername(String username) {
return selectOne(AdminUserDO::getUsername, username);
}
default List<AdminUserDO> selectListByDeptIds(Collection<Long> deptIds) {
return selectList(new LambdaQueryWrapperX<AdminUserDO>()
.inIfPresent(AdminUserDO::getDeptId, deptIds));
}
}
# 异常处理
error_handling:
code_prefix: "1-002-XXX-XXX"
format: "模块号-子模块号-序号"
examples:
- code: "1_002_000_000"
message: "登录失败,账号密码不正确"
module: "AUTH"
- code: "1_002_001_000"
message: "已经存在该名字的菜单"
module: "MENU"
- code: "1_002_002_000"
message: "角色不存在"
module: "ROLE"
- code: "1_002_003_000"
message: "用户账号已经存在"
module: "USER"
- code: "1_002_004_000"
message: "已经存在该名字的部门"
module: "DEPT"
- code: "1_002_015_000"
message: "租户不存在"
module: "TENANT"
usage: |
// 在Service中抛出业务异常
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 带参数的异常
throw exception(ROLE_NAME_DUPLICATE, name);
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "CommonResult.error(errorCode)"
usage: |
// 成功响应
return success(userService.getUser(id));
// 失败响应(通常由全局异常处理器处理)
throw exception(USER_NOT_EXISTS);
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增业务功能(以新增'操作日志'功能为例)"
steps:
- step: "1. 创建DO实体类"
detail: |
在 dal/dataobject/logger/ 下创建 OperateLogDO.java
继承 BaseDO 或 TenantBaseDO
使用 @TableName 指定表名
- step: "2. 创建Mapper接口"
detail: |
在 dal/mysql/logger/ 下创建 OperateLogMapper.java
继承 BaseMapperX<OperateLogDO>
- step: "3. 创建Service接口和实现"
detail: |
在 service/logger/ 下创建 OperateLogService.java 接口
在 service/logger/ 下创建 OperateLogServiceImpl.java 实现
使用 @Service 和 @Resource 注解
- step: "4. 创建Controller"
detail: |
在 controller/admin/logger/ 下创建 OperateLogController.java
使用 @RestController, @RequestMapping, @Tag 等注解
使用 @PreAuthorize 进行权限控制
- step: "5. 创建VO类"
detail: |
在 controller/admin/logger/vo/ 下创建请求和响应VO
使用 @Data, @Valid 等注解进行参数校验
- step: "6. 添加错误码"
detail: |
在 enums/ErrorCodeConstants.java 中添加错误码
格式ErrorCode XXX_NOT_EXISTS = new ErrorCode(1_002_XXX_000, "XXX不存在");
- step: "7. 添加菜单权限"
detail: |
在数据库 system_menu 表中添加菜单记录
配置权限标识如 system:operate-log:query
# 新增渠道/类型示例
new_channel:
title: "新增短信渠道(以新增'华为云短信'为例)"
steps:
- step: "1. 添加渠道枚举"
detail: |
在 SmsChannelEnum 中添加新渠道类型
HUAWEI("huawei", "华为云")
- step: "2. 创建客户端实现"
detail: |
在 framework/sms/core/client/impl/ 下创建 HuaweiSmsClient.java
继承 AbstractSmsClient 抽象类
实现 doSendSms() 方法
- step: "3. 注册到工厂"
detail: |
在 SmsClientFactory 中注册新渠道
clients.put(SmsChannelEnum.HUAWEI, new HuaweiSmsClient());
- step: "4. 添加配置类"
detail: |
创建 HuaweiSmsChannelProperties 配置类
继承 SmsChannelProperties 基类
# 新增字典类型
new_dict:
title: "新增字典类型"
steps:
- step: "1. 通过管理后台添加"
detail: |
登录管理后台 -> 系统管理 -> 字典管理 -> 新增字典类型
填写字典名称、字典类型(如 system_user_sex
- step: "2. 添加字典数据"
detail: |
在字典类型下添加字典数据项
设置字典标签、字典值、排序、状态等
- step: "3. 代码中使用"
detail: |
// 获取字典数据列表
List<DictDataRespDTO> dictList = dictDataApi.getDictDataList("system_user_sex");
// 解析字典值
String label = dictDataApi.parseDictData("system_user_sex", "1");
# 最佳实践
best_practices:
- practice: "权限标识命名规范"
detail: "使用 '模块:功能:操作' 格式,如 system:user:create、system:role:update"
- practice: "数据权限使用"
detail: "在Service方法上使用 @DataPermission 注解控制数据权限,使用 enable=false 关闭"
- practice: "缓存使用"
detail: "权限相关数据使用 @Cacheable 缓存,修改时使用 @CacheEvict 清除"
- practice: "多租户注意"
detail: "不需要租户隔离的表使用 @TenantIgnore 注解,如字典表、菜单表"
- practice: "事务处理"
detail: "跨数据源操作使用 @DSTransactional单数据源使用 @Transactional"
- practice: "日志记录"
detail: "使用 @OperateLog 注解记录操作日志,使用 Lombok 的 @Slf4j 记录调试日志"
- practice: "参数校验"
detail: "VO类使用 @Valid、@NotNull、@NotBlank 等注解进行参数校验"
- practice: "API设计"
detail: "跨模块调用通过 API 层,不要直接调用 Service 层"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-framework-common"
api: "CommonResult, PageResult, ErrorCode"
purpose: "通用响应、分页、错误码"
- module: "yudao-framework-security"
api: "SecurityFrameworkUtils, @PreAuthorize"
purpose: "安全框架、权限注解"
- module: "yudao-framework-tenant"
api: "TenantBaseDO, @TenantIgnore"
purpose: "多租户支持"
- module: "yudao-framework-mybatis"
api: "BaseMapperX, BaseDO"
purpose: "MyBatis增强"
- module: "yudao-framework-redis"
api: "@Cacheable, RedisKeyConstants"
purpose: "Redis缓存"
- module: "yudao-module-infra"
api: "FileApi"
purpose: "文件上传"
# 外部依赖(第三方库)
external:
- name: "spring-boot-starter-web"
version: "2.7.x"
purpose: "Web框架"
- name: "mybatis-plus-boot-starter"
version: "3.5.x"
purpose: "MyBatis增强"
- name: "spring-boot-starter-security"
version: "2.7.x"
purpose: "安全框架"
- name: "spring-boot-starter-validation"
version: "2.7.x"
purpose: "参数校验"
- name: "spring-boot-starter-cache"
version: "2.7.x"
purpose: "缓存支持"
- name: "mapstruct"
version: "1.5.x"
purpose: "对象转换"
- name: "lombok"
version: "1.18.x"
purpose: "代码简化"
- name: "hutool-all"
version: "5.8.x"
purpose: "工具类库"
- name: "knife4j-spring-boot-starter"
version: "3.0.x"
purpose: "API文档"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义,所有业务异常的统一入口"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java"
purpose: "用户实体类,核心聚合根"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java"
purpose: "角色实体类,权限载体"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/MenuDO.java"
purpose: "菜单实体类,权限最小单元"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java"
purpose: "权限服务实现,核心业务逻辑"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java"
purpose: "认证服务实现,登录登出逻辑"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java"
purpose: "认证控制器,登录接口入口"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java"
purpose: "权限API接口跨模块调用入口"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/permission/DataScopeEnum.java"
purpose: "数据权限枚举5种数据范围定义"
- path: "yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/permission/MenuTypeEnum.java"
purpose: "菜单类型枚举,目录/菜单/按钮"

View File

@@ -0,0 +1,832 @@
# Skill 文件 - yudao-module-wms 模块
# 仓储管理系统知识提取
skill:
id: "skill-wms"
name: "Warehouse Management Skill"
version: "1.0.0"
module_path: "yudao-module-wms"
created_at: "2026-06-08"
updated_at: "2026-06-08"
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: |
wms 模块是仓储管理系统,提供仓储业务全流程管理能力:
1. 仓库管理 - 仓库基本信息维护,支持编码、名称、排序
2. 物料管理 - 物料主数据管理包含品牌、分类树结构、SKU多规格
3. 商户管理 - 客户/供应商/既是客户也是供应商的业务伙伴管理
4. 库存管理 - 基于 SKU + 仓库维度的库存余额管理,只读设计
5. 入库管理 - 入库单创建、审核、完成、取消,完成入库增加库存
6. 出库管理 - 出库单创建、审核、完成、取消,完成出库减少库存
7. 移库管理 - 库间调拨单创建、审核、完成、取消,完成移库双向变更库存
8. 盘点管理 - 库存盘点单创建、审核、完成、取消,完成盘点按实际数量调整库存
9. 首页统计 - 订单汇总、订单趋势、库存汇总等仪表盘数据
定位面向仓储管理场景管理仓库、物料、库存及四大单据的完整业务闭环。SKU 作为库存单位,库存以 SKU + 仓库维度跟踪。
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles:
- principle: "单据头/明细模式 (Header + Detail Pattern)"
description: "所有四大单据(入库、出库、移库、盘点)均采用 Header + Detail 主子表结构,单据头存储汇总信息,明细行存储 SKU 级别数据"
- principle: "只读库存控制器"
description: "库存和库存历史 Controller 均为只读所有库存变更必须通过单据完成complete操作触发确保数据一致性"
- principle: "并发安全库存更新"
description: "使用 SELECT ... FOR UPDATE 行锁 + DuplicateKeyException 捕获实现库存更新的并发安全和懒初始化"
- principle: "VO 组装模式"
description: "通过批量查询关联实体到 Map 中,再通过 BeanUtils.toBean() 丰富 VO 对象,避免 N+1 查询"
- principle: "单据状态机"
description: "PREPARE(0) -> FINISHED(4) 或 CANCELED(5),单据只有在 PREPARE 状态下才能完成或取消"
- principle: "SKU 作为库存单位"
description: "库存以 SKU + 仓库维度跟踪,不跟踪物料级别库存,支持多规格管理"
- principle: "订单类型常量分段"
description: "WmsOrderTypeConstants 定义订单类型范围:入库 [100,200)、出库 [200,300),用于库存历史来源标识"
- principle: "细粒度权限控制"
description: "权限标识格式 wms:{entity}:{action},如 wms:receipt-order:create"
- principle: "Excel 导出全覆盖"
description: "所有主数据和单据均支持 Excel 导出功能"
- principle: "字典集成"
description: "单据状态、子类型等使用系统字典管理"
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates:
- name: "物料聚合"
type: "聚合根"
entities: ["ItemDO", "ItemSkuDO", "ItemBrandDO", "ItemCategoryDO"]
description: "ItemDO 是物料聚合根ItemSkuDO 是物料的多规格 SKUItemBrandDO 和 ItemCategoryDO 是物料的品牌和分类维度"
- name: "商户聚合"
type: "聚合根"
entities: ["MerchantDO"]
description: "MerchantDO 是商户聚合根,管理客户和供应商信息"
- name: "仓库聚合"
type: "聚合根"
entities: ["WarehouseDO"]
description: "WarehouseDO 是仓库聚合根,管理仓库基本信息"
- name: "库存聚合"
type: "聚合根"
entities: ["InventoryDO", "InventoryHistoryDO"]
description: "InventoryDO 以 SKU + 仓库为唯一维度管理库存余额InventoryHistoryDO 记录每笔库存变动"
- name: "入库单聚合"
type: "聚合根"
entities: ["ReceiptOrderDO", "ReceiptOrderDetailDO"]
description: "ReceiptOrderDO 是入库单聚合根ReceiptOrderDetailDO 是入库明细行,一对多关系"
- name: "出库单聚合"
type: "聚合根"
entities: ["ShipmentOrderDO", "ShipmentOrderDetailDO"]
description: "ShipmentOrderDO 是出库单聚合根ShipmentOrderDetailDO 是出库明细行,一对多关系"
- name: "移库单聚合"
type: "聚合根"
entities: ["MovementOrderDO", "MovementOrderDetailDO"]
description: "MovementOrderDO 是移库单聚合根MovementOrderDetailDO 是移库明细行,一对多关系"
- name: "盘点单聚合"
type: "聚合根"
entities: ["CheckOrderDO", "CheckOrderDetailDO"]
description: "CheckOrderDO 是盘点单聚合根CheckOrderDetailDO 是盘点明细行,一对多关系"
value_objects:
- name: "WmsOrderTypeConstants"
description: "订单类型常量,定义各单据类型范围,用于库存历史来源标识"
- name: "OrderStatusEnum"
description: "单据状态枚举PREPARE(0), FINISHED(4), CANCELED(5)"
services:
- name: "ItemService"
description: "物料管理服务,提供物料及 SKU 的 CRUD 操作"
- name: "ItemBrandService"
description: "品牌管理服务"
- name: "ItemCategoryService"
description: "分类管理服务,支持树形结构操作"
- name: "MerchantService"
description: "商户管理服务"
- name: "WarehouseService"
description: "仓库管理服务"
- name: "InventoryService"
description: "库存管理服务,提供库存查询和变更方法"
- name: "InventoryHistoryService"
description: "库存历史服务,记录每笔库存变动"
- name: "ReceiptOrderService"
description: "入库单管理服务"
- name: "ShipmentOrderService"
description: "出库单管理服务"
- name: "MovementOrderService"
description: "移库单管理服务"
- name: "CheckOrderService"
description: "盘点单管理服务"
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间 API 接口,供其他模块 RPC 调用"
components: []
- name: "controller"
purpose: "HTTP 接口层,提供 RESTful API"
components:
- "admin/item - 物料管理接口"
- "admin/item-sku - SKU 查询接口(只读)"
- "admin/item-brand - 品牌管理接口"
- "admin/item-category - 分类管理接口"
- "admin/merchant - 商户管理接口"
- "admin/warehouse - 仓库管理接口"
- "admin/inventory - 库存查询接口(只读)"
- "admin/inventory-history - 库存历史查询接口(只读)"
- "admin/receipt-order - 入库单管理接口"
- "admin/receipt-order-detail - 入库明细查询接口"
- "admin/shipment-order - 出库单管理接口"
- "admin/shipment-order-detail - 出库明细查询接口"
- "admin/movement-order - 移库单管理接口"
- "admin/movement-order-detail - 移库明细查询接口"
- "admin/check-order - 盘点单管理接口"
- "admin/check-order-detail - 盘点明细查询接口"
- "admin/home-statistics - 首页统计接口"
- name: "service"
purpose: "业务逻辑层"
components:
- "item - 物料服务ItemService, ItemSkuService"
- "item-brand - 品牌服务ItemBrandService"
- "item-category - 分类服务ItemCategoryService"
- "merchant - 商户服务MerchantService"
- "warehouse - 仓库服务WarehouseService"
- "inventory - 库存服务InventoryService, InventoryHistoryService"
- "receipt-order - 入库单服务ReceiptOrderService, ReceiptOrderDetailService"
- "shipment-order - 出库单服务ShipmentOrderService, ShipmentOrderDetailService"
- "movement-order - 移库单服务MovementOrderService, MovementOrderDetailService"
- "check-order - 盘点单服务CheckOrderService, CheckOrderDetailService"
- name: "dal"
purpose: "数据访问层"
components:
- "dataobject - DO 实体类定义"
- "mysql - MyBatis Mapper 接口"
- name: "enums"
purpose: "枚举和常量定义"
components:
- "WmsOrderTypeConstants - 订单类型常量(入库[100,200)、出库[200,300)等)"
- "ErrorCodeConstants - 错误码常量"
- "OrderStatusEnum - 单据状态枚举"
# 设计模式应用
design_patterns:
- pattern: "单据头/明细模式 (Header + Detail Pattern)"
location: "service/*-order/"
purpose: "所有四大单据采用主子表结构Header 存储汇总信息Detail 存储 SKU 级别数据"
- pattern: "并发安全模式 (Pessimistic Locking)"
location: "service/inventory/"
purpose: "SELECT ... FOR UPDATE 行锁 + DuplicateKeyException 捕获实现库存更新并发安全"
- pattern: "VO 组装模式 (VO Assembly Pattern)"
location: "service/*/xxxServiceImpl.java"
purpose: "批量查询关联实体到 Map通过 BeanUtils.toBean() 丰富 VO避免 N+1"
- pattern: "状态机模式 (State Machine)"
location: "service/*-order/"
purpose: "单据状态PREPARE(0) -> FINISHED(4) / CANCELED(5),操作前校验状态"
- pattern: "只读控制器模式"
location: "controller/admin/inventory/"
purpose: "库存和库存历史 Controller 为只读,所有变更通过单据完成触发"
# 模块间通信
communication:
apis: []
consumers:
- module: "yudao-module-system"
api: "AdminUserApi"
purpose: "批量获取用户信息,用于 VO 组装展示用户名"
mq: []
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO"
description: "所有 DO 继承 BaseDO包含 creator, createTime, updater, updateTime, deleted 字段。"
# 核心数据表
tables:
# ========== 物料相关 ==========
- name: "wms_item_brand"
comment: "品牌表"
entity: "ItemBrandDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "品牌编号,主键" }
- { name: "name", type: "VARCHAR", comment: "品牌名称" }
- { name: "sort", type: "INT", comment: "排序" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- name: "wms_item_category"
comment: "分类表"
entity: "ItemCategoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "分类编号,主键" }
- { name: "name", type: "VARCHAR", comment: "分类名称" }
- { name: "parent_id", type: "BIGINT", comment: "父分类编号0=顶级)" }
- { name: "sort", type: "INT", comment: "排序" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_parent_id", columns: ["parent_id"] }
- name: "wms_item"
comment: "物料表"
entity: "ItemDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "物料编号,主键" }
- { name: "name", type: "VARCHAR", comment: "物料名称" }
- { name: "code", type: "VARCHAR", comment: "物料编码" }
- { name: "unit", type: "VARCHAR", comment: "计量单位" }
- { name: "category_id", type: "BIGINT", comment: "分类编号,关联 ItemCategoryDO" }
- { name: "brand_id", type: "BIGINT", comment: "品牌编号,关联 ItemBrandDO" }
- { name: "status", type: "TINYINT", comment: "物料状态" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "uk_code", columns: ["code"], unique: true }
- { name: "idx_category_id", columns: ["category_id"] }
- { name: "idx_brand_id", columns: ["brand_id"] }
- name: "wms_item_sku"
comment: "物料 SKU 表"
entity: "ItemSkuDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "SKU 编号,主键" }
- { name: "item_id", type: "BIGINT", comment: "物料编号,关联 ItemDO" }
- { name: "sku_no", type: "VARCHAR", comment: "SKU 编号/条码" }
- { name: "specifications", type: "VARCHAR", comment: "规格属性" }
- { name: "bar_code", type: "VARCHAR", comment: "条形码" }
- { name: "weight", type: "DECIMAL", comment: "重量" }
- { name: "volume", type: "DECIMAL", comment: "体积" }
- { name: "purchase_price", type: "DECIMAL", comment: "采购价格" }
- { name: "selling_price", type: "DECIMAL", comment: "销售价格" }
- { name: "status", type: "TINYINT", comment: "SKU 状态" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_item_id", columns: ["item_id"] }
- { name: "idx_sku_no", columns: ["sku_no"] }
# ========== 商户与仓库 ==========
- name: "wms_merchant"
comment: "商户表"
entity: "MerchantDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "商户编号,主键" }
- { name: "name", type: "VARCHAR", comment: "商户名称" }
- { name: "type", type: "TINYINT", comment: "商户类型1=客户 2=供应商 3=两者)" }
- { name: "contact", type: "VARCHAR", comment: "联系人" }
- { name: "phone", type: "VARCHAR", comment: "联系电话" }
- { name: "address", type: "VARCHAR", comment: "地址" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
- name: "wms_warehouse"
comment: "仓库表"
entity: "WarehouseDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "仓库编号,主键" }
- { name: "code", type: "VARCHAR", comment: "仓库编码" }
- { name: "name", type: "VARCHAR", comment: "仓库名称" }
- { name: "sort", type: "INT", comment: "排序" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "uk_code", columns: ["code"], unique: true }
# ========== 库存相关 ==========
- name: "wms_inventory"
comment: "库存表"
entity: "InventoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "库存编号,主键" }
- { name: "sku_id", type: "BIGINT", comment: "SKU 编号,关联 ItemSkuDO" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库编号,关联 WarehouseDO" }
- { name: "quantity", type: "DECIMAL", comment: "库存数量" }
- { name: "price", type: "DECIMAL", comment: "库存均价" }
indexes:
- { name: "uk_sku_warehouse", columns: ["sku_id", "warehouse_id"], unique: true }
- name: "wms_inventory_history"
comment: "库存历史表"
entity: "InventoryHistoryDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "历史编号,主键" }
- { name: "sku_id", type: "BIGINT", comment: "SKU 编号,关联 ItemSkuDO" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库编号,关联 WarehouseDO" }
- { name: "before_quantity", type: "DECIMAL", comment: "变动前数量" }
- { name: "after_quantity", type: "DECIMAL", comment: "变动后数量" }
- { name: "quantity", type: "DECIMAL", comment: "变动数量(正=增加,负=减少)" }
- { name: "price", type: "DECIMAL", comment: "变动价格" }
- { name: "source_order_type", type: "INTEGER", comment: "来源单据类型" }
- { name: "source_order_id", type: "BIGINT", comment: "来源单据编号" }
- { name: "source_order_no", type: "VARCHAR", comment: "来源单号" }
indexes:
- { name: "idx_sku_warehouse", columns: ["sku_id", "warehouse_id"] }
- { name: "idx_source_order", columns: ["source_order_type", "source_order_id"] }
# ========== 入库单相关 ==========
- name: "wms_receipt_order"
comment: "入库单表"
entity: "ReceiptOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "入库单编号,主键" }
- { name: "no", type: "VARCHAR", comment: "入库单号" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库编号,关联 WarehouseDO" }
- { name: "merchant_id", type: "BIGINT", comment: "商户编号,关联 MerchantDO" }
- { name: "status", type: "TINYINT", comment: "单据状态0=PREPARE, 4=FINISHED, 5=CANCELED" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "uk_no", columns: ["no"], unique: true }
- { name: "idx_warehouse_id", columns: ["warehouse_id"] }
- { name: "idx_status", columns: ["status"] }
- name: "wms_receipt_order_detail"
comment: "入库明细表"
entity: "ReceiptOrderDetailDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "明细编号,主键" }
- { name: "order_id", type: "BIGINT", comment: "入库单编号,关联 ReceiptOrderDO" }
- { name: "sku_id", type: "BIGINT", comment: "SKU 编号,关联 ItemSkuDO" }
- { name: "quantity", type: "DECIMAL", comment: "入库数量" }
- { name: "price", type: "DECIMAL", comment: "入库单价" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_order_id", columns: ["order_id"] }
# ========== 出库单相关 ==========
- name: "wms_shipment_order"
comment: "出库单表"
entity: "ShipmentOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "出库单编号,主键" }
- { name: "no", type: "VARCHAR", comment: "出库单号" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库编号,关联 WarehouseDO" }
- { name: "merchant_id", type: "BIGINT", comment: "商户编号,关联 MerchantDO" }
- { name: "status", type: "TINYINT", comment: "单据状态0=PREPARE, 4=FINISHED, 5=CANCELED" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "uk_no", columns: ["no"], unique: true }
- { name: "idx_warehouse_id", columns: ["warehouse_id"] }
- { name: "idx_status", columns: ["status"] }
- name: "wms_shipment_order_detail"
comment: "出库明细表"
entity: "ShipmentOrderDetailDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "明细编号,主键" }
- { name: "order_id", type: "BIGINT", comment: "出库单编号,关联 ShipmentOrderDO" }
- { name: "sku_id", type: "BIGINT", comment: "SKU 编号,关联 ItemSkuDO" }
- { name: "quantity", type: "DECIMAL", comment: "出库数量" }
- { name: "price", type: "DECIMAL", comment: "出库单价" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_order_id", columns: ["order_id"] }
# ========== 移库单相关 ==========
- name: "wms_movement_order"
comment: "移库单表"
entity: "MovementOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "移库单编号,主键" }
- { name: "no", type: "VARCHAR", comment: "移库单号" }
- { name: "from_warehouse_id", type: "BIGINT", comment: "源仓库编号,关联 WarehouseDO" }
- { name: "to_warehouse_id", type: "BIGINT", comment: "目标仓库编号,关联 WarehouseDO" }
- { name: "status", type: "TINYINT", comment: "单据状态0=PREPARE, 4=FINISHED, 5=CANCELED" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "uk_no", columns: ["no"], unique: true }
- { name: "idx_from_warehouse_id", columns: ["from_warehouse_id"] }
- { name: "idx_to_warehouse_id", columns: ["to_warehouse_id"] }
- { name: "idx_status", columns: ["status"] }
- name: "wms_movement_order_detail"
comment: "移库明细表"
entity: "MovementOrderDetailDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "明细编号,主键" }
- { name: "order_id", type: "BIGINT", comment: "移库单编号,关联 MovementOrderDO" }
- { name: "sku_id", type: "BIGINT", comment: "SKU 编号,关联 ItemSkuDO" }
- { name: "quantity", type: "DECIMAL", comment: "移库数量" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_order_id", columns: ["order_id"] }
# ========== 盘点单相关 ==========
- name: "wms_check_order"
comment: "盘点单表"
entity: "CheckOrderDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "盘点单编号,主键" }
- { name: "no", type: "VARCHAR", comment: "盘点单号" }
- { name: "warehouse_id", type: "BIGINT", comment: "仓库编号,关联 WarehouseDO" }
- { name: "status", type: "TINYINT", comment: "单据状态0=PREPARE, 4=FINISHED, 5=CANCELED" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "uk_no", columns: ["no"], unique: true }
- { name: "idx_warehouse_id", columns: ["warehouse_id"] }
- { name: "idx_status", columns: ["status"] }
- name: "wms_check_order_detail"
comment: "盘点明细表"
entity: "CheckOrderDetailDO"
extends: "BaseDO"
columns:
- { name: "id", type: "BIGINT", comment: "明细编号,主键" }
- { name: "order_id", type: "BIGINT", comment: "盘点单编号,关联 CheckOrderDO" }
- { name: "sku_id", type: "BIGINT", comment: "SKU 编号,关联 ItemSkuDO" }
- { name: "system_quantity", type: "DECIMAL", comment: "系统数量" }
- { name: "actual_quantity", type: "DECIMAL", comment: "实际数量" }
- { name: "remark", type: "VARCHAR", comment: "备注" }
indexes:
- { name: "idx_order_id", columns: ["order_id"] }
# 表关系ER关系
relationships:
- from: "wms_item"
to: "wms_item_category"
type: "N:1"
foreign_key: "category_id"
- from: "wms_item"
to: "wms_item_brand"
type: "N:1"
foreign_key: "brand_id"
- from: "wms_item_sku"
to: "wms_item"
type: "N:1"
foreign_key: "item_id"
- from: "wms_inventory"
to: "wms_item_sku"
type: "N:1"
foreign_key: "sku_id"
- from: "wms_inventory"
to: "wms_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
- from: "wms_inventory_history"
to: "wms_item_sku"
type: "N:1"
foreign_key: "sku_id"
- from: "wms_inventory_history"
to: "wms_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
- from: "wms_receipt_order"
to: "wms_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
- from: "wms_receipt_order"
to: "wms_merchant"
type: "N:1"
foreign_key: "merchant_id"
- from: "wms_receipt_order_detail"
to: "wms_receipt_order"
type: "N:1"
foreign_key: "order_id"
- from: "wms_receipt_order_detail"
to: "wms_item_sku"
type: "N:1"
foreign_key: "sku_id"
- from: "wms_shipment_order"
to: "wms_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
- from: "wms_shipment_order"
to: "wms_merchant"
type: "N:1"
foreign_key: "merchant_id"
- from: "wms_shipment_order_detail"
to: "wms_shipment_order"
type: "N:1"
foreign_key: "order_id"
- from: "wms_shipment_order_detail"
to: "wms_item_sku"
type: "N:1"
foreign_key: "sku_id"
- from: "wms_movement_order"
to: "wms_warehouse"
type: "N:1"
foreign_key: "from_warehouse_id"
comment: "源仓库"
- from: "wms_movement_order"
to: "wms_warehouse"
type: "N:1"
foreign_key: "to_warehouse_id"
comment: "目标仓库"
- from: "wms_movement_order_detail"
to: "wms_movement_order"
type: "N:1"
foreign_key: "order_id"
- from: "wms_movement_order_detail"
to: "wms_item_sku"
type: "N:1"
foreign_key: "sku_id"
- from: "wms_check_order"
to: "wms_warehouse"
type: "N:1"
foreign_key: "warehouse_id"
- from: "wms_check_order_detail"
to: "wms_check_order"
type: "N:1"
foreign_key: "order_id"
- from: "wms_check_order_detail"
to: "wms_item_sku"
type: "N:1"
foreign_key: "sku_id"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations:
- "@Tag(name = '管理后台 - xxx') - Swagger 文档标签"
- "@RestController - REST 控制器"
- "@RequestMapping('/wms/xxx') - 请求路径前缀"
- "@Validated - 参数校验"
- "@PreAuthorize('@ss.hasPermission('wms:xxx:action')') - 权限控制"
example: |
@Tag(name = "管理后台 - 入库单")
@RestController
@RequestMapping("/wms/receipt-order")
@Validated
public class ReceiptOrderController {
@Resource
private ReceiptOrderService receiptOrderService;
@PostMapping("/create")
@Operation(summary = "创建入库单")
@PreAuthorize("@ss.hasPermission('wms:receipt-order:create')")
public CommonResult<Long> createReceiptOrder(@Valid @RequestBody ReceiptOrderSaveReqVO reqVO) {
return success(receiptOrderService.createReceiptOrder(reqVO));
}
@PutMapping("/complete")
@Operation(summary = "完成入库")
@PreAuthorize("@ss.hasPermission('wms:receipt-order:update')")
public CommonResult<Boolean> completeReceiptOrder(@RequestParam("id") Long id) {
receiptOrderService.completeReceiptOrder(id);
return success(true);
}
}
# Service层规范
service:
interface_pattern: "XxxService 接口定义业务方法XxxServiceImpl 实现类"
impl_pattern: |
@Service
@Validated
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
// 使用 static final 定义常量
// 使用 @Transactional 注解控制事务
// 使用 validateXxxExists 方法校验存在性
// 使用 exception(ErrorCode) 抛出业务异常
}
example: |
@Service
public class ReceiptOrderServiceImpl implements ReceiptOrderService {
@Resource
private ReceiptOrderMapper receiptOrderMapper;
@Resource
private ReceiptOrderDetailMapper receiptOrderDetailMapper;
@Resource
private InventoryService inventoryService;
@Override
@Transactional(rollbackFor = Exception.class)
public void completeReceiptOrder(Long id) {
// 1. 校验单据存在且状态为 PREPARE
ReceiptOrderDO order = receiptOrderMapper.selectById(id);
validateReceiptOrderCanComplete(order);
// 2. 遍历明细行,增加库存
List<ReceiptOrderDetailDO> details = receiptOrderDetailMapper.selectListByOrderId(id);
for (ReceiptOrderDetailDO detail : details) {
inventoryService.addStock(detail.getSkuId(), order.getWarehouseId(),
detail.getQuantity(), detail.getPrice(),
WmsOrderTypeConstants.RECEIPT_ORDER_TYPE, order.getId(), order.getNo());
}
// 3. 更新单据状态为 FINISHED
receiptOrderMapper.updateById(new ReceiptOrderDO().setId(id)
.setStatus(OrderStatusEnum.FINISHED.getStatus()));
}
}
# 数据访问规范
dal:
mapper_pattern: |
public interface XxxMapper extends BaseMapperX<XxxDO> {
// 继承 BaseMapperX 获得通用 CRUD 方法
// 自定义查询方法使用 @Select 注解或 XML
// 分页查询返回 PageResult<XxxDO>
}
example: |
public interface InventoryMapper extends BaseMapperX<InventoryDO> {
default PageResult<InventoryDO> selectPage(InventoryPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InventoryDO>()
.eqIfPresent(InventoryDO::getSkuId, reqVO.getSkuId())
.eqIfPresent(InventoryDO::getWarehouseId, reqVO.getWarehouseId())
.orderByDesc(InventoryDO::getId));
}
}
# 库存并发安全模式
inventory_concurrency:
description: "库存更新使用 SELECT ... FOR UPDATE 行锁 + DuplicateKeyException 捕获实现并发安全"
example: |
@Service
public class InventoryServiceImpl implements InventoryService {
@Override
@Transactional(rollbackFor = Exception.class)
public void addStock(Long skuId, Long warehouseId, BigDecimal quantity,
BigDecimal price, Integer sourceOrderType,
Long sourceOrderId, String sourceOrderNo) {
// 1. 查询库存记录(带行锁)
InventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseIdForUpdate(skuId, warehouseId);
BigDecimal beforeQuantity;
if (inventory == null) {
// 2. 首次入库,尝试插入(懒初始化)
try {
inventory = new InventoryDO().setSkuId(skuId)
.setWarehouseId(warehouseId).setQuantity(quantity).setPrice(price);
inventoryMapper.insert(inventory);
beforeQuantity = BigDecimal.ZERO;
} catch (DuplicateKeyException e) {
// 3. 并发插入冲突,重新查询
inventory = inventoryMapper.selectBySkuIdAndWarehouseIdForUpdate(skuId, warehouseId);
beforeQuantity = inventory.getQuantity();
inventory.setQuantity(beforeQuantity.add(quantity));
inventoryMapper.updateById(inventory);
}
} else {
// 4. 已有记录,直接更新
beforeQuantity = inventory.getQuantity();
inventory.setQuantity(beforeQuantity.add(quantity));
inventoryMapper.updateById(inventory);
}
// 5. 记录库存变动历史
inventoryHistoryService.createHistory(skuId, warehouseId,
beforeQuantity, inventory.getQuantity(), quantity, price,
sourceOrderType, sourceOrderId, sourceOrderNo);
}
}
# 异常处理
error_handling:
code_prefix: "wms 模块错误码需遵循项目统一的错误码分配规则"
examples:
- message: "物料不存在"
- message: "仓库不存在"
- message: "商户不存在"
- message: "SKU 不存在"
- message: "库存不足"
- message: "单据状态不允许操作"
- message: "品牌存在关联数据不允许删除"
- message: "分类存在子分类不允许删除"
# 统一响应
response:
success_pattern: "CommonResult.success(data)"
error_pattern: "throw exception(ErrorCode)"
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增单据类型
new_channel:
title: "新增仓储单据类型"
steps:
- step: "1. 分配订单类型常量"
description: "在 WmsOrderTypeConstants 中分配新的类型范围"
code: |
// 示例:新增调拨单,类型范围 [500, 600)
public static final int TRANSFER_ORDER_TYPE_BEGIN = 500;
public static final int TRANSFER_ORDER_TYPE_END = 600;
- step: "2. 创建单据头和明细表"
description: "创建主子表,单据头包含 no、warehouse_id、status 等字段"
code: |
CREATE TABLE wms_transfer_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
no VARCHAR(64) NOT NULL,
warehouse_id BIGINT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
remark VARCHAR(512),
-- BaseDO 字段
creator VARCHAR(64), create_time DATETIME, updater VARCHAR(64),
update_time DATETIME, deleted BIT DEFAULT 0,
UNIQUE KEY uk_no (no)
);
- step: "3. 创建 DO、Mapper、Service、Controller"
description: "按照既有模式创建完整的 CRUD 代码"
- step: "4. 实现完成/取消逻辑"
description: "在 Service 中实现单据完成时的库存变更逻辑"
- step: "5. 添加权限配置"
description: "在系统中配置权限标识 wms:transfer-order:{action}"
# 新增物料属性
new_feature:
title: "扩展物料属性"
steps:
- step: "1. 添加字段到 wms_item 或 wms_item_sku"
description: "根据属性粒度决定添加到物料表还是 SKU 表"
- step: "2. 更新 DO 类"
description: "在对应的 DO 类中添加字段"
- step: "3. 更新 VO 类"
description: "在 SaveReqVO 和 RespVO 中添加字段"
- step: "4. 更新 Controller 和 Service"
description: "确保新字段在 CRUD 流程中正确处理"
- step: "5. 更新 Excel 导出模板"
description: "在导出 VO 中添加 @ExcelProperty 注解"
# 最佳实践
best_practices:
- practice: "库存更新必须使用行锁"
description: "所有库存更新操作必须使用 SELECT ... FOR UPDATE 行锁,避免并发更新丢失"
- practice: "单据完成操作使用事务"
description: "单据完成涉及状态更新、库存变更、历史记录,必须在同一事务中完成"
- practice: "VO 组装使用批量查询"
description: "通过 AdminUserApi.getUserMap() 等批量查询避免 N+1 问题"
- practice: "单据操作前校验状态"
description: "完成、取消、更新、删除操作前必须校验单据状态为 PREPARE"
- practice: "订单类型常量分段管理"
description: "新单据类型必须在 WmsOrderTypeConstants 中分配范围,避免类型冲突"
- practice: "SKU 作为库存单位"
description: "库存以 SKU + 仓库维度管理,物料级别不跟踪库存"
- practice: "分类树一次性加载"
description: "分类查询建议一次性加载所有数据到内存后构建树,避免递归查询数据库"
- practice: "Excel 导出控制数据量"
description: "大数据量导出使用 EasyExcel 流式写入,避免内存溢出"
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal:
- module: "yudao-module-system"
purpose: "AdminUserApi - 批量获取用户信息用于 VO 组装"
- module: "yudao-framework-common"
purpose: "通用工具类、CommonResult、PageResult 等"
- module: "yudao-framework-mybatis"
purpose: "MyBatis-Plus 封装、BaseMapperX、BaseDO"
# 外部依赖(第三方库)
external:
- name: "MyBatis-Plus"
purpose: "ORM 框架,提供 CRUD 封装"
- name: "Hutool"
purpose: "Java 工具类库"
- name: "Swagger/OpenAPI"
purpose: "API 文档注解"
- name: "Apache POI / EasyExcel"
purpose: "Excel 导入导出"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/InventoryServiceImpl.java"
purpose: "库存服务实现,核心库存变更逻辑(并发安全)"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/InventoryHistoryServiceImpl.java"
purpose: "库存历史服务,记录每笔库存变动"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/receiptorder/ReceiptOrderServiceImpl.java"
purpose: "入库单服务实现,完成入库触发库存增加"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/shipmentorder/ShipmentOrderServiceImpl.java"
purpose: "出库单服务实现,完成出库触发库存减少"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/movementorder/MovementOrderServiceImpl.java"
purpose: "移库单服务实现,完成移库触发双向库存变更"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/checkorder/CheckOrderServiceImpl.java"
purpose: "盘点单服务实现,完成盘点触发库存调整"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/item/ItemServiceImpl.java"
purpose: "物料服务实现,包含 SKU 嵌入管理"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/InventoryMapper.java"
purpose: "库存 Mapper含 FOR UPDATE 查询方法"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/WmsOrderTypeConstants.java"
purpose: "订单类型常量,定义各单据类型范围"
- path: "yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java"
purpose: "错误码常量定义"

View File

@@ -0,0 +1,91 @@
# 工厂模式知识库
pattern:
id: "factory-pattern"
name: "工厂模式"
category: "创建型模式"
description: "定义一个创建对象的接口,让子类决定实例化哪一个类,使一个类的实例化延迟到其子类"
# 模式结构
structure:
participants:
- name: "Factory"
role: "抽象工厂接口"
description: "声明创建产品对象的抽象方法"
- name: "ConcreteFactory"
role: "具体工厂"
description: "实现抽象工厂接口,创建具体产品"
- name: "Product"
role: "抽象产品"
description: "定义产品的共同接口"
- name: "ConcreteProduct"
role: "具体产品"
description: "实现抽象产品接口的具体类"
# 项目中的应用
applications:
- module: "pay"
location: "PayClientFactory"
purpose: "创建支付客户端实例"
code_path: "yudao-module-pay/.../pay/core/client/PayClientFactory.java"
implementation: |
public interface PayClientFactory {
PayClient getPayClient(Long channelId);
<Config extends PayClientConfig> PayClient createOrUpdatePayClient(
Long channelId, String channelCode, Config config);
}
- module: "infra/sms"
location: "SmsClientFactory"
purpose: "创建短信客户端实例"
code_path: "yudao-module-infra/.../sms/core/client/SmsClientFactory.java"
implementation: |
public class SmsClientFactoryImpl implements SmsClientFactory {
private final ConcurrentMap<Long, AbstractSmsClient> channelIdClients = new ConcurrentHashMap<>();
@Override
public SmsClient createOrUpdateSmsClient(SmsChannelProperties properties) {
AbstractSmsClient client = channelIdClients.get(properties.getId());
if (client == null) {
client = this.createSmsClient(properties);
client.init();
channelIdClients.put(client.getId(), client);
} else {
client.refresh(properties);
}
return client;
}
}
# 使用场景
scenarios:
- "需要根据配置动态创建不同类型的客户端"
- "需要管理多个同类对象的实例(如缓存)"
- "创建逻辑复杂,需要封装"
- "需要支持扩展新的产品类型"
# 优点
advantages:
- "解耦创建逻辑和使用逻辑"
- "便于扩展新产品类型"
- "可以缓存和复用对象实例"
- "统一管理对象生命周期"
# 缺点
disadvantages:
- "增加了类的数量"
- "需要维护工厂类的实现"
# 扩展指南
extension_guide:
title: "如何新增产品类型"
steps:
- step: 1
action: "创建具体产品类"
description: "实现Product接口"
- step: 2
action: "修改工厂创建逻辑"
description: "在工厂中添加新类型的创建分支"
- step: 3
action: "配置支持"
description: "添加配置枚举或配置项"

View File

@@ -0,0 +1,58 @@
# 设计模式索引
patterns:
# 创建型模式
- id: "factory-pattern"
name: "工厂模式"
file: "factory-pattern.yaml"
modules: ["pay", "infra"]
description: "创建对象实例的工厂接口,支持多类型扩展"
# 行为型模式
- id: "template-method-pattern"
name: "模板方法模式"
file: "template-method-pattern.yaml"
modules: ["pay", "infra"]
description: "定义算法骨架,子类实现具体步骤"
- id: "strategy-pattern"
name: "策略模式"
file: "strategy-pattern.yaml"
modules: ["pay", "infra", "ai"]
description: "封装可互换的算法族"
# 模式选择指南
selection_guide:
- scenario: "需要根据配置创建不同类型的客户端"
patterns: ["factory-pattern", "strategy-pattern"]
example: "支付渠道、短信渠道"
- scenario: "需要统一处理流程,但具体实现不同"
patterns: ["template-method-pattern"]
example: "支付下单流程、短信发送流程"
- scenario: "需要动态切换算法或行为"
patterns: ["strategy-pattern"]
example: "AI模型切换、支付方式切换"
# 模式组合
combinations:
- name: "工厂 + 策略"
description: "工厂创建策略实例"
usage: "PayClientFactory创建PayClient实例"
- name: "工厂 + 模板方法"
description: "工厂创建模板方法的具体实现"
usage: "SmsClientFactory创建AbstractSmsClient子类"
- name: "模板方法 + 策略"
description: "模板方法定义骨架,策略实现变化部分"
usage: "AbstractPayClient定义骨架具体PayClient实现策略"
# 扩展指南
extension:
new_pattern:
steps:
- "创建模式知识库文件: skills/patterns/{pattern}-pattern.yaml"
- "更新此索引文件"
- "关联应用到相关模块"

View File

@@ -0,0 +1,136 @@
# 策略模式知识库
pattern:
id: "strategy-pattern"
name: "策略模式"
category: "行为型模式"
description: "定义一系列算法,把它们封装起来,并使它们可互相替换,让算法独立于使用它的客户而变化"
# 模式结构
structure:
participants:
- name: "Strategy"
role: "策略接口"
description: "定义所有支持的算法的公共接口"
- name: "ConcreteStrategy"
role: "具体策略"
description: "实现策略接口的具体算法"
- name: "Context"
role: "上下文"
description: "维护对策略对象的引用"
# 项目中的应用
applications:
- module: "pay"
location: "PayClient接口及其实现"
purpose: "支持多渠道支付"
code_path: "yudao-module-pay/.../pay/core/client/PayClient.java"
strategies:
- name: "AlipayQrPayClient"
description: "支付宝扫码支付"
- name: "AlipayAppPayClient"
description: "支付宝App支付"
- name: "WxNativePayClient"
description: "微信Native支付"
- name: "WxAppPayClient"
description: "微信App支付"
implementation: |
public interface PayClient<Config extends PayClientConfig> {
Long getId();
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
}
- module: "infra/sms"
location: "SmsClient接口及其实现"
purpose: "支持多渠道短信发送"
strategies:
- name: "AliyunSmsClient"
description: "阿里云短信"
- name: "TencentSmsClient"
description: "腾讯云短信"
- name: "HuaweiSmsClient"
description: "华为云短信"
- name: "QiniuSmsClient"
description: "七牛云短信"
- module: "ai"
location: "ChatModel接口及其实现"
purpose: "支持多AI模型"
strategies:
- name: "OpenAIChatModel"
description: "OpenAI聊天模型"
- name: "BaiduChatModel"
description: "百度文心一言"
- name: "ZhipuChatModel"
description: "智谱AI"
- name: "TongyiChatModel"
description: "阿里通义千问"
# 使用场景
scenarios:
- "需要在运行时选择算法"
- "有多个类只在行为上有差异"
- "需要动态切换算法"
- "需要隐藏算法实现细节"
# 优点
advantages:
- "算法可自由切换"
- "避免使用多重条件判断"
- "易于扩展新策略"
- "符合开闭原则"
# 缺点
disadvantages:
- "客户端需要了解所有策略"
- "策略过多时类数量增加"
# 与工厂模式配合
factory_integration:
description: "策略模式通常与工厂模式配合使用,由工厂创建具体策略实例"
example:
factory: "PayClientFactory"
purpose: "根据渠道配置创建对应的PayClient实例"
code: |
// 通过工厂获取策略
PayClient client = payClientFactory.getPayClient(channelId);
// 使用策略
PayOrderRespDTO response = client.unifiedOrder(reqDTO);
# 扩展指南
extension_guide:
title: "如何新增策略"
steps:
- step: 1
action: "创建策略实现类"
description: "实现策略接口"
- step: 2
action: "定义配置类"
description: "如果需要配置参数"
- step: 3
action: "修改工厂"
description: "在工厂中添加创建分支"
- step: 4
action: "添加枚举"
description: "如果需要类型枚举"
example:
title: "新增短信渠道"
code: |
// 1. 创建实现类
public class XxxSmsClient extends AbstractSmsClient {
@Override
protected void doInit() { ... }
@Override
public SmsSendResultDTO sendSms(SmsSendMessageDTO message) { ... }
}
// 2. 添加渠道枚举
public enum SmsChannelEnum {
XXX(10, "XXX短信");
}
// 3. 修改工厂创建逻辑
case XXX: return new XxxSmsClient(properties);

View File

@@ -0,0 +1,119 @@
# 模板方法模式知识库
pattern:
id: "template-method-pattern"
name: "模板方法模式"
category: "行为型模式"
description: "定义一个操作中的算法骨架,将一些步骤延迟到子类中,使得子类可以不改变算法结构即可重定义算法的某些步骤"
# 模式结构
structure:
participants:
- name: "AbstractClass"
role: "抽象类"
description: "定义抽象方法和模板方法,模板方法调用抽象方法"
- name: "ConcreteClass"
role: "具体类"
description: "实现抽象方法,完成具体逻辑"
# 项目中的应用
applications:
- module: "pay"
location: "AbstractPayClient"
purpose: "统一支付流程骨架"
code_path: "yudao-module-pay/.../pay/core/client/impl/AbstractPayClient.java"
implementation: |
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient<Config> {
// 模板方法:统一下单
@Override
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
try {
return doUnifiedOrder(reqDTO); // 调用子类实现
} catch (Throwable ex) {
log.error("[unifiedOrder]异常", ex);
throw buildPayException(ex);
}
}
// 抽象方法:由子类实现具体下单逻辑
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable;
// 抽象方法:初始化
protected abstract void doInit();
}
- module: "infra/sms"
location: "AbstractSmsClient"
purpose: "统一短信发送流程"
code_path: "yudao-module-infra/.../sms/core/client/AbstractSmsClient.java"
implementation: |
public abstract class AbstractSmsClient implements SmsClient {
// 模板方法定义发送流程
}
# 模板方法流程
template_methods:
- name: "unifiedOrder"
module: "pay"
steps:
- "参数校验"
- "调用子类实现下单"
- "异常处理和转换"
hook_methods:
- "doUnifiedOrder()"
- "doInit()"
# 使用场景
scenarios:
- "多个子类有公共行为,但具体实现不同"
- "需要控制算法的整体流程"
- "需要在不同点调用不同的实现"
# 优点
advantages:
- "复用公共代码"
- "控制算法流程"
- "符合开闭原则"
- "便于维护和扩展"
# 缺点
disadvantages:
- "每个实现都需要定义子类"
- "增加了类的数量"
# 扩展指南
extension_guide:
title: "如何新增具体实现"
steps:
- step: 1
action: "创建具体类"
description: "继承抽象类"
- step: 2
action: "实现抽象方法"
description: "doUnifiedOrder(), doInit()等"
- step: 3
action: "注册到工厂"
description: "在工厂中添加创建逻辑"
example:
title: "新增支付渠道"
code: |
public class XxxPayClient extends AbstractPayClient<XxxPayClientConfig> {
@Override
protected void doInit() {
// 初始化SDK
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
// 调用第三方支付API
}
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 调用第三方退款API
}
}

View File

@@ -0,0 +1,139 @@
# 模块 Skill 提取提示词模板
## 角色定义
你是一个资深的企业级Java应用架构师精通DDD领域驱动设计、微服务架构和Spring Boot生态。
## 任务目标
为指定模块提取完整的 Skill 文档,形成可复用的知识资产。
## 分析框架(五阶段)
---
### 第一阶段:设计理念提取
从以下维度分析模块的设计理念:
1. **业务定位**
- 模块解决什么业务问题?
- 模块在整个系统中的定位?
- 与其他模块的边界是什么?
2. **设计原则**
- 遵循了哪些设计原则SOLID、DDD等
- 有哪些关键的架构决策?
- 为什么这样设计?
3. **领域模型**
- 核心领域对象有哪些?
- 领域对象之间的关系?
- 聚合根是什么?值对象有哪些?
---
### 第二阶段:架构设计提取
分析模块的分层架构:
1. **目录结构分析**
```
yudao-module-{xxx}/
├── api/ # API接口层模块间通信
├── controller/ # 控制器层admin/app双端
├── convert/ # 转换层(对象映射)
├── dal/ # 数据访问层dataobject/mysql/redis
├── service/ # 业务逻辑层
├── enums/ # 枚举定义
├── framework/ # 模块框架配置
├── job/ # 定时任务
└── mq/ # 消息队列
```
2. **设计模式应用**
- 识别工厂模式、策略模式、模板方法等
- 分析模式应用场景和位置
- 说明模式选择的理由
3. **模块间通信**
- API接口定义
- 消息队列使用
- 事件驱动设计
---
### 第三阶段:数据表设计提取
分析数据模型:
1. **实体继承体系**
- BaseDO基础字段createTime, updateTime, creator, updater, deleted
- TenantBaseDO多租户字段tenantId
2. **核心表结构**
- 表名、说明、核心字段、关联关系
- 主键策略、索引设计
3. **表关系**
- 外键关系
- 一对多/多对多关系
---
### 第四阶段:代码使用设计提取
提取代码规范和模式:
1. **Controller层规范**
- @Tag、@Operation 注解
- @PreAuthorize 权限控制
- CommonResult 统一返回
- PageResult 分页封装
2. **Service层规范**
- 接口定义规范
- 事务处理规范
- 异常处理规范ErrorCodeConstants
3. **数据访问规范**
- Mapper接口定义
- 分页查询实现
- 数据权限控制
---
### 第五阶段:扩展指南提取
总结如何基于此模块扩展:
1. **新增业务功能步骤**
- 步骤化说明
- 关键代码示例
2. **新增渠道/类型示例**
- 具体实现步骤
- 需要修改的文件
3. **最佳实践总结**
- 开发注意事项
- 常见问题解决
---
## 输出格式
生成两个文件:
1. `skill-{module}.yaml` - 结构化Skill文档使用模板格式
2. `skill-{module}.md` - 可读性文档Markdown格式
---
## 关键参考文件
| 文件类型 | 路径模式 |
|---------|---------|
| Controller | `yudao-module-{xxx}/.../controller/` |
| Service | `yudao-module-{xxx}/.../service/` |
| DO实体 | `yudao-module-{xxx}/.../dal/dataobject/` |
| Mapper | `yudao-module-{xxx}/.../dal/mysql/` |
| 枚举 | `yudao-module-{xxx}/.../enums/` |
| 错误码 | `yudao-module-{xxx}/.../ErrorCodeConstants.java` |

View File

@@ -0,0 +1,161 @@
# Skill 模板文件
# 用于提取模块知识的标准格式
skill:
id: "skill-{module}"
name: "{模块名称} Skill"
version: "1.0.0"
module_path: "yudao-module-{module}"
created_at: ""
updated_at: ""
# ============================================
# 第一阶段:设计理念
# ============================================
philosophy:
# 业务定位:模块解决什么业务问题?在整个系统中的定位?
business_position: ""
# 设计原则遵循了哪些设计原则SOLID、DDD有哪些架构决策
design_principles: []
# 领域模型:核心领域对象有哪些?领域对象之间的关系?聚合根是什么?
domain_model:
aggregates: []
# - name: "聚合根名称"
# type: "聚合根"
# entities: ["关联实体"]
value_objects: []
services: []
# ============================================
# 第二阶段:架构设计
# ============================================
architecture:
# 分层架构
layers:
- name: "api"
purpose: "模块间API接口"
components: []
- name: "controller"
purpose: "HTTP接口"
components: []
- name: "service"
purpose: "业务逻辑"
components: []
- name: "dal"
purpose: "数据访问"
components: []
# 设计模式应用
design_patterns: []
# - pattern: "Factory"
# location: "具体文件路径"
# purpose: "模式用途说明"
# 模块间通信
communication:
apis: [] # 对外暴露的API
consumers: [] # 消费的其他模块API
mq: [] # 消息队列使用
# ============================================
# 第三阶段:数据表设计
# ============================================
data_model:
# 实体继承体系
entity_hierarchy:
base: "BaseDO | TenantBaseDO"
description: ""
# 核心数据表
tables: []
# - name: "表名"
# comment: "表说明"
# entity: "DO类名"
# extends: "BaseDO/TenantBaseDO"
# columns:
# - { name: "字段名", type: "类型", comment: "说明" }
# indexes:
# - { name: "索引名", columns: ["字段"] }
# 表关系ER关系
relationships: []
# - from: "表A"
# to: "表B"
# type: "1:N | N:1 | N:N"
# foreign_key: "外键字段"
# ============================================
# 第四阶段:代码使用设计
# ============================================
code_patterns:
# Controller层规范
controller:
annotations: [] # 常用注解
example: ""
# Service层规范
service:
interface_pattern: ""
impl_pattern: ""
example: ""
# 数据访问规范
dal:
mapper_pattern: ""
example: ""
# 异常处理
error_handling:
code_prefix: ""
examples: []
# 统一响应
response:
success_pattern: ""
error_pattern: ""
# ============================================
# 第五阶段:扩展指南
# ============================================
extension_guide:
# 新增功能步骤
new_feature:
title: "新增业务功能"
steps: []
# 新增渠道/类型示例
new_channel:
title: "新增渠道/类型"
steps: []
# 最佳实践
best_practices: []
# ============================================
# 依赖关系
# ============================================
dependencies:
# 内部依赖(其他模块)
internal: []
# - module: "模块名"
# api: "API接口名"
# purpose: "依赖用途"
# 外部依赖(第三方库)
external: []
# - name: "库名"
# version: "版本"
# purpose: "用途"
# ============================================
# 关键文件清单
# ============================================
key_files:
- path: ""
purpose: ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,363 @@
# 芋道源码 Skills 使用指南
> 快速查找适合你开发场景的提示词文档,让 AI 助手更精准地理解你的需求。
---
## 自动引用机制说明
**从 2026-03-31 起,使用样例支持自动引用设计规范**
每个使用样例文档头部包含 YAML front matter 配置,声明该场景需要引用的规范文件:
```yaml
---
references:
design: # 设计规范(全局约束)
- skills/design/db-designer.yaml
- skills/design/entity-designer.yaml
module_guide: # 模块引用提示
prompt: "请指定目标模块"
mapping:
mes: skills/modules/mes/skill-mes.yaml
patterns: # 设计模式引用
- skills/patterns/factory-pattern.yaml
---
```
**使用方式**
1. AI 读取使用样例时自动识别 `references` 配置
2. AI 自动加载引用的设计规范文件内容
3. 用户在提示词中指定模块名后AI 加载对应模块 skill
4. 生成的代码符合所有规范定义的标准
---
## 目录
1. [使用指南概述](#使用指南概述)
2. [场景分类](#场景分类)
3. [使用流程](#使用流程)
4. [FAQ](#faq)
---
## 使用指南概述
本目录提供芋道源码项目的 AI 辅助开发使用场景指南。每个场景文档都包含:
- **适用情况**:什么场景下使用该提示词
- **输入要求**:需要提供哪些信息
- **预期输出**AI 将生成什么样的代码或文档
- **使用示例**:实际案例演示
### Skills 资源概览
| 资源类型 | 目录位置 | 说明 |
|---------|---------|------|
| 模块 Skills | `skills/modules/` | 各功能模块的完整技术规范 |
| 设计模式 | `skills/patterns/` | 常用设计模式的实现模板 |
| 文档模板 | `skills/templates/` | Skill 文档编写模板 |
---
## 场景分类
### 一、快速上手
适用于初次接触项目或快速了解某个模块的开发者。
| 场景 | 适用情况 | 文档链接 | 一句话描述 |
|-----|---------|---------|-----------|
| 了解项目结构 | 刚接触项目,需要快速了解整体架构 | `skills/index.yaml` | 查看 12 个核心模块的技术规范索引 |
| 系统模块入门 | 需要了解用户、角色、权限等基础功能 | [skill-system.yaml](../modules/system/skill-system.yaml) | 系统管理模块完整开发指南 |
| 基础设施入门 | 需要了解文件、配置、任务等基础服务 | [skill-infra.yaml](../modules/infra/skill-infra.yaml) | 基础设施模块完整开发指南 |
| **实体类实现** | 需要从 SQL 到实体类创建完整 CRUD 功能 | [entity-implementation.md](entity-implementation.md) | 从建表到完整接口的端到端指南 |
**推荐使用流程**
```
阅读 index.yaml --> 选择目标模块 --> 阅读对应 skill-*.yaml --> 开始开发
```
---
### 二、扩展模块
适用于在现有模块基础上添加新功能的场景。
| 场景 | 适用情况 | 文档链接 | 一句话描述 |
|-----|---------|---------|-----------|
| 扩展系统模块 | 添加新的系统管理功能 | [skill-system.yaml](../modules/system/skill-system.yaml) | 遵循系统模块规范扩展功能 |
| 扩展支付模块 | 添加新的支付渠道或支付方式 | [skill-pay.yaml](../modules/pay/skill-pay.yaml) | 遵循支付模块规范扩展功能 |
| 扩展会员模块 | 添加会员等级、积分等功能 | [skill-member.yaml](../modules/member/skill-member.yaml) | 遵循会员模块规范扩展功能 |
| 扩展商城模块 | 添加商品、订单相关功能 | [skill-mall.yaml](../modules/mall/skill-mall.yaml) | 遵循商城模块规范扩展功能 |
| 扩展 CRM 模块 | 添加客户管理相关功能 | [skill-crm.yaml](../modules/crm/skill-crm.yaml) | 遵循 CRM 模块规范扩展功能 |
| 扩展工作流模块 | 添加流程审批相关功能 | [skill-bpm.yaml](../modules/bpm/skill-bpm.yaml) | 遵循工作流模块规范扩展功能 |
**扩展开发要点**
1. 阅读目标模块的 skill 文档
2. 理解现有代码结构和命名规范
3. 参考文档中的代码示例
4. 遵循文档中的开发约束
---
### 三、改造模块
适用于修改现有功能或重构代码的场景。
| 场景 | 适用情况 | 文档链接 | 一句话描述 |
|-----|---------|---------|-----------|
| 改造系统功能 | 修改用户、角色、菜单等核心功能 | [skill-system.yaml](../modules/system/skill-system.yaml) | 系统模块改造技术规范 |
| 改造支付流程 | 修改支付、退款等核心流程 | [skill-pay.yaml](../modules/pay/skill-pay.yaml) | 支付模块改造技术规范 |
| 改造订单流程 | 修改订单创建、支付、发货流程 | [skill-mall.yaml](../modules/mall/skill-mall.yaml) | 商城模块改造技术规范 |
| 改造审批流程 | 修改工作流定义和审批逻辑 | [skill-bpm.yaml](../modules/bpm/skill-bpm.yaml) | 工作流模块改造技术规范 |
**改造开发要点**
1. 重点关注文档中的"核心流程"部分
2. 了解模块间的依赖关系
3. 注意数据库字段和索引的影响
4. 遵循向后兼容原则
---
### 四、新增模块
适用于创建全新业务模块的场景。
| 场景 | 适用情况 | 文档链接 | 一句话描述 |
|-----|---------|---------|-----------|
| 新增业务模块 | 创建全新的业务功能模块 | [skill-template.yaml](../templates/skill-template.yaml) | Skill 文档编写模板 |
| 应用设计模式 | 需要使用设计模式优化代码 | [patterns/](../patterns/) | 设计模式实现模板 |
**新增模块参考**
参考现有模块的 skill 文档结构:
- 核心模块参考:`skill-system.yaml``skill-infra.yaml`
- 业务模块参考:`skill-mall.yaml``skill-crm.yaml`
- 技术模块参考:`skill-bpm.yaml``skill-ai.yaml`
**新增模块步骤**
1. 使用 `skill-template.yaml` 创建新模块文档
2. 参考 `extraction-prompt.md` 提取技术规范
3. 按照模块规范编写代码
4. 更新 `skills/index.yaml` 索引
---
### 五、设计模式应用
适用于需要使用设计模式优化代码结构的场景。
| 模式 | 适用情况 | 文档链接 | 一句话描述 |
|-----|---------|---------|-----------|
| 工厂模式 | 需要根据条件创建不同类型的对象 | [factory-pattern.yaml](../patterns/factory-pattern.yaml) | 对象创建的统一管理 |
| 策略模式 | 需要在多种算法或策略间切换 | [strategy-pattern.yaml](../patterns/strategy-pattern.yaml) | 算法族的统一接口 |
| 模板方法模式 | 需要定义算法骨架,子类实现细节 | [template-method-pattern.yaml](../patterns/template-method-pattern.yaml) | 算法骨架的固定结构 |
**设计模式选择指南**
```
是否需要创建对象? --> 多种类型 --> 工厂模式
|
v
是否需要切换算法? --> 多种策略 --> 策略模式
|
v
是否需要固定流程? --> 流程相同,细节不同 --> 模板方法模式
```
---
## 使用流程
### 整体流程图
```
+-------------------+
| 明确开发需求 |
+--------+----------+
|
+--------------+--------------+
| |
v v
+-------+-------+ +--------+--------+
| 新增功能? | | 改造现有功能? |
+-------+-------+ +--------+--------+
| |
+-------+-------+ |
| +-------+-------+
v | v
+-----+-----+ +-----+-----+ +-----+-----+
| 新增模块 | | 扩展现有 | | 改造模块 |
+-----+-----+ | 模块 | +-----+-----+
| +-----+-----+ |
v | v
+-----+-----+ v +-----+-----+
| 使用模板 | +-----+-----+ | 查看对应 |
| 创建文档 | | 查看对应 | | 模块文档 |
+-----+-----+ | 模块文档 | +-----+-----+
| +-----+-----+ |
v | v
+-----+-----+ v +-----+-----+
| 参考现有 | +-----+-----+ | 关注核心 |
| 模块规范 | | 遵循规范 | | 流程部分 |
+-----+-----+ | 开发 | +-----+-----+
| +-----+-----+ |
+-----------+-----------------+---------------+
|
v
+-------+-------+
| 开发完成 |
+---------------+
```
### 快速查找流程
1. **确定场景类型**:根据上方的场景分类确定你的开发场景
2. **查找对应文档**:在场景表格中找到对应的文档链接
3. **阅读技术规范**:打开对应的 skill 文档,了解技术要求
4. **开始开发**:按照文档规范进行开发
### 模块优先级参考
根据 `skills/index.yaml` 中定义的优先级:
| 优先级 | 模块 | 说明 |
|-------|------|------|
| P1 (核心) | system, infra, pay | 系统核心功能,建议优先熟悉 |
| P2 (业务) | member, mall, crm, bpm | 主要业务功能,使用频率高 |
| P3 (扩展) | erp, ai, iot, mp, report | 扩展业务功能,按需使用 |
---
## FAQ
### Q1: 如何选择正确的 skill 文档?
**A:** 根据你的开发任务类型选择:
- 快速上手:从 `skill-system.yaml``skill-infra.yaml` 开始
- 扩展功能:选择对应业务模块的 skill 文档
- 改造功能:重点阅读对应模块的"核心流程"部分
- 新增模块:使用 `skill-template.yaml` 作为模板
### Q2: skill 文档包含哪些内容?
**A:** 标准的 skill 文档包含以下部分:
1. **模块概述**:模块功能和技术栈说明
2. **目录结构**:模块的代码组织结构
3. **核心概念**:模块涉及的核心概念和术语
4. **技术规范**:命名规范、代码风格、注解使用等
5. **核心流程**:关键业务流程的实现方式
6. **数据模型**:数据库表结构和实体关系
7. **API 规范**:接口设计和参数说明
8. **代码示例**:常见开发场景的代码参考
9. **注意事项**:开发中需要注意的问题
### Q3: 如何使用设计模式文档?
**A:** 设计模式文档提供:
1. 模式的概念和适用场景
2. 项目中的实际应用案例
3. 代码实现模板
4. 使用注意事项
当你需要优化代码结构或解决特定设计问题时,参考对应的设计模式文档。
### Q4: 如何创建新的 skill 文档?
**A:** 创建新模块的 skill 文档步骤:
1. 复制 `skills/templates/skill-template.yaml` 作为起点
2. 参考 `skills/templates/extraction-prompt.md` 了解提取方法
3. 参考现有模块的 skill 文档作为示例
4. 按照模板结构填写模块信息
5. 更新 `skills/index.yaml` 添加新模块索引
### Q5: 如何理解模块之间的依赖关系?
**A:** 模块依赖关系:
```
system (系统管理) <-- 几乎所有模块都依赖
|
+-- member (会员) <-- mall, crm
|
+-- infra (基础设施) <-- 所有模块
|
+-- pay (支付) <-- mall, member
```
开发时注意:
- system 模块提供用户、权限等基础能力
- infra 模块提供文件、配置、任务等基础服务
- 业务模块之间可能存在依赖关系
### Q6: 如何快速找到某个功能的实现位置?
**A:** 使用 skill 文档的"目录结构"和"核心流程"部分:
1. 在 skill 文档中查找对应功能的目录位置
2. 阅读"核心流程"了解实现逻辑
3. 参考"代码示例"找到具体实现
### Q7: 文档中的代码示例可以直接使用吗?
**A:** 可以,但需要注意:
1. 代码示例遵循项目规范,可作为模板
2. 使用前需要根据实际业务需求调整
3. 注意替换示例中的占位符和示例数据
4. 建议先在测试环境验证
---
## 附录
### 模块文档索引
| 模块 | 文档路径 | 行数 | 状态 |
|-----|---------|------|------|
| 系统管理 | [skill-system.yaml](../modules/system/skill-system.yaml) | 839 | 已完成 |
| 基础设施 | [skill-infra.yaml](../modules/infra/skill-infra.yaml) | 700+ | 已完成 |
| 支付模块 | [skill-pay.yaml](../modules/pay/skill-pay.yaml) | 654 | 已完成 |
| 会员模块 | [skill-member.yaml](../modules/member/skill-member.yaml) | 608 | 已完成 |
| 商城模块 | [skill-mall.yaml](../modules/mall/skill-mall.yaml) | 650+ | 已完成 |
| CRM 模块 | [skill-crm.yaml](../modules/crm/skill-crm.yaml) | 850+ | 已完成 |
| ERP 模块 | [skill-erp.yaml](../modules/erp/skill-erp.yaml) | 900+ | 已完成 |
| 工作流模块 | [skill-bpm.yaml](../modules/bpm/skill-bpm.yaml) | 650+ | 已完成 |
| AI 模块 | [skill-ai.yaml](../modules/ai/skill-ai.yaml) | 670+ | 已完成 |
| 物联网模块 | [skill-iot.yaml](../modules/iot/skill-iot.yaml) | 721 | 已完成 |
| 公众号模块 | [skill-mp.yaml](../modules/mp/skill-mp.yaml) | 660+ | 已完成 |
| 报表模块 | [skill-report.yaml](../modules/report/skill-report.yaml) | 320+ | 已完成 |
### 使用指南文档索引
| 文档 | 适用场景 | 一句话描述 |
|-----|---------|-----------|
| [quick-start.md](quick-start.md) | 快速上手 | Skill 文档快速使用指南和常用提示词模板 |
| [entity-implementation.md](entity-implementation.md) | 实体类开发 | 从 SQL 建表到完整 CRUD 接口的端到端指南 |
| [extend-module.md](extend-module.md) | 扩展模块 | 在现有模块基础上添加新功能 |
| [new-module.md](new-module.md) | 新增模块 | 创建全新的业务模块 |
| [refactor-module.md](refactor-module.md) | 改造模块 | 修改现有功能或重构代码 |
| [pattern-usage.md](pattern-usage.md) | 设计模式 | 常用设计模式的使用场景和实现方法 |
### 更新记录
| 日期 | 版本 | 说明 |
|-----|------|------|
| 2026-03-25 | v1.1 | 新增实体类实现完整流程指南 |
| 2026-03-18 | v1.0 | 初始版本,创建使用指南索引 |
---
> 提示:本文档会随着项目发展持续更新。如有问题或建议,请提交 Issue 或 Pull Request。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,594 @@
# Skill 快速上手指南
> 本文档提供快速使用 Skill 文档的提示词模板,帮助开发者快速定位代码、理解业务、完成日常开发任务。
---
## 一、Skill 快速使用指南
### 什么是 Skill 文档
Skill 文档是从 ruoyi-vue-pro 项目代码中提取的结构化知识库,包含:
- **设计理念**:业务定位、设计原则、领域模型
- **架构设计**:分层架构、设计模式、模块通信
- **数据表设计**:实体关系、表结构、字段说明
- **代码规范**Controller/Service/DAL 层代码模式
- **扩展指南**:新增功能步骤、最佳实践
### 如何使用 Skill 文档
```
使用 Skill 文档,帮我 [任务描述]
参考 Skill 文档:
- skills/modules/[模块名]/skill-[模块名].yaml
```
**示例**
```
使用 Skill 文档,帮我在 system 模块添加一个"操作日志"功能。
参考 skills/modules/system/skill-system.yaml
```
---
## 二、一分钟提示词模板
### 基础模板
```
参考 Skill 文档 skills/modules/[模块]/skill-[模块].yaml帮我完成以下任务
[具体任务描述]
要求:
1. 遵循项目现有的代码规范
2. 使用统一响应格式 CommonResult
3. 添加必要的权限控制
```
### 快速填充指南
| 场景 | 模块路径 | 模块名 |
|------|---------|--------|
| 用户/角色/权限/菜单 | yudao-module-system | system |
| 文件/配置/任务/日志 | yudao-module-infra | infra |
| 支付/退款/钱包 | yudao-module-pay | pay |
| 会员/积分/等级/签到 | yudao-module-member | member |
| 商品/订单/促销 | yudao-module-mall | mall |
| 客户/线索/合同 | yudao-module-crm | crm |
| 采购/销售/库存 | yudao-module-erp | erp |
| 工作流/审批 | yudao-module-bpm | bpm |
| AI/大模型 | yudao-module-ai | ai |
| 物联网设备 | yudao-module-iot | iot |
| 微信公众号 | yudao-module-mp | mp |
| 报表 | yudao-module-report | report |
---
## 三、常见场景快速提示词
### 3.1 查找代码位置
```
参考 Skill 文档,帮我找到 [功能] 相关的代码位置:
- Controller 入口
- Service 实现
- Mapper 接口
- 数据表定义
模块:[模块名]
功能关键词:[关键词]
```
**示例**
```
参考 skills/modules/system/skill-system.yaml帮我找到用户登录相关的代码位置
- Controller 入口
- Service 实现
- 认证逻辑
```
---
### 3.2 理解业务逻辑
```
参考 Skill 文档 skills/modules/[模块]/skill-[模块].yaml帮我理解 [业务场景] 的完整流程:
1. 业务入口在哪里?
2. 核心业务逻辑在哪个 Service
3. 涉及哪些数据表?
4. 状态流转是怎样的?
```
**示例**
```
参考 skills/modules/mall/skill-mall.yaml帮我理解订单支付的完整流程
1. 支付入口在哪里?
2. 核心支付逻辑在哪个 Service
3. 支付回调如何处理?
4. 订单状态如何流转?
```
---
### 3.3 修复 Bug
```
参考 Skill 文档 skills/modules/[模块]/skill-[模块].yaml帮我分析和修复以下问题
问题描述:[Bug 描述]
错误信息:[错误日志或异常信息]
复现步骤:[如何复现]
请帮我:
1. 定位问题代码位置
2. 分析问题原因
3. 提供修复方案
4. 说明需要修改的文件
```
**示例**
```
参考 skills/modules/pay/skill-pay.yaml帮我分析和修复以下问题
问题描述:支付回调后订单状态未更新
错误信息:无异常,但订单一直是待支付状态
请帮我:
1. 定位回调处理代码
2. 分析状态更新逻辑
3. 检查事务是否正确
```
---
### 3.4 添加字段
```
参考 Skill 文档 skills/modules/[模块]/skill-[模块].yaml帮我在 [实体] 中添加新字段:
字段名:[字段名]
字段类型:[类型]
字段用途:[用途说明]
需要修改:
1. DO 实体类
2. 数据库表(提供 SQL
3. VO 类(请求/响应)
4. Controller如有需要
```
**示例**
```
参考 skills/modules/member/skill-member.yaml帮我在会员用户中添加新字段
字段名vipExpireTime
字段类型LocalDateTime
字段用途VIP 会员过期时间
需要修改:
1. MemberUserDO
2. member_user 表
3. 相关 VO 类
```
---
### 3.5 添加接口
```
参考 Skill 文档 skills/modules/[模块]/skill-[模块].yaml帮我添加一个新接口
接口名称:[接口名]
接口路径:[HTTP 方法和路径]
功能描述:[功能说明]
请求参数:[参数列表]
响应数据:[响应结构]
权限标识:[权限码]
请按照项目规范生成:
1. Controller 方法
2. Service 接口和实现
3. VO 类
```
**示例**
```
参考 skills/modules/system/skill-system.yaml帮我添加一个新接口
接口名称:获取当前用户信息
接口路径GET /system/user/current
功能描述:获取当前登录用户的详细信息
权限标识system:user:query
请按照项目规范生成完整代码。
```
---
### 3.6 添加权限
```
参考 Skill 文档 skills/modules/system/skill-system.yaml帮我添加一个新的权限
权限名称:[权限名]
权限标识:[权限码,格式:模块:功能:操作]
父菜单ID[父菜单 ID]
权限类型:[菜单/按钮]
请提供:
1. 菜单/按钮的 SQL 插入语句
2. Controller 中的 @PreAuthorize 注解示例
```
**示例**
```
参考 skills/modules/system/skill-system.yaml帮我添加一个新的权限
权限名称:导出用户
权限标识system:user:export
父菜单ID用户管理菜单 ID
权限类型:按钮
请提供:
1. 按钮的 SQL 插入语句
2. Controller 中的权限注解示例
```
---
## 四、Skill 文档快速导航
### 4.1 模块索引
| 模块 | 文档路径 | 核心功能 | 关键实体 |
|------|---------|---------|---------|
| **system** | skills/modules/system/skill-system.yaml | 用户、角色、权限、菜单、租户、字典 | AdminUserDO, RoleDO, MenuDO |
| **infra** | skills/modules/infra/skill-infra.yaml | 文件、配置、任务、日志、代码生成 | FileDO, JobDO, ConfigDO |
| **pay** | skills/modules/pay/skill-pay.yaml | 支付、退款、钱包、转账 | PayOrderDO, PayRefundDO, PayWalletDO |
| **member** | skills/modules/member/skill-member.yaml | 会员、积分、等级、签到、地址 | MemberUserDO, MemberLevelDO |
| **mall** | skills/modules/mall/skill-mall.yaml | 商品、订单、促销、统计 | ProductSpuDO, TradeOrderDO, CouponDO |
| **crm** | skills/modules/crm/skill-crm.yaml | 线索、客户、商机、合同、回款 | CrmCustomerDO, CrmContractDO |
| **erp** | skills/modules/erp/skill-erp.yaml | 采购、销售、库存、财务 | ErpPurchaseDO, ErpSaleDO |
| **bpm** | skills/modules/bpm/skill-bpm.yaml | 流程定义、流程实例、任务 | BpmProcessDefinitionDO |
| **ai** | skills/modules/ai/skill-ai.yaml | AI 模型、对话、绘图 | AiChatMessageDO, AiImageDO |
| **iot** | skills/modules/iot/skill-iot.yaml | 设备、产品、物模型 | IotDeviceDO, IotProductDO |
| **mp** | skills/modules/mp/skill-mp.yaml | 公众号、菜单、消息 | MpAccountDO, MpMessageDO |
| **report** | skills/modules/report/skill-report.yaml | 报表、数据源、图表 | ReportDataSourceDO |
### 4.2 错误码前缀速查
| 模块 | 错误码前缀 | 示例 |
|------|-----------|------|
| system | 1_002_XXX_XXX | 1_002_000_000 登录失败 |
| infra | 1_001_XXX_XXX | 1_001_000_000 文件不存在 |
| pay | 1_007_XXX_XXX | 1_007_002_000 支付订单不存在 |
| member | 1_004_XXX_XXX | 1_004_001_000 用户不存在 |
| product | 1_008_XXX_XXX | 1_008_005_000 商品不存在 |
| trade | 1_011_XXX_XXX | 1_011_000_011 订单不存在 |
| promotion | 1_013_XXX_XXX | 1_013_004_000 优惠券模板不存在 |
### 4.3 权限标识规范
```
格式:模块:功能:操作
示例:
- system:user:query # 查询用户
- system:user:create # 创建用户
- system:user:update # 更新用户
- system:user:delete # 删除用户
- system:user:export # 导出用户
```
---
## 五、常用代码片段
### 5.1 Controller 层
```java
@Tag(name = "管理后台 - [功能名]")
@RestController
@RequestMapping("/[模块]/[功能]")
@Validated
public class XxxController {
@Resource
private XxxService xxxService;
@PostMapping("/create")
@Operation(summary = "创建[功能]")
@PreAuthorize("@ss.hasPermission('[模块]:[功能]:create')")
public CommonResult<Long> createXxx(@Valid @RequestBody XxxSaveReqVO createReqVO) {
return success(xxxService.createXxx(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新[功能]")
@PreAuthorize("@ss.hasPermission('[模块]:[功能]:update')")
public CommonResult<Boolean> updateXxx(@Valid @RequestBody XxxSaveReqVO updateReqVO) {
xxxService.updateXxx(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除[功能]")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('[模块]:[功能]:delete')")
public CommonResult<Boolean> deleteXxx(@RequestParam("id") Long id) {
xxxService.deleteXxx(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得[功能]")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('[模块]:[功能]:query')")
public CommonResult<XxxRespVO> getXxx(@RequestParam("id") Long id) {
XxxDO xxx = xxxService.getXxx(id);
return success(BeanUtils.toBean(xxx, XxxRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得[功能]分页")
@PreAuthorize("@ss.hasPermission('[模块]:[功能]:query')")
public CommonResult<PageResult<XxxRespVO>> getXxxPage(@Valid XxxPageReqVO pageReqVO) {
PageResult<XxxDO> pageResult = xxxService.getXxxPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, XxxRespVO.class));
}
}
```
### 5.2 Service 层
```java
// 接口
public interface XxxService {
Long createXxx(@Valid XxxSaveReqVO createReqVO);
void updateXxx(@Valid XxxSaveReqVO updateReqVO);
void deleteXxx(Long id);
XxxDO getXxx(Long id);
PageResult<XxxDO> getXxxPage(XxxPageReqVO pageReqVO);
}
// 实现
@Service
@Validated
public class XxxServiceImpl implements XxxService {
@Resource
private XxxMapper xxxMapper;
@Override
public Long createXxx(XxxSaveReqVO createReqVO) {
XxxDO xxx = BeanUtils.toBean(createReqVO, XxxDO.class);
xxxMapper.insert(xxx);
return xxx.getId();
}
@Override
public void updateXxx(XxxSaveReqVO updateReqVO) {
// 校验存在
validateXxxExists(updateReqVO.getId());
// 更新
XxxDO updateObj = BeanUtils.toBean(updateReqVO, XxxDO.class);
xxxMapper.updateById(updateObj);
}
@Override
public void deleteXxx(Long id) {
// 校验存在
validateXxxExists(id);
// 删除
xxxMapper.deleteById(id);
}
private void validateXxxExists(Long id) {
if (xxxMapper.selectById(id) == null) {
throw exception(XXX_NOT_EXISTS);
}
}
@Override
public XxxDO getXxx(Long id) {
return xxxMapper.selectById(id);
}
@Override
public PageResult<XxxDO> getXxxPage(XxxPageReqVO pageReqVO) {
return xxxMapper.selectPage(pageReqVO);
}
}
```
### 5.3 Mapper 层
```java
@Mapper
public interface XxxMapper extends BaseMapperX<XxxDO> {
default PageResult<XxxDO> selectPage(XxxPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<XxxDO>()
.likeIfPresent(XxxDO::getName, reqVO.getName())
.eqIfPresent(XxxDO::getStatus, reqVO.getStatus())
.betweenIfPresent(XxxDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(XxxDO::getId));
}
default List<XxxDO> selectListByStatus(Integer status) {
return selectList(XxxDO::getStatus, status);
}
}
```
### 5.4 DO 实体类
```java
@TableName("[表名]")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class XxxDO extends TenantBaseDO { // 或 BaseDO
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 名称
*/
private String name;
/**
* 状态
*/
private Integer status;
// ... 其他字段
}
```
### 5.5 VO 类
```java
// 请求 VO新增/修改共用)
@Data
public class XxxSaveReqVO {
@Schema(description = "编号", example = "1")
private Long id; // 更新时必填
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试")
@NotBlank(message = "名称不能为空")
private String name;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
private Integer status;
}
// 分页请求 VO
@Data
@EqualsAndHashCode(callSuper = true)
public class XxxPageReqVO extends PageParam {
@Schema(description = "名称", example = "测试")
private String name;
@Schema(description = "状态", example = "0")
private Integer status;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
// 响应 VO
@Data
public class XxxRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试")
private String name;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
```
### 5.6 错误码定义
```java
// 在 ErrorCodeConstants.java 中添加
ErrorCode XXX_NOT_EXISTS = new ErrorCode(1_002_XXX_000, "[功能]不存在");
ErrorCode XXX_NAME_DUPLICATE = new ErrorCode(1_002_XXX_001, "已存在该名字的[功能]");
```
### 5.7 跨模块 API 调用
```java
// API 接口定义
public interface XxxApi {
CommonResult<XxxRespDTO> getXxx(Long id);
CommonResult<List<XxxRespDTO>> getXxxList(Collection<Long> ids);
}
// API 实现
@RestController
@FeignClient(name = ApiConstants.NAME)
public class XxxApiImpl implements XxxApi {
@Resource
private XxxService xxxService;
@Override
public CommonResult<XxxRespDTO> getXxx(Long id) {
XxxDO xxx = xxxService.getXxx(id);
return success(BeanUtils.toBean(xxx, XxxRespDTO.class));
}
}
// 调用方式
@Resource
private XxxApi xxxApi;
public void someMethod() {
XxxRespDTO xxx = xxxApi.getXxx(id).getCheckedData();
}
```
---
## 六、快速参考
### 6.1 分层架构速记
```
Controller (HTTP入口)
Service (业务逻辑)
Mapper (数据访问)
Database
```
### 6.2 常用注解速查
| 层级 | 常用注解 |
|------|---------|
| Controller | @RestController, @RequestMapping, @Tag, @Operation, @PreAuthorize |
| Service | @Service, @Validated, @Transactional |
| Mapper | @Mapper |
| DO | @TableName, @TableId, @Data |
| VO | @Data, @Schema, @NotBlank, @NotNull |
### 6.3 命名规范
| 类型 | 命名规则 | 示例 |
|------|---------|------|
| Controller | XxxController | UserController |
| Service接口 | XxxService | UserService |
| Service实现 | XxxServiceImpl | UserServiceImpl |
| Mapper | XxxMapper | UserMapper |
| DO | XxxDO | UserDO |
| 请求VO | XxxSaveReqVO / XxxPageReqVO | UserSaveReqVO |
| 响应VO | XxxRespVO | UserRespVO |
| API接口 | XxxApi | UserApi |
| 错误码 | XXX_NOT_EXISTS | USER_NOT_EXISTS |
---
**提示**:使用 Skill 文档时,直接复制对应的提示词模板,替换占位符即可快速获得准确的代码生成和问题解答。

File diff suppressed because it is too large Load Diff

2
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/cache
/project.local.yml

View File

@@ -0,0 +1,33 @@
# Memory Maintenance
## Discovery Model
- Core principle: progressive discovery through references, building a graph of memories.
- Initially, agents are provided with the list of all memories (names only).
- Agents should read `mem:core` as the top-level entry point (graph root).
This memory should contain references to other memories covering major project domains.
The referenced memories shall, in turn, shall contain references to even more specific memories, and so on.
The depth of the graph shall depend on the project complexity.
- Use topics/folders to group related memories in order to make the content structure explicit.
Folders can mirror project structure (e.g. modules like frontend/backend) or topics like debugging, architecture, etc.
- Memory references must use a mem: prefix inside backticks, e.g. `mem:frontend/core`.
The surrounding text should clearly indicate when to read the memory/which content to expect.
The text should provide more precise guidance than the memory name alone,
i.e. avoid a reference like "frontend debugging: `mem:frontend/debugging` and instead make clear which aspects of frontend debugging are covered.
- Memories themselves should not contain information about when to read them; this is the responsibility of the referring memory.
## Style
Dense agent notes, not prose docs. Prefer invariants, terse bullets.
Avoid obvious context, rationale, and examples unless they prevent likely mistakes.
Keep guidance durable and generalizable, not task-local.
## Add/update threshold
Add or update memories only with stable, non-obvious project conventions that avoid complex rediscovery in the future.
Do not add: quick-read facts; generic language/framework knowledge; one-off task notes; volatile line-level details; behavior likely to change soon.
## Maintenance Actions
- Renaming memories: References are updated automatically if handled via Serena's memory rename tool.
- Checking for stale memories (e.g. after deletion): Call `serena memories check` for a report.

167
.serena/project.yml Normal file
View File

@@ -0,0 +1,167 @@
# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "ruoyi-vue-pro"
# list of language servers to start when using the LSP backend; choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# go groovy haskell haxe hlsl
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor php_phpantom powershell
# python python_basedpyright python_jedi python_pyrefly python_ty
# qml r rego ruby ruby_solargraph
# rust scala scss solidity svelte
# swift systemverilog terraform toml typescript
# typescript_vts vue yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some language servers require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple language servers, the first language server that supports a given file will be used for that file.
# The first language server is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
language_servers:
- java
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180.0
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}
# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
- "."
# list of additional workspace folder paths for cross-package reference support.
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tools.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
ls_additional_workspace_folders: []
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
# Example:
# ignored_paths:
# - "examples/**"
# - ".worktrees/**"
# - "**/bin/**"
# - "**/obj/**"
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
fixed_tools: []
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
# for this project.
# This setting can, in turn, be overridden by CLI parameters (--mode).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
default_modes:
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
added_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []

84
CLAUDE.md Normal file
View File

@@ -0,0 +1,84 @@
# CLAUDE.md — 恭学教育
## 项目定位
恭学教育是基于 RuoYi-Vue-Pro 的教育 SaaS 平台,后端技术栈为 Spring Boot 4、MyBatis-Plus、PostgreSQL、Redis。
主要开发入口:
- `yudao-module-education/`:教育业务模块
- `yudao-server/`:应用启动模块,默认端口 `48080`
- `sql/postgresql/`PostgreSQL 初始化与历史 SQL
- `tools/education-student-harness/`:学生端 Playwright E2E 测试
分支、工作区状态、运行中的容器和临时任务属于动态信息;执行任务时从 Git、配置文件和运行环境读取不在本文档固化。
## 工作方式
### Serena 优先
编码任务开始前调用 Serena `initial_instructions` 并激活项目。优先使用 Serena 完成符号检索、引用分析和结构化编辑Serena 不可用或不适合时再使用通用文件与命令工具。互不依赖的查询或编辑应批量调用。
### 遵循现有代码
- 修改前检查工作区,保留用户已有的未提交修改。
- 代码风格、命名、注释密度和分层方式与相邻代码保持一致。
- 优先复用框架现有能力,避免为单一场景建立平行抽象。
- 只修改当前任务需要的文件;发现相邻问题时先判断是否影响本次交付。
### Skills
项目级 Skills 位于 `.claude/skills/`,已有规范索引见 `.claude/skills/index.yaml`
数据库结构、索引、约束、数据回填、必要种子数据、基线或 Flyway 配置发生变化时,使用项目 Skill
```text
/flyway-postgresql
```
Flyway 的版本分配、接管策略、验证步骤以 `.claude/skills/flyway-postgresql/SKILL.md` 为唯一事实来源。
## 架构约束
### Education 模块
- `yudao.education.catalog-mode` 控制目录数据源:
- `SCALAR_READ`:通过 `ScalarCatalogProvider` 访问 HTTP 数据源。
- `JAVA_READ`:通过 `JavaCatalogProvider` 直连 PostgreSQL。
- `QuestionCatalogServiceImpl` 返回前端前必须剥离答案与解析等敏感字段。
- 题目不可见或数据源不可用时采用 fail-closed不进行静默降级。
- 多租户业务 DO 继承 `TenantBaseDO`,由 MyBatis-Plus 注入 `tenant_id`
### PostgreSQL
项目运行数据库为 PostgreSQL。Java 注解 SQL、MyBatis XML、测试 SQL 和运行配置均使用 PostgreSQL 方言。
- 主键:`BIGINT GENERATED BY DEFAULT AS IDENTITY`
- 时间:`TIMESTAMP`,默认当前时间使用 `CURRENT_TIMESTAMP`
- 布尔:`BOOLEAN`,按需使用 `NOT NULL DEFAULT false`
- 幂等插入:`ON CONFLICT ... DO NOTHING`
- Upsert`ON CONFLICT (...) DO UPDATE SET ... EXCLUDED.column`
- 空值兜底:`COALESCE`;时间格式化:`TO_CHAR`;日期字段提取:`EXTRACT`
- 有界删除使用有序、限量的主键子查询或 CTE。
- DO 主键遵循项目既有的 `@TableId``@KeySequence("{table}_seq")` 模式。
### Flyway
运行时 migration 放在所属模块:
```text
<module>/src/main/resources/db/migration/<module>/
```
已在共享环境执行的 migration 是不可变发布历史。数据库修复通过更高版本的向前 migration 完成,生产环境保持 `clean-disabled: true`
## 验证
按改动范围执行最小充分验证。后端主链路至少运行:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
涉及行为变化时运行对应模块的聚焦测试;涉及数据库 migration 时还要确认脚本被打包到模块的 `target/classes/db/migration/`。只有实际执行过 PostgreSQL migration才能报告数据库迁移成功否则明确说明仅完成静态检查或编译验证。

11
CONTEXT-MAP.md Normal file
View File

@@ -0,0 +1,11 @@
# Context Map
## Contexts
- [Education](./yudao-module-education/CONTEXT.md) — owns education content, practice, assessment, and learning-state language.
## Relationships
- **Education → Member**: Education references the authenticated Member user as the student identity; it does not own credentials or generic user accounts.
- **Education → System**: Education consumes tenant and authorization capabilities; it does not own generic tenants or RBAC.
- **Education → Infra**: Education composes file, job, messaging, and audit capabilities for education workflows.

View File

@@ -0,0 +1,38 @@
# Current State
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Executive summary
Phase 0 remains a read-only architecture assessment, not an implementation claim. Verified evidence shows the target is on feature/education-core-loop with a heavily dirty worktree, while the source checkout has no local or remote feature/education-core-loop ref and is main at 033701a. The target contains a meaningful committed core-loop slice (ce02f8a) plus substantial dirty provider/catalog/session work, so all roadmap status must distinguish committed, dirty, absent, and runtime-unverified behavior. EDU-003 has now decided public tenant resolution: Origin/Referer are forgeable browser-context claims rather than trusted identity; headless lookup uses the constrained System-name Public Tenant Handle; successful lookup accepts tenant-existence disclosure while unknown/disabled/expired failures are identical; exact wire errors, canonical host-only websites, a secure-default local flag, and abuse controls are assigned to EDU-004. This is distinct from the already verified authenticated tenant mismatch rejection in TenantSecurityWebFilter; /education/context still requires EDU-004 Member/UserType enforcement. The most reliable core-loop slice remains provider-neutral fail-closed question content handling across the active default Scalar path, conditional Java path, safe catalog projection, and persisted session restoration. Core-loop schema is not proven to be in active Flyway: V4010 is a placeholder, V4020 is catalog-only, and practice/report/idempotency DDL is untracked manual SQL. Native reads are an intentional explicit-scope bypass requiring mapper audit, while schema foreign keys do not enforce tenant-consistent graphs. Phase 0 coverage must also add first-class Auth/Profile/extended Learning, granular tenant-admin, and granular platform-admin groups. Paid access, provider authority, option schema, public graph semantics, and source baseline remain product/architecture decisions.
## Verified program decisions
- Verified source provenance is limited: /Users/tiku1/code/tiku-backend has only main and origin/main at 033701a785c7012139e7f86995eea6041225592e; no local or remote feature/education-core-loop ref exists. Use main/033701a provisionally only, or obtain explicit approval for that baseline.
- Verified target branch is feature/education-core-loop and its worktree is dirty. Current read-only inventory reports 65 modified tracked files and 97 untracked entries; preserve all, and do not rely on an older 21-untracked count.
- Classify target behavior as committed-and-tested, committed-but-not-runtime-verified, dirty/uncommitted, or absent before scheduling work. ce02f8a is committed core-loop evidence; native provider/catalog and much of the schema are dirty.
- Verified V4010 is SELECT 1 and V4020 is native catalog only. Practice/report/idempotency/wrong/favorite DDL in sql/postgresql/education is untracked/manual and not proven active Flyway. Convert required DDL to immutable module-owned PostgreSQL Flyway migrations before claiming schema delivery; never modify published migrations.
- Keep PostgreSQL/Flyway as the only new schema delivery mechanism. Historical MySQL files and root SQL are not active delivery unless explicitly labeled archival/manual and removed from operational runbooks.
- Treat public tenant resolution origin-binding absence as a P0 correction, not merely a richer-legacy gap. Separately acknowledge that TenantSecurityWebFilter already rejects authenticated tenant/header mismatch; the remaining principal issue is missing Member/UserType enforcement in /education/context.
- Treat hostname port handling as a verified internal contradiction requiring alignment across implementation, properties, API documentation, System lookup normalization, and tests.
- Treat native catalog isolation as an intentional TenantUtils.executeIgnore/manual-scope boundary, not evidence of a current leak. Make mapper audit and tenant/scope-consistent graph constraints concrete blockers before authoring.
- Make the first slice provider-neutral or cover both providers because SCALAR_READ is the verified default and Java provider is conditional. The slice must include fresh browsing and persisted session restoration, with a common option-schema contract and fail-closed behavior.
- Do not treat submit idempotency as complete: check-then-insert is not an atomic claim. Reserve keys atomically and define crash recovery before the submit slice.
- Add first-class Auth/Profile/extended Learning, tenant appearance/integrations/secrets/codes, and granular platform-admin capability groups so every required legacy cluster has a disposition.
- Do not expose paid/private practice until entitlement semantics and public target contracts are decided.
- No tests, builds, PostgreSQL connections, Flyway execution, or runtime verification were performed; all conclusions are static repository evidence unless explicitly marked otherwise.
## Unknowns
- EDU-003 decided the public resolver threat model and contract. Browser headers are forgeable context claims; a constrained Public Tenant Handle supports headless clients; success discloses tenant existence; unknown/disabled/expired failures are identical; exact errors, canonical websites, local activation, Member-only context, and abuse controls are assigned to EDU-004.
- Whether a future System-owned immutable Tenant Code or authenticated/signed locator is required beyond the accepted public-handle contract.
- The valid option schema for each question type, including whether absent options are legal; whether malformed published content is omitted or produces a controlled source failure.
- Whether PUBLIC tenant_id=0 rows may reference only PUBLIC parents, whether tenant-owned rows may reference global rows, and the precise composite constraint/trigger strategy.
- Whether untracked /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education files are intended for promotion into Flyway or are design/manual artifacts.
- Whether V4010/V4020 or any manual core-loop DDL has ever run successfully in PostgreSQL; no runtime migration evidence exists.
- Whether target test H2 MODE=MYSQL is test-only and compatible with PostgreSQL-only delivery.
- Which Auth/Profile/extended Learning semantics are replaced by Member/System/Infra versus Education-owned, including vocabulary, leaderboard, stats, trend, feedback, exam dates, notifications, points, and badges.
- Whether tenant appearance, domains, payment accounts, auth providers, secrets, activation codes, coupons, integrations, marketing, public-bank grants, and sync are in scope or explicitly retired.
- Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
- Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
- Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.

View File

@@ -0,0 +1,17 @@
# Capability Migration Matrix
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
Statuses are restricted to the Goal vocabulary. Evidence marked as verified is static repository evidence.
| Legacy capability | Legacy code location | Legacy database objects | Business value | Target module | Existing capability to reuse | Education gap | Other-module change | Priority | Risk | Verification | Status | Evidence | Open decision |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenant resolution, identity, and student context | /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/locator.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/resolver.ts | System tenant and website/domain records are authoritative.<br>Legacy tenant, domain, branding, identity, membership, and RLS objects are reference-only and must not be copied mechanically. | Resolves the pre-login tenant and authenticated student context needed to route users into the correct education tenant without trusting client identity. | System + Member + framework tenant support with an Education context adapter | System TenantCommonApi/TenantApiImpl, TenantContextHolder, TenantSecurityWebFilter, Member/System authentication and UserTypeEnum, framework tenant injection and tenant-ignore only for explicitly authorized platform operations. | Verified: EducationTenantController currently accepts caller-supplied hostname or tenantName under @PermitAll. EDU-003 establishes that Origin/Referer are forgeable browser-context claims rather than trusted identity, success discloses available-tenant existence, and legacy tenantName is unsupported. Verified: TenantSecurityWebFilter already rejects authenticated LoginUser/request-tenant mismatches and requires a tenant on non-ignored URLs. Verified: EducationContextController checks login presence and tenant validity but not LoginUser.userType. Verified: hostname documentation says no port while implementation preserves legal ports and tests expect port preservation. Decision: EDU-004 must implement the exact Public Tenant Handle, canonical website, secure local flag, wire-error, Member-principal, redaction, and abuse-control contract. | System/framework tenant and security boundaries remain authoritative; Education adapts them. EDU-003 retained generic System lookup APIs and selected Member-only context enforcement. | P0 | High: wrong pre-login tenant selection, tenant discovery, or treating an admin ID as a Member ID can cross security boundaries. | Add exact Origin/Referer claim, forged-header threat-model, requested-host conflict, explicit handle, legacy tenantName rejection, lifecycle-indistinguishability, canonical website, local-flag, normalization, anonymous, missing-tenant, mismatch, Member/admin-principal, redaction, and abuse-control tests. Static audit is not runtime proof. | partially migrated | Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantController.java:51-63,109-132,159-177.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java:66-105.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/EducationContextController.java:43-57.<br>Verified source baseline /Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/locator.ts:90-151 requires production origin/request-host checks.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantResolveIntegrationTest.java:80-89. | EDU-003 accepted: Origin/Referer are forgeable browser-context claims; headless lookup uses the constrained System-name Public Tenant Handle; successful lookup discloses existence while unknown/disabled/expired are indistinguishable; host identity and canonical stored websites are host-only; local fallback has a secure-default flag; context is Member-only. Future signed locator or immutable System Tenant Code remains optional later scope. Source feature ref remains unavailable. |
| Student core learning loop | /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts:23-113<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/learning/use-cases.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/learning/access.ts | Education catalog and practice/report/idempotency/wrong-question/favorite tables.<br>Verified: native catalog V4020 is a dirty module resource; practice/report/idempotency DDL is currently in untracked sql/postgresql/education files rather than proven active Flyway history. | Provides the student loop from published catalog browsing through practice creation, answer saving, restore, submission, report, wrong questions, and favorites. | Education | Education provider/adapter boundary, Member/System identity, database uniqueness and transactions, framework locks/idempotency only as supplements—not replacements—for atomic database claims. | Verified: commit ce02f8a contains committed access/core controllers, safe projections, and focused tests, while native provider/catalog and additional core-loop work are dirty; these statuses must be separated. Verified: native and Scalar providers disagree on malformed/absent options; QuestionCatalogService and SessionResponseAssembler can emit apparently valid empty options. Verified: submit idempotency performs check-then-insert rather than atomic initial reservation. Inference: core-loop completion and concurrency guarantees are not established. | Education owns education-domain state and orchestration; Member/System context is reused. Paid/private access remains blocked on an entitlement decision. | P0 | High: corrupt assessment content, mode-dependent behavior, duplicate state transitions, answer leakage, or unauthorized access. | Provider-neutral tests across Scalar and Java, browsing/collection/practice-create/restore safe projections, malformed/unavailable/unpublished fail-closed cases, cross-tenant cases, and PostgreSQL concurrent same-key/different-key submit tests. | partially migrated | Verified ce02f8a, target repository, for committed EducationAccessService, core controllers/services, projections, and HTTP/service tests.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProperties.java:34-36 defaults to SCALAR_READ.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/integration/scalar/config/ScalarAutoConfiguration.java:30-35 selects Scalar for SCALAR_READ/missing mode.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/provider/JavaCatalogProvider.java:397-417, ScalarCatalogProvider.java:736-751, QuestionCatalogServiceImpl.java:182-202, SessionResponseAssembler.java:69-89.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/practice/PracticeSessionServiceImpl.java:294-307,397-409.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education/009-education-idempotency-unified.sql:81, but no execution evidence. | Select the pilot-authoritative provider or require a provider-neutral contract; define valid option structure by question type and absent-option semantics; define atomic submit claim/crash recovery; decide entitlement contract before paid/private practice. |
| Education catalog and question content | /Users/tiku1/code/tiku-backend/apps/api/src/nest/catalog.module.ts:74-138<br>/Users/tiku1/code/tiku-backend/apps/nest/tenant-content.module.ts | V4020 native catalog tables for regions, schools, majors, subjects, categories, banks, questions, versions, content, collections, blueprints, and bindings.<br>Legacy catalog/question/content/asset tables, constraints, functions, triggers, grants, and RLS are reference objects requiring semantic mapping. | Supplies reusable published catalog, question, classification, and content reads for student and future admin workflows. | Education | Education Provider boundary, explicit CatalogScopeQuery, framework tenant context, Infra File public API for future assets. | Verified: current native reads intentionally run inside TenantUtils.executeIgnore and apply explicit scope predicates; this is a controlled manual-isolation boundary, not proof of a current leak. Verified: V4020 uses ordinary single-column foreign keys, so tenant-owned/public graph consistency is not enforced. Inference: every mapper needs audit and content admission needs composite constraints or equivalent enforcement. | Education owns domain reads; Infra File may later provide asset transport. No provider expansion should occur before provider authority and graph-integrity rules are decided. | P0 | High if manual scope is bypassed or invalid cross-tenant/public relationships are admitted. | Inventory every mapper, provider contract tests, invalid graph insert tests, malformed/unpublished tests, PostgreSQL Flyway syntax/resource-packaging checks, and runtime migration evidence only when executed. | partially migrated | Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/provider/JavaCatalogProvider.java:82-89.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/dal/mysql/catalog/CatalogScopeQuery.java:12-20.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/resources/db/migration/education/V4020__create_native_catalog.sql:38-123,133-152,160-265,324-386.<br>Verified /Users/tiku1/code/tiku-backend/supabase/migrations/202606210008_content_navigation_practice.sql:3-178. | Choose Scalar-only, native PostgreSQL, or explicit coexistence; define tenant_id=0 PUBLIC graph semantics and composite-key strategy; decide whether source RLS/functions/triggers are contractual. |
| Auth, student profile, and extended learning | /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/profile.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/profile/ | Legacy auth/session/verification/OAuth/phone-binding objects.<br>Legacy profile/check-in/points/tasks/exchange/notifications/badges/feedback/exam-countdown objects.<br>Legacy leaderboard/history/report/stats/trend/vocabulary progress/review/favorites/stats objects. | Covers broader student learning and profile experiences beyond the core loop, preserving discoverable legacy behavior and its disposition. | Member + System + Education, with Infra composition | Member/System auth and profile primitives, System/Infra notifications, Member points/levels where semantics match, Education-specific projections and authorization. | Verified source inventory shows these are distinct required Phase 0 domains, not merely generic context or secondary engagement. Target ownership and compatibility are not established. Inference: the definition-of-done is unsupported until each endpoint/state family is classified. | Member/System own authentication and generic membership; Education owns education-specific profile/progress projections. System/Infra may own notifications, while product owners must decide points, badges, feedback, exams, and vocabulary ownership. | P1 | High for auth compatibility and medium for omitted student progress/profile behavior. | Endpoint/API mapping, principal and tenant tests, profile redaction, progress/report compatibility, vocabulary state transitions, and explicit retired/product-decision checks. | pending migration | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts:31-56.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/profile.module.ts:38-87.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts:23-60,70-113.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/docs/education/migration/GOAL.md:126-141,393-405. | For every Auth/Profile/extended Learning family, assign Member/System/Education/Infra ownership, compatibility requirement, data disposition, and phase; decide vocabulary, analytics, feedback, exam dates, notifications, points, and badges. |
| Tenant education operations, appearance, integrations, secrets, and codes | /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-classes.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-appearance.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-integrations.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-secrets.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-codes.module.ts | Legacy classes, student relationships, supervision, roles/configuration, branding/settings/themes, domains, payment accounts, auth-provider configuration, tenant secrets, activation codes, coupons/redemptions, integrations, and marketing objects. | Enables tenant administrators to operate education organizations while preserving separate security and ownership boundaries. | Education + System + Member + Mall/Pay + Infra | System Tenant/RBAC/DataPermission/AdminUserApi, Member relationships, Mall/Pay/Member APIs, Infra secret/file/message/audit facilities. | Verified: classes/supervision were only part of the source tenant-admin surface. Appearance/theme lifecycle, domains/payment/auth integrations, secret rotation, and codes/coupons are separate migration/security surfaces with no verified target equivalent. Inference: collapsing them into one row would hide authorization and secret-handling decisions. | System RBAC/DataPermission and tenant configuration are reused; Mall/Pay/Member own commercial primitives; Infra owns secrets/messaging/files where applicable; Education owns only domain relationships and configuration extensions. | P1 | High: admin scope, secret leakage, payment configuration, and code redemption errors. | Permission matrix, row-scope negatives, secret redaction/rotation, integration authorization, code/coupon idempotency, audit, and cross-tenant tests. | pending migration | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-appearance.module.ts:17-41.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-integrations.module.ts:18-42.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-secrets.module.ts:13-21.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-codes.module.ts:12-31.<br>Verified /Users/tiku1/code/tiku-backend/supabase/migrations/202606290002_tenant_classes.sql.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java:12-32. | Define class/student/teacher scope semantics and separately decide appearance, domain, payment/auth integration, secret, activation-code, coupon, public-bank grant, and marketing ownership or retirement. |
| Platform administration and governance | /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-overview.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-permissions.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-*.module.ts | Legacy platform staff, tenant lifecycle/billing profiles, public question-bank grant/sync, SaaS plan/invoice/usage/overage/dunning, audit/export/alert/notification-channel, and permission objects. | Provides platform staff and governance over tenants, staff lifecycle, public-bank grants, SaaS plans, billing, usage, dunning, audits, alerts, and permissions. | System + Pay + Mall + Infra + CRM with Education extensions | System RBAC/DataPermission/AdminUserApi, authorized tenant-ignore mechanisms, Pay/Mall/Infra/CRM public APIs, audit/logging. | Verified source surface is broader than one aggregated platform-admin row. Target seams exist, but object-level ownership, data scopes, and cross-tenant operation policy remain incomplete. | System, Pay, Mall, Infra, CRM, and Education-specific extension permissions; Education must not duplicate platform ledgers or generic administration. | P1 | High access-control and financial-governance risk. | Permission matrix, platform-admin integration, cross-tenant negative, audit-redaction, billing/usage reconciliation, and alert/export tests. | pending migration | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-overview.module.ts.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-permissions.module.ts.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java:12-20.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java:7-57. | Define separate Student App, Tenant Admin, Platform Admin, public, and internal policies; map each platform surface to System/Pay/Mall/Infra/CRM/Education or explicit retirement. |
| Commercialization and growth | /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-orders.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-payments.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-growth.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-crm.module.ts | Legacy product/order/payment/event/entitlement/coupon/refund/reconciliation/commission/referral/points/dunning objects.<br>Target Mall/Pay/Member tables remain authoritative; Education may add minimal binding records. | Supports paid products, fulfillment, entitlements, refunds, reconciliation, commissions, referrals, and CRM conversion without recreating platform ledgers. | Mall + Pay + Member + CRM with Education binding | Mall/Pay DTO APIs, Member identity/entitlement/points, CRM services, Infra Job/MQ/audit. | Verified target Pay/Mall APIs expose core seams, but scoped entitlement issuance/revocation, activation codes, reconciliation, commissions, dunning, and referral semantics are not proven. One prior evidence path was malformed; corrected source location is /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts. | Mall Trade/Product, Pay, Member entitlement/points, CRM, Infra jobs/events/audit; Education owns product-to-education bindings and fulfillment orchestration only. | P2 | High financial and authorization risk. | Callback/idempotency/amount/refund, entitlement lifecycle, reconciliation, and education fulfillment contract tests. | product decision required | Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java:13-38.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java:12-30.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java:12-38.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-orders.module.ts:15-70 and /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/docs/education/migration/GOAL.md:36-40,226-228. | Choose entitlement/activation-code/coupon model and confirm issuance, revocation, callback, refund, reconciliation, commission, and referral contracts before paid practice. |
| Background processing, assets, and operational platform | /Users/tiku1/code/tiku-backend/apps/worker/src/worker-jobs.ts<br>/Users/tiku1/code/tiku-backend/apps/worker/src/jobs/imports.ts<br>/Users/tiku1/code/tiku-backend/apps/worker/src/jobs/exports.ts<br>/Users/tiku1/code/tiku-backend/apps/asset-scanner/src/ | Legacy worker queues, leases, retries/dead letters, imports/exports, reconciliation, notification/audit, usage, and security scan state.<br>Target owns business state in domain modules and uses platform execution primitives; do not copy queue tables wholesale. | Preserves operational reliability for imports, exports, payments, CRM, scanning, notifications, retries, and audit while removing dependence on NestJS workers. | Infra platform plus owning domain modules | Infra Job, Redis MQ, File, locks, idempotency, logging, tracing, Excel utilities, tenant propagation. | Verified target primitives exist, but durable claim/lease/heartbeat/retry and malware-scanner equivalence are not proven. Education import/export business state is absent or not verified. | Infra Job/MQ/File/logging/observability plus owning Education/Pay/Mall/CRM handlers; scanner deployment or adapter ownership must be decided. | P1 | High operational and security risk. | Concurrent claim/lease/recovery, retries/dead letters, scan fail-closed, file access, tenant propagation, audit, and deployment smoke tests. | partially migrated | Verified /Users/tiku1/code/tiku-backend/apps/worker/src/worker-jobs.ts:24-220 and /Users/tiku1/code/tiku-backend/apps/worker/src/jobs/imports.ts:87-260.<br>Verified /Users/tiku1/code/tiku-backend/apps/asset-scanner/src/scanner.service.ts:23-50.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/RedisMQTemplate.java.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java. | Confirm Infra claim/lease semantics and scanner ownership, file privacy/retention, legacy asset migration/re-scan, and duplicate-safe at-least-once processing. |
| Secondary learning, media, AI, and engagement | /Users/tiku1/code/tiku-backend/apps/api/src/nest/scoreline.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/video.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/nest/ai.module.ts<br>/Users/tiku1/code/tiku-backend/apps/api/src/features/profile/ | Legacy scoreline, vocabulary, handbook, video entitlement/progress, recommendation, notification, badge, exam-date, and analytics objects. | Delivers selected recommendation, scoreline, vocabulary, handbook, video, AI, notification, badge, exam-date, and engagement experiences after ownership and priority are explicit. | Education plus AI/Infra/Member/System | AI services, Infra File/notifications, Member points/levels, Education authorization/projections. | Verified legacy capabilities exist, but target equivalence and priority are not established. These cannot remain an undifferentiated P3 bucket if Phase 0 must give every capability a disposition. | AI, Infra File/messaging, Member growth primitives, System notifications, and Education extensions. | P3 | Medium-to-high due to entitlement, media access, sensitive reporting, and unclear scope. | Per-capability contract, authorization, entitlement, export/redaction, and migration compatibility tests. | product decision required | Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/scoreline.module.ts:207-226.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/video.module.ts:431-464.<br>Verified /Users/tiku1/code/tiku-backend/apps/api/src/nest/ai.module.ts:120-139.<br>Verified /Users/tiku1/code/tiku-backend/supabase/migrations/202606210009_content_import_vocabulary_handbook.sql.<br>Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java. | For each capability, assign Education, existing platform ownership, explicit retirement, or later product scope; decide entitlement and safe export/redaction requirements. |

View File

@@ -0,0 +1,110 @@
# Legacy API Mapping
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
This Phase 0 artifact maps API families rather than all 342 operations. Endpoint-level method/path/request/response mapping remains required before implementing each family.
## Tenant resolution, identity, and student context
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/locator.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/tenant/resolver.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** System + Member + framework tenant support with an Education context adapter
- **Reuse:** System TenantCommonApi/TenantApiImpl, TenantContextHolder, TenantSecurityWebFilter, Member/System authentication and UserTypeEnum, framework tenant injection and tenant-ignore only for explicitly authorized platform operations.
- **Migration conclusion:** decision complete; implementation pending EDU-004
- **Selected contract:** Public inputs are **Tenant Locator Claims**, not authenticated identity. Production browser routing derives a host-only claim from valid HTTP(S) `Origin`, falling back to `Referer`; both are forgeable by non-browser callers and provide browser UX consistency only. A supplied hostname may confirm that claim, and disagreement is a conflict. Headless clients use `tenantHandle`, explicitly defined as the target's unique System tenant `name` under a case-sensitive constrained public contract because no distinct stable Tenant Code exists. Legacy public `tenantName` is rejected rather than silently aliased.
- **Threat model:** Successful resolution returns tenant ID/display name and therefore permits available-tenant existence probing. Only unknown, disabled, and expired tenants are indistinguishable. Reuse public throttling/ingress controls and structured abuse metrics; a deployment requiring spoof resistance needs a future authenticated/signed locator, not trust in Origin/Referer or forwarding-header controls.
- **Normalization and compatibility:** Host identity is lowercase, trimmed, trailing-dot-free, bracket-free for IPv6, and independent of all ports. DNS hosts, IPv4, and IPv6 are accepted when valid; credentials, paths, multi-value input, malformed authorities, and unsupported schemes are rejected. Canonical `system_tenant.websites` values for this resolver are host-only. Scheme/path/port-bearing stored values do not silently normalize or match; configuration correction is required, or a separate Flyway/data ticket if automated correction is later approved.
- **Local activation:** Only `yudao.education.tenant-resolution.local-development-enabled=true` enables local/request-host fallback; default and absence are false, and profiles are not authoritative. With the flag true, code-less configured local-host resolution is allowed and an explicit handle takes precedence.
- **Authenticated context:** `/education/context` accepts only a Student Principal: an authenticated `LoginUser` with `userType == UserTypeEnum.MEMBER`. User and tenant IDs remain security/tenant-context derived. `TenantSecurityWebFilter` already fills a missing tenant from the authenticated principal, rejects principal/request-tenant mismatch, requires a tenant for non-ignored URLs, and validates tenant availability; EDU-004 preserves rather than duplicates these checks.
- **Ownership and seam:** Login-method metadata belongs to Member authentication, not System tenant metadata or Education. EDU-004 removes/deprecates Education `loginMethods` unless a minimal Member-owned interface is proven necessary. Retain generic System-owned `TenantCommonApi`; make lookup methods required and add focused `TenantApiImpl` contract tests. `EducationTenantController` remains the public claim-consistency/redaction adapter.
- **Exact public wire contract:** Framework business responses remain HTTP 200. Invalid/malformed/missing/local-forbidden claim is code `1005001003`, message `租户识别请求无效`, null data. Domain/handle or requested-host conflict is code `1005001008`, message `租户识别信息冲突`, null data. Unknown/disabled/expired is code `1005001004`, message `当前租户不可用`, null data, with identical shape. Success is code 0 and data contains only `tenantId` and `displayName`; never expose or echo handle, websites, expiry, package, status, private config, or login methods.
- **Required EDU-004 verification:** Education HTTP tests cover Origin resolution, Referer fallback, forged-header threat-model naming, malformed Origin, Origin/requested-host conflict, untrusted arbitrary hostname, explicit handle, legacy tenantName rejection, unavailable-state exact wire equivalence, host normalization, canonical/non-canonical website behavior, local flag false/true and precedence, domain/handle conflict, anonymous/ADMIN/MEMBER context, redaction, and abuse-control attachment/metrics where a reusable seam exists. System owns adapter contract tests; framework owns existing missing-tenant and authenticated mismatch tests.
- **Decision evidence:** [`issues/EDU-003-tenant-resolution-decision.md`](issues/EDU-003-tenant-resolution-decision.md). Static only; no production or database change was made.
## Student core learning loop
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts:23-113, /Users/tiku1/code/tiku-backend/apps/api/src/features/learning/use-cases.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/learning/access.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education
- **Reuse:** Education provider/adapter boundary, Member/System identity, database uniqueness and transactions, framework locks/idempotency only as supplements—not replacements—for atomic database claims.
- **Migration conclusion:** partially migrated
- **Contract gap:** Verified: commit ce02f8a contains committed access/core controllers, safe projections, and focused tests, while native provider/catalog and additional core-loop work are dirty; these statuses must be separated. Verified: native and Scalar providers disagree on malformed/absent options; QuestionCatalogService and SessionResponseAssembler can emit apparently valid empty options. Verified: submit idempotency performs check-then-insert rather than atomic initial reservation. Inference: core-loop completion and concurrency guarantees are not established.
- **Required verification:** Provider-neutral tests across Scalar and Java, browsing/collection/practice-create/restore safe projections, malformed/unavailable/unpublished fail-closed cases, cross-tenant cases, and PostgreSQL concurrent same-key/different-key submit tests.
- **Open decision:** Select the pilot-authoritative provider or require a provider-neutral contract; define valid option structure by question type and absent-option semantics; define atomic submit claim/crash recovery; decide entitlement contract before paid/private practice.
## Education catalog and question content
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/catalog.module.ts:74-138, /Users/tiku1/code/tiku-backend/apps/nest/tenant-content.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education
- **Reuse:** Education Provider boundary, explicit CatalogScopeQuery, framework tenant context, Infra File public API for future assets.
- **Migration conclusion:** partially migrated
- **Contract gap:** Verified: current native reads intentionally run inside TenantUtils.executeIgnore and apply explicit scope predicates; this is a controlled manual-isolation boundary, not proof of a current leak. Verified: V4020 uses ordinary single-column foreign keys, so tenant-owned/public graph consistency is not enforced. Inference: every mapper needs audit and content admission needs composite constraints or equivalent enforcement.
- **Required verification:** Inventory every mapper, provider contract tests, invalid graph insert tests, malformed/unpublished tests, PostgreSQL Flyway syntax/resource-packaging checks, and runtime migration evidence only when executed.
- **Open decision:** Choose Scalar-only, native PostgreSQL, or explicit coexistence; define tenant_id=0 PUBLIC graph semantics and composite-key strategy; decide whether source RLS/functions/triggers are contractual.
## Auth, student profile, and extended learning
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/auth.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/profile.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/learning.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/profile/
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Member + System + Education, with Infra composition
- **Reuse:** Member/System auth and profile primitives, System/Infra notifications, Member points/levels where semantics match, Education-specific projections and authorization.
- **Migration conclusion:** pending migration
- **Contract gap:** Verified source inventory shows these are distinct required Phase 0 domains, not merely generic context or secondary engagement. Target ownership and compatibility are not established. Inference: the definition-of-done is unsupported until each endpoint/state family is classified.
- **Required verification:** Endpoint/API mapping, principal and tenant tests, profile redaction, progress/report compatibility, vocabulary state transitions, and explicit retired/product-decision checks.
- **Open decision:** For every Auth/Profile/extended Learning family, assign Member/System/Education/Infra ownership, compatibility requirement, data disposition, and phase; decide vocabulary, analytics, feedback, exam dates, notifications, points, and badges.
## Tenant education operations, appearance, integrations, secrets, and codes
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-classes.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-appearance.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-integrations.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-secrets.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/tenant-admin-codes.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education + System + Member + Mall/Pay + Infra
- **Reuse:** System Tenant/RBAC/DataPermission/AdminUserApi, Member relationships, Mall/Pay/Member APIs, Infra secret/file/message/audit facilities.
- **Migration conclusion:** pending migration
- **Contract gap:** Verified: classes/supervision were only part of the source tenant-admin surface. Appearance/theme lifecycle, domains/payment/auth integrations, secret rotation, and codes/coupons are separate migration/security surfaces with no verified target equivalent. Inference: collapsing them into one row would hide authorization and secret-handling decisions.
- **Required verification:** Permission matrix, row-scope negatives, secret redaction/rotation, integration authorization, code/coupon idempotency, audit, and cross-tenant tests.
- **Open decision:** Define class/student/teacher scope semantics and separately decide appearance, domain, payment/auth integration, secret, activation-code, coupon, public-bank grant, and marketing ownership or retirement.
## Platform administration and governance
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-overview.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-permissions.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/platform-admin-*.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** System + Pay + Mall + Infra + CRM with Education extensions
- **Reuse:** System RBAC/DataPermission/AdminUserApi, authorized tenant-ignore mechanisms, Pay/Mall/Infra/CRM public APIs, audit/logging.
- **Migration conclusion:** pending migration
- **Contract gap:** Verified source surface is broader than one aggregated platform-admin row. Target seams exist, but object-level ownership, data scopes, and cross-tenant operation policy remain incomplete.
- **Required verification:** Permission matrix, platform-admin integration, cross-tenant negative, audit-redaction, billing/usage reconciliation, and alert/export tests.
- **Open decision:** Define separate Student App, Tenant Admin, Platform Admin, public, and internal policies; map each platform surface to System/Pay/Mall/Infra/CRM/Education or explicit retirement.
## Commercialization and growth
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-orders.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-payments.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-growth.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/referral-crm.module.ts
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Mall + Pay + Member + CRM with Education binding
- **Reuse:** Mall/Pay DTO APIs, Member identity/entitlement/points, CRM services, Infra Job/MQ/audit.
- **Migration conclusion:** product decision required
- **Contract gap:** Verified target Pay/Mall APIs expose core seams, but scoped entitlement issuance/revocation, activation codes, reconciliation, commissions, dunning, and referral semantics are not proven. One prior evidence path was malformed; corrected source location is /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.
- **Required verification:** Callback/idempotency/amount/refund, entitlement lifecycle, reconciliation, and education fulfillment contract tests.
- **Open decision:** Choose entitlement/activation-code/coupon model and confirm issuance, revocation, callback, refund, reconciliation, commission, and referral contracts before paid practice.
## Background processing, assets, and operational platform
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/worker/src/worker-jobs.ts, /Users/tiku1/code/tiku-backend/apps/worker/src/jobs/imports.ts, /Users/tiku1/code/tiku-backend/apps/worker/src/jobs/exports.ts, /Users/tiku1/code/tiku-backend/apps/asset-scanner/src/
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Infra platform plus owning domain modules
- **Reuse:** Infra Job, Redis MQ, File, locks, idempotency, logging, tracing, Excel utilities, tenant propagation.
- **Migration conclusion:** partially migrated
- **Contract gap:** Verified target primitives exist, but durable claim/lease/heartbeat/retry and malware-scanner equivalence are not proven. Education import/export business state is absent or not verified.
- **Required verification:** Concurrent claim/lease/recovery, retries/dead letters, scan fail-closed, file access, tenant propagation, audit, and deployment smoke tests.
- **Open decision:** Confirm Infra claim/lease semantics and scanner ownership, file privacy/retention, legacy asset migration/re-scan, and duplicate-safe at-least-once processing.
## Secondary learning, media, AI, and engagement
- **Legacy locations:** /Users/tiku1/code/tiku-backend/apps/api/src/nest/scoreline.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/video.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/nest/ai.module.ts, /Users/tiku1/code/tiku-backend/apps/api/src/features/profile/
- **Legacy authorization semantics to preserve:** tenant boundary, principal type, visibility, idempotency, state transitions, and redaction as applicable.
- **Target:** Education plus AI/Infra/Member/System
- **Reuse:** AI services, Infra File/notifications, Member points/levels, Education authorization/projections.
- **Migration conclusion:** product decision required
- **Contract gap:** Verified legacy capabilities exist, but target equivalence and priority are not established. These cannot remain an undifferentiated P3 bucket if Phase 0 must give every capability a disposition.
- **Required verification:** Per-capability contract, authorization, entitlement, export/redaction, and migration compatibility tests.
- **Open decision:** For each capability, assign Education, existing platform ownership, explicit retirement, or later product scope; decide entitlement and safe export/redaction requirements.

View File

@@ -0,0 +1,145 @@
# Database Object Mapping
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Global disposition rules
- Ordinary education tables and constraints become immutable module-owned PostgreSQL Flyway migrations.
- Supabase RLS maps primarily to framework tenant isolation and application authorization, not copied RLS.
- RPCs/functions map to Java services unless database execution is demonstrably the better boundary.
- Triggers map to transactions, events, jobs, or audit facilities unless database enforcement is required.
- Storage maps to Infra File; auth schema maps to Member/System.
- Current manual SQL is not considered executed or active Flyway history without runtime evidence.
## Tenant resolution, identity, and student context
### Source and target objects
- System tenant and website/domain records are authoritative.
- Legacy tenant, domain, branding, identity, membership, and RLS objects are reference-only and must not be copied mechanically.
### Disposition
- **Target owner:** System + Member + framework tenant support with an Education context adapter
- **Current status:** partially migrated
- **Required cross-module treatment:** System/framework tenant and security boundaries remain authoritative; Education adapts them. EDU-003 retains generic required `TenantCommonApi` lookup methods and assigns Member-only context enforcement to EDU-004.
- **Risk:** High: the unauthenticated resolver permits available-tenant existence probing, while wrong authenticated principal/tenant handling can cross security boundaries.
- **Accepted decision:** Browser headers are forgeable context claims; headless clients use the constrained System-name Public Tenant Handle; unknown/disabled/expired failures are identical. Canonical website values for Public Tenant Resolution are normalized host-only strings. Scheme/path/port-bearing stored values are not silently normalized and require configuration correction. If automated correction is later required, create a separate Flyway/data ticket and invoke `flyway-postgresql`; EDU-003/EDU-004 authorize no database change.
## Student core learning loop
### Source and target objects
- Education catalog and practice/report/idempotency/wrong-question/favorite tables.
- Verified: native catalog V4020 is a dirty module resource; practice/report/idempotency DDL is currently in untracked sql/postgresql/education files rather than proven active Flyway history.
### Disposition
- **Target owner:** Education
- **Current status:** partially migrated
- **Required cross-module treatment:** Education owns education-domain state and orchestration; Member/System context is reused. Paid/private access remains blocked on an entitlement decision.
- **Risk:** High: corrupt assessment content, mode-dependent behavior, duplicate state transitions, answer leakage, or unauthorized access.
- **Decision still required:** Select the pilot-authoritative provider or require a provider-neutral contract; define valid option structure by question type and absent-option semantics; define atomic submit claim/crash recovery; decide entitlement contract before paid/private practice.
## Education catalog and question content
### Source and target objects
- V4020 native catalog tables for regions, schools, majors, subjects, categories, banks, questions, versions, content, collections, blueprints, and bindings.
- Legacy catalog/question/content/asset tables, constraints, functions, triggers, grants, and RLS are reference objects requiring semantic mapping.
### Disposition
- **Target owner:** Education
- **Current status:** partially migrated
- **Required cross-module treatment:** Education owns domain reads; Infra File may later provide asset transport. No provider expansion should occur before provider authority and graph-integrity rules are decided.
- **Risk:** High if manual scope is bypassed or invalid cross-tenant/public relationships are admitted.
- **Decision still required:** Choose Scalar-only, native PostgreSQL, or explicit coexistence; define tenant_id=0 PUBLIC graph semantics and composite-key strategy; decide whether source RLS/functions/triggers are contractual.
## Auth, student profile, and extended learning
### Source and target objects
- Legacy auth/session/verification/OAuth/phone-binding objects.
- Legacy profile/check-in/points/tasks/exchange/notifications/badges/feedback/exam-countdown objects.
- Legacy leaderboard/history/report/stats/trend/vocabulary progress/review/favorites/stats objects.
### Disposition
- **Target owner:** Member + System + Education, with Infra composition
- **Current status:** pending migration
- **Required cross-module treatment:** Member/System own authentication and generic membership; Education owns education-specific profile/progress projections. System/Infra may own notifications, while product owners must decide points, badges, feedback, exams, and vocabulary ownership.
- **Risk:** High for auth compatibility and medium for omitted student progress/profile behavior.
- **Decision still required:** For every Auth/Profile/extended Learning family, assign Member/System/Education/Infra ownership, compatibility requirement, data disposition, and phase; decide vocabulary, analytics, feedback, exam dates, notifications, points, and badges.
## Tenant education operations, appearance, integrations, secrets, and codes
### Source and target objects
- Legacy classes, student relationships, supervision, roles/configuration, branding/settings/themes, domains, payment accounts, auth-provider configuration, tenant secrets, activation codes, coupons/redemptions, integrations, and marketing objects.
### Disposition
- **Target owner:** Education + System + Member + Mall/Pay + Infra
- **Current status:** pending migration
- **Required cross-module treatment:** System RBAC/DataPermission and tenant configuration are reused; Mall/Pay/Member own commercial primitives; Infra owns secrets/messaging/files where applicable; Education owns only domain relationships and configuration extensions.
- **Risk:** High: admin scope, secret leakage, payment configuration, and code redemption errors.
- **Decision still required:** Define class/student/teacher scope semantics and separately decide appearance, domain, payment/auth integration, secret, activation-code, coupon, public-bank grant, and marketing ownership or retirement.
## Platform administration and governance
### Source and target objects
- Legacy platform staff, tenant lifecycle/billing profiles, public question-bank grant/sync, SaaS plan/invoice/usage/overage/dunning, audit/export/alert/notification-channel, and permission objects.
### Disposition
- **Target owner:** System + Pay + Mall + Infra + CRM with Education extensions
- **Current status:** pending migration
- **Required cross-module treatment:** System, Pay, Mall, Infra, CRM, and Education-specific extension permissions; Education must not duplicate platform ledgers or generic administration.
- **Risk:** High access-control and financial-governance risk.
- **Decision still required:** Define separate Student App, Tenant Admin, Platform Admin, public, and internal policies; map each platform surface to System/Pay/Mall/Infra/CRM/Education or explicit retirement.
## Commercialization and growth
### Source and target objects
- Legacy product/order/payment/event/entitlement/coupon/refund/reconciliation/commission/referral/points/dunning objects.
- Target Mall/Pay/Member tables remain authoritative; Education may add minimal binding records.
### Disposition
- **Target owner:** Mall + Pay + Member + CRM with Education binding
- **Current status:** product decision required
- **Required cross-module treatment:** Mall Trade/Product, Pay, Member entitlement/points, CRM, Infra jobs/events/audit; Education owns product-to-education bindings and fulfillment orchestration only.
- **Risk:** High financial and authorization risk.
- **Decision still required:** Choose entitlement/activation-code/coupon model and confirm issuance, revocation, callback, refund, reconciliation, commission, and referral contracts before paid practice.
## Background processing, assets, and operational platform
### Source and target objects
- Legacy worker queues, leases, retries/dead letters, imports/exports, reconciliation, notification/audit, usage, and security scan state.
- Target owns business state in domain modules and uses platform execution primitives; do not copy queue tables wholesale.
### Disposition
- **Target owner:** Infra platform plus owning domain modules
- **Current status:** partially migrated
- **Required cross-module treatment:** Infra Job/MQ/File/logging/observability plus owning Education/Pay/Mall/CRM handlers; scanner deployment or adapter ownership must be decided.
- **Risk:** High operational and security risk.
- **Decision still required:** Confirm Infra claim/lease semantics and scanner ownership, file privacy/retention, legacy asset migration/re-scan, and duplicate-safe at-least-once processing.
## Secondary learning, media, AI, and engagement
### Source and target objects
- Legacy scoreline, vocabulary, handbook, video entitlement/progress, recommendation, notification, badge, exam-date, and analytics objects.
### Disposition
- **Target owner:** Education plus AI/Infra/Member/System
- **Current status:** product decision required
- **Required cross-module treatment:** AI, Infra File/messaging, Member growth primitives, System notifications, and Education extensions.
- **Risk:** Medium-to-high due to entitlement, media access, sensitive reporting, and unclear scope.
- **Decision still required:** For each capability, assign Education, existing platform ownership, explicit retirement, or later product scope; decide entitlement and safe export/redaction requirements.

View File

@@ -0,0 +1,68 @@
# Module Reuse Map
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
Education must call public APIs, framework extension points, or events. It must not depend on another module's internal ServiceImpl, Mapper, or DO.
## Tenant resolution, identity, and student context
- **Target owner:** System + Member + framework tenant support with an Education context adapter
- **Public/framework capability to reuse:** System TenantCommonApi/TenantApiImpl, TenantContextHolder, TenantSecurityWebFilter, Member/System authentication and UserTypeEnum, framework tenant injection and tenant-ignore only for explicitly authorized platform operations.
- **Education-owned gap:** Verified: EducationTenantController accepts caller-supplied hostname or tenantName under @PermitAll and does not enforce the source resolver's production Origin/Referer/request-host binding. Verified: TenantSecurityWebFilter already rejects authenticated LoginUser/request-tenant mismatches and requires a tenant on non-ignored URLs. Verified: EducationContextController checks login presence and tenant validity but not LoginUser.userType. Verified: hostname documentation says no port while implementation preserves legal ports and tests expect port preservation. Inference: arbitrary tenant discovery, admin-principal acceptance, and port inconsistency are P0/P1 trust-boundary risks until policy is fixed.
- **Allowed external-module change:** System/framework tenant and security boundaries remain authoritative; Education should only adapt them. A generic tenant lookup contract and possibly Member-only context policy require explicit decisions.
## Student core learning loop
- **Target owner:** Education
- **Public/framework capability to reuse:** Education provider/adapter boundary, Member/System identity, database uniqueness and transactions, framework locks/idempotency only as supplements—not replacements—for atomic database claims.
- **Education-owned gap:** Verified: commit ce02f8a contains committed access/core controllers, safe projections, and focused tests, while native provider/catalog and additional core-loop work are dirty; these statuses must be separated. Verified: native and Scalar providers disagree on malformed/absent options; QuestionCatalogService and SessionResponseAssembler can emit apparently valid empty options. Verified: submit idempotency performs check-then-insert rather than atomic initial reservation. Inference: core-loop completion and concurrency guarantees are not established.
- **Allowed external-module change:** Education owns education-domain state and orchestration; Member/System context is reused. Paid/private access remains blocked on an entitlement decision.
## Education catalog and question content
- **Target owner:** Education
- **Public/framework capability to reuse:** Education Provider boundary, explicit CatalogScopeQuery, framework tenant context, Infra File public API for future assets.
- **Education-owned gap:** Verified: current native reads intentionally run inside TenantUtils.executeIgnore and apply explicit scope predicates; this is a controlled manual-isolation boundary, not proof of a current leak. Verified: V4020 uses ordinary single-column foreign keys, so tenant-owned/public graph consistency is not enforced. Inference: every mapper needs audit and content admission needs composite constraints or equivalent enforcement.
- **Allowed external-module change:** Education owns domain reads; Infra File may later provide asset transport. No provider expansion should occur before provider authority and graph-integrity rules are decided.
## Auth, student profile, and extended learning
- **Target owner:** Member + System + Education, with Infra composition
- **Public/framework capability to reuse:** Member/System auth and profile primitives, System/Infra notifications, Member points/levels where semantics match, Education-specific projections and authorization.
- **Education-owned gap:** Verified source inventory shows these are distinct required Phase 0 domains, not merely generic context or secondary engagement. Target ownership and compatibility are not established. Inference: the definition-of-done is unsupported until each endpoint/state family is classified.
- **Allowed external-module change:** Member/System own authentication and generic membership; Education owns education-specific profile/progress projections. System/Infra may own notifications, while product owners must decide points, badges, feedback, exams, and vocabulary ownership.
## Tenant education operations, appearance, integrations, secrets, and codes
- **Target owner:** Education + System + Member + Mall/Pay + Infra
- **Public/framework capability to reuse:** System Tenant/RBAC/DataPermission/AdminUserApi, Member relationships, Mall/Pay/Member APIs, Infra secret/file/message/audit facilities.
- **Education-owned gap:** Verified: classes/supervision were only part of the source tenant-admin surface. Appearance/theme lifecycle, domains/payment/auth integrations, secret rotation, and codes/coupons are separate migration/security surfaces with no verified target equivalent. Inference: collapsing them into one row would hide authorization and secret-handling decisions.
- **Allowed external-module change:** System RBAC/DataPermission and tenant configuration are reused; Mall/Pay/Member own commercial primitives; Infra owns secrets/messaging/files where applicable; Education owns only domain relationships and configuration extensions.
## Platform administration and governance
- **Target owner:** System + Pay + Mall + Infra + CRM with Education extensions
- **Public/framework capability to reuse:** System RBAC/DataPermission/AdminUserApi, authorized tenant-ignore mechanisms, Pay/Mall/Infra/CRM public APIs, audit/logging.
- **Education-owned gap:** Verified source surface is broader than one aggregated platform-admin row. Target seams exist, but object-level ownership, data scopes, and cross-tenant operation policy remain incomplete.
- **Allowed external-module change:** System, Pay, Mall, Infra, CRM, and Education-specific extension permissions; Education must not duplicate platform ledgers or generic administration.
## Commercialization and growth
- **Target owner:** Mall + Pay + Member + CRM with Education binding
- **Public/framework capability to reuse:** Mall/Pay DTO APIs, Member identity/entitlement/points, CRM services, Infra Job/MQ/audit.
- **Education-owned gap:** Verified target Pay/Mall APIs expose core seams, but scoped entitlement issuance/revocation, activation codes, reconciliation, commissions, dunning, and referral semantics are not proven. One prior evidence path was malformed; corrected source location is /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.
- **Allowed external-module change:** Mall Trade/Product, Pay, Member entitlement/points, CRM, Infra jobs/events/audit; Education owns product-to-education bindings and fulfillment orchestration only.
## Background processing, assets, and operational platform
- **Target owner:** Infra platform plus owning domain modules
- **Public/framework capability to reuse:** Infra Job, Redis MQ, File, locks, idempotency, logging, tracing, Excel utilities, tenant propagation.
- **Education-owned gap:** Verified target primitives exist, but durable claim/lease/heartbeat/retry and malware-scanner equivalence are not proven. Education import/export business state is absent or not verified.
- **Allowed external-module change:** Infra Job/MQ/File/logging/observability plus owning Education/Pay/Mall/CRM handlers; scanner deployment or adapter ownership must be decided.
## Secondary learning, media, AI, and engagement
- **Target owner:** Education plus AI/Infra/Member/System
- **Public/framework capability to reuse:** AI services, Infra File/notifications, Member points/levels, Education authorization/projections.
- **Education-owned gap:** Verified legacy capabilities exist, but target equivalence and priority are not established. These cannot remain an undifferentiated P3 bucket if Phase 0 must give every capability a disposition.
- **Allowed external-module change:** AI, Infra File/messaging, Member growth primitives, System notifications, and Education extensions.

View File

@@ -0,0 +1,34 @@
# Commit Review: 11e9cc68547cf271b9d5de60bf41b3f71899d1db (11e9cc6), target repository
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Retain
- Education reactor registration, Member/server activation, Education server dependency, allocated error-code range, and XML correction unless later evidence disproves necessity.
## Adjust
- Treat historical MySQL schema/seed/rollback as archival or explicitly manual only; it is not a PostgreSQL/Flyway delivery path.
- If global menu entries 6800/6801 remain required, replace them with an approved Education-owned PostgreSQL Flyway seed migration.
- Remove operational documentation that invokes mysql or destructive rollback scripts; use forward correction and audited administrative procedures.
## Replace
- Required seed behavior with higher-version immutable PostgreSQL Flyway migration.
## Remove by forward correction
- README/runbook claims of MySQL active delivery and destructive rollback.
- The MySQL menu seed as active delivery after approved Flyway replacement.
## Pending decisions
- Whether menu entries 6800/6801 remain product scope.
- Whether historical sql/mysql/education artifacts remain labeled archival/manual material.
## Evidence
- Verified /Users/tiku1/code/ruoyi-vue-pro/pom.xml:18-19.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-server/pom.xml:35-47.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java:31-46.
- Verified /Users/tiku1/code/ruoyi-vue-pro/sql/mysql/education/000-education-schema.sql:1-5, 000-education-seed.sql:1-15, 000-education-rollback.sql:1-8.

View File

@@ -0,0 +1,37 @@
# Commit Review: 0f846fdaf5377f00347b05b9950b53f92e4dc6df (0f846fd), target repository
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Retain
- TenantApiImpl public-service dependency, @PermitAll/@TenantIgnore pre-login route subject to security tests, and context derived from framework state rather than request tenant/user IDs.
## Adjust
- Retain TenantCommonApi/TenantApiImpl as a candidate generic lookup seam, but add contract tests and resolve DTO/null/serialization compatibility.
- Replace unsupported default methods with abstract methods or a separate optional capability interface unless compatibility evidence requires them.
- Make public resolution origin-binding, principal policy, port policy, and loginMethods ownership explicit.
- Correct contradictory hostname documentation and test filter-chain/API behavior.
## Replace
- Unsupported default-method expansion with the selected interface design.
## Remove by forward correction
- Contradictory hostname/loginMethods documentation after policy decision.
## Pending decisions
- Public origin-binding versus intentionally public lookup.
- Member-only context versus System/admin support.
- Host-only versus authority-with-port identity.
- Platform ownership of loginMethods and public error taxonomy.
## Evidence
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java:28-56.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/dto/TenantRespDTO.java:14-42.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/tenant/TenantApiImpl.java:19-49.
- Verified /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantController.java:51-177 and /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/EducationContextController.java:43-57.
- Verified mismatch rejection in /Users/tiku1/code/ruoyi-vue-pro/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java:66-105.

View File

@@ -0,0 +1,67 @@
# Architecture and Product Decisions
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Decisions and constraints established by static evidence
### EDU-003 accepted decision — tenant resolution and student principal
The durable rationale, threat model, exact wire contract, compatibility policy, and EDU-004 test matrix are recorded in [`issues/EDU-003-tenant-resolution-decision.md`](issues/EDU-003-tenant-resolution-decision.md).
1. A **Tenant Locator Claim** is unauthenticated input, not identity. Browser `Origin` then `Referer` provides browser-context consistency evidence but is forgeable by non-browser callers.
2. A supplied hostname may only confirm browser-context evidence; disagreement is a conflict. Preserving browser headers or rejecting forwarding-header forgery does not authenticate `Origin`/`Referer`.
3. Headless clients use `tenantHandle`. The target currently has no distinct stable Tenant Code, so this is explicitly the unique System tenant `name` under a case-sensitive `^[A-Za-z0-9._-]{2,64}$`, operationally immutable public contract. Legacy public `tenantName` is rejected rather than aliased.
4. Public resolution discloses tenant existence on success. The guarantee is only that unknown, disabled, and expired tenants share one response. Reuse public throttling/ingress controls and emit structured probing metrics; spoof-resistant deployments require a future signed/authenticated locator.
5. Host identity is lowercase, trimmed, trailing-dot-free, bracket-free for IPv6, and port-independent. Canonical System website entries for this resolver are host-only; non-canonical scheme/path/port entries do not match and require configuration correction or a separately scoped Flyway/data ticket.
6. Local fallback is enabled only by `yudao.education.tenant-resolution.local-development-enabled`, default `false`; profiles do not enable it. With the flag true, code-less configured local-host resolution is permitted, while an explicit handle takes precedence.
7. `/education/context` is Member-only, obtains the full `LoginUser`, and continues deriving IDs only from security and tenant contexts. `TenantSecurityWebFilter` remains responsible for authenticated missing-tenant, mismatch, and availability checks.
8. Login-method metadata belongs to Member authentication. EDU-004 removes/deprecates Education `loginMethods` unless a minimal Member-owned interface is first proven necessary.
9. Exact public business failures use HTTP 200/CommonResult: invalid locator `1005001003` / `租户识别请求无效`; conflict `1005001008` / `租户识别信息冲突`; unknown/disabled/expired `1005001004` / `当前租户不可用`; all have null data and redacted detail. Success is code 0 and only `tenantId` plus `displayName`.
10. Retain `TenantCommonApi` as the generic System-owned seam. EDU-004 makes lookup methods required and adds System-owned `TenantApiImpl` contract tests; no Education locator concept enters System.
11. EDU-003 made no production, database, or Flyway change; verification is static only.
### EDU-005 accepted decision — PostgreSQL/Flyway takeover
The durable artifact classification, adoption matrix, version allocation, backfill policy, documentation corrections, and real-PostgreSQL verification gates are recorded in [`issues/EDU-005-flyway-takeover-decision.md`](issues/EDU-005-flyway-takeover-decision.md).
1. Education schema delivery is exclusively module-owned PostgreSQL Flyway under `yudao-module-education/src/main/resources/db/migration/education/`; root PostgreSQL scripts are manual bootstrap/design history and MySQL scripts are obsolete archives.
2. V4010 (`SELECT 1`) and V4020 (native catalog) remain byte-for-byte frozen because execution outside the inspected environment is unverified. The next planned project-wide version is V4030, subject to a fresh version scan at implementation time.
3. V4030 owns the final Practice core-loop schema, including sessions/questions, reports/details, wrong questions, favorites, and unified `education_idempotency`. Fresh schema does not create legacy answer/submit idempotency tables.
4. Existing manually bootstrapped databases require explicit schema comparison and adoption. A verified V4020-equivalent catalog may use an environment-specific 4020 baseline; incompatible environments require a higher-version correction, never falsified history.
5. Legacy idempotency data is backfilled into the unified table before any later forward cleanup. Legacy tables are preserved during initial adoption.
6. The Education capability menu seed is a separate conditional V4040 owner only if the administrator endpoint remains approved; role assignment is not seeded.
7. Docker/manual SQL initialization and MySQL rollback runbooks must be removed from active operations when EDU-006 lands. EDU-016's temporary test bridge becomes Flyway-driven after equivalence is proven.
8. The inspected local disposable `postgresdb` had no Flyway history and no Education tables. EDU-005 ran no migration and makes no migration-success claim.
### Other established constraints
1. Verified source provenance is limited: /Users/tiku1/code/tiku-backend has only main and origin/main at 033701a785c7012139e7f86995eea6041225592e; no local or remote feature/education-core-loop ref exists. Use main/033701a provisionally only, or obtain explicit approval for that baseline.
2. Verified target branch is feature/education-core-loop and its worktree is dirty. Preserve all existing changes and classify current state at execution time.
3. Classify target behavior as committed-and-tested, committed-but-not-runtime-verified, dirty/uncommitted, or absent before scheduling work. ce02f8a is committed core-loop evidence; native provider/catalog and much of the schema are dirty.
4. Verified V4010 is SELECT 1 and V4020 is native catalog only. Practice/report/idempotency/wrong/favorite DDL in sql/postgresql/education is untracked/manual and not proven active Flyway. Convert required DDL to immutable module-owned PostgreSQL Flyway migrations before claiming schema delivery; never modify published migrations.
5. Keep PostgreSQL/Flyway as the only new schema delivery mechanism. Historical MySQL files and root SQL are not active delivery unless explicitly labeled archival/manual and removed from operational runbooks.
6. Treat public tenant resolution as an unauthenticated disclosure surface requiring exact redaction and abuse controls. Separately preserve `TenantSecurityWebFilter` authenticated mismatch checks; the remaining principal issue is Member/UserType enforcement in `/education/context`.
7. Treat hostname port and stored-website representation as verified contradictions requiring alignment across implementation, properties, API documentation, System lookup behavior, configuration, and tests.
8. Treat native catalog isolation as an intentional TenantUtils.executeIgnore/manual-scope boundary, not evidence of a current leak. Make mapper audit and tenant/scope-consistent graph constraints concrete blockers before authoring.
9. Make the first slice provider-neutral or cover both providers because SCALAR_READ is the verified default and Java provider is conditional. The slice must include fresh browsing and persisted session restoration, with a common option-schema contract and fail-closed behavior.
10. Do not treat submit idempotency as complete: check-then-insert is not an atomic claim. Reserve keys atomically and define crash recovery before the submit slice.
11. Add first-class Auth/Profile/extended Learning, tenant appearance/integrations/secrets/codes, and granular platform-admin capability groups so every required legacy cluster has a disposition.
12. Do not expose paid/private practice until entitlement semantics and public target contracts are decided.
13. No tests, builds, PostgreSQL connections, Flyway execution, or runtime verification were performed by the Phase 0 assessment unless a later ticket explicitly records otherwise.
## Unresolved decisions
1. The valid option schema for each question type, including whether absent options are legal; whether malformed published content is omitted or produces a controlled source failure.
2. Whether PUBLIC tenant_id=0 rows may reference only PUBLIC parents, whether tenant-owned rows may reference global rows, and the precise composite constraint/trigger strategy.
3. Whether untracked /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education files are intended for promotion into Flyway or are design/manual artifacts.
4. Whether V4010/V4020 or any manual core-loop DDL has ever run successfully in PostgreSQL; no runtime migration evidence exists.
5. Which Auth/Profile/extended Learning semantics are replaced by Member/System/Infra versus Education-owned, including vocabulary, leaderboard, stats, trend, feedback, exam dates, notifications, points, and badges.
6. Whether tenant appearance, domains, payment accounts, auth providers, secrets, activation codes, coupons, integrations, marketing, public-bank grants, and sync are in scope or explicitly retired.
7. Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
8. Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
9. Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.
10. Whether a future System-owned immutable Tenant Code or signed bootstrap locator is required beyond the accepted public-handle/existence-disclosure contract.
## Decision rule
Questions answerable from code, Git history, configuration, tests, or documentation must be investigated. Only genuine product choices should be escalated. Hard-to-reverse decisions should become ADRs before dependent implementation begins.

View File

@@ -0,0 +1,165 @@
# Vertical Slice Roadmap
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
Tickets are vertical behaviors, not technical layers. Work blockers first and use a fresh implementation context per ticket.
## EDU-P0-S0 — Baseline, completeness, and architecture decision gate
- **Outcome:** Verified baseline, capability/API/database/module-reuse maps, commit reviews, decision register, corrected documentation plan, and bounded first-slice specification.
- **Risk:** High provenance and operational risk; no implementation should begin from an unclassified dirty baseline.
- **Blockers:**
- Obtain or explicitly approve source baseline main/033701a because no source feature/education-core-loop ref exists.
- Record current target status and preserve all 65 modified tracked and 97 untracked entries.
- Complete the ten GOAL.md Phase 0 artifact dispositions and separate committed, dirty, absent, and runtime-unverified behavior.
- Decide tenant origin-binding, principal policy, provider authority, option schema, and PUBLIC graph semantics.
- **Verification:**
- Read GOAL.md and target/source rules and module documentation.
- Record both Git statuses, histories, and refs without destructive commands.
- Review 11e9cc6, 0f846fd, and committed core-loop evidence ce02f8a.
- Confirm no tests, builds, PostgreSQL connections, Flyway migrations, or runtime flows were executed.
- Inventory Auth/Profile/extended Learning, granular tenant-admin, platform-admin, worker/scanner, and database object surfaces.
## EDU-P0-S1 — Provider-neutral safe question and session restoration
- **Outcome:** Both provider modes and restored sessions fail closed for malformed/unavailable published question content while preserving tenant scope and safe projections.
- **Risk:** Medium implementation risk and high assessment-integrity/security risk if any alternate path remains fail-open.
- **Blockers:**
- EDU-P0-S0 provider and option-contract decisions.
- Existing dirty provider/session files must be separated from unrelated work.
- Safe response and question-type option semantics must be fixed.
- **Verification:**
- Provider contract tests for Scalar and Java.
- Browsing, collection, practice-create/restore, malformed, unavailable, unpublished, cross-tenant, PUBLIC, and sensitive-field tests.
- Run focused tests/compile/diff checks only after authorization and report exact results.
## EDU-P2-S2 — Create and restore practice
- **Outcome:** A student creates and restores a tenant-scoped practice session from valid published content without client-supplied identity or tenant IDs.
- **Risk:** Medium; session ownership, graph scope, and schema packaging require negative tests.
- **Blockers:**
- EDU-P0-S1 safe-content contract.
- Core-loop schema must be promoted into active module-owned Flyway history.
- Provider authority and existing dirty session implementation classification.
- **Verification:**
- Tenant/user context and Member-principal tests.
- Restore ownership, cross-tenant denial, PUBLIC-scope tests.
- PostgreSQL uniqueness, transaction, packaging, and migration execution checks if schema changes are approved.
## EDU-P2-S3 — Idempotent answer save
- **Outcome:** A student saves one answer idempotently with explicit duplicate/conflicting-payload semantics and no sensitive-field exposure.
- **Risk:** Medium-to-high due to concurrent writes, stale versions, and answer leakage.
- **Blockers:**
- EDU-P2-S2 session state.
- Existing answer/idempotency schema and option snapshot contract.
- Entitlement decision for non-public/private content.
- **Verification:**
- Same-payload duplicate and conflicting-payload tests.
- Concurrent PostgreSQL uniqueness/transaction tests.
- Tenant isolation, stale-version, safe-response, and malformed-snapshot tests.
## EDU-P2-S4 — Atomic submit, report, wrong questions, and favorites
- **Outcome:** A student atomically claims and submits a session, reads an immutable report, and receives consistent wrong-question/favorite projections.
- **Risk:** High; current check-then-insert submit idempotency is not sufficient.
- **Blockers:**
- EDU-P2-S3 answer state.
- Atomic submit-key reservation and crash recovery design.
- Scoring/report immutability and entitlement decisions.
- **Verification:**
- ON CONFLICT/atomic claim concurrency tests.
- Processing-row crash recovery and retry semantics.
- Immutable report/scoring, duplicate submission, wrong-question/favorite idempotency, sensitive-field, tenant, and unauthorized tests.
## EDU-P3-S5 — Tenant content publication and graph integrity
- **Outcome:** Tenant administrators author, classify, publish, and safely retire question content with tenant-consistent graph integrity.
- **Risk:** High due to publication, admin scope, public graph, and student-read consistency.
- **Blockers:**
- Core loop verified.
- Provider and education content model decisions.
- System RBAC/DataPermission policy.
- **Verification:**
- Admin permission/row-scope tests.
- Composite tenant/scope relationship constraint or equivalent enforcement tests.
- Publication visibility/provider consistency and safe-projection regression tests.
## EDU-P3-S6 — Content imports, exports, assets, and scanning
- **Outcome:** Tenant administrators import/export education content with durable business state, leases, retries, duplicate-safe processing, file security, and audit.
- **Risk:** High operational and security risk.
- **Blockers:**
- Publication model.
- Infra File contract and scanner ownership.
- Infra Job/MQ durable claim/lease semantics.
- **Verification:**
- Preview/execute state machine.
- Atomic claim/lease/heartbeat/expiry/retry/dead-letter tests.
- MIME/size/object-key/scan fail-closed tests.
- Tenant propagation, audit redaction, and partial-failure tests.
## EDU-P4-S7 — Classes and education relationships
- **Outcome:** Tenant administrators manage classes, education student relationships, invitations, supervision, and education operations with explicit scope.
- **Risk:** High authorization risk.
- **Blockers:**
- Education relationship model.
- System RBAC/DataPermission scope rules.
- Member relationship contract and CRM supervision decision.
- **Verification:**
- Student/teacher/class permission matrix.
- Cross-class/cross-tenant negative and duplicate invitation tests.
- Audit redaction and operation-log tests.
## EDU-P4-S8 — Tenant configuration, integrations, and access operations
- **Outcome:** Selected tenant appearance, integrations, secrets, activation codes, coupons, and public-bank access capabilities have explicit owners and safe contracts.
- **Risk:** High because secret, payment configuration, redemption, and public-bank synchronization boundaries differ.
- **Blockers:**
- Appearance/domain/integration/secrets/codes ownership decisions.
- System tenant configuration and secret APIs.
- Mall/Pay/Member entitlement and code contracts.
- **Verification:**
- Secret redaction/rotation and authorization tests.
- Domain/auth-provider/payment-account configuration tests.
- Code/coupon redemption idempotency and audit tests.
- Public-bank grant/sync and cross-tenant tests.
## EDU-P5-S9 — Education commercialization binding
- **Outcome:** Education products bind to commerce purchases and Member entitlements without duplicated financial ledgers.
- **Risk:** High financial and authorization risk.
- **Blockers:**
- Product binding model.
- Mall/Pay public APIs.
- Member entitlement decision and callback/refund semantics.
- **Verification:**
- Order/payment/refund callback contracts.
- Entitlement issuance/revocation/expiry and idempotent fulfillment.
- Reconciliation, commission/referral, authorization, and audit tests.
## EDU-P5-S10 — Extended student and secondary learning waves
- **Outcome:** Selected Auth/Profile/extended Learning/scoreline/vocabulary/video/AI/notification/badge/exam capabilities are migrated, replaced, retired, or deferred with traceable decisions.
- **Risk:** Medium-to-high due to omitted student contracts, media entitlement, and unclear ownership.
- **Blockers:**
- Explicit scope for each secondary capability.
- AI/File/Member/System/Infra contracts and entitlement model.
- **Verification:**
- Per-capability endpoint/data/authorization contract tests.
- Progress/report/vocabulary state tests.
- Media entitlement, safe export/redaction, tenant isolation, and retirement compatibility tests.
## EDU-P6-S11 — Operational independence and legacy exit
- **Outcome:** Background and platform operations run independently of NestJS with documented retries, scanning, audit, notifications, observability, and deployment evidence.
- **Risk:** High deployment and reliability risk.
- **Blockers:**
- All owner and contract decisions.
- Operational deployment, scanner, observability, and legacy exit plan.
- **Verification:**
- Worker/job deployment smoke tests.
- At-least-once duplicate/dead-letter and scanner health/security tests.
- PostgreSQL migration execution evidence.
- Runbook and documentation consistency review.

View File

@@ -0,0 +1,51 @@
# First Recommended Slice: EDU-P0-S1
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
## Provider-neutral fail-closed safe question browsing and session restoration
### Rationale
The initial native-only slice was corrected because SCALAR_READ is the verified default and JavaCatalogProvider is conditional. A provider-neutral safe-content contract is the smallest observable correction that addresses both active and optional paths, avoids provider-authority assumptions, protects the dirty worktree, and covers the second fail-open path in restored sessions.
### Scope
- Define and document the common option validity contract, including whether absent options are legal for each question type; invalid published question payloads must be omitted or return a controlled failure, never an apparently valid empty-options question.
- Apply and test the contract for both ScalarCatalogProvider and JavaCatalogProvider, despite SCALAR_READ being the current default, so provider mode cannot change safety behavior.
- Apply and test safe projection for single-question browsing and collection/catalog browsing paths.
- Make SessionResponseAssembler reject, mark unavailable, or otherwise fail closed on malformed persisted snapshots; do not silently convert parse failure to an empty list.
- Verify publication/status/visibility and unavailable-provider fail-closed behavior at the service boundary.
- Add cross-tenant and PUBLIC-scope negative tests, while recording that current native reads use an intentional TenantUtils.executeIgnore/manual predicate boundary.
- Separate committed behavior from dirty behavior in the implementation report; do not claim runtime verification.
- Critical files for implementation: /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/provider/JavaCatalogProvider.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/ScalarCatalogProvider.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/question/QuestionCatalogServiceImpl.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/practice/SessionResponseAssembler.java; /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/test/java/cn/iocoder/yudao/module/education/service/catalog/CatalogServiceImplTest.java.
### Reuse boundaries
- Reuse /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/service/catalog/CatalogProvider.java and existing provider selection; do not choose a new provider in this slice.
- Reuse existing safe question DTO/assembler boundaries in QuestionCatalogServiceImpl; never return DOs, answers, explanations, correctness flags, or admin metadata.
- Apply one shared, provider-neutral option validation contract at the provider-to-safe-question/session-snapshot boundary; do not duplicate divergent validation rules.
- Reuse framework TenantContextHolder and the existing explicit CatalogScopeQuery predicate. Do not broaden TenantUtils.executeIgnore or add a custom tenant bypass.
- Keep changes inside Education unless a proven public contract gap requires a minimal separately owned interface; do not alter System, Member, Scalar infrastructure, or database schema speculatively.
- Preserve and classify the existing dirty tree; implementation must be serial and must not overwrite unrelated files.
### Database change
No database change for the bounded correctness slice. Do not modify V4010 or V4020. Do not promote untracked SQL during this slice. If later graph-integrity or core-loop schema work is approved, create new immutable module-owned PostgreSQL Flyway migrations under /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/resources/db/migration/education/ after schema review; never claim execution without a real PostgreSQL run.
### Test plan
- Focused unit/provider contract tests for null, absent, empty, malformed, structurally invalid, blank-label, duplicate/order-invalid options, and valid question-type-specific payloads.
- Service tests proving malformed published content cannot produce a student-visible apparently valid safe question.
- Session create/restore tests proving malformed persisted snapshots fail closed and valid snapshots retain safe fields only.
- Tests for single browse, collection browse, unpublished/invisible content, unavailable provider, disabled feature, cross-tenant access, and tenant_id=0 PUBLIC scope.
- If both providers cannot yet share a concrete contract, add contract tests parameterized over each implementation and record the remaining provider decision rather than silently selecting one.
- No concurrency test is required for this read-only slice, but atomic submit-idempotency reservation and crash recovery must block the later submit slice.
- After implementation authorization only: run focused Education tests, git diff --check, and mvn -pl yudao-server -am -DskipTests clean compile; if schema files remain unchanged, do not claim Flyway execution.
### Rollback
Application-level rollback is configuration/provider disablement or restoration of the prior provider behavior after review, without destructive Git or database rollback. No database rollback applies because this slice has no schema change. If malformed persisted snapshots are encountered, fail closed with a controlled unavailable/corrupt-content outcome rather than silently restoring an empty-options question.
### Authorization gate
This document selects and specifies the first slice; it does not authorize broad implementation. Before editing, re-read the dirty working tree, isolate existing user changes, state the exact files to touch, and execute the slice test-first.

View File

@@ -0,0 +1,15 @@
# Documentation Corrections
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
These corrections are identified but not applied by the read-only discovery workflow.
- Correct /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/README.md:4-20,171-192,393-401 to separate committed versus dirty work, remove MySQL/rollback delivery claims, document PostgreSQL/Flyway forward-only operations, and state that V4010/V4020 and core-loop schema execution are unverified.
- Correct /Users/tiku1/code/ruoyi-vue-pro/docs/education/student-core-learning-loop-prd.md:20-26,142-143,200-201 so it does not claim RuoYi/MySQL, ordered reversible SQL, or absent Flyway.
- Correct /Users/tiku1/code/ruoyi-vue-pro/docs/education/pilot-acceptance-runbook.md:29-37,63-68,78-85 to require actual PostgreSQL/Flyway execution evidence and forward correction rather than MySQL rollback.
- Align /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/controller/app/tenant/EducationTenantController.java:55-59 documentation, /Users/tiku1/code/ruoyi-vue-pro/yudao-module-education/src/main/java/cn/iocoder/yudao/module/education/config/EducationProperties.java:53-59, implementation, System website normalization, and tests on port policy.
- Document the provider-neutral malformed-option contract across JavaCatalogProvider, ScalarCatalogProvider, QuestionCatalogServiceImpl, and SessionResponseAssembler, including question-type-specific absent-option policy.
- Document that native catalog scope is a deliberate TenantUtils.executeIgnore/manual predicate boundary and add mapper-audit plus graph-integrity design notes.
- Label /Users/tiku1/code/ruoyi-vue-pro/sql/mysql/education/ and /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education/ according to actual operational status; do not present either as active delivery until the latter is promoted into Flyway.
- Correct the commercialization legacy path to /Users/tiku1/code/tiku-backend/apps/api/src/nest/commerce-reconciliation.module.ts.
- Create or maintain the ten required Phase 0 artifacts named by /Users/tiku1/code/ruoyi-vue-pro/docs/education/migration/GOAL.md:147-160, without claiming they exist unless verified.

View File

@@ -0,0 +1,174 @@
# Provider-neutral question content safety contract
> Status: proposed for EDU-P0-S1
>
> Evidence basis: current Education providers and services plus the legacy question import and learning rules. This document defines the implementation contract; it does not report tests as executed.
## Decision
Student-visible question content is validated by one provider-neutral contract before it is projected as a safe question or persisted/restored as a practice snapshot. Provider-specific parsing may reject malformed transport data earlier, but switching between Scalar and Java must not change whether the same logical question is considered safe.
Invalid or unsupported content fails closed. It must not be normalized into an apparently valid question with an empty option list.
## Question-type families
Type matching is case-insensitive after trimming. Persisted and returned canonical values remain an implementation concern; validation uses the following families.
### Option-backed
```text
choice
multi
multi_choice
judge
image
```
An option-backed question requires:
- at least two options;
- every option object to be non-null;
- a non-blank string `label`;
- a non-blank string `content`;
- labels unique after trimming;
- each `order`, when present, to be a finite number;
- no duplicate non-null order value;
- no answer-bearing field in the student-safe projection or snapshot.
Options are emitted in deterministic order: numeric `order` first, preserving source order only when order is absent. This slice does not infer or repair missing labels, contents, or order values.
### Optionless
```text
fill
text
terms
short_answer
composition
discuss
translation
case_analysis
brief_analysis
calculation
analysis_design
combination
solution
```
An optionless question may have null or empty options. If options are supplied, the content is inconsistent with its type and fails closed rather than silently discarding them.
This slice only establishes safe display and snapshot behavior. It does not add text-answer submission or scoring semantics. Existing answer-saving behavior must not pretend an optionless question is option-backed.
### Composite
```text
reading
```
A composite question requires sub-questions in the legacy model. The current target `CatalogQuestionDTO`, safe response, and practice snapshot do not carry a supported sub-question contract. Therefore a top-level `reading` question is unsupported by EDU-P0-S1 and fails closed rather than appearing as an optionless standalone question.
Supporting composite questions requires a later explicit model and API contract.
### Unknown or missing type
A null, blank, or unknown type fails closed. The target must not default unknown content to `choice`, because doing so can turn malformed content into a different assessment.
## Common option shape
The provider-neutral safe option shape is:
```text
label: non-blank string, unique after trim
content: non-blank string
order: optional finite number, unique when present
```
Provider DTOs may temporarily contain `isCorrect` for internal scoring or migration needs, but that field and all answer-bearing fields are discarded before creation of a Safe Question or the student-visible Question Snapshot JSON. A separately stored Protected Answer Key may retain correctness and explanation data for stable server-side scoring, but it is never part of the safe snapshot projection or a pre-submit response.
The safety validator must not require `isCorrect`, because safe restored snapshots intentionally do not store it. Correct-answer completeness is a content-authoring/scoring concern and is outside this read-only display contract.
## Failure semantics
### Fresh provider content
Any of the following makes the provider result unsafe:
- malformed options transport or JSON;
- null option element;
- wrong field types;
- fewer than two options for an option-backed question;
- options on an optionless question;
- blank or duplicate labels;
- blank content;
- non-finite or duplicate explicit order;
- unsupported composite content;
- null, blank, or unknown type.
A single-question request returns the existing controlled unsafe/malformed content error. Page and collection requests fail the response closed rather than silently changing totals or returning a partial assessment set.
### Persisted practice snapshot
A null/empty options value is valid only for an optionless type. Malformed JSON or a shape that violates the type family makes the snapshot unavailable. Session restoration must return a controlled error for the session/question; it must not convert parse failure into `[]`.
No automatic repair is performed during read or restore. Historical repair, quarantine, or backfill requires a separately reviewed migration or administrative process.
## Boundary placement
The contract is applied at two domain boundaries:
1. provider `CatalogQuestionDTO` → student-safe projection or practice-session creation;
2. persisted `PracticeQuestionDO` snapshot → practice-session response.
Both boundaries use the same type classification and option-shape rules. Scalar and Java adapters remain responsible only for transport/database parsing and mapping; they must not define divergent business validity.
## Compatibility notes
- Legacy source evidence identifies `choice`, `multi`, `judge`, and `image` as objective/option-backed and requires at least two options.
- Legacy source recognizes `reading` as a composite type with sub-questions.
- Legacy source recognizes the optionless types listed above and requires answer text for authoring; answer text is intentionally not exposed by the safe read contract.
- Current target tests sometimes construct `choice` questions with null, one, or empty options. Those fixtures describe previous permissive behavior and must be corrected where they cross a student-visible or snapshot boundary.
- `multi_choice` is retained as a target compatibility alias because current submit tests use it, while the legacy canonical type is `multi`.
## Required tests
### Shared contract
- each recognized type is classified correctly;
- type matching trims and ignores case;
- null, blank, and unknown types fail;
- option-backed types reject null, empty, and one-option lists;
- optionless types accept null/empty and reject supplied options;
- `reading` fails as unsupported composite content;
- null elements, wrong field types, blank labels, duplicate labels, blank contents, non-finite orders, and duplicate explicit orders fail;
- valid options preserve safe fields and never expose `isCorrect`.
### Provider paths
Run the same logical contract cases against Scalar and Java mappings. Transport-specific malformed data may fail earlier, but no provider may turn malformed input into an empty valid list.
### Service paths
- single-question browsing fails closed for unsafe content;
- page browsing fails the whole response for unsafe content;
- collection browsing fails the whole response for unsafe content;
- unpublished, hidden, inactive, disabled-source, and unavailable-source behavior remains fail-closed;
- successful output contains no answer-bearing fields.
### Practice paths
- session creation rejects unsafe source questions before persisting snapshots;
- a valid option-backed snapshot restores its options;
- a valid optionless snapshot restores an empty option list;
- malformed or type-inconsistent snapshots fail closed;
- snapshot JSON contains only `label`, `content`, and `order`;
- cross-tenant and ownership protections remain unchanged.
## Out of scope
- choosing Scalar or Java as the authoritative provider;
- adding composite/sub-question APIs;
- adding subjective answer submission or scoring;
- validating that correct answers exist or are unique;
- repairing historical snapshots;
- changing database schema or Flyway migrations;
- submit-idempotency redesign.

View File

@@ -0,0 +1,432 @@
# Education SaaS Migration Goal
> Status: active goal
>
> Source system: `/Users/tiku1/code/tiku-backend`
>
> Target system: `/Users/tiku1/code/ruoyi-vue-pro`
>
> Target branch at goal creation: `feature/education-core-loop`
>
> Created: 2026-07-29
## 1. Mission
Migrate the valuable business capabilities, data models, rules, state machines, authorization semantics, idempotency guarantees, and API contracts from `tiku-backend` into the RuoYi-Vue-Pro architecture.
This is a capability migration, not a file-by-file TypeScript-to-Java translation.
The resulting system must be a multi-tenant education SaaS backend that:
1. Places education-specific behavior in `yudao-module-education`.
2. Reuses RuoYi-Vue-Pro platform modules before adding new infrastructure.
3. Uses the framework's tenant, authentication, RBAC, logging, file, job, messaging, payment, and membership capabilities.
4. Uses PostgreSQL and module-owned Flyway migrations for all forward database changes.
5. Is independently buildable, testable, migratable, and progressively deployable by vertical slice.
6. Does not require the old NestJS service after migration, except for explicitly documented temporary adapters with an exit plan.
## 2. Non-negotiable architecture rules
### 2.1 Reuse before building
| Legacy capability | Target capability to evaluate first |
|---|---|
| Login, token, refresh, logout, verification | Member/System authentication |
| Student and administrator accounts | Member/System users; Education stores domain extensions only |
| Tenant lookup, status, and isolation | System Tenant and framework tenant support |
| Roles, menus, permissions, data permission | System RBAC |
| Payment, refund, channel, callback | Pay |
| Generic products and orders | Mall and Pay |
| Membership, level, entitlement, points | Member first; Education only orchestrates domain rules |
| Notifications, SMS, email | System/Infra messaging capabilities |
| Uploads and object storage | Infra File |
| Scheduled and background work | Infra Job or existing messaging facilities |
| Audit and operation logs | System/Infra logging |
| AI generation and recommendation | AI |
| CRM leads and customer follow-up | CRM |
| Questions, practice, exams, wrong questions, favorites, reports | Education |
Reuse means depending on public APIs, framework extension points, or events. Education must not depend on another module's internal `ServiceImpl`, Mapper, or DO and must not copy platform implementations.
A change outside Education is allowed only when the existing public capability cannot satisfy the need and the new interface is minimal, generic, backward-compatible, tested, and owned by the module that provides the capability.
### 2.2 Multi-tenancy and identity
- Tenant business DOs inherit `TenantBaseDO`.
- MyBatis-Plus tenant injection remains the normal isolation mechanism.
- Request bodies and query parameters are never trusted for `tenantId` or current `userId`.
- The current tenant and user come from framework security context.
- Student identity reuses Member; administrator identity reuses System.
- Education stores education profiles and relationships, not passwords, tokens, or generic accounts.
- Cross-tenant platform operations use existing tenant-ignore mechanisms with strict permissions; no custom bypass.
- Unique constraints include `tenant_id` whenever uniqueness is tenant-scoped.
### 2.3 Security
- Controllers never return DOs directly.
- Question responses strip answers, explanations, scoring rules, correctness flags, and administrative metadata before leaving the service boundary.
- Invisible questions, unavailable tenants, disabled features, and unavailable catalog sources fail closed.
- Logs do not contain tokens, passwords, verification codes, answers, or payment secrets.
- Student App, Tenant Admin, Platform Admin, public, and internal APIs have explicit and separate authorization models.
- Existing System RBAC and permission annotations are used for admin endpoints.
### 2.4 PostgreSQL and Flyway
All new or changed schema, indexes, constraints, required seed data, backfills, baselines, and Flyway configuration must use the project `flyway-postgresql` skill.
Required conventions include:
- `BIGINT GENERATED BY DEFAULT AS IDENTITY`
- `TIMESTAMP` and `CURRENT_TIMESTAMP`
- PostgreSQL `BOOLEAN`
- `ON CONFLICT ... DO NOTHING`
- `ON CONFLICT (...) DO UPDATE SET ... EXCLUDED.column`
- `COALESCE`, `TO_CHAR`, and `EXTRACT` where applicable
- module migration path: `<module>/src/main/resources/db/migration/<module>/`
Published migrations are immutable. Corrections use higher-version forward migrations. Historical `sql/mysql/education` files are not the delivery mechanism for new database changes. Application-layer tenant isolation must not be replaced by copied Supabase RLS.
Only an actual successful run against PostgreSQL may be reported as a successful database migration. Static SQL review, compilation, packaging, or resource copying must be described accurately as such.
## 3. Required Phase 0 investigation
Do not start broad feature implementation before completing this investigation.
### 3.1 Repository and rule inspection
Read and obey:
- target `CLAUDE.md`;
- target `yudao-module-education/README.md`;
- source `README.md`;
- applicable `AGENTS.md`, module READMEs, database documentation, and `.claude/skills/index.yaml`;
- actual runtime configuration and Git state.
Inspect both repositories' working trees and histories. Preserve all existing uncommitted work: no reset, destructive checkout, clean, or unrelated rewrite.
### 3.2 Historical commit review
Review these commits and determine whether their non-Education changes remain justified:
- `11e9cc6 feat(education): add module application shell`
- `0f846fd feat(education): resolve student tenant context`
Review at least:
- root `pom.xml`;
- `yudao-server/pom.xml`;
- `ServiceErrorCodeRange`;
- `TenantCommonApi`;
- `TenantRespDTO`;
- `TenantApiImpl`;
- historical `sql/mysql/education` artifacts;
- Education tenant-resolution logic.
Classify each design as retain, adjust, replace, remove by forward correction, or pending decision. Do not revert whole commits merely because one part is unsuitable.
### 3.3 Legacy capability inventory
Scan at least:
```text
apps/api/src/features
apps/api/src/nest
apps/worker/src
apps/asset-scanner/src
packages
supabase/migrations
supabase/seed*
docs
```
Cluster capabilities rather than mechanically mapping every endpoint. Cover Auth, Tenant, Profile, Learning, Catalog, Scoreline, Video, AI, Tenant Content, Tenant Admin, Platform Admin, Referral, Commerce, Worker, Asset Scanner, tables, indexes, constraints, RLS, functions, triggers, and seeds.
### 3.4 Target capability inventory
Inspect Education's current implementation and reusable capabilities in System, Member, Pay, Mall, Infra, AI, CRM, framework starters, and Server integration. Account for both committed and uncommitted implementation; do not rebuild existing slices.
## 4. Required migration artifacts
Maintain these artifacts under `docs/education/migration/` or a reviewed scratch equivalent while discovery is incomplete:
1. `current-state.md` — verified implementation and working-tree state.
2. `capability-matrix.md` — grouped legacy-to-target capability matrix.
3. `api-mapping.md` — legacy method/path and authorization to target contract.
4. `database-object-mapping.md` — table/RLS/function/trigger/storage disposition.
5. `module-reuse-map.md` — reusable public APIs and identified gaps.
6. `commit-review-11e9cc6.md`.
7. `commit-review-0f846fd.md`.
8. `decisions.md` — unresolved product or architecture decisions and ADR links.
9. `slice-roadmap.md` — vertical slices with blocking edges.
10. `first-slice.md` — first incomplete, bounded, low-risk delivery slice.
Each capability-matrix row must include:
```text
legacy capability
legacy code location
legacy database objects
business value
target module
existing capability to reuse
Education gap
whether another module must change
priority
risk
verification method
current status
evidence
open decision
```
Allowed status values:
- replaced by RuoYi-Vue-Pro;
- migrated;
- partially migrated;
- pending migration;
- explicitly retired;
- product decision required.
## 5. Delivery phases
### Phase 0 — inventory and architecture mapping
Complete the artifacts above, review the two historical commits, assess current uncommitted work, correct confirmed obsolete documentation, and select the first incomplete vertical slice.
### Phase 1 — tenant, identity, and permission baseline
Reuse System Tenant and Member/System Auth, unify Student/Tenant Admin/Platform Admin identity rules, verify cross-tenant protections, and decide whether the `TenantCommonApi` extension is a valid generic API.
### Phase 2 — student core learning loop
Verify and complete only genuine gaps in:
```text
catalog browsing
→ safe question browsing
→ create practice
→ save answer
→ restore practice
→ submit
→ report
→ wrong questions
→ favorites
```
The current branch may already implement much of this phase. Review before adding anything.
### Phase 3 — education content management
Question banks, questions, classifications, catalogs, publishing, imports/exports, resource associations, question videos, and content access control.
### Phase 4 — tenant education management
Classes, education student relationships, invitations, education roles, tenant education configuration, education points/badges, learning insight, and operations metrics. Generic users, roles, and tenants remain in Member/System.
### Phase 5 — commercialization
Products, orders, payments, refunds, subscriptions or entitlements, reconciliation, collection, commission, and referral relationships. Prefer composition of Mall, Pay, Member, and CRM. Education owns only education-domain bindings and orchestration.
### Phase 6 — asynchronous and operational capabilities
Imports/exports, content processing, billing work, notifications, resource scanning, audit, retries, and observability using Infra Job, messaging, File, and logging capabilities.
Each phase must be independently compilable, testable, deployable, and reversible at the application/configuration level.
## 6. Workflow operating model
This goal is executed as a decision-first, multi-session program:
```text
Phase 0 read-only multi-agent discovery
→ Wayfinder-style decision map
→ domain modeling and module-boundary design
→ migration specification
→ blocker-aware vertical-slice tickets
→ one fresh implementation context per ticket
→ TDD and PostgreSQL/Flyway when applicable
→ standards/spec review
→ security review
→ simplification
→ focused and integration verification
```
### 6.1 Multi-agent use
Use multi-agent workflows for broad read-only discovery, independent commit reviews, module capability mapping, adversarial verification, and completeness checks.
Do not allow multiple agents to edit the current dirty working tree concurrently. Implementation is serial by default. Isolated worktrees are permitted only for independent file sets with an explicit integration plan.
### 6.2 Ticket shape
Tickets are vertical behaviors, not technical layers. A ticket may include migration, DO, Mapper, Service, Controller, tests, and documentation needed to deliver one observable capability.
Good examples:
- a student can browse published catalog content in the current tenant;
- a student can create and restore a practice session;
- a student can idempotently save one answer;
- a student can idempotently submit and read an immutable report;
- a tenant administrator can publish a question.
Avoid tickets such as “create all DOs” or “create all Controllers.” Declare blocking edges explicitly and implement blockers first.
### 6.3 Skill selection
- `flyway-postgresql`: every database or Flyway change.
- `mattpocock-skills:domain-modeling`: ambiguous or overloaded education language.
- `mattpocock-skills:codebase-design`: module interfaces, provider/adapter seams, and public API boundaries.
- `mattpocock-skills:research`: external primary-source research, not local repository inventory.
- `mattpocock-skills:prototype`: throwaway executable exploration for a single unresolved design question.
- `mattpocock-skills:tdd`: red-green implementation of a concrete behavior.
- `mattpocock-skills:diagnosing-bugs`: hard defects after establishing a reliable failing command.
- `mattpocock-skills:code-review`: standards and specification review from a fixed Git point.
- `security-review`: tenant, identity, authorization, secret, answer, payment, and file boundaries.
- `simplify`: reuse and structural cleanup after correctness review.
- `run` and `webapp-testing`: real application and student-flow verification.
If a named planning skill is unavailable, preserve the same artifacts and gates using repository documents, issue files, and the workflow tool rather than skipping the phase.
## 7. Implementation rules
Follow the target layering:
```text
controller
service
dal/dataobject
dal/mysql
convert
enums
api
framework/integration
```
- Controllers perform protocol adaptation and validation.
- Services own transactions, state transitions, authorization-relevant domain checks, and idempotency semantics.
- Mappers own data access only.
- VO, DTO, and DO responsibilities remain distinct.
- Use project `CommonResult`, paging, validation, conversion, exception, error-code, Redis, lock, transaction, and audit facilities.
- External or legacy coexistence is hidden behind explicit Provider/Adapter boundaries.
- Do not introduce NestJS runtime dependencies or reproduce NestJS Guard/Decorator architecture.
Idempotency and consistency requirements:
- database uniqueness is the final idempotency guard;
- critical writes are transactional;
- do not rely only on check-then-insert;
- duplicate-request response semantics are explicit;
- concurrency is tested;
- external payment, notification, and file calls do not create long database transactions;
- at-least-once consumers define duplicate handling.
## 8. Verification gates
Every implementation slice runs the minimum sufficient focused tests plus at least:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
Behavior changes require focused tests in Education and every affected module. Database changes additionally require PostgreSQL syntax validation, module packaging, and confirmation that migration files appear under `target/classes/db/migration/`.
Test applicable negative and concurrent scenarios:
- cross-tenant access;
- unauthenticated access;
- unauthorized access;
- duplicate request;
- concurrent request;
- sensitive-field leakage;
- catalog source failure;
- disabled feature;
- historical-data compatibility.
## 9. Per-slice reporting contract
Before implementation, report:
1. legacy capability and evidence;
2. legacy files and database objects;
3. target module;
4. RuoYi-Vue-Pro capabilities reused;
5. why another module will or will not change;
6. database changes;
7. tests;
8. risks and rollback method.
After implementation, report:
1. changed files;
2. reused modules;
3. new Education domain capability;
4. reasons for every non-Education change;
5. replaced legacy code;
6. unmigrated capabilities;
7. commands actually run and results;
8. whether PostgreSQL migration was actually executed;
9. known risks and recommended next slice.
Do not state “complete” without verifiable files and command results.
## 10. Prohibitions
Do not:
- embed the old NestJS project;
- mechanically translate all TypeScript files or all 342 APIs;
- duplicate authentication, tenant, RBAC, payment, membership, notification, file, job, or audit platforms in Education;
- trust client `userId` or `tenantId`;
- expose answers or explanations;
- introduce MySQL dialect or new MySQL delivery scripts;
- bypass Flyway or modify published migrations;
- depend on internal implementations of other modules;
- weaken security for backward compatibility;
- overwrite unrelated uncommitted work;
- use destructive Git commands;
- claim tests or migrations succeeded without running them;
- begin a broad implementation before Phase 0 identifies the actual gaps.
## 11. Definition of done
The migration is complete only when:
1. every legacy capability has a reuse, migration, retirement, or decision status;
2. Education-specific behavior resides in Education;
3. platform capabilities are reused through appropriate boundaries;
4. every non-Education modification has a necessity statement and tests;
5. tenant isolation and sensitive-question-field controls are verified;
6. database changes use PostgreSQL Flyway;
7. core vertical flows have automated tests;
8. required compile and diff checks pass;
9. documentation matches the current PostgreSQL/Flyway architecture;
10. existing user changes have not been overwritten;
11. the target can run without the old service, or every temporary dependency has an owner and exit plan.
## 12. Immediate execution directive
Phase 0 inventory and the first safe-question slice have been executed. Continue through the blocker-aware tickets under [`docs/education/migration/issues/`](issues/README.md).
Current execution order:
1. `EDU-002` — restore the full Practice regression baseline;
2. `EDU-003` — decide tenant resolution and student-principal policy;
3. `EDU-004` — enforce the selected tenant/identity policy;
4. `EDU-005` — decide PostgreSQL/Flyway takeover;
5. `EDU-006` — deliver the approved Practice schema through module-owned Flyway;
6. `EDU-007` through `EDU-009` — verify and complete the student core loop;
7. later phases proceed only when their ticket blockers are complete.
Before every ticket:
1. read this Goal, the ticket, relevant decisions, and current Git status;
2. preserve all existing uncommitted work;
3. state the legacy capability, reuse boundary, database impact, tests, risk, and rollback;
4. use a fresh implementation context and work serially in the dirty tree;
5. use `flyway-postgresql` for any database or Flyway change;
6. finish with focused tests, `git diff --check`, and `mvn -pl yudao-server -am -DskipTests clean compile`;
7. report exact results and never claim PostgreSQL migration success without a real successful run.
Questions that can be answered from code, Git history, configuration, tests, or documentation must be investigated rather than asked. Ask only for genuine product decisions whose outcomes materially change implementation.

View File

@@ -0,0 +1,37 @@
# EDU-000 — Phase 0 inventory and architecture map
- **Status:** done
- **Type:** discovery
- **Phase:** 0
- **Blockers:** none
## Outcome
A verified static map of the source system, target capabilities, historical commits, database objects, reusable modules, unresolved decisions, and vertical-slice roadmap exists under `docs/education/migration/`.
## Delivered artifacts
- `00-current-state.md`
- `01-capability-matrix.md`
- `02-api-mapping.md`
- `03-database-object-mapping.md`
- `04-module-reuse-map.md`
- `05-commit-review-11e9cc6.md`
- `06-commit-review-0f846fd.md`
- `07-decisions.md`
- `08-slice-roadmap.md`
- `09-first-slice.md`
- `10-documentation-corrections.md`
## Evidence and caveats
- Investigation was read-only and multi-agent.
- No PostgreSQL migration was executed during discovery.
- The source baseline is provisionally `main` at `033701a`; no source `feature/education-core-loop` ref was found.
- The target worktree is dirty and must remain protected.
- Completing this ticket did not resolve the product and architecture decisions recorded in `07-decisions.md`.
## Verification
- Artifacts generated and inspected.
- `git diff --check -- docs/education/migration` passed at delivery time.

View File

@@ -0,0 +1,55 @@
# EDU-001 — Provider-neutral safe question content
- **Status:** done with recorded follow-up coverage
- **Type:** implementation
- **Phase:** 0 / core-loop prerequisite
- **Blockers:** EDU-000
## Student outcome
A student cannot receive or restore an apparently valid question when its type, visibility, or options are malformed. Student-visible question content and practice snapshot JSON do not expose answer-bearing fields.
## Scope delivered
- Shared question-type and option-shape contract.
- Option-backed, optionless, unsupported composite, and unknown type handling.
- Fail-closed single/page/collection safe projection.
- Fail-closed practice creation for disabled/unavailable provider, invisible question, and unsafe options.
- Strict practice snapshot restoration.
- Answer-free option snapshot JSON.
## Relevant files
- `docs/education/migration/11-question-content-safety-contract.md`
- `yudao-module-education/CONTEXT.md`
- `service/question/QuestionContentSafety.java`
- `service/question/QuestionCatalogServiceImpl.java`
- `service/practice/PracticeSessionServiceImpl.java`
- `service/practice/SessionResponseAssembler.java`
- corresponding focused tests
## Acceptance criteria
- [x] Invalid option-backed content fails closed.
- [x] Valid optionless content may have no options.
- [x] `reading` and unknown types fail closed until modeled.
- [x] Safe responses and option snapshot JSON exclude correctness and explanation fields.
- [x] Malformed persisted snapshots do not become empty valid options.
- [x] Disabled/unavailable providers and invisible questions cannot create sessions.
- [x] Focused safety tests pass.
- [x] Required compile and diff checks pass.
## Follow-up coverage
- Add a clean JavaCatalogProvider public-seam/PostgreSQL contract test when the native catalog test harness is established.
- Add explicit cross-tenant and `tenant_id=0` PUBLIC graph tests in the native catalog/graph-integrity slice.
- Do not test private provider parsing through reflection.
## Verification recorded
```text
Focused tests: 112 run, 0 failures, 0 errors
git diff --check: passed
yudao-server clean compile: BUILD SUCCESS
PostgreSQL migration: not applicable and not executed
```

View File

@@ -0,0 +1,104 @@
# EDU-002 — Restore the full Practice regression baseline
- **Status:** completed as test-context repair; PostgreSQL persistence coverage moved to EDU-016
- **Type:** test-enablement vertical slice
- **Phase:** 0 / Phase 2 prerequisite
- **Blockers:** EDU-001
## Outcome
The complete Practice test set starts reliably and distinguishes test-context failures from real behavior regressions across create, answer, restore, submit, report, wrong-question, and favorite flows.
## Why this is next
The direct EDU-001 tests pass, but broader Practice tests currently fail during Spring test-context creation because test configurations that import `PracticeSessionServiceImpl` do not consistently provide its current `ScoringService` dependency. Some tests also use name-based `@Resource` injection against Mapper proxies, producing type mismatches. Continuing core-loop work without this feedback loop would hide regressions.
## Existing code and data
- No legacy capability is being newly migrated.
- No database object changes are required.
- Existing Education test SQL and Mapper test infrastructure are reused.
## Scope
1. Inventory every test that imports, instantiates, or indirectly creates `PracticeSessionServiceImpl`.
2. For each test context, choose one explicit dependency strategy:
- import the real `ScoringServiceImpl` when scoring behavior is under test; or
- provide `@MockitoBean ScoringService` when the test is outside the scoring seam.
3. Replace ambiguous name-based Mapper injection only where it currently prevents the target tests from starting.
4. Run the complete focused Practice regression set.
5. Classify remaining failures as:
- test assembly defect;
- existing product defect;
- expected contract change from EDU-001;
- unrelated dirty-worktree issue.
6. Fix only test-assembly defects in this ticket. Create separate tickets for product defects.
## Reuse boundaries
- Reuse `BaseDbUnitTest`, existing Education test SQL, Spring `@Import`, and `@MockitoBean`.
- Do not create a parallel test framework.
- Do not modify System, Member, database schema, or production state machines.
- Do not weaken assertions merely to make tests green.
## Target test set
```text
PracticeSessionServiceImplTest
PracticeAnswerServiceImplTest
PracticeSubmitServiceImplTest
PracticeSubmitProjectionIntegrationTest
PracticeSessionControllerHttpTest
PracticeAnswerControllerHttpTest
PracticeSessionControllerSubmitHttpTest
WrongQuestionServiceImplTest
FavoriteServiceImplTest
```
## Acceptance criteria
- [ ] Every target class starts its Spring/JUnit context.
- [ ] No target class fails because `ScoringService` is missing.
- [ ] No target class fails from avoidable Mapper bean-name/type injection ambiguity.
- [ ] EDU-001 safe-content assertions remain green.
- [ ] Any actual behavior failure is documented with reproducible command and assigned a separate ticket.
- [ ] No production behavior or database schema is changed unless a failing regression proves it is necessary and the ticket is explicitly amended.
## Test command
```bash
mvn -pl yudao-module-education \
-Dtest='PracticeSessionServiceImplTest,PracticeAnswerServiceImplTest,PracticeSubmitServiceImplTest,PracticeSubmitProjectionIntegrationTest,PracticeSessionControllerHttpTest,PracticeAnswerControllerHttpTest,PracticeSessionControllerSubmitHttpTest,WrongQuestionServiceImplTest,FavoriteServiceImplTest' \
-Dsurefire.failIfNoSpecifiedTests=false \
test
```
Then:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
## Risk and rollback
- **Risk:** Low production risk; medium risk of exposing pre-existing behavior defects.
- **Rollback:** Revert only this ticket's test assembly changes. There is no database rollback.
## Completion result
Test-context assembly was repaired:
- `ScoringService` is now explicitly mocked in Practice contexts that are not testing scoring itself.
- `PracticeQuestionMapper` fields use type-based injection where name-based `@Resource` resolved the wrong MyBatis proxy.
- Controller tests and `PracticeSessionServiceImplTest` start and pass.
The expanded regression run then exposed a separate infrastructure limitation rather than a remaining Spring context defect: H2 cannot execute the production PostgreSQL `ON CONFLICT` statements, and the unified `education_idempotency` test table was missing. A temporary H2 table definition was added so table absence no longer masks the dialect issue, but PostgreSQL conflict semantics cannot be made truthful on H2. The required follow-up is [`EDU-016`](EDU-016-postgresql-persistence-tests.md).
## Verification result
- Controller Practice tests: 39 passed.
- `PracticeSessionServiceImplTest`: 24 passed before PostgreSQL-dialect persistence paths were included.
- Full targeted suite starts after dependency/injection repair, then fails on confirmed H2/PostgreSQL dialect mismatch and downstream assertions.
- No production behavior was changed by EDU-002.

View File

@@ -0,0 +1,160 @@
# EDU-003 — Decide tenant resolution and student-principal policy
- **Status:** done — corrected policy and EDU-004 test seams are implementation-ready
- **Type:** decision
- **Phase:** 1
- **Blockers:** EDU-000
## Decision outcome
Public tenant resolution accepts caller-supplied locator claims and is not an authentication boundary. Browser `Origin`/`Referer` evidence improves browser-context consistency but is forgeable by non-browser clients. The accepted contract therefore documents tenant-existence disclosure, uses one redacted unavailable response for unknown/disabled/expired tenants, requires abuse controls, and reserves authenticated/signed locators for deployments that require spoof resistance. Authenticated Education context is Member-only. This ticket records policy and tests-to-write only; it changes no production behavior.
## Domain language
- A **Tenant Locator Claim** is an unauthenticated pre-login value used to request tenant selection: either a browser-context hostname claim or an explicit Public Tenant Handle. It is not proof of caller identity or tenant authorization.
- **Browser-context evidence** is a normalized host derived from `Origin`, falling back to `Referer`. It can bind browser UX inputs consistently, but any HTTP client can forge it.
- A **Public Tenant Handle** is the current System tenant's unique `name` used as an exact public lookup key because the target has no separate stable tenant-code capability. It is not called a Tenant Code. It is case-sensitive, must match `^[A-Za-z0-9._-]{2,64}$`, and administrators must treat it as immutable after publication. A future mutable display label must be a separate field.
- A **Student Principal** is an authenticated identity whose `LoginUser.userType` is `UserTypeEnum.MEMBER`. A generic authenticated account is not necessarily a Student Principal.
- **Public Tenant Resolution** maps a Tenant Locator Claim to minimal login-routing fields. It intentionally discloses existence when a claim succeeds; it does not disclose whether a failed tenant is unknown, disabled, or expired.
## Evidence reviewed
- Legacy `apps/api/src/features/tenant/locator.ts` parses and compares `Origin`, `Referer`, request hosts, and a tenant code, but does not authenticate header provenance.
- Legacy `resolver.ts` compares the source `slug` with the explicit code, proving the source Tenant Code was distinct from its display name.
- The target has no System-owned stable tenant-code field or API. `system_tenant.name` is unique and mutable through administration; it is the only current exact generic lookup key.
- Current `EducationTenantController` accepts arbitrary public `hostname` or `tenantName`, preserves ports, returns status and Education-configured `loginMethods`, and exposes distinct unknown/disabled/expired errors.
- `EducationProperties.hostnameTenantMap` documents lowercase host-only keys without ports, while current implementation and tests preserve ports.
- `system_tenant.websites` is an exact string-list lookup and existing target tests demonstrate values containing a scheme. No normalization seam currently makes those values host-only.
- `EducationContextController` derives IDs from framework contexts but reads only the user ID and therefore does not reject an authenticated ADMIN principal.
- `TenantSecurityWebFilter` already fills a missing request tenant from the authenticated principal, rejects authenticated principal/request-tenant mismatch, requires a tenant for non-ignored URLs, and validates tenant availability.
- `TenantCommonApi` exposes generic System-owned tenant lookup methods; `TenantApiImpl` implements them, but the interface currently hides missing adapters behind `UnsupportedOperationException` defaults and has no focused owning-module contract test.
- No verified Member public interface advertises enabled login methods. `MemberConfigApi` currently exposes points configuration only.
## ADR: public tenant-resolution and student-principal policy
### Status
Accepted for EDU-004.
### Context and trade-off
The resolver is public and cannot authenticate `Origin`, `Referer`, or ordinary query/header values. Browser headers are useful for consistent browser routing, not identity. A successful lookup necessarily distinguishes an available tenant from a failed candidate when it returns routing fields. The contract can hide lifecycle state among failures, but cannot honestly promise general non-enumeration without an unguessable or signed locator.
The target also lacks the source system's distinct stable tenant code. Adding one would require a separately designed System-owned capability and likely data work. For the current slice, the existing unique System tenant `name` is explicitly exposed as a constrained Public Tenant Handle; it is no longer mislabeled as a Tenant Code.
### Decision
1. **Production browser-context binding, not trusted identity**
- Derive browser-context evidence from a valid HTTP(S) `Origin`; if absent, use a valid HTTP(S) `Referer`.
- A supplied `hostname` may only confirm that evidence. A mismatch is a public locator conflict.
- `Origin` and `Referer` are untrusted caller claims. Proxy preservation and forwarding-header controls do not make them authentic and are not cited as spoofing protection.
- A non-browser/headless caller can forge either header and probe hostnames. This accepted threat is handled through the public disclosure policy and abuse controls below.
- A deployment requiring spoof resistance must replace this public mode with an authenticated/signed locator or a host value supplied through a separately designed trusted-proxy boundary. That stronger mode is not implemented by EDU-004.
2. **Explicit headless handle and legacy query compatibility**
- A headless client may submit `tenantHandle`, defined above as the existing System tenant unique `name` under a constrained public contract.
- The legacy public `tenantName` query is unsupported and must be rejected, not silently aliased. EDU-004 adds a compatibility test for its rejection/removal.
- A browser domain claim and explicit `tenantHandle` may be supplied together only when both resolve to the same tenant; disagreement is a public locator conflict.
3. **Local-development activation seam**
- The sole authority is `yudao.education.tenant-resolution.local-development-enabled`.
- Its secure default is `false`; absence means production-safe behavior. Spring profile names and environment names do not implicitly enable it.
- Only developer workstations and automated tests may set it to `true`; shared, staging, and production deployments must keep it `false`.
- When enabled, a configured local request host (`localhost`, `*.localhost`, loopback IPv4, `0.0.0.0`, or `::1`) may resolve without a handle. If `tenantHandle` is also present, the explicit handle takes precedence.
- EDU-004 tests code-less local host, local host plus handle, and rejection of local/request-host fallback when the flag is absent or false.
4. **Hostname identity and normalization**
- Tenant hostname identity is host-only: trim whitespace, lowercase, remove one trailing dot, remove IPv6 brackets, and discard default or non-default ports.
- Accept valid DNS hosts, IPv4, and IPv6; reject credentials, paths, comma-separated/multi-value input, malformed authorities, and unsupported schemes.
- `localhost:48080` normalizes to `localhost`.
- Canonical `system_tenant.websites` entries used for this resolver are host-only values in the same normalized form. Entries containing a scheme, path, credentials, comma-separated values, or a port are legacy/non-canonical configuration and are not matched by Public Tenant Resolution.
- EDU-004 implements canonical exact lookup and focused tests; it does not silently normalize legacy stored candidates at read time. Tenant administrators must correct non-canonical website configuration before enabling domain resolution. If later inventory requires automated data correction, that becomes a separately scoped Flyway/data ticket using `flyway-postgresql`; EDU-004 must not claim such correction.
5. **Authenticated Education context**
- `/education/context` obtains the full `LoginUser`, rejects missing authentication, and rejects `userType != UserTypeEnum.MEMBER`.
- User and tenant IDs continue to come only from security and tenant contexts.
- EDU-004 preserves and does not duplicate or bypass `TenantSecurityWebFilter` mismatch and availability checks.
6. **Login-method metadata ownership**
- Login-method metadata belongs to Member authentication, not System tenant metadata and not Education.
- EDU-004 removes `loginMethods` from Education resolution and deprecates Education configuration/documentation that presents it as authoritative.
- If later routing proves it necessary, introduce only a minimal Member-owned public interface with focused Member tests; do not create tenant-specific auth configuration in Education.
7. **Exact external wire contract**
- The target framework represents business failures as HTTP `200 OK` with a `CommonResult` envelope. EDU-004 keeps that convention; tests assert both transport status and envelope.
- Malformed, missing, locally forbidden, or otherwise unsupported locator claim: HTTP `200`; `CommonResult.code = 1005001003`; `msg = "租户识别请求无效"`; `data = null`.
- Domain/handle or browser-evidence/requested-host conflict: HTTP `200`; `CommonResult.code = 1005001008` (new stable Education business code); `msg = "租户识别信息冲突"`; `data = null`.
- Unknown, disabled, or expired tenant: HTTP `200`; `CommonResult.code = 1005001004`; `msg = "当前租户不可用"`; `data = null`.
- Messages contain no rejected host/handle, lifecycle status, System exception text, or lookup detail. Logs may record a reason category and correlation metadata but must not log secrets or echo unsanitized header values.
- Unknown, disabled, and expired paths must have identical status, code, message, JSON field set, null-data shape, and no intentional timing distinction. System errors remain internal.
- Success is HTTP `200`, `code = 0`, `msg = ""`, and data contains only `tenantId` and `displayName`. `displayName` currently comes from the System tenant `name`; because that same field is the current Public Tenant Handle, an exact handle lookup necessarily returns the submitted handle as `displayName`. A future non-echoing mutable label requires a separate System-owned public display field. The response contains no separate handle field, raw status, websites, expiry, package, private configuration, internal lifecycle detail, or `loginMethods`.
8. **Disclosure and abuse threat model**
- The resolver is not generally non-enumerating: a valid Public Tenant Handle or domain claim yields success with tenant ID/display name, while an unavailable candidate yields the generic failure.
- The accepted guarantee is only unknown/disabled/expired indistinguishability.
- EDU-004 must attach the public resolver to the repository's existing public API rate-limiting/ingress mechanism where available, emit structured success/failure-category security metrics, and document alerting for sustained candidate probing. If no reusable limiter seam exists, EDU-004 records that operational blocker rather than inventing an Education-only limiter.
9. **System seam**
- Retain `TenantCommonApi` as the generic System-owned seam; do not add Education-specific locator, branding, redaction, or login-method concepts.
- Replace `UnsupportedOperationException` lookup defaults with required abstract methods and add focused `TenantApiImpl` contract tests.
- `EducationTenantController` remains the public adapter applying claim consistency, canonical website policy, availability coarsening, exact errors, and redaction.
### Rejected alternatives
- Treating `Origin` or `Referer` as authenticated tenant identity.
- Claiming forwarding-header ingress controls authenticate browser headers.
- Claiming general non-enumeration while successful lookup returns identifying fields.
- Silently aliasing System tenant `name` to the distinct Tenant Code domain term.
- Continuing the legacy `tenantName` public query.
- Silently normalizing scheme/path/port-bearing stored website values during lookup.
- Port-sensitive tenant identity.
- ADMIN accepted as Student Principal.
- Education-owned login methods or an Education-specific System API.
### Consequences
- EDU-004 intentionally changes current query, response, error, local-mode, website, and port behavior.
- Published Public Tenant Handles use the System tenant unique `name`; renaming one is a breaking login-routing change until a genuine stable System-owned code exists.
- Non-canonical website entries require configuration correction before domain resolution is enabled; no database change is authorized here.
- Public existence disclosure is accepted and must be monitored and throttled. Strong spoof resistance requires a future signed/authenticated locator design.
## EDU-004 exact test matrix
| Scenario | Exact expected behavior | Owning test seam |
|---|---|---|
| Valid production `Origin` | Resolve normalized domain claim; success HTTP 200/code 0 | Education controller HTTP test |
| Missing `Origin`, valid `Referer` | Resolve normalized Referer host; success HTTP 200/code 0 | Education controller HTTP test |
| Forged but syntactically valid browser header | Documented as accepted untrusted claim; no authenticity assertion | Education controller test name/documentation |
| Malformed `Origin` | HTTP 200/code 1005001003/generic message/null data; no fallback | Education controller HTTP test |
| Origin/requested-host mismatch | HTTP 200/code 1005001008/generic conflict/null data | Education controller HTTP test |
| Arbitrary production hostname without browser evidence | HTTP 200/code 1005001003 | Education controller HTTP test |
| Explicit `tenantHandle` | Exact case-sensitive constrained System-name lookup | Education controller HTTP test |
| Legacy `tenantName` query | Rejected/unsupported; HTTP 200/code 1005001003 | Education controller compatibility HTTP test |
| Unknown, disabled, expired | Identical HTTP 200/code 1005001004/message/JSON/null data | Education controller HTTP parameterized test |
| Host case/trailing dot/IPv4/IPv6/ports | Canonical host-only identity; all ports discarded | Education normalization/HTTP tests |
| Non-canonical stored website candidate | Not matched; generic unavailable response | System adapter fixture plus Education HTTP test |
| Local flag absent/false | Local/request-host fallback rejected with code 1005001003 | Education controller HTTP test |
| Local flag true, code-less local host | Configured local host may resolve | Education controller HTTP test |
| Local flag true, local host plus handle | Explicit handle takes precedence | Education controller HTTP test |
| Domain/handle agreement | Resolve one tenant | Education controller HTTP test |
| Domain/handle conflict | HTTP 200/code 1005001008 | Education controller HTTP test |
| Anonymous `/education/context` | Existing unauthorized contract | Education context HTTP test |
| Missing tenant or authenticated mismatch | Existing filter behavior remains active | Framework `TenantSecurityWebFilter` tests |
| ADMIN/MEMBER principal | ADMIN rejected; MEMBER accepted with context-derived IDs | Education context HTTP tests |
| Public field redaction | Success has only tenantId/displayName; failure has code/msg/data only | Education controller HTTP test |
| `TenantCommonApi` adapters | Required methods delegate/map; no unsupported defaults | System `TenantApiImpl` contract test |
| Abuse controls | Reused limiter/ingress attachment and structured category metric proven, or blocker recorded | Configuration/integration test where seam exists |
## Acceptance criteria
- [x] Browser headers are described as forgeable consistency evidence, not trusted identity.
- [x] Public existence disclosure and the narrower lifecycle-indistinguishability guarantee are explicit.
- [x] Public Tenant Handle is distinguished from the source Tenant Code and has exact mutability/case/format semantics.
- [x] Local behavior has one named, secure-default configuration seam and unambiguous precedence.
- [x] Canonical stored website compatibility policy is selected without claiming data migration.
- [x] Every error category has exact HTTP status, stable `CommonResult` code, message, data shape, and redaction rules.
- [x] EDU-004 has exact production/test seams and legacy `tenantName` compatibility coverage.
## Verification
Static design review only. Reviewed legacy `locator.ts`/`resolver.ts`, current Education controller/properties/error codes, `CommonResult` and global error handling, `TenantSecurityWebFilter`, `TenantCommonApi`/`TenantApiImpl`, System tenant name/website storage and tests, Member public interfaces, the completed EDU-016 ticket, tracker, and dirty working tree. No production implementation, build, database connection, Flyway execution, or migration was performed by EDU-003.

View File

@@ -0,0 +1,139 @@
# EDU-004 — Enforce tenant resolution and student identity boundaries
- **Status:** done — accepted tenant-locator and Member-principal policy implemented and verified; ingress/IP-only probing throttle remains an operational blocker
- **Type:** implementation
- **Phase:** 1
- **Blockers:** EDU-003
## Selected policy from EDU-003
- `Origin` then `Referer` supplies forgeable browser-context evidence, not trusted identity. It binds browser UX inputs but does not prevent non-browser spoofing.
- Headless clients use `tenantHandle`, the existing System tenant unique `name` under an exact constrained public contract; do not call it a Tenant Code.
- Reject/remove the legacy public `tenantName` query as a separate compatibility behavior.
- Production domain/handle agreement is checked; disagreement is a locator conflict.
- Host identity removes case, whitespace, trailing dot, IPv6 brackets, and all ports.
- Local fallback is controlled only by `yudao.education.tenant-resolution.local-development-enabled`, default `false`; profiles do not implicitly enable it.
- Canonical System website values for this resolver are normalized host-only strings. Scheme/path/port-bearing stored candidates are not silently normalized and require configuration correction or a separate future Flyway/data ticket.
- `/education/context` accepts only an authenticated `UserTypeEnum.MEMBER` Student Principal.
- Unknown/disabled/expired failures are identical, but successful resolution still discloses tenant existence. Reuse rate limiting/ingress controls and emit abuse-monitoring metrics.
- Login-method metadata is Member-owned; remove/deprecate Education `loginMethods` unless a minimal Member public interface is first proven necessary.
- Retain generic `TenantCommonApi`, make its lookup methods required, and add System-owned `TenantApiImpl` contract tests.
See [`EDU-003-tenant-resolution-decision.md`](EDU-003-tenant-resolution-decision.md) for rationale and exact threat model.
## Implementation result
Implemented the accepted public HTTP contracts for tenant resolution and Member-only Education context. The resolver treats `Origin`, `Referer`, `hostname`, and `tenantHandle` only as forgeable locator claims; successful responses expose only `tenantId` and `displayName`, and unknown/disabled/expired tenants share the exact unavailable envelope. Local fallback is controlled exclusively by the secure-default property, including browser-derived loopback hosts, and only configured local hosts use the Education mapping. Production domains always use the canonical System website lookup. `TenantCommonApi` lookup methods are now required and System-owned adapter tests prove exact delegation and DTO mapping. Existing `TenantSecurityWebFilter` production behavior was unchanged and is covered by focused filter-boundary regression tests.
No adequate reusable candidate-probing rate-limit attachment was found. The existing `ClientIpRateLimiterKeyResolver` includes attacker-controlled method arguments in its key, so attaching it would create per-candidate limits rather than an IP-only probing limit. No Education-only limiter was introduced; ingress/IP-only throttling and alerting remain an operational blocker. The Education module also has no direct generic Micrometer dependency seam, so structured resolver metrics remain part of the same operational blocker rather than adding an Education-only dependency or abstraction.
System currently has no separate public tenant display field: its unique `name` is both the Public Tenant Handle and the only public label available through `TenantCommonApi`. Therefore handle-based success returns that same value as `displayName`; tests now model this real exact-name adapter behavior. Suppressing that value requires a future generic System-owned display-field capability, not an Education workaround.
No database or Flyway change was made or executed.
## Student outcome
A student receives minimal login-routing data for an available tenant, while authenticated Education endpoints reject wrong tenants and non-Member principals. The public resolver does not claim that caller-supplied locator headers authenticate tenant identity.
## Scope
- browser-context domain claim selection from `Origin`, then `Referer`;
- requested-host confirmation and domain/handle conflict handling;
- explicit `tenantHandle` exact lookup and legacy `tenantName` rejection/removal;
- host-only normalization and canonical stored-website behavior;
- secure-default local-development configuration seam and precedence;
- Member/student principal enforcement for `/education/context`;
- exact public `CommonResult` contract and safe response fields;
- minimal generic `TenantCommonApi` adjustment with System-owned tests;
- reuse of public resolver rate limiting/ingress controls and structured abuse metrics where an existing seam is available.
## Exact external contract
All business outcomes use the framework convention of HTTP `200 OK` with `CommonResult`:
| Category | HTTP | `CommonResult.code` | `msg` | `data` |
|---|---:|---:|---|---|
| Malformed/missing/untrusted/locally forbidden locator | 200 | `1005001003` | `租户识别请求无效` | `null` |
| Requested-host/domain/handle conflict | 200 | `1005001008` | `租户识别信息冲突` | `null` |
| Unknown, disabled, or expired tenant | 200 | `1005001004` | `当前租户不可用` | `null` |
| Success | 200 | `0` | empty string | object containing only `tenantId`, `displayName`; current `displayName` is System tenant `name` and therefore equals a successful handle claim |
Failure messages and JSON shape never contain the rejected host/handle, lifecycle state, System exception detail, or lookup reason. Unknown, disabled, and expired paths must be byte-shape equivalent after normal serialization and have no intentional timing distinction.
## Reuse boundaries
- Reuse `TenantSecurityWebFilter`, `TenantContextHolder`, System Tenant public APIs, Member/System security context, `UserTypeEnum`, and an existing public rate-limit/ingress seam if present.
- Do not duplicate tenant tables, token logic, login methods, RBAC, or a generic rate-limiter in Education.
- Education must not depend on System internal Services, Mappers, or DOs.
- A non-Education change must be generic, minimal, backward-compatible, and tested in its owning module.
- If no reusable abuse-control seam exists, record the operational blocker; do not invent an Education-only infrastructure abstraction.
## Required TDD tests
### Education tenant controller HTTP tests
- valid production `Origin`; valid `Referer` fallback;
- syntactically valid forged header is treated only as an untrusted claim, with no authenticity assertion;
- malformed Origin and no attacker-selected fallback: exact HTTP/code/msg/data;
- Origin/requested-host mismatch: exact conflict contract;
- arbitrary production hostname without browser evidence: exact invalid contract;
- explicit case-sensitive `tenantHandle` with `^[A-Za-z0-9._-]{2,64}$` validation;
- legacy `tenantName` query rejected/removed independently;
- domain/handle agreement and conflict;
- unknown/disabled/expired exact identical wire shape;
- case, trailing dot, DNS, IPv4, bracketed IPv6, default and non-default port normalization;
- local flag absent/false rejects local/request-host fallback;
- local flag true permits code-less configured local host;
- local flag true plus handle gives the handle precedence;
- non-canonical stored website candidate does not match;
- success exposes only `tenantId` and System tenant `name` as `displayName`; for handle lookup this necessarily equals the submitted handle until System owns a separate public display field; no separate handle field, status, websites, expiry, package, private config, or `loginMethods`.
### Education context HTTP tests
- unauthenticated context rejected;
- ADMIN principal rejected;
- MEMBER principal accepted and IDs derived only from security/tenant contexts.
### System contract tests
- `TenantCommonApi` lookup methods are required, not optional unsupported defaults;
- `TenantApiImpl` exact name and canonical website lookups delegate and map DTOs;
- non-canonical website candidates are not silently normalized by the Public Tenant Resolution path.
### Framework/configuration tests
- existing missing-tenant and authenticated tenant/header mismatch filter behavior remains active;
- local-development flag defaults false and is not inferred from a Spring profile;
- reusable limiter/ingress attachment and structured success/failure-category metric are verified where an existing seam is identified.
## Acceptance criteria
- [x] Client `tenantId` and `userId` are never authoritative.
- [x] Browser headers are documented and implemented as forgeable context claims, not authentication.
- [x] Public existence disclosure is accepted; only unknown/disabled/expired status is indistinguishable.
- [x] `tenantHandle` is not mislabeled as Tenant Code; mutability, case, and format match EDU-003.
- [x] Legacy `tenantName` is rejected/removed and covered by a compatibility test.
- [x] Local fallback uses the named secure-default property and unambiguous precedence.
- [x] Canonical website compatibility policy is implemented without silent legacy normalization.
- [x] Exact HTTP/CommonResult code/message/data contracts are asserted.
- [x] Authenticated Student context enforces Member principal type.
- [x] Existing framework mismatch checks remain active and are not bypassed.
- [x] Public abuse-control gap is truthfully recorded; no inadequate or Education-only limiter was introduced.
- [x] Every non-Education modification has a necessity explanation and focused owning-module tests.
- [x] API documentation matches implementation and tests.
## Verification
Run focused Education, System, framework, and configuration tests as applicable, then:
```bash
git diff --check
mvn -pl yudao-server -am -DskipTests clean compile
```
No database change is in scope. If investigation proves automated website data correction is required, stop that part, create a separate blocked data/Flyway ticket, and invoke `flyway-postgresql` before any schema/data work.
## Risk and rollback
- **Risk:** High security, disclosure, and login-routing impact.
- **Rollback:** Application/configuration rollback to the previous resolver adapter; keep local mode disabled by default and do not weaken authenticated tenant checks. No destructive tenant-data operation.

View File

@@ -0,0 +1,160 @@
# EDU-005 — Decide PostgreSQL/Flyway takeover strategy
- **Status:** done — forward-only takeover and EDU-006 version plan accepted
- **Type:** decision
- **Phase:** 1 / Phase 2 prerequisite
- **Blockers:** EDU-002
## Decision outcome
Education schema ownership moves exclusively to module-owned PostgreSQL Flyway migrations under `yudao-module-education/src/main/resources/db/migration/education/`. The existing root `sql/postgresql/education/` files are classified as manual bootstrap/design history, not Flyway history. The MySQL files are obsolete archival artifacts and must not remain in operational instructions.
`V4010__initialize_education_flyway.sql` and `V4020__create_native_catalog.sql` are uncommitted working-tree resources in this checkout, and the inspected local disposable PostgreSQL database has no `flyway_schema_history` table and no Education tables. This proves neither migration ran in that database, but it does not prove they never ran in another environment. To avoid assigning a second meaning to a potentially distributed version, EDU-006 must preserve both files byte-for-byte and allocate new work from `V4030`.
No migration or production schema change was executed by EDU-005.
## Evidence and classification
| Artifact | Verified state | Classification | Forward disposition |
|---|---|---|---|
| `V4010__initialize_education_flyway.sql` | Untracked module resource; contains only `SELECT 1`; packaged in current `target/classes` | Potentially distributed Flyway history; execution unverified | Freeze byte-for-byte; do not repurpose |
| `V4020__create_native_catalog.sql` | Untracked module resource; owns 11 native catalog tables; differs materially from manual `008` | Potentially distributed Flyway history; execution unverified | Freeze byte-for-byte; do not replace with manual `008` |
| `sql/postgresql/education/002``005`, `007`, `009` | Untracked manual scripts implementing the current Practice/report/wrong/favorite/unified-idempotency model in stages | Manual bootstrap/design history, not active Flyway | Consolidate the approved final state into higher Flyway versions; do not copy the obsolete intermediate tables as fresh schema |
| `sql/postgresql/education/008` | Manual native-catalog design predating/diverging from V4020 | Superseded manual design | V4020 remains the only intended Flyway owner of native catalog schema |
| `sql/postgresql/education/000``001` | Placeholder schema plus menu/tenant seed scripts | Manual bootstrap/seed history | Do not create a separate `education` schema; evaluate the still-used `education:capability` menu seed separately |
| `sql/mysql/education/**` | Historical MySQL schema, seeds, and rollback scripts | Obsolete archive | Remove from runbooks; retain only if explicitly labeled non-operational archive |
| `src/test/resources/sql/postgresql/create_tables.sql` | EDU-016 disposable test bridge; 140 PostgreSQL persistence tests passed against it | Temporary test fixture | Replace with Flyway-driven test setup after EDU-006 proves equivalent schema |
| Docker init mounts for manual Education SQL | Dirty Docker configuration mounts `000``009` directly | Obsolete delivery path | Remove Education manual mounts after Flyway takeover; the server owns migration execution |
## Approved schema owner map
### V4020 owner — unchanged
V4020 exclusively owns the native catalog tables:
- `education_region`, `education_school`, `education_major`, `education_subject`, `education_category`;
- `education_content_entry`, `education_content_node`;
- `education_question_collection`, `education_question`, `education_practice_blueprint`;
- `education_question_collection_question`.
EDU-006 does not fold Practice schema into V4020 and does not silently substitute manual `008`.
### EDU-006 new owner — Practice final state
The new Practice migration owns the final runtime shape of:
- `education_practice_session` and `education_practice_question`;
- `education_practice_report` and `education_practice_report_detail`;
- `education_wrong_question` and `education_wrong_question_idempotency`;
- `education_favorite`;
- `education_idempotency`.
The approved fresh schema does **not** create `education_answer_idempotency` or `education_submit_idempotency`. Current production services use `IdempotencyStoreMapper` and `education_idempotency`; the old DOs/Mappers are unused compatibility residue and must be removed or explicitly isolated during EDU-006.
The migration must include all columns already required by runtime code and the proven EDU-016 bridge, including `client_sequence`, `last_client_sequence`, `review_fingerprint`, protected answer snapshots, report content snapshots, JSONB fields, tenant IDs, logical-delete fields, and observed conflict/query indexes.
## Version plan for EDU-006
Version numbers are project-wide. With V4020 frozen, the next allocated version is:
1. **V4030 — Practice core-loop final schema and adoption**
- Create the final tables, columns, constraints, and indexes listed above.
- Be adoption-aware for databases that contain manually bootstrapped Practice tables.
- Validate existing column types and required uniqueness before treating existing objects as compatible; fail closed on incompatible shapes rather than silently accepting them.
- Backfill the unified idempotency table from legacy answer/submit tables when those tables exist.
- Preserve old idempotency tables during the initial adoption migration; do not make data destruction a prerequisite for application rollout.
2. **V4040 — deterministic Education capability seed, only if still approved**
- Seed menu IDs `6800`/`6801` idempotently if the existing `EducationCapabilityController` remains an exposed administrator capability.
- Keep role assignment outside the migration.
- If the capability endpoint/menu is retired before EDU-006, omit this migration rather than seeding dead UI.
3. **Later forward cleanup migration**
- Drop legacy `education_answer_idempotency` and `education_submit_idempotency` only after every adopted environment has verified backfill counts, the application no longer contains active references, and a separately reviewed forward cleanup is approved.
If repository-wide migration inventory changes before implementation, EDU-006 must re-run the version scan and use the next unused project-wide version instead of blindly taking V4030/V4040.
## Existing-environment takeover classes
`baseline-on-migrate=true` with baseline `4009` is an adoption aid, not proof that Education objects match Flyway.
1. **Empty or platform-only database, no Education tables**
- Use baseline `4009` only when the non-empty platform schema requires adoption.
- Run V4010, V4020, then V4030+ normally.
2. **Manual Practice tables exist, native catalog tables do not**
- Baseline `4009` may be used.
- V4020 creates catalog objects; V4030 validates/adopts Practice objects and performs required backfills.
3. **Manual catalog tables equivalent to V4020 already exist, no Flyway history**
- Do not run V4020 into colliding tables.
- First compare the actual schema with the frozen V4020 contract.
- For a verified equivalent environment, use a one-time environment-specific baseline at `4020`, then run V4030+. This records adoption, not execution of V4020, and must be documented per environment.
- If the schema is not equivalent, correct it through an explicit higher-version adoption path; do not falsify history or edit V4020.
4. **Flyway history already contains V4010 and/or V4020**
- Compare script/checksum/success with the frozen resources.
- Never edit an executed script. Any mismatch or failed row blocks rollout until an environment-specific repair decision is reviewed.
5. **Unknown shared environment**
- No migration rollout is authorized until its Education tables and `flyway_schema_history` are inventoried.
After all existing environments carry an explicit baseline/history record, changing `baseline-on-migrate` to `false` is a separate reviewed configuration ticket. `validate-on-migrate=true`, `clean-disabled=true`, and `out-of-order=false` remain mandatory.
## Data compatibility and backfill rules
- Copy legacy answer and submit idempotency rows into `education_idempotency` with deterministic operation values and `ON CONFLICT ... DO NOTHING` only after verifying duplicate-key/request-hash compatibility.
- Preserve original IDs only if required by references; otherwise allow identity allocation and verify semantic row counts by operation.
- Existing Practice tables must be compared with the final DO/Mapper contract, not merely checked for table-name existence.
- JSON snapshot fields use PostgreSQL `JSONB` where current runtime/test behavior expects JSONB normalization.
- Tenant-scoped uniqueness includes `tenant_id` where the business key is tenant-local. `education_practice_report` uses `(tenant_id, session_id)` as the final unique report key.
- Do not copy Supabase RLS. Framework tenant isolation remains primary; database constraints enforce integrity and idempotency.
- No destructive rollback SQL is delivered. Recovery is application rollback plus a higher-version forward correction.
## Documentation and operational corrections
EDU-006 must update operational documentation in the same slice:
- replace `yudao-module-education/README.md` MySQL apply/rollback commands with Flyway/PostgreSQL forward-only instructions;
- remove the manual Education SQL mounts from `script/docker/docker-compose.yml` so a fresh Docker database is not initialized outside Flyway before server startup;
- label `sql/postgresql/education/` and `sql/mysql/education/` as non-operational history or move them to an explicitly archival location without rewriting history;
- replace the EDU-016 temporary PostgreSQL schema bridge with Flyway-driven setup after equivalence is proven;
- state per environment whether Flyway was actually run, validated, or only packaged/compiled.
## Required EDU-006 verification
Static and build gates:
```bash
git diff --check
mvn -pl yudao-module-education -am -DskipTests clean package
find yudao-module-education/target/classes/db/migration/education -type f -print
mvn -pl yudao-server -am -DskipTests clean compile
```
Real disposable PostgreSQL gate:
1. initialize a disposable platform database or approved baseline fixture;
2. run Flyway migrate using the server's exact migration locations and PostgreSQL driver;
3. run Flyway validate;
4. inspect `flyway_schema_history` with version, script, checksum, and success;
5. inspect all approved tables, columns, constraints, indexes, and backfill counts;
6. run the EDU-016 PostgreSQL persistence suite against the migrated schema;
7. test at least the empty/platform-only path and one representative manually bootstrapped adoption path.
Only these real successful executions may be reported as migration success.
## Acceptance criteria
- [x] No published or potentially distributed migration is authorized for editing.
- [x] Every required table/index/constraint/seed has one intended Flyway owner and version range.
- [x] Manual SQL is not silently treated as executed history.
- [x] The plan includes migration packaging and real PostgreSQL execution evidence requirements.
- [x] Documentation correction scope is explicit.
## Verification performed by EDU-005
- Read project Flyway rules, local/dev Flyway configuration, server dependencies, all active migration locations, manual PostgreSQL/MySQL artifacts, core-loop DOs/Mappers, PostgreSQL test bridge, Docker initialization, Git history, and dirty-tree state.
- Inspected the reachable disposable `postgresdb` container. Target identity was database `postgres`, user `postgres`, schema `public`; it contained no `flyway_schema_history` relation and no Education tables. Container startup logs state that `/docker-entrypoint-initdb.d/*` was ignored because the volume was already initialized.
- Confirmed V4010/V4020 are currently packaged under `target/classes/db/migration/education/` from a prior build.
- Did not modify migration SQL, application code, server configuration, Docker configuration, or any database object.
- Did not run Flyway migrate/validate and does not claim a successful database migration.
## Risk and rollback
- **Risk:** High. Existing manually initialized databases may have partially overlapping or divergent table shapes, and a false baseline can hide incompatibility.
- **Rollback:** No rollback is required for this decision-only ticket. EDU-006 uses forward migrations, preserves legacy idempotency tables during first adoption, and supports application rollback without `flyway clean` or destructive down scripts.

View File

@@ -0,0 +1,83 @@
# EDU-006 — Deliver Practice schema through module-owned Flyway
- **Status:** done — V4030 delivered and verified on an isolated disposable PostgreSQL test database
- **Type:** database implementation
- **Phase:** 2 prerequisite
- **Blockers:** EDU-005
## Implementation result
`V4030__create_and_adopt_practice_schema.sql` now owns the final Practice session/question, report/detail, wrong-question/idempotency, favorite, and unified idempotency schema. Fresh databases do not create legacy answer/submit idempotency tables. Compatible manually initialized Practice tables receive missing final columns and JSONB alignment; legacy answer/submit idempotency rows are backfilled into `education_idempotency` while the source tables remain untouched. A conflicting request hash fails the migration transactionally.
The EDU-016 test seam now runs the real Flyway chain in a random disposable PostgreSQL schema. Its temporary `create_tables.sql` bridge was removed. The established 140 persistence tests and five migration-contract scenarios pass together.
Manual Education SQL mounts were removed from Docker Compose, and the active Education README and Pilot runbook now describe PostgreSQL/Flyway forward-only delivery. V4010/V4020 remained byte-for-byte unchanged. V4040 was not created because the capability menu still lacks a complete admin UI/product decision; no empty placeholder version was introduced.
No shared or production database was migrated. Successful migration evidence applies only to the isolated no-volume PostgreSQL container and random schemas used by the tests.
## Scope
Implement only the objects approved in EDU-005, potentially covering:
- practice sessions and question snapshots;
- answer and submit idempotency;
- reports and report details;
- wrong questions and favorites;
- tenant-scoped unique constraints;
- indexes required by verified query paths;
- necessary menu/permission seed data;
- compatible backfills for existing development data.
Exact objects and versions are determined by EDU-005 and `flyway-postgresql`.
## Architecture rules
- Use PostgreSQL dialect only.
- Tenant-scoped uniqueness includes `tenant_id` where required.
- Database uniqueness is the final idempotency guard.
- Use `ON CONFLICT` where approved by the design.
- Do not copy Supabase auth/RLS as the application isolation model.
- Do not add destructive rollback migrations.
## Acceptance criteria
- [x] New migrations use versions allocated by `flyway-postgresql`.
- [x] V4010/V4020 remain unchanged as potentially distributed history.
- [x] Migrations are packaged under `target/classes/db/migration/education/`.
- [x] Annotated SQL and Mapper behavior match PostgreSQL constraints.
- [x] Focused repository/integration tests cover uniqueness and tenant scope.
- [x] Real PostgreSQL Flyway migrate/validate succeeds in an isolated disposable test environment.
- [x] Operational docs no longer instruct users to apply MySQL or manual Education SQL for these objects.
## Verification
At minimum:
```bash
git diff --check
mvn -pl yudao-module-education -am -DskipTests clean package
find yudao-module-education/target/classes/db/migration/education -type f
mvn -pl yudao-server -am -DskipTests clean compile
```
Also run the PostgreSQL commands prescribed by `flyway-postgresql` when an authorized database is available.
## Verification performed
Against an isolated PostgreSQL container bound only to `127.0.0.1`, the focused Flyway suite exercised fresh migration, baseline-4009 adoption, compatible session/question plus legacy-idempotency adoption, conflict with existing unified history, and conflicting duplicate legacy keys. Together with the existing persistence suite: 145 tests passed, 0 failures, 0 errors, 0 skipped. Every test schema was dropped and the no-volume container was stopped.
Also completed:
```text
mvn -pl yudao-module-education -am -DskipTests clean package — BUILD SUCCESS
V4010, V4020, V4030 present under target/classes/db/migration/education/
mvn -pl yudao-server -am -DskipTests clean compile — BUILD SUCCESS
git diff --check — passed
```
V4010 and V4020 retained their pre-ticket SHA-256 values. No shared or production PostgreSQL database was changed.
## Risk and rollback
- **Risk:** High data compatibility and deployment-order risk.
- **Rollback:** Forward correction migration plus application rollback. Never use `clean` or destructive rollback in shared environments.

View File

@@ -0,0 +1,57 @@
# EDU-007 — Verify tenant-scoped practice creation and restoration
- **Status:** done — bounded create/restore contract verified against V4030 PostgreSQL
- **Type:** implementation/verification
- **Phase:** 2
- **Blockers:** EDU-004, EDU-006
## Implementation result
The existing create/restore aggregate was retained and re-verified rather than rebuilt. Practice endpoints now require a `UserTypeEnum.MEMBER` Student Principal and continue deriving user/tenant only from the authenticated principal. Concurrent creation now uses the existing PostgreSQL `ON CONFLICT DO NOTHING` mapper seam, avoiding a query inside an aborted duplicate-key transaction. Real PostgreSQL tests prove identical concurrent requests return one session and conflicting fingerprints produce one winner plus one idempotency mismatch.
Focused tests also prove provider failure leaves no partial state and restore uses the persisted Question Snapshot after source content changes. The service create/restore suite now runs on the V4030 Flyway-owned PostgreSQL schema.
PUBLIC catalog read predicates and provider-neutral safety remain unchanged. Cross-scope PUBLIC graph-integrity semantics remain an explicit later architecture decision; EDU-007 does not claim or invent those constraints. Legacy entitlement/quota, timed practice, rich blueprint/random/review modes, and discovery of multiple active sessions are outside this bounded ticket.
## Scope
- Verify or complete session creation idempotency.
- Verify session ownership and tenant isolation.
- Restore immutable safe question snapshots.
- Preserve provider-neutral question safety from EDU-001.
- Verify PUBLIC catalog reads continue using the existing explicit scope predicates; tenant-consistent PUBLIC graph constraints remain blocked on the graph decision.
- Remove no existing core-loop behavior unless a regression proves it invalid.
## Acceptance criteria
- [x] Current user and tenant derive from a Member security principal at the controller boundary.
- [x] Duplicate client session ID with identical fingerprint returns the existing session.
- [x] Conflicting fingerprint, user, or tenant does not expose the existing session.
- [x] Underfilled, invisible, malformed, disabled, and unavailable content fails closed.
- [x] Restored content remains stable after source content changes.
- [x] Cross-tenant and wrong-user access is denied.
- [x] Provider-neutral question safety and existing PUBLIC read predicates are preserved; graph-integrity enforcement remains blocked on the recorded architecture decision.
- [x] PostgreSQL uniqueness and transaction behavior are tested against the EDU-006 schema.
## Verification
Focused create/restore service, Mapper, controller, and PostgreSQL tests; then required diff/compile gates.
## Verification performed
Against an isolated PostgreSQL database using the V4010/V4020/V4030 Flyway chain:
```text
PracticeSessionControllerHttpTest: 15 passed
PracticeSessionServiceImplTest: 25 passed
PracticeSessionServicePostgreSqlIntegrationTest: 2 passed
Total: 42 passed
Failures/errors/skipped: 0
```
The PostgreSQL concurrency tests use bounded latches and prove both identical and conflicting fingerprint races. No database migration was added or changed by EDU-007.
## Risk and rollback
- **Risk:** Medium session-ownership and compatibility risk.
- **Rollback:** Application rollback; retain forward-compatible schema.

View File

@@ -0,0 +1,53 @@
# EDU-008 — Verify idempotent answer saving
- **Status:** done — answer idempotency and rollback contract verified on PostgreSQL
- **Type:** implementation/verification
- **Phase:** 2
- **Blockers:** EDU-007
## Implementation result
The existing unified PostgreSQL idempotency claim remains the final guard. Matching keys replay only a complete, valid stored response; null, blank, malformed, or structurally incomplete replay data now fails closed without re-executing the answer mutation. Completion of a newly claimed response must update exactly one idempotency row before session/question state changes.
Answer saving remains limited to Option-backed Questions. Optionless/free-text behavior is explicitly rejected until a separate subjective-answer contract is designed. The answer HTTP seam now uses the EDU-007 Member principal and TenantContextHolder boundary and rejects ADMIN principals.
PostgreSQL tests prove same-key/same-payload replay, same-key/different-payload conflict including concurrent requests, different-key CAS serialization, stale version/sequence rejection, user/tenant/state/question ownership checks, full rollback of answer/session/claim state, refresh recovery, and no answer-key/explanation leakage. No migration was added or executed by EDU-008.
## Scope
- Same key and same canonical payload replays the original result.
- Same key and different payload returns a conflict.
- Database uniqueness is the final idempotency guard.
- Session version and client sequence prevent stale updates.
- Selected options are validated against the safe snapshot contract.
- Optionless answer behavior remains blocked until explicitly designed; do not pretend it is option-backed.
## Acceptance criteria
- [x] Same-payload duplicate semantics are explicit and tested.
- [x] Conflicting payload is rejected.
- [x] Concurrent same-key and different-key behavior is tested on PostgreSQL.
- [x] Stale session version and stale per-session command sequence are rejected.
- [x] Wrong user, tenant, session state, or question membership is rejected.
- [x] Responses and restored sessions contain no answer key or explanation.
- [x] Failure does not partially update answer, sequence, session version, or the idempotency claim.
## Verification
Focused answer service/controller/Mapper tests, PostgreSQL concurrency tests, EDU-001 regressions, and required diff/compile gates.
## Verification performed
```text
PracticeAnswerControllerHttpTest: 16 passed
PracticeAnswerServiceImplTest: 37 passed on PostgreSQL/V4030
Total focused: 53 passed
Failures/errors/skipped: 0
```
The service suite includes concurrent same-key identical and conflicting payloads plus exact different-key winner/loser assertions. Incomplete replay rows fail closed, and rollback assertions cover session version, session sequence, question state, and claim removal.
## Risk and rollback
- **Risk:** Medium-to-high concurrency and offline-retry risk.
- **Rollback:** Application rollback with schema retained; forward migration for any constraint correction.

View File

@@ -0,0 +1,69 @@
# EDU-009 — Atomic submit, immutable report, wrong questions, and favorites
- **Status:** done
- **Type:** implementation
- **Phase:** 2
- **Blockers:** EDU-008 (done)
## Student outcome
A student can retry submission safely, receive exactly one immutable report, and see consistent wrong-question and favorite projections.
## Core defect to resolve
The current submit path has evidence of check-then-insert idempotency. This ticket must define and implement an atomic initial claim with crash recovery before treating submission as complete.
## Scope
- Atomic submit-key reservation using PostgreSQL uniqueness/`ON CONFLICT` or the approved project mechanism.
- Explicit processing/completed/failed or equivalent recovery semantics.
- Same-key replay and conflicting-payload behavior.
- Single state transition from active session to submitted.
- Immutable scoring/report snapshot.
- Duplicate-safe wrong-question projection.
- Favorite behavior remains independent and tenant/user scoped.
- No external calls inside a long database transaction.
## Acceptance criteria
- [x] Concurrent same-key same-payload requests converge on one report.
- [x] Same key with different payload is rejected.
- [x] Different keys racing on one session produce at most one committed submit.
- [x] A crash after claim has a documented retry/recovery result.
- [x] Report content remains stable after question mutation.
- [x] Wrong-question projection is idempotent.
- [x] Report and history access enforce user and tenant ownership.
- [x] Pre-submit responses never expose protected answer data; post-submit response follows the approved report contract.
## Recovery contract
- The submit key is serialized with a PostgreSQL transaction-level advisory lock, then reserved with
`INSERT ... ON CONFLICT DO NOTHING` before session/report writes.
- The claim uses `PROCESSING` with a unique token and lease timestamp; report, report detail,
wrong-question projection, session CAS, and token-checked claim completion run in the same transaction.
- A normal processing failure rolls the full transaction back, including a newly inserted claim. If a previously
committed/manual `PROCESSING` claim exists (for example after legacy partial persistence), a retry can take over
the matching claim after the 120-second lease expires. A mismatched payload can never take over the claim.
- A completed claim stores `report_id` and the complete immutable response as `COMPLETED`; same-key retries
replay only a structurally complete matching response. Malformed or incomplete committed rows fail closed.
- A different key that loses the session race is completed against the immutable winner report, so retries of
either accepted key remain stable.
No external provider call occurs in the submit transaction: scoring uses the persisted session question snapshot.
V4040 adds the claim token/timestamp columns and normalizes migrated successful `SUBMIT_SESSION` rows from
legacy `ACCEPTED` to `COMPLETED` without changing V4030 release history.
## Verification
PostgreSQL concurrency tests are mandatory, along with service/controller/projection tests and required diff/compile gates.
Focused PostgreSQL verification on 2026-07-30:
- `PracticeSubmitServiceImplTest`: 38 passed.
- Submit/report controller, projection, wrong-question, and favorite suites: 129 passed total.
- Failures, errors, skipped: 0.
## Risk and rollback
- **Risk:** High state-machine and data-consistency risk.
- **Rollback:** Disable practice writes or roll back application version; repair through forward migration only.

View File

@@ -0,0 +1,33 @@
# EDU-010 — Tenant content publication and graph integrity
- **Status:** blocked
- **Type:** implementation program
- **Phase:** 3
- **Blockers:** EDU-004, EDU-009, provider-authority decision, PUBLIC graph-semantics decision
## Tenant-admin outcome
Authorized tenant administrators can author, classify, publish, archive, and retire education content without creating cross-tenant or invalid PUBLIC/tenant relationships, and student reads remain consistent with publication state.
## Scope
- Question banks, questions, versions, classifications, catalogs, collections, blueprints, and bindings.
- Draft/published/archived lifecycle.
- System RBAC/DataPermission enforcement.
- Tenant-consistent graph constraints or equivalent transactional enforcement.
- Provider consistency between authoring source and student reads.
- Safe projections preserved from EDU-001.
## Acceptance criteria
- [ ] Admin permission and data-scope matrix is explicit.
- [ ] Cross-tenant graph relationships cannot be persisted.
- [ ] PUBLIC and tenant-owned reference rules are enforced and tested.
- [ ] Unpublished/archived content is never student-visible.
- [ ] Publication is transactional and auditable.
- [ ] Database changes use `flyway-postgresql` and forward migrations.
## Risk and rollback
- **Risk:** High content-integrity and authorization risk.
- **Rollback:** Disable authoring/publishing and roll back application; preserve data and correct forward.

View File

@@ -0,0 +1,32 @@
# EDU-011 — Content import, export, assets, and scanning
- **Status:** blocked
- **Type:** implementation program
- **Phase:** 3 / 6
- **Blockers:** EDU-010, Infra File contract, scanner ownership decision, durable job claim decision
## Tenant-admin outcome
Administrators can import and export education content through durable, duplicate-safe jobs, with secure files, malware scanning, tenant propagation, audit, retries, and partial-failure reporting.
## Reuse
- Education owns import/export business state and content validation.
- Infra owns File, Job/MQ, locks, logging, and audit primitives.
- Scanner integration sits behind a clear adapter; Education does not implement generic storage or scheduling.
## Acceptance criteria
- [ ] Preview and execute are distinct states.
- [ ] Jobs use atomic claim/lease/heartbeat/recovery semantics.
- [ ] At-least-once retries are duplicate-safe.
- [ ] File type, size, object key, access, and retention are enforced.
- [ ] Scanning fails closed.
- [ ] Tenant context propagates into asynchronous handlers.
- [ ] Exports redact answers and private fields according to authorization.
- [ ] Partial failures and dead letters are visible and auditable.
## Risk and rollback
- **Risk:** High operational and file-security risk.
- **Rollback:** Disable job handlers and preserve job/business state for forward recovery.

View File

@@ -0,0 +1,33 @@
# EDU-012 — Classes and education relationships
- **Status:** blocked
- **Type:** implementation program
- **Phase:** 4
- **Blockers:** EDU-004, education relationship model decision, Member relationship contract, System data-scope policy
## Tenant-admin outcome
Tenant administrators manage classes, student education relationships, invitations, and supervision within explicit tenant and row-level scopes while Member/System remain the owners of generic users and roles.
## Scope
- Education class entity and membership relationships.
- Student/teacher/class domain roles without duplicating System RBAC.
- Invitations and duplicate-safe acceptance.
- Education profile extensions.
- Supervision relationships and data scopes if retained.
- Audit and operation logging.
## Acceptance criteria
- [ ] Generic account, password, token, tenant, and role tables are not duplicated.
- [ ] Student/teacher/class permission matrix is documented and tested.
- [ ] Cross-class and cross-tenant access is denied.
- [ ] Invitation acceptance is idempotent and auditable.
- [ ] Platform-admin tenant-ignore operations are explicit and permission guarded.
- [ ] Database changes use `flyway-postgresql`.
## Risk and rollback
- **Risk:** High authorization and relationship-integrity risk.
- **Rollback:** Disable management endpoints and correct relationships through audited forward operations.

View File

@@ -0,0 +1,33 @@
# EDU-013 — Education commercialization binding
- **Status:** blocked
- **Type:** decision and implementation program
- **Phase:** 5
- **Blockers:** product/entitlement model, Mall/Pay API assessment, Member entitlement decision, EDU-004
## Outcome
Education products and access rights are connected to Mall, Pay, Member, and CRM without creating a parallel product, order, payment, refund, membership, or financial ledger in Education.
## Education ownership
Education may own only domain bindings and fulfillment orchestration, such as:
- education product to course/exam/content binding;
- entitlement scope and education-resource association;
- duplicate-safe fulfillment event state where no platform facility exists.
## Acceptance criteria
- [ ] Mall/Pay/Member/CRM public contracts are mapped before implementation.
- [ ] Payment callbacks and refunds remain in Pay.
- [ ] Generic products/orders remain in Mall where applicable.
- [ ] Entitlement issuance, revocation, expiry, and refund effects are explicit and idempotent.
- [ ] Paid/private practice remains inaccessible until entitlement checks are complete.
- [ ] Reconciliation and commission/referral ownership is explicit.
- [ ] Financial and authorization tests cover duplicate callbacks and cross-tenant access.
## Risk and rollback
- **Risk:** Very high financial and access-control risk.
- **Rollback:** Disable fulfillment handlers and paid access; preserve financial ledgers in their owning modules.

View File

@@ -0,0 +1,37 @@
# EDU-014 — Extended student and secondary learning waves
- **Status:** blocked
- **Type:** decision map followed by implementation tickets
- **Phase:** 5
- **Blockers:** explicit product scope, EDU-004, entitlement model, AI/File/Member/System/Infra contract assessment
## Outcome
Every legacy Auth/Profile/extended Learning, scoreline, vocabulary, video, AI, notification, badge, feedback, and exam-date capability receives a traceable conclusion: replaced, migrated, retired, deferred, or product decision required.
## Required decomposition
Do not implement this as one large ticket. Create one child ticket per selected capability family after ownership is decided. At minimum assess:
- Auth compatibility and phone/OAuth binding;
- profile and education profile extensions;
- vocabulary learning/review;
- scoreline and admissions content;
- video entitlement and progress;
- recommendation and AI generation;
- notifications and reminders;
- points, badges, check-ins, feedback, exam countdowns;
- learning analytics, leaderboard, trends, and reports.
## Acceptance criteria
- [ ] Each family has target owner, reusable public capability, data disposition, API conclusion, priority, and tests.
- [ ] Member/System/Infra/AI capabilities are reused rather than copied.
- [ ] Sensitive reports and exports are redacted.
- [ ] Media and AI access follows entitlement and tenant rules.
- [ ] Retired capabilities have compatibility and data-retention conclusions.
## Risk and rollback
- **Risk:** Medium-to-high scope and entitlement risk.
- **Rollback:** Per child ticket; this parent is a planning gate.

View File

@@ -0,0 +1,42 @@
# EDU-015 — Operational independence and legacy exit
- **Status:** blocked
- **Type:** integration and deployment program
- **Phase:** 6
- **Blockers:** EDU-011, EDU-013, EDU-014 child decisions, all temporary-adapter owners and exit plans
## Outcome
The target backend runs its selected education capabilities without depending on the old NestJS API, worker, Supabase auth/storage, or asset-scanner deployment, except for explicitly time-bounded adapters with owners and exit dates.
## Scope
- Replace selected Worker jobs with Infra Job/MQ and owning-domain handlers.
- Complete retries, dead letters, audit, notifications, and observability.
- Complete file/scanner deployment or approved alternative.
- Remove or disable temporary Scalar/legacy adapters according to provider strategy.
- Reconcile migrated data and operational runbooks.
- Prove deployment, startup, Flyway, and core user flows.
## Acceptance criteria
- [ ] Every temporary legacy dependency has an owner, telemetry, failure policy, and exit date.
- [ ] At-least-once consumers are duplicate-safe.
- [ ] Job retries/dead letters and scanner health are observable.
- [ ] PostgreSQL migrations are actually executed and validated in an authorized environment.
- [ ] Student and selected admin E2E flows pass against the target only.
- [ ] Runbooks contain no active MySQL/manual-SQL or obsolete NestJS startup requirement.
- [ ] Rollback and incident procedures are documented.
## Verification
- Application startup and health.
- Flyway history and migration execution.
- Worker/job deployment smoke tests.
- Playwright student harness and selected admin flows.
- Logs, metrics, traces, retry/dead-letter, and scanner health checks.
## Risk and rollback
- **Risk:** High deployment and production reliability risk.
- **Rollback:** Per capability using application/configuration rollback while preserving forward database history.

View File

@@ -0,0 +1,64 @@
# EDU-016 — Move PostgreSQL-specific Education persistence tests off H2
- **Status:** done — focused PostgreSQL persistence suite passes against the reachable `postgresdb` seam
- **Type:** test infrastructure / PostgreSQL integration
- **Phase:** 0 / database prerequisite
- **Blockers:** EDU-002, EDU-005 for final schema ownership
## Confirmed defect
Education unit tests use H2 with `MODE=MYSQL`, while production Mapper SQL intentionally uses PostgreSQL `ON CONFLICT`. H2 rejects the annotated SQL for favorites, wrong-question idempotency, unified answer/submit idempotency, and review-session conflict handling. Switching H2 to PostgreSQL mode does not solve this: H2 still rejects `ON CONFLICT` and also exposes Boolean/integer compatibility differences.
This prevents the full Practice/Wrong/Favorite regression suite from exercising production persistence semantics.
## Outcome
PostgreSQL-specific persistence behavior runs against real ephemeral PostgreSQL in the test suite, while fast database-independent tests may remain on H2 where their SQL is portable.
## Scope
- Select the repository-standard PostgreSQL integration-test mechanism, preferably Testcontainers or an existing project fixture.
- Move tests that execute `ON CONFLICT`, PostgreSQL JSON/JSONB, identity, Boolean, or concurrency semantics onto PostgreSQL.
- Keep controller and pure domain tests database-independent.
- Align test schema with module-owned Flyway after EDU-005/EDU-006; avoid maintaining a divergent hand-written full schema long term.
- Cover at least:
- `IdempotencyStoreMapper.insertIgnore`;
- wrong-question idempotency insert;
- favorite upsert;
- review-session insert-ignore;
- concurrent answer and submit claims.
## Acceptance criteria
- [x] No test relies on H2 to validate PostgreSQL `ON CONFLICT` semantics.
- [x] PostgreSQL tests execute the same Mapper SQL as production.
- [x] Test database is isolated and disposable.
- [x] Schema setup uses Flyway or an explicitly temporary bridge with an exit ticket.
- [x] Concurrency tests are stable and prove unique-constraint behavior.
- [x] CI prerequisites and local commands are documented.
## Verification
Completed against the existing reachable `postgresdb` service with credentials supplied only through `EDU_TEST_POSTGRES_*` environment variables:
```bash
mvn -pl yudao-module-education \
-Dtest=PracticeSessionMapperTest,FavoriteServiceImplTest,PracticeAnswerServiceImplTest,PracticeSubmitServiceImplTest,PracticeSubmitProjectionIntegrationTest,WrongQuestionServiceImplTest test
```
Result: 140 tests passed, 0 failures, 0 errors. The same 140-test suite also passed with JUnit class parallelism explicitly enabled, proving the shared schema resource lock prevents class-level collisions. The suite executes production Mapper SQL on PostgreSQL, including `ON CONFLICT` and JSONB behavior. A JUnit resource lock serializes PostgreSQL test classes that share one random JVM-scoped schema; each class closes its Spring context before the inherited lifecycle drops and recreates that schema, preventing cached contexts from reusing a dropped schema.
The temporary schema bridge was removed by EDU-006. PostgreSQL persistence tests now create their disposable random schema through the module-owned Flyway chain (`V4010`, `V4020`, `V4030`) and retain only `clean.sql` for per-test data isolation.
Local/CI prerequisites:
- a reachable disposable PostgreSQL database;
- PostgreSQL JDBC connectivity from the Maven process;
- non-blank `EDU_TEST_POSTGRES_HOST`, `EDU_TEST_POSTGRES_PORT`, `EDU_TEST_POSTGRES_DB`, `EDU_TEST_POSTGRES_USER`, and `EDU_TEST_POSTGRES_PASSWORD` values.
No Testcontainers or other external dependency was added. No PostgreSQL Flyway migration was executed or authorized by this ticket.
## Risk and rollback
- **Risk:** Medium CI/runtime cost; high value for persistence confidence.
- **Rollback:** Keep prior fast tests temporarily, but do not restore false H2 coverage claims for PostgreSQL SQL.

View File

@@ -0,0 +1,59 @@
# Education Migration Tickets
This directory turns [`GOAL.md`](../GOAL.md) into executable, blocker-aware vertical slices.
## Workflow
1. Work only tickets whose blockers are complete.
2. Start each implementation ticket in a fresh context after reading `GOAL.md`, the ticket, relevant decisions, and current Git status.
3. Preserve the dirty working tree. Implementation is serial unless an isolated worktree and integration plan are explicit.
4. Apply TDD at the ticket's declared seams.
5. Use `flyway-postgresql` for every schema, index, constraint, seed, baseline, backfill, or Flyway configuration change.
6. Close with focused tests, `git diff --check`, and `mvn -pl yudao-server -am -DskipTests clean compile`.
7. Report exact commands and results. Never report PostgreSQL migration success without an actual successful PostgreSQL run.
## Status vocabulary
- `done`: implemented and verified to the ticket's current acceptance criteria.
- `in-progress`: currently being implemented.
- `ready`: all blockers complete and no unresolved decision prevents work.
- `blocked`: depends on another ticket or product decision.
- `decision`: produces a recorded decision rather than production behavior.
## Ticket graph
```text
EDU-000 Phase 0 artifacts done
├── EDU-001 Safe question content done, follow-up coverage remains
├── EDU-002 Practice regression baseline done
│ ├── EDU-016 PostgreSQL persistence tests done; temporary bridge blocked on EDU-005/EDU-006
│ └── EDU-006 Practice schema Flyway done
│ ├── EDU-007 Create/restore practice done
│ ├── EDU-008 Idempotent answer save done
│ └── EDU-009 Atomic submit/report done
├── EDU-003 Tenant resolution decision done
│ └── EDU-004 Tenant/identity security done; ingress/IP-only probing throttle remains operational blocker
└── EDU-005 PostgreSQL/Flyway takeover decision done
EDU-009 + provider/content decisions
└── EDU-010 Tenant content publication blocked
└── EDU-011 Import/export/assets/scanning blocked
EDU-004
└── EDU-012 Classes and education relationships blocked
Commerce ownership decisions
└── EDU-013 Education commercialization blocked
All owner/contract decisions
└── EDU-014 Extended learning waves blocked
└── EDU-015 Operational independence blocked
```
## Recommended execution order
1. Select the next unblocked content-management decision/ticket after EDU-009.
## Phase 0 completion caveat
Phase 0 artifacts exist, but several architecture and product decisions remain open. `EDU-000` is considered complete as an inventory deliverable, not as resolution of every decision it discovered.

View File

@@ -28,8 +28,8 @@ yudao:
## 3. 发布步骤
1. 备份 Education 相关表,并记录应用版本与数据库版本
2. 执行尚未应用的正向 SQL不得执行 rollback SQL。
1. 备份 Education 相关表,并记录应用版本与 `flyway_schema_history`
2. 使用 Server 配置的 PostgreSQL Flyway 执行 migrate 和 validate检查版本、脚本、checksum 与 success不得手工应用 Education SQL 或执行 rollback SQL。
3. 先以 `catalog-read-enabled=false``practice-write-enabled=false` 部署应用。
4. 验证 System、Infra、Member 基础 smoke。
5. 仅对 Pilot 租户开启题库读取,完成 Scalar 只读 smoke。
@@ -65,7 +65,7 @@ yudao:
1. 设置 `catalog-read-enabled=false`,停止新的 Scalar 读取。
2. 保持 `enabled=true`,使已有会话、报告、错题和收藏仍可访问。
3. 如需冻结新写入,再设置 `practice-write-enabled=false`
4. 验证 Education MySQL 表行数和历史查询均未减少。
4. 验证 Education PostgreSQL 表行数和历史查询均未减少。
### 练习写入熔断
@@ -78,11 +78,11 @@ yudao:
### 应用回滚
1. 将应用回滚到上一已验证版本。
2. 保留所有 Education 表和数据,不执行 `sql/mysql/education/*-rollback.sql`
3. 若旧版本与新 schema 不兼容,保持功能关闭并前滚修复;不得通过删表恢复服务。
2. 保留所有 Education 表和数据,不执行 `flyway clean`、手工删除或任何 `*-rollback.sql`
3. 若旧版本与新 schema 不兼容,保持功能关闭并通过更高版本 Flyway migration 前滚修复;不得通过删表恢复服务。
4. 重新验证 Member 登录、System 租户和 Infra 日志功能。
> `*-rollback.sql` 是显式数据销毁工具,不是常规应用版本回滚步骤。
> 历史 `*-rollback.sql` 是数据销毁工具且不属于当前交付机制,不是常规应用版本回滚步骤。
## 6. 可观测性

View File

@@ -0,0 +1,225 @@
# Scalar (tiku-backend) 接口契约 —— 从源码提取
日期2026-07-28
来源:`/Users/tiku1/code/tiku-backend` 仓库 NestJS Controller、DTO 和装饰器
状态:源码级契约冻结,真实 endpoint 验证需启动完整运行时Supabase + API server
## 运行时信息
- 框架NestJS + Fastify
- OpenAPI 路径:`/openapi.json`(仅非生产环境)
- Scalar 文档:`/docs`(仅非生产环境)
- 认证Bearer TokenJWT+ `x-tenant-id` header
- 响应 envelope`{ items/item, meta: { requestId } }`
## RuoYi Education 实际调用的路径
以下为 RuoYi `ScalarCatalogProvider` 使用的只读 GET 路径:
### 1. GET /api/catalog/regions
- **安全方案**: `x-tenant-id` header (`@ApiSecurity('tenant-id')`, `@TenantAccess()`)
- **查询参数**: `regionId?` (UUID, 可选)
- **响应**: `{ items: CatalogRegionResponseDto[], meta: { requestId } }`
- **CatalogRegionResponseDto 字段**:
- `id` (uuid, 必填)
- `legacyId` (string, nullable)
- `name` (string, 必填)
- `code` (string, nullable)
- `shortName` (string, nullable)
- `fullName` (string, nullable)
- `icon` (string, nullable)
- `pinyin` (string, nullable)
- `isHot` (boolean, 必填)
- `isActive` (boolean, 必填)
- `order` (number, 必填)
### 2. GET /api/catalog/region-modules
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?` (UUID)
- **响应**: `{ items: CatalogRegionModuleResponseDto[], meta }`
- **字段**: id, legacyId, regionId, name, type, icon, color, textColor, description, route, isPrimarySchoolModule, isActive, order
### 3. GET /api/catalog/module-nodes
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `regionId?` (UUID)
- `moduleId?` (UUID)
- `parentId?` (string, "root" 表示根节点)
- **响应**: `{ items: CatalogEntityDto[], meta }` — 通用实体列表
### 4. GET /api/catalog/schools
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?`, `schoolId?`
- **响应**: `{ items: CatalogSchoolResponseDto[], meta }`
- **字段**: id, legacyId, regionId, moduleId, name, professionalExamDate, metadata, createdAt, updatedAt
### 5. GET /api/catalog/majors
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?`, `schoolId?`, `majorId?`, `moduleId?`, `type?`
- **响应**: `{ items: CatalogMajorResponseDto[], meta }`
### 6. GET /api/catalog/subjects
- **安全方案**: `x-tenant-id`
- **查询参数**: `regionId?`, `schoolId?`, `majorId?` (UUID), `moduleId?` (UUID), `type?` (string)
- **响应**: `{ items: CatalogEntityDto[], meta }`
### 7. GET /api/catalog/categories
- **安全方案**: `x-tenant-id`
- **查询参数**: `subjectId?` (UUID), `nodeId?` (UUID, 旧导航节点)
- **响应**: `{ items: CatalogEntityDto[], meta }`
### 8. GET /api/catalog/questions
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `subjectId?` (UUID)
- `categoryId?` (UUID)
- `nodeId?` (UUID, 旧导航节点)
- `entryId?` (UUID)
- `contentNodeId?` (UUID)
- `collectionId?` (UUID)
- `questionIds?` (string | string[], 逗号分隔或重复传参)
- `limit?` (int, 1-2000)
- **响应**: `{ items: QuestionResponseDto[], meta }`
- **QuestionResponseDto 字段** (extends CatalogEntityDto):
- `id` (uuid)
- `type` (string, 必填 — 题型)
- `typeLabel` (string, nullable)
- `difficulty` (number, nullable)
- `content` (unknown, 题干)
- `options` (array, 选项列表)
- `explanation` (string, nullable — **敏感字段**)
- `hasVideoExplanation` (boolean)
- 继承字段: legacyId, name, title, regionId, order, isActive, description, metadata
- **注意**: `options` 中包含正确选项标记、`explanation` 包含答案解析。RuoYi 在返回学生端 DTO 前必须剥离这些字段。
### 9. GET /api/catalog/content-entries
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `regionId?` (UUID)
- `entryType?` (string)
- `includeHidden?` (boolean, default false)
- **响应**: `{ items: ContentEntryResponseDto[], meta }`
### 10. GET /api/catalog/content-nodes
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `entryId` (UUID, **必填**)
- `parentId?` (string, "root" 表示根节点)
- `mode?` ('children' | 'flat', default 'children')
- `includeInactive?` (boolean, default false)
- `markerType?` (string)
- **响应**: `{ items: ContentNodeResponseDto[], meta }`
### 11. GET /api/catalog/question-collections
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `regionId?` (UUID)
- `entryId?` (UUID)
- `nodeId?` (UUID)
- `collectionType?` (string)
- `limit?` (int, 1-2000)
- **响应**: `{ items: QuestionCollectionResponseDto[], meta }`
### 12. GET /api/catalog/question-collections/questions
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `collectionId` (UUID, **必填**)
- `limit?` (int, 1-2000)
- **响应**: `{ items: QuestionResponseDto[], meta }`
### 13. GET /api/catalog/practice-blueprints
- **安全方案**: `x-tenant-id`
- **查询参数**:
- `entryId?` (UUID)
- `nodeId?` (UUID)
- `collectionId?` (UUID)
- `mode?` (string)
- `limit?` (int, 1-2000)
- **响应**: `{ items: PracticeBlueprintResponseDto[], meta }`
- **字段**: id, mode, entryId, nodeId, collectionId, questionLimit, durationMinutes + CatalogEntityDto 继承字段
## 认证机制
### 租户识别
- 所有 catalog 路径使用 `@TenantAccess()` 装饰器 → `AccessPolicy { kind: 'tenant' }`
- 租户 ID 从 `x-tenant-id` 请求头提取CORS 白名单包含此头)
- `Principal` 装饰器从请求上下文提取 `principal.tenant.tenantId`
### Bearer Token
- catalog 的大多数端点不需要 Bearer只读、租户级访问
- `assets``assets/download``assets/preview` 需要 `@ApiBearerAuth()`
- 学习写入路径 (`/api/learning/*`) 需要 `@ApiBearerAuth()` + `@TenantUserAccess()`
## 响应格式
### 成功
```json
{
"items": [...],
"meta": { "requestId": "uuid" }
}
```
```json
{
"item": {...},
"meta": { "requestId": "uuid" }
}
```
### 错误
```json
{
"error": "面向调用方的错误信息",
"code": "REQUIRED_FIELD",
"requestId": "uuid",
"meta": { "requestId": "uuid" }
}
```
## 与 RuoYi adapter 的差异
| 项目 | RuoYi (Java) 假设 | Scalar (tiku-backend) 实际 |
|------|-------------------|---------------------------|
| 基础路径 | 配置的 `base-url` | `/api/catalog/*` |
| 认证头 | `Authorization: Bearer <token>` | 大多数 catalog 端点只需 `x-tenant-id`,不需要 Bearer |
| 租户头 | `x-tenant-id` | `x-tenant-id` ✅ 一致 |
| 分页 | `page` + `pageSize` | `limit` (1-2000),无 page 参数! |
| 题目过滤 | `published=true&hidden=false` 由 RuoYi 追加 | Scalar 端已有 `isActive` 过滤,但无 `published`/`hidden` query 参数 |
| 响应 envelope | 预期 `items` + 可能的 `total` | `items` + `meta.requestId`,无 `total` 字段! |
| 正确答案 | RuoYi 在返回学生端前剥离 | `QuestionResponseDto.options` 包含正确选项标记 |
## ⚠️ 关键差异
1. **分页**: RuoYi `ScalarCatalogProvider` 使用 `page` + `pageSize` query 参数,但 Scalar 只接受 `limit`。Java 端第 526-540 行固定追加 `page``pageSize` —— 这些参数在 Scalar controller 中不存在,会被忽略。
2. **total 字段**: RuoYi adapter 期望服务端返回 `total` 用于分页,但 Scalar 响应没有此字段。如果 RuoYi 依赖 `total` 做前端分页计算,需要确认 adapter 如何处理。
3. **published/hidden**: RuoYi 端固定追加 `published=true&hidden=false`,但这些参数在 Scalar controller DTO 中未定义。Scalar 的过滤逻辑在 repository 层而非 query 参数层。
## OpenAPI 生成方式
```bash
# 需要 Docker + Supabase 运行
cd tiku-backend
npm run supabase:start
npm run dev:api
curl http://127.0.0.1:8787/openapi.json > /tmp/tiku-openapi.json
# 或通过测试套件(也会启动真实服务器)
BACKEND_TEST_SKIP_DATABASE=true npm run test:backend:migration
# 生成文件:/tmp/tiku-openapi.json
```
当前环境不具备 Supabase/Docker无法生成运行时 OpenAPI JSON。源码级契约已在此文档冻结。

View File

@@ -45,5 +45,5 @@ docker compose --env-file docker.env up -d
- admin ui: http://localhost:8080
- api server: http://localhost:48080
- mysql: root/123456, port: 3306
- postgresql: root/123456, port: 5432
- redis: port: 6379

View File

@@ -1,21 +1,19 @@
version: "3.4"
name: yudao-system
services:
mysql:
container_name: yudao-mysql
image: mysql:8
postgres:
container_name: yudao-postgres
image: postgres:17-alpine
restart: unless-stopped
tty: true
ports:
- "3306:3306"
- "5432:5432"
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-ruoyi-vue-pro}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
POSTGRES_DB: ${POSTGRES_DB:-ruoyi-vue-pro}
POSTGRES_USER: ${POSTGRES_USER:-root}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456}
volumes:
- mysql:/var/lib/mysql/
- ./sql/mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/ruoyi-vue-pro.sql:ro
- postgres:/var/lib/postgresql/data/
- ../../sql/postgresql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/000-ruoyi-vue-pro.sql:ro
redis:
container_name: yudao-redis
@@ -44,15 +42,15 @@ services:
-Djava.security.egd=file:/dev/./urandom
}
ARGS:
--spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:mysql://yudao-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
--spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:postgresql://yudao-postgres:5432/ruoyi-vue-pro}
--spring.datasource.dynamic.datasource.master.username=${MASTER_DATASOURCE_USERNAME:-root}
--spring.datasource.dynamic.datasource.master.password=${MASTER_DATASOURCE_PASSWORD:-123456}
--spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:mysql://yudao-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
--spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:postgresql://yudao-postgres:5432/ruoyi-vue-pro}
--spring.datasource.dynamic.datasource.slave.username=${SLAVE_DATASOURCE_USERNAME:-root}
--spring.datasource.dynamic.datasource.slave.password=${SLAVE_DATASOURCE_PASSWORD:-123456}
--spring.data.redis.host=${REDIS_HOST:-yudao-redis}
depends_on:
- mysql
- postgres
- redis
admin:
@@ -78,7 +76,7 @@ services:
- server
volumes:
mysql:
postgres:
driver: local
redis:
driver: local

View File

@@ -1,13 +1,14 @@
## mysql
MYSQL_DATABASE=ruoyi-vue-pro
MYSQL_ROOT_PASSWORD=123456
## postgresql
POSTGRES_DB=ruoyi-vue-pro
POSTGRES_USER=root
POSTGRES_PASSWORD=123456
## server
JAVA_OPTS=-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom
MASTER_DATASOURCE_URL=jdbc:mysql://yudao-mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
MASTER_DATASOURCE_USERNAME=root
MASTER_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD}
MASTER_DATASOURCE_URL=jdbc:postgresql://yudao-postgres:5432/${POSTGRES_DB}
MASTER_DATASOURCE_USERNAME=${POSTGRES_USER}
MASTER_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
SLAVE_DATASOURCE_URL=${MASTER_DATASOURCE_URL}
SLAVE_DATASOURCE_USERNAME=${MASTER_DATASOURCE_USERNAME}
SLAVE_DATASOURCE_PASSWORD=${MASTER_DATASOURCE_PASSWORD}

View File

@@ -0,0 +1,21 @@
-- =============================================
-- Education 模块 — 题库目录表回滚 DDL
-- Ticket #20 rollback
-- =============================================
-- WARNING: This is an explicit data-destruction script.
-- It is NOT part of application version rollback.
-- Application version rollback preserves these tables and their data;
-- this script is only used when permanently removing the catalog module.
-- =============================================
DROP TABLE IF EXISTS `education_question_collection_question`;
DROP TABLE IF EXISTS `education_practice_blueprint`;
DROP TABLE IF EXISTS `education_question`;
DROP TABLE IF EXISTS `education_question_collection`;
DROP TABLE IF EXISTS `education_content_node`;
DROP TABLE IF EXISTS `education_content_entry`;
DROP TABLE IF EXISTS `education_category`;
DROP TABLE IF EXISTS `education_subject`;
DROP TABLE IF EXISTS `education_major`;
DROP TABLE IF EXISTS `education_school`;
DROP TABLE IF EXISTS `education_region`;

View File

@@ -0,0 +1,349 @@
-- =============================================
-- Education 模块 — 题库目录原生表 DDL
-- Ticket #20: 原生实现题库目录浏览,替换 Scalar adapter
-- Migration: 008
-- Prerequisites: 000-education-schema.sql (database creation)
-- 001-education-tenant-seed.sql (tenant seed data)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Verify none of these tables already exist:
-- SELECT TABLE_NAME FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND TABLE_NAME IN (
-- 'education_region', 'education_school', 'education_major',
-- 'education_subject', 'education_category',
-- 'education_content_entry', 'education_content_node',
-- 'education_question_collection', 'education_question',
-- 'education_practice_blueprint', 'education_question_collection_question'
-- );
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 1. 地区表
-- =============================================
-- Indexes:
-- idx_tenant — tenant-scoped queries (tenant-aware interceptor default)
-- idx_tenant_active_order — active region listing ordered by display order
CREATE TABLE `education_region` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统地区 ID',
`name` VARCHAR(100) NOT NULL COMMENT '地区名称',
`code` VARCHAR(32) DEFAULT NULL COMMENT '地区编码',
`short_name` VARCHAR(50) DEFAULT NULL COMMENT '地区简称',
`full_name` VARCHAR(200) DEFAULT NULL COMMENT '地区全称',
`icon` VARCHAR(500) DEFAULT NULL COMMENT '地区图标 URL',
`pinyin` VARCHAR(200) DEFAULT NULL COMMENT '地区拼音',
`is_hot` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否热门地区',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_active_order` (`tenant_id`, `is_active`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-地区';
-- =============================================
-- 2. 院校表
-- =============================================
CREATE TABLE `education_school` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统院校 ID',
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
`module_id` BIGINT DEFAULT NULL COMMENT '地区模块 ID',
`name` VARCHAR(200) NOT NULL COMMENT '院校名称',
`professional_exam_date` VARCHAR(200) DEFAULT NULL COMMENT '专业考试日期说明',
`metadata` JSON DEFAULT NULL COMMENT '院校扩展数据',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_region` (`tenant_id`, `region_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-院校';
-- =============================================
-- 3. 专业表
-- =============================================
CREATE TABLE `education_major` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统专业 ID',
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
`school_id` BIGINT DEFAULT NULL COMMENT '院校 ID',
`name` VARCHAR(200) NOT NULL COMMENT '专业名称',
`description` VARCHAR(500) DEFAULT NULL COMMENT '专业说明',
`study_tips` VARCHAR(500) DEFAULT NULL COMMENT '学习建议',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_school` (`tenant_id`, `school_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-专业';
-- =============================================
-- 4. 科目表
-- =============================================
CREATE TABLE `education_subject` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
`school_id` BIGINT DEFAULT NULL COMMENT '院校 ID',
`major_id` BIGINT DEFAULT NULL COMMENT '专业 ID',
`module_id` BIGINT DEFAULT NULL COMMENT '地区模块 ID',
`name` VARCHAR(200) NOT NULL COMMENT '科目名称',
`type` VARCHAR(50) DEFAULT NULL COMMENT '科目类型',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_major` (`tenant_id`, `major_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-科目';
-- =============================================
-- 5. 题目分类表
-- =============================================
CREATE TABLE `education_category` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`subject_id` BIGINT DEFAULT NULL COMMENT '科目 ID',
`legacy_node_id` VARCHAR(64) DEFAULT NULL COMMENT '旧导航节点 ID',
`name` VARCHAR(200) NOT NULL COMMENT '分类名称',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_subject` (`tenant_id`, `subject_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题目分类';
-- =============================================
-- 6. 内容入口表
-- =============================================
-- Indexes:
-- uk_tenant_entry_key — per-tenant uniqueness for entry key
CREATE TABLE `education_content_entry` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统内容入口 ID',
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
`entry_key` VARCHAR(100) NOT NULL COMMENT '内容入口唯一键',
`name` VARCHAR(200) NOT NULL COMMENT '内容入口名称',
`entry_type` VARCHAR(50) NOT NULL COMMENT '内容入口类型',
`icon` VARCHAR(500) DEFAULT NULL COMMENT '图标',
`route` VARCHAR(200) DEFAULT NULL COMMENT '前端路由',
`description` VARCHAR(500) DEFAULT NULL COMMENT '入口说明',
`visibility` VARCHAR(50) NOT NULL DEFAULT 'PUBLIC' COMMENT '可见性',
`access_rules` JSON DEFAULT NULL COMMENT '访问规则 JSON',
`layout_config` JSON DEFAULT NULL COMMENT '布局配置 JSON',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
UNIQUE KEY `uk_tenant_entry_key` (`tenant_id`, `entry_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-内容入口';
-- =============================================
-- 7. 内容节点表(树形结构)
-- =============================================
-- Indexes:
-- idx_entry_parent — supports tree navigation: find children of a parent within an entry
-- idx_tenant_entry — tenant-scoped queries by entry
CREATE TABLE `education_content_node` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`entry_id` BIGINT NOT NULL COMMENT '内容入口 ID',
`parent_id` BIGINT DEFAULT NULL COMMENT '父节点 IDNULL=根节点)',
`name` VARCHAR(200) NOT NULL COMMENT '节点名称',
`title` VARCHAR(200) DEFAULT NULL COMMENT '节点标题',
`node_type` VARCHAR(50) NOT NULL COMMENT '节点类型',
`marker_type` VARCHAR(50) DEFAULT NULL COMMENT '节点标记类型',
`depth` INT NOT NULL DEFAULT 0 COMMENT '树深度',
`is_leaf` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否叶子节点',
`is_selectable` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否可被选择',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`metadata` JSON DEFAULT NULL COMMENT '扩展元数据',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_entry_parent` (`entry_id`, `parent_id`),
KEY `idx_tenant_entry` (`tenant_id`, `entry_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-内容节点';
-- =============================================
-- 8. 题集表
-- =============================================
CREATE TABLE `education_question_collection` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`entry_id` BIGINT DEFAULT NULL COMMENT '内容入口 ID',
`node_id` BIGINT DEFAULT NULL COMMENT '内容节点 ID',
`name` VARCHAR(200) NOT NULL COMMENT '题集名称',
`title` VARCHAR(200) DEFAULT NULL COMMENT '题集标题',
`collection_type` VARCHAR(50) NOT NULL COMMENT '题集类型',
`question_count` INT NOT NULL DEFAULT 0 COMMENT '题目数量',
`duration_minutes` INT DEFAULT NULL COMMENT '建议作答时长(分钟)',
`access_rules` JSON DEFAULT NULL COMMENT '访问规则 JSON',
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`metadata` JSON DEFAULT NULL COMMENT '扩展元数据',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_entry` (`tenant_id`, `entry_id`),
KEY `idx_tenant_node` (`tenant_id`, `node_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题集';
-- =============================================
-- 9. 题目表(核心表)
-- =============================================
-- Indexes:
-- idx_tenant_published — 查询已发布题目(主查询路径)
-- idx_tenant_collection — 按题集筛选
-- idx_tenant_subject — 按科目筛选
-- idx_tenant_node — 按内容节点筛选
-- idx_tenant_type_diff — 按题型和难度筛选
-- Security note: correct_answer 和 explanation 列存在但需在 service 层过滤;
-- 学生端 DTO 绝不得包含这两列。
-- Options JSON: 存储 label, content, isCorrect, order — 完整选项数据。
-- Service 层在学生端返回前剥离 isCorrect。
CREATE TABLE `education_question` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`content_version` INT NOT NULL DEFAULT 1 COMMENT '内容版本号(递增,保证历史报告稳定性)',
`stem` TEXT NOT NULL COMMENT '题干',
`type` VARCHAR(32) NOT NULL COMMENT '题型',
`type_label` VARCHAR(50) DEFAULT NULL COMMENT '题型显示名称',
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度',
`question_content` JSON DEFAULT NULL COMMENT '题干结构化内容(兼容复杂题型)',
`options` JSON NOT NULL COMMENT '选项 JSON [{label, content, isCorrect, order}]',
`correct_answer` VARCHAR(500) DEFAULT NULL COMMENT '正确答案(敏感字段,学生端不可返回)',
`explanation` TEXT DEFAULT NULL COMMENT '答案解析(敏感字段,学生端不可返回)',
`analysis` TEXT DEFAULT NULL COMMENT '深度解析(敏感字段)',
`status` VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED' COMMENT '题目状态PUBLISHED-已发布, HIDDEN-已隐藏, DRAFT-草稿, INACTIVE-停用',
`is_published` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否已发布',
`collection_id` BIGINT DEFAULT NULL COMMENT '所属题集 ID',
`subject_id` BIGINT DEFAULT NULL COMMENT '所属科目 ID',
`node_id` BIGINT DEFAULT NULL COMMENT '所属内容节点 ID',
`tags` JSON DEFAULT NULL COMMENT '标签 JSON 数组',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
`metadata` JSON DEFAULT NULL COMMENT '扩展元数据',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_published` (`tenant_id`, `is_published`, `status`),
KEY `idx_tenant_collection` (`tenant_id`, `collection_id`),
KEY `idx_tenant_subject` (`tenant_id`, `subject_id`),
KEY `idx_tenant_node` (`tenant_id`, `node_id`),
KEY `idx_tenant_type_diff` (`tenant_id`, `type`, `difficulty`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题目';
-- =============================================
-- 10. 练习蓝图表
-- =============================================
CREATE TABLE `education_practice_blueprint` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
`mode` VARCHAR(50) NOT NULL COMMENT '练习模式',
`entry_id` BIGINT DEFAULT NULL COMMENT '内容入口 ID',
`node_id` BIGINT DEFAULT NULL COMMENT '内容节点 ID',
`collection_id` BIGINT DEFAULT NULL COMMENT '题集 ID',
`question_limit` INT DEFAULT NULL COMMENT '题目数量限制',
`duration_minutes` INT DEFAULT NULL COMMENT '建议作答时长(分钟)',
`eligible_count` INT NOT NULL DEFAULT 0 COMMENT '符合条件的题目数',
`total_count` INT NOT NULL DEFAULT 0 COMMENT '题库总数',
`available_types` JSON DEFAULT NULL COMMENT '可用题型列表 JSON',
`available_difficulties` JSON DEFAULT NULL COMMENT '可用难度列表 JSON',
`min_questions` INT NOT NULL DEFAULT 1 COMMENT '最少题目数',
`max_questions` INT NOT NULL DEFAULT 200 COMMENT '最多题目数',
`suggested_count` INT NOT NULL DEFAULT 20 COMMENT '建议题目数',
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_tenant_collection` (`tenant_id`, `collection_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习蓝图';
-- =============================================
-- 11. 题集-题目关联表(多对多)
-- =============================================
CREATE TABLE `education_question_collection_question` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`collection_id` BIGINT NOT NULL COMMENT '题集 ID',
`question_id` BIGINT NOT NULL COMMENT '题目 ID',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序值',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_collection_question` (`collection_id`, `question_id`),
KEY `idx_question` (`question_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题集题目关联';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify all tables exist:
-- SELECT TABLE_NAME FROM information_schema.tables
-- WHERE table_schema = DATABASE()
-- AND TABLE_NAME LIKE 'education_%'
-- ORDER BY TABLE_NAME;
-- Verify indexes (sample):
-- SHOW INDEX FROM education_question WHERE Key_name IN ('idx_tenant_published', 'idx_tenant_collection');
-- SHOW INDEX FROM education_content_node WHERE Key_name = 'idx_entry_parent';
-- Verify no orphan data (should be 0 after fresh migration for catalog tables):
-- SELECT 'region', COUNT(*) FROM education_region
-- UNION ALL SELECT 'school', COUNT(*) FROM education_school
-- UNION ALL SELECT 'major', COUNT(*) FROM education_major
-- UNION ALL SELECT 'subject', COUNT(*) FROM education_subject
-- UNION ALL SELECT 'category', COUNT(*) FROM education_category
-- UNION ALL SELECT 'content_entry', COUNT(*) FROM education_content_entry
-- UNION ALL SELECT 'content_node', COUNT(*) FROM education_content_node
-- UNION ALL SELECT 'question_collection', COUNT(*) FROM education_question_collection
-- UNION ALL SELECT 'question', COUNT(*) FROM education_question
-- UNION ALL SELECT 'practice_blueprint', COUNT(*) FROM education_practice_blueprint
-- UNION ALL SELECT 'collection_question', COUNT(*) FROM education_question_collection_question;

174
sql/mysql/member-init.sql Normal file
View File

@@ -0,0 +1,174 @@
-- =============================================
-- Member 模块建表 — 从 PostgreSQL 测试 SQL 转换为 MySQL
-- 用于本地开发环境补全 member 表
-- =============================================
-- 1. 会员配置表
CREATE TABLE IF NOT EXISTS member_config (
id bigint NOT NULL AUTO_INCREMENT,
group_id bigint NULL DEFAULT NULL COMMENT '用户分组编号',
level_id bigint NULL DEFAULT NULL COMMENT '等级编号',
tenant_id bigint NOT NULL DEFAULT 0,
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员配置表';
-- 2. 会员标签表
CREATE TABLE IF NOT EXISTS member_tag (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL COMMENT '标签名称',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员标签';
-- 3. 用户分组表
CREATE TABLE IF NOT EXISTS member_group (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL COMMENT '分组名称',
remark varchar(255) DEFAULT '' COMMENT '备注',
status tinyint NOT NULL DEFAULT 0 COMMENT '状态',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员分组';
-- 4. 会员等级表(完整版,覆盖之前创建的简化版)
CREATE TABLE IF NOT EXISTS member_level (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL COMMENT '等级名称',
experience int NOT NULL DEFAULT 0 COMMENT '所需经验',
level int NOT NULL DEFAULT 1 COMMENT '等级',
discount_percent int NOT NULL DEFAULT 100 COMMENT '折扣百分比',
icon varchar(255) DEFAULT '' COMMENT '图标',
background_url varchar(255) DEFAULT '' COMMENT '背景图',
status tinyint NOT NULL DEFAULT 0 COMMENT '状态',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级';
-- 5. 会员等级记录表
CREATE TABLE IF NOT EXISTS member_level_record (
id bigint NOT NULL AUTO_INCREMENT,
user_id bigint NOT NULL COMMENT '用户编号',
level_id bigint NOT NULL COMMENT '等级编号',
level_name varchar(50) DEFAULT '' COMMENT '等级名称',
experience int NOT NULL DEFAULT 0 COMMENT '当前经验',
remark varchar(255) DEFAULT '' COMMENT '备注',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级记录';
-- 6. 会员经验记录表
CREATE TABLE IF NOT EXISTS member_experience_record (
id bigint NOT NULL AUTO_INCREMENT,
user_id bigint NOT NULL COMMENT '用户编号',
biz_type varchar(50) NOT NULL COMMENT '业务类型',
biz_id varchar(100) NOT NULL COMMENT '业务编号',
title varchar(100) NOT NULL COMMENT '标题',
experience int NOT NULL COMMENT '经验',
total_experience int NOT NULL DEFAULT 0 COMMENT '累计经验',
description varchar(255) DEFAULT '' COMMENT '描述',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员经验记录';
-- 7. 会员积分记录表
CREATE TABLE IF NOT EXISTS member_point_record (
id bigint NOT NULL AUTO_INCREMENT,
user_id bigint NOT NULL COMMENT '用户编号',
biz_type varchar(50) NOT NULL COMMENT '业务类型',
biz_id varchar(100) NOT NULL COMMENT '业务编号',
title varchar(100) NOT NULL COMMENT '标题',
point int NOT NULL COMMENT '积分',
total_point int NOT NULL DEFAULT 0 COMMENT '累计积分',
description varchar(255) DEFAULT '' COMMENT '描述',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员积分记录';
-- 8. 会员签到配置表
CREATE TABLE IF NOT EXISTS member_sign_in_config (
id bigint NOT NULL AUTO_INCREMENT,
day int NOT NULL COMMENT '签到天数',
point int NOT NULL DEFAULT 0 COMMENT '奖励积分',
experience int NOT NULL DEFAULT 0 COMMENT '奖励经验',
status tinyint NOT NULL DEFAULT 0 COMMENT '状态',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员签到配置';
-- 9. 会员签到记录表
CREATE TABLE IF NOT EXISTS member_sign_in_record (
id bigint NOT NULL AUTO_INCREMENT,
user_id bigint NOT NULL COMMENT '用户编号',
day int NOT NULL COMMENT '签到天数',
point int NOT NULL DEFAULT 0 COMMENT '获得积分',
experience int NOT NULL DEFAULT 0 COMMENT '获得经验',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员签到记录';
-- 10. 会员地址表
CREATE TABLE IF NOT EXISTS member_address (
id bigint NOT NULL AUTO_INCREMENT,
user_id bigint NOT NULL COMMENT '用户编号',
name varchar(20) NOT NULL COMMENT '收件人姓名',
mobile varchar(20) NOT NULL COMMENT '手机号',
area_id bigint NOT NULL COMMENT '地区编号',
detail_address varchar(250) NOT NULL COMMENT '详细地址',
default_status bit(1) NOT NULL DEFAULT b'0' COMMENT '是否默认',
creator varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员地址';
-- =============================================
-- 种子数据
-- =============================================
INSERT IGNORE INTO member_level (id, name, level, experience, status, tenant_id) VALUES (1, '普通会员', 1, 0, 0, 1);

View File

@@ -0,0 +1,8 @@
-- =============================================
-- Education 模块种子数据回滚 (PostgreSQL)
-- =============================================
DELETE FROM system_menu
WHERE id = 6801 AND permission = 'education:capability' AND parent_id = 6800;
DELETE FROM system_menu
WHERE id = 6800 AND path = '/education' AND name = '教育管理';

View File

@@ -0,0 +1,7 @@
-- =============================================
-- Education 模块 DDL (PostgreSQL)
-- 当前为应用外壳阶段,无业务表;后续票据在此追加 CREATE TABLE 语句。
-- =============================================
-- 占位education 模块当前无业务表
CREATE SCHEMA IF NOT EXISTS education;

View File

@@ -0,0 +1,15 @@
-- =============================================
-- Education 模块种子数据 (PostgreSQL)
-- 菜单 ID 范围6800-6899
-- 权限标识前缀education:
-- 可重复执行;角色授权由管理员按租户完成
-- =============================================
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 6800, '教育管理', '', 1, 50, 0, '/education', 'ep:school', NULL, NULL, 0, true, true, true, 'admin', NOW(), 'admin', NOW(), false
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 6800);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 6801, '能力查询', 'education:capability', 3, 1, 6800, '', '', '', NULL, 0, true, true, true, 'admin', NOW(), 'admin', NOW(), false
WHERE EXISTS (SELECT 1 FROM system_menu WHERE id = 6800 AND path = '/education' AND deleted = false)
AND NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 6801);

View File

@@ -0,0 +1 @@
-- Ticket #3 creates no database records, so rollback is intentionally a no-op.

View File

@@ -0,0 +1,3 @@
-- Ticket #3 adds no administrator permission.
-- Tenant resolution is @PermitAll and current education context only requires an authenticated Member session.
-- Therefore no system_menu rows are required for this vertical slice.

View File

@@ -0,0 +1,32 @@
-- =============================================
-- Education 模块 — 练习会话与题目快照回滚
-- Ticket #6 / Migration 002
-- =============================================
--
-- WARNING: This file contains NO executable SQL.
-- Destructive rollback (DROP TABLE) requires manual operator verification.
--
-- Manual rollback procedure (operator must execute):
-- 1. Verify no other tables depend on these tables:
-- SELECT tc.table_name, kcu.column_name, ccu.table_name AS referenced_table
-- FROM information_schema.table_constraints tc
-- JOIN information_schema.key_column_usage kcu
-- ON tc.constraint_name = kcu.constraint_name
-- JOIN information_schema.constraint_column_usage ccu
-- ON tc.constraint_name = ccu.constraint_name
-- WHERE tc.constraint_type = 'FOREIGN KEY'
-- AND ccu.table_name IN ('education_practice_session', 'education_practice_question');
-- Result MUST be empty before proceeding.
--
-- 2. Verify the tables contain only data from this migration:
-- SELECT COUNT(*) AS session_count FROM education_practice_session;
-- SELECT COUNT(*) AS question_count FROM education_practice_question;
-- Operator must confirm these counts are acceptable to destroy.
--
-- 3. After verification, execute:
-- DROP TABLE IF EXISTS education_practice_question;
-- DROP TABLE IF EXISTS education_practice_session;
--
-- DO NOT uncomment or execute the lines below without operator verification.
-- -- DROP TABLE IF EXISTS education_practice_question;
-- -- DROP TABLE IF EXISTS education_practice_session;

View File

@@ -0,0 +1,105 @@
-- =============================================
-- Education 模块 — 练习会话与题目快照 DDL
-- Ticket #6: 练习会话创建、题目快照、恢复与状态机
-- Migration: 002
-- Prerequisites: 000-education-schema.sql (database creation)
-- 001-education-tenant-seed.sql (tenant seed data)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- This migration MUST fail if either table already exists (no IF NOT EXISTS).
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = current_database()
-- AND table_name IN ('education_practice_session', 'education_practice_question');
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 练习会话表
-- =============================================
-- Indexes:
-- uk_tenant_client_session — per-tenant uniqueness for clientSessionId idempotency.
-- Used by: selectByTenantAndClientSessionId (idempotent create check),
-- INSERT ... ON CONFLICT ... for concurrent-create race resolution.
-- idx_tenant_user_status — covers getCurrentSession (latest ACTIVE by tenant+user)
-- and ownership queries. Column order: (tenant_id, user_id, status) so the
-- index supports both filtering by tenant+user and tenant+user+status.
-- Lock impact: INSERT acquires a row-level lock on the unique index entry;
-- concurrent inserts with same (tenant_id, client_session_id) serialize naturally.
-- No additional table-level locks required.
CREATE TABLE education_practice_session (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 会话主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
user_id BIGINT NOT NULL /* Member 用户编号 */,
client_session_id VARCHAR(36) NOT NULL /* 客户端生成的会话标识UUID用于幂等创建 */,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'
/* 会话状态ACTIVE-进行中, SUBMITTED-已提交, EXPIRED-已过期, CANCELLED-已取消 */,
question_count INT NOT NULL DEFAULT 0 /* 题目总数 */,
collection_id VARCHAR(64) DEFAULT NULL /* 源题集 ID */,
node_id VARCHAR(64) DEFAULT NULL /* 源目录节点 ID */,
type VARCHAR(32) DEFAULT NULL /* 筛选题型 */,
difficulty VARCHAR(32) DEFAULT NULL /* 筛选难度 */,
version INT NOT NULL DEFAULT 0 /* 乐观锁版本号 */,
creator VARCHAR(64) DEFAULT '' /* 创建者 */,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 创建时间 */,
updater VARCHAR(64) DEFAULT '' /* 更新者 */,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 更新时间 */,
deleted BOOLEAN NOT NULL DEFAULT false /* 是否删除 */
);
COMMENT ON TABLE education_practice_session IS '教育-练习会话';
CREATE UNIQUE INDEX uk_tenant_client_session ON education_practice_session (tenant_id, client_session_id);
CREATE INDEX idx_tenant_user_status ON education_practice_session (tenant_id, user_id, status);
-- =============================================
-- 练习会话题目快照表
-- =============================================
-- Indexes:
-- uk_session_sequence — per-session uniqueness for question sequence numbers.
-- Used by: insertBatch to ensure no duplicate sequences within a session.
-- idx_session_id — covers selectBySessionIdOrderBySequence (load all questions
-- for a session, ordered by sequence). Also used by cascade delete lookups.
-- Lock impact: INSERT acquires gap locks within session_id range on uk_session_sequence;
-- concurrent inserts into different sessions are independent.
-- Options column: JSONB data type stores only label, content, order — never isCorrect.
-- Application layer (optionsToSafeJson) strips correctness before storage.
CREATE TABLE education_practice_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
session_id BIGINT NOT NULL /* 会话 ID */,
sequence INT NOT NULL /* 题目序号1-based服务端固定 */,
question_id VARCHAR(64) NOT NULL /* 原始题目 ID */,
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 快照时的题目内容版本 */,
stem TEXT NOT NULL /* 题干快照 */,
type VARCHAR(32) NOT NULL /* 题型快照 */,
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
options JSONB NOT NULL /* 选项快照 JSON不含 isCorrect */,
correct_answer TEXT DEFAULT NULL /* 正确答案快照 */,
explanation TEXT DEFAULT NULL /* 解析快照 */,
selected_answer TEXT DEFAULT NULL /* 学生已选答案 */,
is_answered BOOLEAN NOT NULL DEFAULT false /* 是否已作答 */,
creator VARCHAR(64) DEFAULT '' /* 创建者 */,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 创建时间 */,
updater VARCHAR(64) DEFAULT '' /* 更新者 */,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 更新时间 */,
deleted BOOLEAN NOT NULL DEFAULT false /* 是否删除 */
);
COMMENT ON TABLE education_practice_question IS '教育-练习会话题目快照';
CREATE UNIQUE INDEX uk_session_sequence ON education_practice_question (session_id, sequence);
CREATE INDEX idx_session_id ON education_practice_question (session_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify tables exist with correct structure:
-- \d education_practice_session
-- \d education_practice_question
-- Verify unique keys are enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_session' AND indexname = 'uk_tenant_client_session';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_question' AND indexname = 'uk_session_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_practice_session;
-- SELECT COUNT(*) FROM education_practice_question;

View File

@@ -0,0 +1,35 @@
-- =============================================
-- Education 模块 — 答案保存幂等性回滚 (PostgreSQL)
-- Ticket #7 / Migration 003
-- =============================================
--
-- WARNING: This file contains NO executable SQL.
-- Destructive rollback requires manual operator verification.
--
-- Manual rollback procedure (operator must execute):
-- 1. Verify no other tables depend on education_answer_idempotency:
-- SELECT tc.table_name, kcu.column_name, ccu.table_name AS referenced_table
-- FROM information_schema.table_constraints AS tc
-- JOIN information_schema.key_column_usage AS kcu
-- ON tc.constraint_name = kcu.constraint_name
-- JOIN information_schema.constraint_column_usage AS ccu
-- ON ccu.constraint_name = tc.constraint_name
-- WHERE tc.constraint_type = 'FOREIGN KEY'
-- AND ccu.table_name = 'education_answer_idempotency'
-- AND tc.table_catalog = current_database();
-- Result MUST be empty before proceeding.
--
-- 2. Verify the table contains only data from this migration:
-- SELECT COUNT(*) AS idempotency_count FROM education_answer_idempotency;
-- Operator must confirm this count is acceptable to destroy.
--
-- 3. Verify no application code depends on client_sequence column:
-- Search codebase for 'clientSequence' / 'client_sequence' references.
--
-- 4. After verification, execute:
-- DROP TABLE IF EXISTS education_answer_idempotency;
-- ALTER TABLE education_practice_question DROP COLUMN client_sequence;
--
-- DO NOT uncomment or execute the lines below without operator verification.
-- -- DROP TABLE IF EXISTS education_answer_idempotency;
-- -- ALTER TABLE education_practice_question DROP COLUMN client_sequence;

View File

@@ -0,0 +1,113 @@
-- =============================================
-- Education 模块 — 答案保存幂等性 DDL (PostgreSQL)
-- Ticket #7: 答案命令幂等、乐观锁并发控制、答案恢复
-- Migration: 003
-- Prerequisites: 002-education-practice-session.sql (session + question snapshots)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name = 'education_answer_idempotency';
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN ('education_practice_session', 'education_practice_question');
-- Result MUST be 2.
-- =============================================
-- 答案命令幂等表
-- =============================================
-- Purpose: Provide durable idempotency for answer save commands.
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response.
-- Same key + different request_hash → conflict.
-- Concurrent same-key inserts are resolved by unique constraint race handling.
--
-- Indexes:
-- uk_answer_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
-- INSERT during answer save. DuplicateKeyException catch for concurrent-create race resolution.
-- idx_tenant_session — covers lookup by session for audit/debug.
--
-- response_json: Stores the serialized answer response for replay after network timeout/retry.
-- request_hash: SHA-256 of canonical payload (sorted JSON fields) for content-based dedup.
CREATE TABLE education_answer_idempotency (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
operation VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_ANSWER',
idempotency_key VARCHAR(64) NOT NULL,
request_hash VARCHAR(64) NOT NULL,
session_id BIGINT NOT NULL,
question_id VARCHAR(64) NOT NULL,
selected_answer TEXT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
response_json TEXT NOT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_answer_idempotency IS '教育-答案命令幂等记录';
COMMENT ON COLUMN education_answer_idempotency.id IS '主键';
COMMENT ON COLUMN education_answer_idempotency.tenant_id IS '租户编号';
COMMENT ON COLUMN education_answer_idempotency.user_id IS '答题用户编号';
COMMENT ON COLUMN education_answer_idempotency.operation IS '操作类型SUBMIT_ANSWER';
COMMENT ON COLUMN education_answer_idempotency.idempotency_key IS '客户端幂等键UUID';
COMMENT ON COLUMN education_answer_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
COMMENT ON COLUMN education_answer_idempotency.session_id IS '会话 ID';
COMMENT ON COLUMN education_answer_idempotency.question_id IS '题目 ID';
COMMENT ON COLUMN education_answer_idempotency.selected_answer IS '学生已选答案';
COMMENT ON COLUMN education_answer_idempotency.status IS '状态ACCEPTED-已接受, CONFLICT-冲突';
COMMENT ON COLUMN education_answer_idempotency.response_json IS '首次成功响应 JSON用于重试重放';
COMMENT ON COLUMN education_answer_idempotency.creator IS '创建者';
COMMENT ON COLUMN education_answer_idempotency.create_time IS '创建时间';
COMMENT ON COLUMN education_answer_idempotency.updater IS '更新者';
COMMENT ON COLUMN education_answer_idempotency.update_time IS '更新时间';
COMMENT ON COLUMN education_answer_idempotency.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_answer_idempotency ON education_answer_idempotency (tenant_id, user_id, operation, idempotency_key);
CREATE INDEX idx_tenant_session ON education_answer_idempotency (tenant_id, session_id);
-- =============================================
-- PracticeQuestionDO: add client_sequence column
-- =============================================
-- Purpose: Track the last accepted client command sequence per question.
-- Rejects stale clientSequence: only sequences strictly greater than the
-- stored value are accepted (monotonic forward progression).
-- NULL means no answer has been accepted yet.
ALTER TABLE education_practice_question
ADD COLUMN client_sequence INT DEFAULT NULL;
CREATE INDEX idx_client_sequence ON education_practice_question (client_sequence);
-- =============================================
-- PracticeSessionDO: add last_client_sequence column
-- =============================================
-- Purpose: Session-wide monotonic counter for client commands.
-- Rejects stale clientSequence across questions (not just per-question).
-- CAS incrementVersion now updates this column alongside version.
-- NULL means no answer has been accepted yet for this session.
ALTER TABLE education_practice_session
ADD COLUMN last_client_sequence INT DEFAULT NULL;
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new table exists:
-- \d education_answer_idempotency
-- Verify unique key is enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_answer_idempotency' AND indexname = 'uk_answer_idempotency';
-- Verify column added to question table:
-- SELECT column_name, data_type, column_default
-- FROM information_schema.columns
-- WHERE table_catalog = current_database()
-- AND table_name = 'education_practice_question'
-- AND column_name = 'client_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_answer_idempotency;

View File

@@ -0,0 +1,50 @@
-- =============================================
-- Education 模块 — Ticket #8 迁移回滚 (PostgreSQL)
-- 004-education-submit-report-rollback.sql
-- =============================================
--
-- WARNING: This file contains NO executable SQL.
-- Destructive rollback requires manual operator verification.
--
-- Manual rollback procedure (operator must execute):
-- 1. Verify no other tables depend on these tables:
-- SELECT
-- tc.table_name,
-- kcu.column_name,
-- ccu.table_name AS referenced_table_name
-- FROM information_schema.table_constraints AS tc
-- JOIN information_schema.key_column_usage AS kcu
-- ON tc.constraint_name = kcu.constraint_name
-- JOIN information_schema.constraint_column_usage AS ccu
-- ON ccu.constraint_name = tc.constraint_name
-- WHERE tc.constraint_type = 'FOREIGN KEY'
-- AND ccu.table_name IN ('education_submit_idempotency',
-- 'education_practice_report', 'education_practice_report_detail')
-- AND ccu.table_catalog = current_database();
-- Result MUST be empty before proceeding.
--
-- 2. Verify columns are not referenced by application code:
-- Search codebase for 'correct_answer' / 'explanation' references in
-- education_practice_question to confirm no other consumers.
--
-- 3. Verify the tables contain only data from this migration:
-- SELECT COUNT(*) AS idempotency_count FROM education_submit_idempotency;
-- SELECT COUNT(*) AS report_count FROM education_practice_report;
-- SELECT COUNT(*) AS detail_count FROM education_practice_report_detail;
-- Operator must confirm these counts are acceptable to destroy.
--
-- 4. After verification, execute:
-- DROP TABLE IF EXISTS education_practice_report_detail;
-- DROP TABLE IF EXISTS education_practice_report;
-- DROP TABLE IF EXISTS education_submit_idempotency;
-- ALTER TABLE education_practice_question
-- DROP COLUMN correct_answer,
-- DROP COLUMN explanation;
--
-- DO NOT uncomment or execute the lines below without operator verification.
-- -- DROP TABLE IF EXISTS education_practice_report_detail;
-- -- DROP TABLE IF EXISTS education_practice_report;
-- -- DROP TABLE IF EXISTS education_submit_idempotency;
-- -- ALTER TABLE education_practice_question
-- -- DROP COLUMN correct_answer,
-- -- DROP COLUMN explanation;

View File

@@ -0,0 +1,220 @@
-- =============================================
-- Education 模块 — 交卷提交与成绩报告 DDL (PostgreSQL)
-- Ticket #8: 交卷 CAS、保护性答案快照、评分与报告
-- Migration: 004
-- Prerequisites: 003-education-answer-idempotency.sql
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN ('education_submit_idempotency',
-- 'education_practice_report',
-- 'education_practice_report_detail');
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN ('education_practice_session',
-- 'education_practice_question',
-- 'education_answer_idempotency');
-- Result MUST be 3.
-- =============================================
-- PracticeQuestionDO: add protected answer snapshot columns
-- =============================================
-- Purpose: At session creation, snapshot correct_answer and explanation
-- from the full CatalogQuestionDTO. These fields are NEVER exposed
-- before submission (enforced by SafeQuestionRespVO allow-list and
-- PracticeQuestionRespVO which does not include them).
ALTER TABLE education_practice_question
ADD COLUMN correct_answer TEXT DEFAULT NULL,
ADD COLUMN explanation TEXT DEFAULT NULL;
COMMENT ON COLUMN education_practice_question.correct_answer IS '正确答案快照(不可在交卷前暴露)';
COMMENT ON COLUMN education_practice_question.explanation IS '解析快照(不可在交卷前暴露)';
-- =============================================
-- 交卷幂等表
-- =============================================
-- Purpose: Provide durable idempotency for submit-session commands.
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original report.
-- Same key + different request_hash → conflict.
-- Concurrent same-key inserts resolved by unique constraint race handling.
--
-- Indexes:
-- uk_submit_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
-- INSERT during submit. DuplicateKeyException catch for concurrent-create race resolution.
-- idx_submit_session — covers lookup by session for audit/debug.
CREATE TABLE education_submit_idempotency (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
operation VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_SESSION',
idempotency_key VARCHAR(64) NOT NULL,
request_hash VARCHAR(64) NOT NULL,
session_id BIGINT NOT NULL,
report_id BIGINT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
response_json TEXT NOT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_submit_idempotency IS '教育-交卷幂等记录';
COMMENT ON COLUMN education_submit_idempotency.id IS '主键';
COMMENT ON COLUMN education_submit_idempotency.tenant_id IS '租户编号';
COMMENT ON COLUMN education_submit_idempotency.user_id IS '交卷用户编号';
COMMENT ON COLUMN education_submit_idempotency.operation IS '操作类型SUBMIT_SESSION';
COMMENT ON COLUMN education_submit_idempotency.idempotency_key IS '客户端幂等键UUID';
COMMENT ON COLUMN education_submit_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
COMMENT ON COLUMN education_submit_idempotency.session_id IS '会话 ID';
COMMENT ON COLUMN education_submit_idempotency.report_id IS '关联的报告 ID成功时有值';
COMMENT ON COLUMN education_submit_idempotency.status IS '状态ACCEPTED-已接受, CONFLICT-冲突';
COMMENT ON COLUMN education_submit_idempotency.response_json IS '首次成功响应 JSON用于重试重放';
COMMENT ON COLUMN education_submit_idempotency.creator IS '创建者';
COMMENT ON COLUMN education_submit_idempotency.create_time IS '创建时间';
COMMENT ON COLUMN education_submit_idempotency.updater IS '更新者';
COMMENT ON COLUMN education_submit_idempotency.update_time IS '更新时间';
COMMENT ON COLUMN education_submit_idempotency.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_submit_idempotency ON education_submit_idempotency (tenant_id, user_id, operation, idempotency_key);
CREATE INDEX idx_submit_session ON education_submit_idempotency (tenant_id, session_id);
-- =============================================
-- 练习报告表(会话级)
-- =============================================
-- Purpose: Store the computed scoring result for a submitted session.
-- One report per session. Immutable after creation.
-- Question snapshots (stem, selectedAnswer, correctAnswer, explanation)
-- are stored in report_details so source question edits don't affect history.
--
-- Indexes:
-- uk_report_session — one report per session (unique).
-- idx_report_tenant_user — covers paginated history queries for current tenant+user.
-- idx_report_create_time — covers time-sorted listing.
CREATE TABLE education_practice_report (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
session_id BIGINT NOT NULL,
question_count INT NOT NULL,
answered_count INT NOT NULL DEFAULT 0,
unanswered_count INT NOT NULL DEFAULT 0,
correct_count INT NOT NULL DEFAULT 0,
incorrect_count INT NOT NULL DEFAULT 0,
score INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'SUBMITTED',
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_practice_report IS '教育-练习报告';
COMMENT ON COLUMN education_practice_report.id IS '主键';
COMMENT ON COLUMN education_practice_report.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_report.user_id IS '用户编号';
COMMENT ON COLUMN education_practice_report.session_id IS '会话 ID';
COMMENT ON COLUMN education_practice_report.question_count IS '题目总数';
COMMENT ON COLUMN education_practice_report.answered_count IS '已答题数';
COMMENT ON COLUMN education_practice_report.unanswered_count IS '未答题数';
COMMENT ON COLUMN education_practice_report.correct_count IS '正确题数';
COMMENT ON COLUMN education_practice_report.incorrect_count IS '错误题数';
COMMENT ON COLUMN education_practice_report.score IS '得分(整数,满分 100 为基准)';
COMMENT ON COLUMN education_practice_report.status IS '报告状态SUBMITTED';
COMMENT ON COLUMN education_practice_report.creator IS '创建者';
COMMENT ON COLUMN education_practice_report.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_report.updater IS '更新者';
COMMENT ON COLUMN education_practice_report.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_report.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_report_session ON education_practice_report (session_id);
CREATE INDEX idx_report_tenant_user ON education_practice_report (tenant_id, user_id);
CREATE INDEX idx_report_create_time ON education_practice_report (create_time);
-- =============================================
-- 练习报告明细表(逐题结果)
-- =============================================
-- Purpose: Store per-question scoring results at submission time.
-- Includes snapshot of stem, selectedAnswer, correctAnswer, and explanation
-- so that history is stable even if source questions are later edited.
--
-- Indexes:
-- uk_report_sequence — per-report uniqueness for question sequence.
-- idx_detail_session — covers lookup by session for report assembly.
CREATE TABLE education_practice_report_detail (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
report_id BIGINT NOT NULL,
session_id BIGINT NOT NULL,
question_id VARCHAR(64) NOT NULL,
sequence INT NOT NULL,
stem TEXT NOT NULL,
type VARCHAR(32) NOT NULL,
difficulty VARCHAR(32) DEFAULT NULL,
selected_answer TEXT DEFAULT NULL,
correct_answer TEXT DEFAULT NULL,
is_correct BOOLEAN NOT NULL DEFAULT false,
explanation TEXT DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_practice_report_detail IS '教育-练习报告明细';
COMMENT ON COLUMN education_practice_report_detail.id IS '主键';
COMMENT ON COLUMN education_practice_report_detail.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_report_detail.user_id IS '用户编号';
COMMENT ON COLUMN education_practice_report_detail.report_id IS '报告 ID';
COMMENT ON COLUMN education_practice_report_detail.session_id IS '会话 ID';
COMMENT ON COLUMN education_practice_report_detail.question_id IS '原始题目 ID';
COMMENT ON COLUMN education_practice_report_detail.sequence IS '题目序号1-based';
COMMENT ON COLUMN education_practice_report_detail.stem IS '题干快照';
COMMENT ON COLUMN education_practice_report_detail.type IS '题型快照';
COMMENT ON COLUMN education_practice_report_detail.difficulty IS '难度快照';
COMMENT ON COLUMN education_practice_report_detail.selected_answer IS '学生已选答案';
COMMENT ON COLUMN education_practice_report_detail.correct_answer IS '正确答案快照';
COMMENT ON COLUMN education_practice_report_detail.is_correct IS '是否正确';
COMMENT ON COLUMN education_practice_report_detail.explanation IS '解析快照';
COMMENT ON COLUMN education_practice_report_detail.creator IS '创建者';
COMMENT ON COLUMN education_practice_report_detail.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_report_detail.updater IS '更新者';
COMMENT ON COLUMN education_practice_report_detail.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_report_detail.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_report_sequence ON education_practice_report_detail (report_id, sequence);
CREATE INDEX idx_detail_session ON education_practice_report_detail (session_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new tables exist:
-- \d education_submit_idempotency
-- \d education_practice_report
-- \d education_practice_report_detail
-- Verify columns added to question table:
-- SELECT column_name, data_type, column_default
-- FROM information_schema.columns
-- WHERE table_catalog = current_database()
-- AND table_name = 'education_practice_question'
-- AND column_name IN ('correct_answer', 'explanation');
-- Verify unique keys are enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_submit_idempotency' AND indexname = 'uk_submit_idempotency';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_report' AND indexname = 'uk_report_session';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_report_detail' AND indexname = 'uk_report_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_submit_idempotency;
-- SELECT COUNT(*) FROM education_practice_report;
-- SELECT COUNT(*) FROM education_practice_report_detail;

View File

@@ -0,0 +1,25 @@
-- =============================================
-- Education 模块 — 错题本 DDL Rollback
-- Migration: 005
-- =============================================
-- IMPORTANT: This is a documentation-only rollback.
-- No DROP/ALTER/DELETE statements are executed. The wrong_question
-- table is provenance-safe: it only accumulates data and mastering
-- is a status flag. Dropping these tables would lose student error
-- history with no recovery path.
--
-- What this migration created:
-- - education_wrong_question (new table)
-- - education_wrong_question_idempotency (new table)
-- - education_practice_report_detail.options (new column)
-- - education_practice_session.review_fingerprint (new column)
--
-- Manual rollback requires:
-- 1. Verified database backup before rollback
-- 2. Operator approval (DBA sign-off)
-- 3. Provenance of all wrong-question records preserved (exported)
-- 4. Soft-delete via deleted = true before any hard drop
--
-- These tables are NOT deleted by this script. Wrong history is
-- retained; if deletion is required by external policy, consult
-- the DBA for a verified rollback procedure.

View File

@@ -0,0 +1,156 @@
-- =============================================
-- Education 模块 — 错题本 DDL
-- Ticket #9: 错题自动收集、复习练习创建
-- Migration: 005
-- Prerequisites: 004-education-submit-report.sql (report + detail tables)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = current_database()
-- AND table_name IN ('education_wrong_question',
-- 'education_wrong_question_idempotency');
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = current_database()
-- AND table_name IN ('education_practice_report',
-- 'education_practice_report_detail');
-- Result MUST be 2.
-- =============================================
-- PracticeReportDetailDO: add content_version + options snapshot columns
-- =============================================
-- Purpose: At submit time, snapshot question content version and options
-- (without isCorrect) so wrong-question book and review sessions have
-- stable display data. The options are already stripped of isCorrect
-- by the submit flow.
ALTER TABLE education_practice_report_detail
ADD COLUMN content_version VARCHAR(64) NOT NULL DEFAULT '' /* 题目内容版本快照 */,
ADD COLUMN options TEXT DEFAULT NULL /* 选项快照 JSON不含 isCorrect */;
-- =============================================
-- PracticeSessionDO: add review fingerprint column
-- =============================================
-- Purpose: Persist the canonical fingerprint of wrong-question IDs used
-- to create a review session. On idempotent replay, the fingerprint
-- is compared: same tenant+clientSessionId+sameUser+sortedIDs match
-- returns the existing session; different ID set returns
-- SESSION_IDEMPOTENCY_MISMATCH.
ALTER TABLE education_practice_session
ADD COLUMN review_fingerprint VARCHAR(64) DEFAULT NULL /* 复习会话题目指纹SHA-256 of sorted unique wrongQuestionIds */;
-- 错题表
-- =============================================
-- Purpose: Persistent wrong-question book per student.
-- Each (tenant, user, question) is a unique entry.
-- Repeated wrong answers on the SAME question increment wrong_count
-- and update last_wrong_time. The idempotency guard table ensures
-- each (tenant, user, question, report) can upsert at most once.
--
-- master_status values: 'PENDING' (default) | 'MASTERED'
-- Marking mastered retains the full history and count; it does NOT
-- delete or archive the record. Students can optionally un-master.
--
-- Snapshot fields (stem, type, difficulty, options, content_version):
-- populated from the latest report detail that touched this question.
-- These are for listing/detail display without joining report details.
--
-- latest_correct_answer, latest_explanation:
-- also from the latest report detail; available for detail display
-- post-submit (not exposed in review session creation pre-submit).
--
-- Indexes:
-- uk_tenant_user_question — per-tenant, per-user, per-question uniqueness.
-- INSERT ... ON CONFLICT ... DO UPDATE is the primary write path.
-- idx_tenant_user_status — covers filtered list queries (page with status filter).
-- idx_tenant_user_last_wrong — covers time-sorted listing.
CREATE TABLE education_wrong_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
user_id BIGINT NOT NULL /* 学生用户编号 */,
question_id VARCHAR(64) NOT NULL /* 原始题目 ID */,
-- snapshot fields for listing / detail (from latest report detail)
stem TEXT NOT NULL /* 题干快照(最新) */,
type VARCHAR(32) NOT NULL /* 题型快照 */,
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
options JSONB NOT NULL /* 选项快照 JSON不含 isCorrect */,
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 题目内容版本 */,
latest_correct_answer TEXT DEFAULT NULL /* 正确答案快照(最新,供详情展示) */,
latest_explanation TEXT DEFAULT NULL /* 解析快照(最新,供详情展示) */,
-- timing & count
first_wrong_time TIMESTAMP NOT NULL /* 首次错误时间 */,
last_wrong_time TIMESTAMP NOT NULL /* 最近错误时间 */,
wrong_count INT NOT NULL DEFAULT 1 /* 累计错误次数 */,
-- mastery
master_status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
/* 掌握状态PENDING-待掌握, MASTERED-已掌握 */,
mastered_time TIMESTAMP DEFAULT NULL /* 标记掌握时间 */,
-- provenance
last_report_id BIGINT DEFAULT NULL /* 最近关联的报告 ID */,
last_session_id BIGINT DEFAULT NULL /* 最近关联的会话 ID */,
-- audit
creator VARCHAR(64) DEFAULT '' /* 创建者 */,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 创建时间 */,
updater VARCHAR(64) DEFAULT '' /* 更新者 */,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 更新时间 */,
deleted BOOLEAN NOT NULL DEFAULT false /* 是否删除 */
);
COMMENT ON TABLE education_wrong_question IS '教育-错题本';
CREATE UNIQUE INDEX uk_tenant_user_question ON education_wrong_question (tenant_id, user_id, question_id);
CREATE INDEX idx_tenant_user_status ON education_wrong_question (tenant_id, user_id, master_status);
CREATE INDEX idx_tenant_user_last_wrong ON education_wrong_question (tenant_id, user_id, last_wrong_time);
-- =============================================
-- 错题流水幂等表
-- =============================================
-- Purpose: Ensure each (tenant, user, question, report) upserts the
-- wrong-question book exactly once. The submitSession transaction
-- INSERT ... ON CONFLICT DO NOTHING into this table BEFORE the
-- wrong question upsert; a duplicate means this report already
-- contributed to the count. This guards against:
-- - Replayed submit (idempotent resubmit) double-counting
-- - Concurrent submit races where both threads evaluate the
-- same report details
--
-- Indexes:
-- uk_tenant_user_question_report — per (tenant, user, question, report) uniqueness.
-- INSERT ... ON CONFLICT DO NOTHING provides the idempotency guard
-- BEFORE upserting. wrong_question_id is filled after upsert for
-- audit purposes.
-- idx_report — fast lookup by report for audit/debug.
CREATE TABLE education_wrong_question_idempotency (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
user_id BIGINT NOT NULL /* 学生用户编号 */,
wrong_question_id BIGINT DEFAULT NULL /* 错题记录 IDupsert 后填充) */,
report_id BIGINT NOT NULL /* 报告 ID */,
question_id VARCHAR(64) NOT NULL /* 题目 ID */,
creator VARCHAR(64) DEFAULT '' /* 创建者 */,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 创建时间 */,
updater VARCHAR(64) DEFAULT '' /* 更新者 */,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 更新时间 */,
deleted BOOLEAN NOT NULL DEFAULT false /* 是否删除 */
);
COMMENT ON TABLE education_wrong_question_idempotency IS '教育-错题流水幂等';
CREATE UNIQUE INDEX uk_tenant_user_question_report ON education_wrong_question_idempotency (tenant_id, user_id, question_id, report_id);
CREATE INDEX idx_report ON education_wrong_question_idempotency (report_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new tables exist:
-- \d education_wrong_question
-- \d education_wrong_question_idempotency
-- Verify unique keys are enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_wrong_question' AND indexname = 'uk_tenant_user_question';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_wrong_question_idempotency' AND indexname = 'uk_tenant_user_question_report';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_wrong_question;
-- SELECT COUNT(*) FROM education_wrong_question_idempotency;

View File

@@ -0,0 +1,25 @@
-- =============================================
-- Education 模块 — 收藏夹 DDL Rollback (PostgreSQL)
-- Migration: 007
-- =============================================
-- IMPORTANT: This is a documentation-only rollback.
-- No DROP/ALTER/DELETE statements are executed. The favorite
-- table is provenance-safe: it only accumulates user preference
-- data. Dropping this table would lose student favorites with
-- no recovery path.
--
-- What this migration created:
-- - education_favorite (new table)
-- - idx_tenant_user (index)
-- - idx_tenant_user_target_type (index)
-- - uk_tenant_user_target (unique constraint)
--
-- Manual rollback requires:
-- 1. Verified database backup before rollback
-- 2. Operator approval (DBA sign-off)
-- 3. Provenance of all favorite records preserved (exported)
-- 4. Soft-delete via deleted = true before any hard drop
--
-- These tables are NOT deleted by this script. Favorite history is
-- retained; if deletion is required by external policy, consult
-- the DBA for a verified rollback procedure.

View File

@@ -0,0 +1,95 @@
-- =============================================
-- Education 模块 — 收藏夹 DDL (PostgreSQL)
-- Ticket #10: 学生收藏题目
-- Migration: 007
-- Prerequisites: 000-education-schema.sql (base tables)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name = 'education_favorite';
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 收藏表
-- =============================================
-- Purpose: Student favorites for questions with safe snapshots.
-- Each (tenant, user, target_type, target_id) is a unique entry.
-- Logical deletion: setting deleted=true marks as unfavorited.
-- Re-adding after deletion reactivates the row via ON CONFLICT ... DO UPDATE.
--
-- target_type values: 'QUESTION' (extensible enum)
--
-- Snapshot fields (stem, type, difficulty, options, content_version):
-- populated at creation time from the visible question's safe fields.
-- These snapshots preserve the question state as it appeared when favorited,
-- and remain stable even if the source question later changes or becomes unavailable.
--
-- available flag:
-- FALSE when the source question becomes hidden/unpublished after being
-- favorited. Existing favorites with available=FALSE remain listable but
-- display an "unavailable" indicator. New favorites cannot be created for
-- unavailable resources.
--
-- Indexes:
-- uk_tenant_user_target — per (tenant, user, target_type, target_id) uniqueness.
-- INSERT ... ON CONFLICT DO UPDATE is the primary reactivation path.
-- idx_tenant_user — covers listing queries filtered by current tenant+user.
-- idx_tenant_user_target_type — covers target-type-filtered listing.
CREATE TABLE education_favorite (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_id VARCHAR(64) NOT NULL,
-- safe snapshot fields
stem TEXT DEFAULT NULL,
type VARCHAR(32) DEFAULT NULL,
difficulty VARCHAR(32) DEFAULT NULL,
options JSONB DEFAULT NULL,
content_version VARCHAR(64) NOT NULL DEFAULT '',
-- availability
available BOOLEAN NOT NULL DEFAULT true,
-- audit
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT uk_tenant_user_target UNIQUE (tenant_id, user_id, target_type, target_id)
);
COMMENT ON TABLE education_favorite IS '教育-收藏夹';
COMMENT ON COLUMN education_favorite.id IS '主键';
COMMENT ON COLUMN education_favorite.tenant_id IS '租户编号';
COMMENT ON COLUMN education_favorite.user_id IS '学生用户编号';
COMMENT ON COLUMN education_favorite.target_type IS '目标类型QUESTION';
COMMENT ON COLUMN education_favorite.target_id IS '目标 ID题目 ID';
COMMENT ON COLUMN education_favorite.stem IS '题干快照';
COMMENT ON COLUMN education_favorite.type IS '题型快照';
COMMENT ON COLUMN education_favorite.difficulty IS '难度快照';
COMMENT ON COLUMN education_favorite.options IS '选项快照 JSON不含 isCorrect';
COMMENT ON COLUMN education_favorite.content_version IS '题目内容版本';
COMMENT ON COLUMN education_favorite.available IS '源资源是否可用';
COMMENT ON COLUMN education_favorite.creator IS '创建者';
COMMENT ON COLUMN education_favorite.create_time IS '创建时间';
COMMENT ON COLUMN education_favorite.updater IS '更新者';
COMMENT ON COLUMN education_favorite.update_time IS '更新时间';
COMMENT ON COLUMN education_favorite.deleted IS '是否删除';
CREATE INDEX idx_tenant_user ON education_favorite (tenant_id, user_id);
CREATE INDEX idx_tenant_user_target_type ON education_favorite (tenant_id, user_id, target_type);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new table exists:
-- \d education_favorite
-- Verify unique constraint is enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_favorite' AND indexname = 'uk_tenant_user_target';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_favorite;

View File

@@ -0,0 +1,17 @@
-- =============================================
-- Education 模块 — 题库目录表回滚 (PostgreSQL)
-- Migration: 008 rollback
-- Drops all 11 tables created by 008-education-catalog.sql
-- =============================================
DROP TABLE IF EXISTS education_question_collection_question;
DROP TABLE IF EXISTS education_practice_blueprint;
DROP TABLE IF EXISTS education_question;
DROP TABLE IF EXISTS education_question_collection;
DROP TABLE IF EXISTS education_content_node;
DROP TABLE IF EXISTS education_content_entry;
DROP TABLE IF EXISTS education_category;
DROP TABLE IF EXISTS education_subject;
DROP TABLE IF EXISTS education_major;
DROP TABLE IF EXISTS education_school;
DROP TABLE IF EXISTS education_region;

View File

@@ -0,0 +1,530 @@
-- =============================================
-- Education 模块 — 题库目录原生表 DDL (PostgreSQL)
-- Ticket #20: 原生实现题库目录浏览,替换 Scalar adapter
-- Migration: 008
-- Prerequisites: 000-education-schema.sql (database creation)
-- 001-education-tenant-seed.sql (tenant seed data)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Verify none of these tables already exist:
-- SELECT table_name FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN (
-- 'education_region', 'education_school', 'education_major',
-- 'education_subject', 'education_category',
-- 'education_content_entry', 'education_content_node',
-- 'education_question_collection', 'education_question',
-- 'education_practice_blueprint', 'education_question_collection_question'
-- );
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 1. 地区表
-- =============================================
-- Indexes:
-- idx_tenant — tenant-scoped queries (tenant-aware interceptor default)
-- idx_tenant_active_order — active region listing ordered by display order
CREATE TABLE education_region (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
name VARCHAR(100) NOT NULL,
code VARCHAR(32) DEFAULT NULL,
short_name VARCHAR(50) DEFAULT NULL,
full_name VARCHAR(200) DEFAULT NULL,
icon VARCHAR(500) DEFAULT NULL,
pinyin VARCHAR(200) DEFAULT NULL,
is_hot BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_region IS '教育-地区';
COMMENT ON COLUMN education_region.id IS '主键';
COMMENT ON COLUMN education_region.tenant_id IS '租户编号';
COMMENT ON COLUMN education_region.legacy_id IS '旧系统地区 ID';
COMMENT ON COLUMN education_region.name IS '地区名称';
COMMENT ON COLUMN education_region.code IS '地区编码';
COMMENT ON COLUMN education_region.short_name IS '地区简称';
COMMENT ON COLUMN education_region.full_name IS '地区全称';
COMMENT ON COLUMN education_region.icon IS '地区图标 URL';
COMMENT ON COLUMN education_region.pinyin IS '地区拼音';
COMMENT ON COLUMN education_region.is_hot IS '是否热门地区';
COMMENT ON COLUMN education_region.is_active IS '是否启用';
COMMENT ON COLUMN education_region.sort_order IS '显示排序值';
COMMENT ON COLUMN education_region.creator IS '创建者';
COMMENT ON COLUMN education_region.create_time IS '创建时间';
COMMENT ON COLUMN education_region.updater IS '更新者';
COMMENT ON COLUMN education_region.update_time IS '更新时间';
COMMENT ON COLUMN education_region.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_region (tenant_id);
CREATE INDEX idx_tenant_active_order ON education_region (tenant_id, is_active, sort_order);
-- =============================================
-- 2. 院校表
-- =============================================
CREATE TABLE education_school (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
region_id BIGINT DEFAULT NULL,
module_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
professional_exam_date VARCHAR(200) DEFAULT NULL,
metadata JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_school IS '教育-院校';
COMMENT ON COLUMN education_school.id IS '主键';
COMMENT ON COLUMN education_school.tenant_id IS '租户编号';
COMMENT ON COLUMN education_school.legacy_id IS '旧系统院校 ID';
COMMENT ON COLUMN education_school.region_id IS '地区 ID';
COMMENT ON COLUMN education_school.module_id IS '地区模块 ID';
COMMENT ON COLUMN education_school.name IS '院校名称';
COMMENT ON COLUMN education_school.professional_exam_date IS '专业考试日期说明';
COMMENT ON COLUMN education_school.metadata IS '院校扩展数据';
COMMENT ON COLUMN education_school.is_active IS '是否启用';
COMMENT ON COLUMN education_school.sort_order IS '显示排序值';
COMMENT ON COLUMN education_school.creator IS '创建者';
COMMENT ON COLUMN education_school.create_time IS '创建时间';
COMMENT ON COLUMN education_school.updater IS '更新者';
COMMENT ON COLUMN education_school.update_time IS '更新时间';
COMMENT ON COLUMN education_school.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_school (tenant_id);
CREATE INDEX idx_tenant_region ON education_school (tenant_id, region_id);
-- =============================================
-- 3. 专业表
-- =============================================
CREATE TABLE education_major (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
region_id BIGINT DEFAULT NULL,
school_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
description VARCHAR(500) DEFAULT NULL,
study_tips VARCHAR(500) DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_major IS '教育-专业';
COMMENT ON COLUMN education_major.id IS '主键';
COMMENT ON COLUMN education_major.tenant_id IS '租户编号';
COMMENT ON COLUMN education_major.legacy_id IS '旧系统专业 ID';
COMMENT ON COLUMN education_major.region_id IS '地区 ID';
COMMENT ON COLUMN education_major.school_id IS '院校 ID';
COMMENT ON COLUMN education_major.name IS '专业名称';
COMMENT ON COLUMN education_major.description IS '专业说明';
COMMENT ON COLUMN education_major.study_tips IS '学习建议';
COMMENT ON COLUMN education_major.is_active IS '是否启用';
COMMENT ON COLUMN education_major.sort_order IS '显示排序值';
COMMENT ON COLUMN education_major.creator IS '创建者';
COMMENT ON COLUMN education_major.create_time IS '创建时间';
COMMENT ON COLUMN education_major.updater IS '更新者';
COMMENT ON COLUMN education_major.update_time IS '更新时间';
COMMENT ON COLUMN education_major.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_major (tenant_id);
CREATE INDEX idx_tenant_school ON education_major (tenant_id, school_id);
-- =============================================
-- 4. 科目表
-- =============================================
CREATE TABLE education_subject (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
region_id BIGINT DEFAULT NULL,
school_id BIGINT DEFAULT NULL,
major_id BIGINT DEFAULT NULL,
module_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
type VARCHAR(50) DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_subject IS '教育-科目';
COMMENT ON COLUMN education_subject.id IS '主键';
COMMENT ON COLUMN education_subject.tenant_id IS '租户编号';
COMMENT ON COLUMN education_subject.region_id IS '地区 ID';
COMMENT ON COLUMN education_subject.school_id IS '院校 ID';
COMMENT ON COLUMN education_subject.major_id IS '专业 ID';
COMMENT ON COLUMN education_subject.module_id IS '地区模块 ID';
COMMENT ON COLUMN education_subject.name IS '科目名称';
COMMENT ON COLUMN education_subject.type IS '科目类型';
COMMENT ON COLUMN education_subject.is_active IS '是否启用';
COMMENT ON COLUMN education_subject.sort_order IS '显示排序值';
COMMENT ON COLUMN education_subject.creator IS '创建者';
COMMENT ON COLUMN education_subject.create_time IS '创建时间';
COMMENT ON COLUMN education_subject.updater IS '更新者';
COMMENT ON COLUMN education_subject.update_time IS '更新时间';
COMMENT ON COLUMN education_subject.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_subject (tenant_id);
CREATE INDEX idx_tenant_major ON education_subject (tenant_id, major_id);
-- =============================================
-- 5. 题目分类表
-- =============================================
CREATE TABLE education_category (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
subject_id BIGINT DEFAULT NULL,
legacy_node_id VARCHAR(64) DEFAULT NULL,
name VARCHAR(200) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_category IS '教育-题目分类';
COMMENT ON COLUMN education_category.id IS '主键';
COMMENT ON COLUMN education_category.tenant_id IS '租户编号';
COMMENT ON COLUMN education_category.subject_id IS '科目 ID';
COMMENT ON COLUMN education_category.legacy_node_id IS '旧导航节点 ID';
COMMENT ON COLUMN education_category.name IS '分类名称';
COMMENT ON COLUMN education_category.is_active IS '是否启用';
COMMENT ON COLUMN education_category.sort_order IS '显示排序值';
COMMENT ON COLUMN education_category.creator IS '创建者';
COMMENT ON COLUMN education_category.create_time IS '创建时间';
COMMENT ON COLUMN education_category.updater IS '更新者';
COMMENT ON COLUMN education_category.update_time IS '更新时间';
COMMENT ON COLUMN education_category.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_category (tenant_id);
CREATE INDEX idx_tenant_subject ON education_category (tenant_id, subject_id);
-- =============================================
-- 6. 内容入口表
-- =============================================
-- Indexes:
-- uk_tenant_entry_key — per-tenant uniqueness for entry key
CREATE TABLE education_content_entry (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
region_id BIGINT DEFAULT NULL,
entry_key VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
entry_type VARCHAR(50) NOT NULL,
icon VARCHAR(500) DEFAULT NULL,
route VARCHAR(200) DEFAULT NULL,
description VARCHAR(500) DEFAULT NULL,
visibility VARCHAR(50) NOT NULL DEFAULT 'PUBLIC',
access_rules JSONB DEFAULT NULL,
layout_config JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_content_entry IS '教育-内容入口';
COMMENT ON COLUMN education_content_entry.id IS '主键';
COMMENT ON COLUMN education_content_entry.tenant_id IS '租户编号';
COMMENT ON COLUMN education_content_entry.legacy_id IS '旧系统内容入口 ID';
COMMENT ON COLUMN education_content_entry.region_id IS '地区 ID';
COMMENT ON COLUMN education_content_entry.entry_key IS '内容入口唯一键';
COMMENT ON COLUMN education_content_entry.name IS '内容入口名称';
COMMENT ON COLUMN education_content_entry.entry_type IS '内容入口类型';
COMMENT ON COLUMN education_content_entry.icon IS '图标';
COMMENT ON COLUMN education_content_entry.route IS '前端路由';
COMMENT ON COLUMN education_content_entry.description IS '入口说明';
COMMENT ON COLUMN education_content_entry.visibility IS '可见性';
COMMENT ON COLUMN education_content_entry.access_rules IS '访问规则 JSON';
COMMENT ON COLUMN education_content_entry.layout_config IS '布局配置 JSON';
COMMENT ON COLUMN education_content_entry.is_active IS '是否启用';
COMMENT ON COLUMN education_content_entry.sort_order IS '显示排序值';
COMMENT ON COLUMN education_content_entry.creator IS '创建者';
COMMENT ON COLUMN education_content_entry.create_time IS '创建时间';
COMMENT ON COLUMN education_content_entry.updater IS '更新者';
COMMENT ON COLUMN education_content_entry.update_time IS '更新时间';
COMMENT ON COLUMN education_content_entry.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_content_entry (tenant_id);
CREATE UNIQUE INDEX uk_tenant_entry_key ON education_content_entry (tenant_id, entry_key);
-- =============================================
-- 7. 内容节点表(树形结构)
-- =============================================
-- Indexes:
-- idx_entry_parent — supports tree navigation: find children of a parent within an entry
-- idx_tenant_entry — tenant-scoped queries by entry
CREATE TABLE education_content_node (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
entry_id BIGINT NOT NULL,
parent_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
title VARCHAR(200) DEFAULT NULL,
node_type VARCHAR(50) NOT NULL,
marker_type VARCHAR(50) DEFAULT NULL,
depth INT NOT NULL DEFAULT 0,
is_leaf BOOLEAN NOT NULL DEFAULT false,
is_selectable BOOLEAN NOT NULL DEFAULT true,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_content_node IS '教育-内容节点';
COMMENT ON COLUMN education_content_node.id IS '主键';
COMMENT ON COLUMN education_content_node.tenant_id IS '租户编号';
COMMENT ON COLUMN education_content_node.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_content_node.parent_id IS '父节点 IDNULL=根节点)';
COMMENT ON COLUMN education_content_node.name IS '节点名称';
COMMENT ON COLUMN education_content_node.title IS '节点标题';
COMMENT ON COLUMN education_content_node.node_type IS '节点类型';
COMMENT ON COLUMN education_content_node.marker_type IS '节点标记类型';
COMMENT ON COLUMN education_content_node.depth IS '树深度';
COMMENT ON COLUMN education_content_node.is_leaf IS '是否叶子节点';
COMMENT ON COLUMN education_content_node.is_selectable IS '是否可被选择';
COMMENT ON COLUMN education_content_node.is_active IS '是否启用';
COMMENT ON COLUMN education_content_node.sort_order IS '显示排序值';
COMMENT ON COLUMN education_content_node.metadata IS '扩展元数据';
COMMENT ON COLUMN education_content_node.creator IS '创建者';
COMMENT ON COLUMN education_content_node.create_time IS '创建时间';
COMMENT ON COLUMN education_content_node.updater IS '更新者';
COMMENT ON COLUMN education_content_node.update_time IS '更新时间';
COMMENT ON COLUMN education_content_node.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_content_node (tenant_id);
CREATE INDEX idx_entry_parent ON education_content_node (entry_id, parent_id);
CREATE INDEX idx_tenant_entry ON education_content_node (tenant_id, entry_id);
-- =============================================
-- 8. 题集表
-- =============================================
CREATE TABLE education_question_collection (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
entry_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
title VARCHAR(200) DEFAULT NULL,
collection_type VARCHAR(50) NOT NULL,
question_count INT NOT NULL DEFAULT 0,
duration_minutes INT DEFAULT NULL,
access_rules JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_question_collection IS '教育-题集';
COMMENT ON COLUMN education_question_collection.id IS '主键';
COMMENT ON COLUMN education_question_collection.tenant_id IS '租户编号';
COMMENT ON COLUMN education_question_collection.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_question_collection.node_id IS '内容节点 ID';
COMMENT ON COLUMN education_question_collection.name IS '题集名称';
COMMENT ON COLUMN education_question_collection.title IS '题集标题';
COMMENT ON COLUMN education_question_collection.collection_type IS '题集类型';
COMMENT ON COLUMN education_question_collection.question_count IS '题目数量';
COMMENT ON COLUMN education_question_collection.duration_minutes IS '建议作答时长(分钟)';
COMMENT ON COLUMN education_question_collection.access_rules IS '访问规则 JSON';
COMMENT ON COLUMN education_question_collection.is_active IS '是否启用';
COMMENT ON COLUMN education_question_collection.sort_order IS '显示排序值';
COMMENT ON COLUMN education_question_collection.metadata IS '扩展元数据';
COMMENT ON COLUMN education_question_collection.creator IS '创建者';
COMMENT ON COLUMN education_question_collection.create_time IS '创建时间';
COMMENT ON COLUMN education_question_collection.updater IS '更新者';
COMMENT ON COLUMN education_question_collection.update_time IS '更新时间';
COMMENT ON COLUMN education_question_collection.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_question_collection (tenant_id);
CREATE INDEX idx_tenant_entry ON education_question_collection (tenant_id, entry_id);
CREATE INDEX idx_tenant_node ON education_question_collection (tenant_id, node_id);
-- =============================================
-- 9. 题目表(核心表)
-- =============================================
-- Indexes:
-- idx_tenant_published — 查询已发布题目(主查询路径)
-- idx_tenant_collection — 按题集筛选
-- idx_tenant_subject — 按科目筛选
-- idx_tenant_node — 按内容节点筛选
-- idx_tenant_type_diff — 按题型和难度筛选
-- Security note: correct_answer 和 explanation 列存在但需在 service 层过滤;
-- 学生端 DTO 绝不得包含这两列。
-- Options JSONB: 存储 label, content, isCorrect, order — 完整选项数据。
-- Service 层在学生端返回前剥离 isCorrect。
CREATE TABLE education_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
content_version INT NOT NULL DEFAULT 1,
stem TEXT NOT NULL,
type VARCHAR(32) NOT NULL,
type_label VARCHAR(50) DEFAULT NULL,
difficulty VARCHAR(32) DEFAULT NULL,
question_content JSONB DEFAULT NULL,
options JSONB NOT NULL,
correct_answer VARCHAR(500) DEFAULT NULL,
explanation TEXT DEFAULT NULL,
analysis TEXT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED',
is_published BOOLEAN NOT NULL DEFAULT true,
collection_id BIGINT DEFAULT NULL,
subject_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
tags JSONB DEFAULT NULL,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_question IS '教育-题目';
COMMENT ON COLUMN education_question.id IS '主键';
COMMENT ON COLUMN education_question.tenant_id IS '租户编号';
COMMENT ON COLUMN education_question.content_version IS '内容版本号(递增,保证历史报告稳定性)';
COMMENT ON COLUMN education_question.stem IS '题干';
COMMENT ON COLUMN education_question.type IS '题型';
COMMENT ON COLUMN education_question.type_label IS '题型显示名称';
COMMENT ON COLUMN education_question.difficulty IS '难度';
COMMENT ON COLUMN education_question.question_content IS '题干结构化内容(兼容复杂题型)';
COMMENT ON COLUMN education_question.options IS '选项 JSONB [{label, content, isCorrect, order}]';
COMMENT ON COLUMN education_question.correct_answer IS '正确答案(敏感字段,学生端不可返回)';
COMMENT ON COLUMN education_question.explanation IS '答案解析(敏感字段,学生端不可返回)';
COMMENT ON COLUMN education_question.analysis IS '深度解析(敏感字段)';
COMMENT ON COLUMN education_question.status IS '题目状态PUBLISHED-已发布, HIDDEN-已隐藏, DRAFT-草稿, INACTIVE-停用';
COMMENT ON COLUMN education_question.is_published IS '是否已发布';
COMMENT ON COLUMN education_question.collection_id IS '所属题集 ID';
COMMENT ON COLUMN education_question.subject_id IS '所属科目 ID';
COMMENT ON COLUMN education_question.node_id IS '所属内容节点 ID';
COMMENT ON COLUMN education_question.tags IS '标签 JSONB 数组';
COMMENT ON COLUMN education_question.sort_order IS '显示排序值';
COMMENT ON COLUMN education_question.metadata IS '扩展元数据';
COMMENT ON COLUMN education_question.creator IS '创建者';
COMMENT ON COLUMN education_question.create_time IS '创建时间';
COMMENT ON COLUMN education_question.updater IS '更新者';
COMMENT ON COLUMN education_question.update_time IS '更新时间';
COMMENT ON COLUMN education_question.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_question (tenant_id);
CREATE INDEX idx_tenant_published ON education_question (tenant_id, is_published, status);
CREATE INDEX idx_tenant_collection ON education_question (tenant_id, collection_id);
CREATE INDEX idx_tenant_subject ON education_question (tenant_id, subject_id);
CREATE INDEX idx_tenant_node ON education_question (tenant_id, node_id);
CREATE INDEX idx_tenant_type_diff ON education_question (tenant_id, type, difficulty);
-- =============================================
-- 10. 练习蓝图表
-- =============================================
CREATE TABLE education_practice_blueprint (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
mode VARCHAR(50) NOT NULL,
entry_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
collection_id BIGINT DEFAULT NULL,
question_limit INT DEFAULT NULL,
duration_minutes INT DEFAULT NULL,
eligible_count INT NOT NULL DEFAULT 0,
total_count INT NOT NULL DEFAULT 0,
available_types JSONB DEFAULT NULL,
available_difficulties JSONB DEFAULT NULL,
min_questions INT NOT NULL DEFAULT 1,
max_questions INT NOT NULL DEFAULT 200,
suggested_count INT NOT NULL DEFAULT 20,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_practice_blueprint IS '教育-练习蓝图';
COMMENT ON COLUMN education_practice_blueprint.id IS '主键';
COMMENT ON COLUMN education_practice_blueprint.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_blueprint.mode IS '练习模式';
COMMENT ON COLUMN education_practice_blueprint.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_practice_blueprint.node_id IS '内容节点 ID';
COMMENT ON COLUMN education_practice_blueprint.collection_id IS '题集 ID';
COMMENT ON COLUMN education_practice_blueprint.question_limit IS '题目数量限制';
COMMENT ON COLUMN education_practice_blueprint.duration_minutes IS '建议作答时长(分钟)';
COMMENT ON COLUMN education_practice_blueprint.eligible_count IS '符合条件的题目数';
COMMENT ON COLUMN education_practice_blueprint.total_count IS '题库总数';
COMMENT ON COLUMN education_practice_blueprint.available_types IS '可用题型列表 JSONB';
COMMENT ON COLUMN education_practice_blueprint.available_difficulties IS '可用难度列表 JSONB';
COMMENT ON COLUMN education_practice_blueprint.min_questions IS '最少题目数';
COMMENT ON COLUMN education_practice_blueprint.max_questions IS '最多题目数';
COMMENT ON COLUMN education_practice_blueprint.suggested_count IS '建议题目数';
COMMENT ON COLUMN education_practice_blueprint.creator IS '创建者';
COMMENT ON COLUMN education_practice_blueprint.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_blueprint.updater IS '更新者';
COMMENT ON COLUMN education_practice_blueprint.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_blueprint.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_practice_blueprint (tenant_id);
CREATE INDEX idx_tenant_collection ON education_practice_blueprint (tenant_id, collection_id);
-- =============================================
-- 11. 题集-题目关联表(多对多)
-- =============================================
CREATE TABLE education_question_collection_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
collection_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
sort_order INT NOT NULL DEFAULT 0
);
COMMENT ON TABLE education_question_collection_question IS '教育-题集题目关联';
COMMENT ON COLUMN education_question_collection_question.id IS '主键';
COMMENT ON COLUMN education_question_collection_question.collection_id IS '题集 ID';
COMMENT ON COLUMN education_question_collection_question.question_id IS '题目 ID';
COMMENT ON COLUMN education_question_collection_question.sort_order IS '排序值';
CREATE UNIQUE INDEX uk_collection_question ON education_question_collection_question (collection_id, question_id);
CREATE INDEX idx_question ON education_question_collection_question (question_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify all tables exist:
-- SELECT table_name FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name LIKE 'education_%'
-- ORDER BY table_name;
-- Verify indexes (sample):
-- -- PostgreSQL equivalent of SHOW INDEX:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_question' AND indexname IN ('idx_tenant_published', 'idx_tenant_collection');
-- SELECT * FROM pg_indexes WHERE tablename = 'education_content_node' AND indexname = 'idx_entry_parent';
-- Verify no orphan data (should be 0 after fresh migration for catalog tables):
-- SELECT 'region' AS tbl, COUNT(*) FROM education_region
-- UNION ALL SELECT 'school', COUNT(*) FROM education_school
-- UNION ALL SELECT 'major', COUNT(*) FROM education_major
-- UNION ALL SELECT 'subject', COUNT(*) FROM education_subject
-- UNION ALL SELECT 'category', COUNT(*) FROM education_category
-- UNION ALL SELECT 'content_entry', COUNT(*) FROM education_content_entry
-- UNION ALL SELECT 'content_node', COUNT(*) FROM education_content_node
-- UNION ALL SELECT 'question_collection', COUNT(*) FROM education_question_collection
-- UNION ALL SELECT 'question', COUNT(*) FROM education_question
-- UNION ALL SELECT 'practice_blueprint', COUNT(*) FROM education_practice_blueprint
-- UNION ALL SELECT 'collection_question', COUNT(*) FROM education_question_collection_question;

View File

@@ -0,0 +1,13 @@
-- =============================================
-- Education 模块 — 统一幂等表 回滚
-- Migration: 009 回滚
-- =============================================
DROP TABLE IF EXISTS education_idempotency;
-- Recreate the original split tables from 003 + 004 migrations if needed:
-- (restore from backup or re-run 003 and 004 DDL scripts)
-- Remove review_fingerprint column
ALTER TABLE education_practice_session
DROP COLUMN IF EXISTS review_fingerprint;

View File

@@ -0,0 +1,103 @@
-- =============================================
-- Education 模块 — 统一幂等表 DDL (PostgreSQL)
-- Ticket: 合并 Answer + Submit 幂等表为统一 education_idempotency
-- Migration: 009
-- Prerequisites: 002-education-practice-session.sql
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Verify table does not already exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name = 'education_idempotency';
-- Result MUST be 0 before executing this migration.
--
-- NOTE: This table replaces education_answer_idempotency and education_submit_idempotency.
-- Those tables should be dropped after this migration is verified:
-- DROP TABLE IF EXISTS education_answer_idempotency;
-- DROP TABLE IF EXISTS education_submit_idempotency;
-- =============================================
-- 统一幂等表
-- =============================================
-- Purpose: Unified idempotency store for all education write operations.
-- operation field distinguishes: SUBMIT_ANSWER vs SUBMIT_SESSION.
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response.
-- Same key + different request_hash → conflict.
-- PostgreSQL ON CONFLICT DO NOTHING resolves concurrent same-key inserts.
--
-- Indexes:
-- uk_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
-- Used by: IdempotencyStoreMapper.insertIgnore() ON CONFLICT resolution.
-- idx_tenant_session — covers lookup by session for audit/debug.
--
-- Fields specific to SUBMIT_ANSWER: question_id, selected_answer (nullable for SUBMIT_SESSION).
-- Fields specific to SUBMIT_SESSION: report_id (nullable for SUBMIT_ANSWER).
-- response_json: stores serialized response for replay after timeout/retry.
-- request_hash: SHA-256 of canonical payload for content-based dedup.
CREATE TABLE education_idempotency (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
operation VARCHAR(32) NOT NULL,
idempotency_key VARCHAR(64) NOT NULL,
request_hash VARCHAR(64) NOT NULL,
session_id BIGINT NOT NULL,
question_id VARCHAR(64) DEFAULT NULL,
selected_answer TEXT DEFAULT NULL,
report_id BIGINT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
response_json TEXT DEFAULT NULL,
business_payload JSONB DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_idempotency IS '教育-统一幂等记录(合并 answer + submit';
COMMENT ON COLUMN education_idempotency.id IS '主键';
COMMENT ON COLUMN education_idempotency.tenant_id IS '租户编号';
COMMENT ON COLUMN education_idempotency.user_id IS '用户编号';
COMMENT ON COLUMN education_idempotency.operation IS '操作类型SUBMIT_ANSWER-答题, SUBMIT_SESSION-交卷';
COMMENT ON COLUMN education_idempotency.idempotency_key IS '客户端幂等键UUID';
COMMENT ON COLUMN education_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
COMMENT ON COLUMN education_idempotency.session_id IS '会话 ID';
COMMENT ON COLUMN education_idempotency.question_id IS '题目 IDSUBMIT_ANSWER';
COMMENT ON COLUMN education_idempotency.selected_answer IS '学生已选答案SUBMIT_ANSWER';
COMMENT ON COLUMN education_idempotency.report_id IS '关联报告 IDSUBMIT_SESSION';
COMMENT ON COLUMN education_idempotency.status IS '状态ACCEPTED-已接受, CONFLICT-冲突';
COMMENT ON COLUMN education_idempotency.response_json IS '首次成功响应 JSON用于重试重放';
COMMENT ON COLUMN education_idempotency.business_payload IS '业务扩展载荷JSONB';
COMMENT ON COLUMN education_idempotency.creator IS '创建者';
COMMENT ON COLUMN education_idempotency.create_time IS '创建时间';
COMMENT ON COLUMN education_idempotency.updater IS '更新者';
COMMENT ON COLUMN education_idempotency.update_time IS '更新时间';
COMMENT ON COLUMN education_idempotency.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_idempotency ON education_idempotency (tenant_id, user_id, operation, idempotency_key);
CREATE INDEX idx_idempotency_tenant_session ON education_idempotency (tenant_id, session_id);
-- =============================================
-- PracticeSessionDO: add review_fingerprint column
-- =============================================
-- Purpose: Store canonical fingerprint for wrong-question review session idempotency.
-- SHA-256 of comma-joined sorted wrong_question IDs for deterministic comparison.
ALTER TABLE education_practice_session
ADD COLUMN review_fingerprint VARCHAR(64) DEFAULT NULL;
COMMENT ON COLUMN education_practice_session.review_fingerprint IS '错题复习指纹SHA-256用于幂等创建';
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new table exists:
-- \d education_idempotency
-- Verify unique index:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_idempotency';
-- Verify column added:
-- SELECT column_name, data_type FROM information_schema.columns
-- WHERE table_name = 'education_practice_session' AND column_name = 'review_fingerprint';

View File

@@ -31,9 +31,7 @@ public interface TenantCommonApi {
* @param id 租户编号
* @return 租户信息,不存在时返回 null
*/
default TenantRespDTO getTenant(Long id) {
throw new UnsupportedOperationException("getTenant is not implemented");
}
TenantRespDTO getTenant(Long id);
/**
* 根据租户名获得租户信息
@@ -41,9 +39,7 @@ public interface TenantCommonApi {
* @param name 租户名
* @return 租户信息,不存在时返回 null
*/
default TenantRespDTO getTenantByName(String name) {
throw new UnsupportedOperationException("getTenantByName is not implemented");
}
TenantRespDTO getTenantByName(String name);
/**
* 根据域名获得租户信息
@@ -51,8 +47,6 @@ public interface TenantCommonApi {
* @param website 域名
* @return 租户信息,不存在时返回 null
*/
default TenantRespDTO getTenantByWebsite(String website) {
throw new UnsupportedOperationException("getTenantByWebsite is not implemented");
}
TenantRespDTO getTenantByWebsite(String website);
}

View File

@@ -0,0 +1,95 @@
package cn.iocoder.yudao.framework.tenant.core.security;
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.HashSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class TenantSecurityWebFilterTest {
private TenantSecurityWebFilter filter;
private TenantFrameworkService tenantFrameworkService;
@BeforeEach
void setUp() {
WebProperties webProperties = new WebProperties();
TenantProperties tenantProperties = new TenantProperties();
tenantProperties.setIgnoreUrls(new HashSet<>());
tenantFrameworkService = mock(TenantFrameworkService.class);
filter = new TenantSecurityWebFilter(webProperties, tenantProperties, new HashSet<>(),
new GlobalExceptionHandler("test", mock(ApiErrorLogCommonApi.class)), tenantFrameworkService);
}
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
TenantContextHolder.clear();
}
@Test
void missingTenantOnProtectedRequestIsRejected() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/app-api/education/context");
MockHttpServletResponse response = doFilter(request);
assertEquals(400, jsonCode(response));
}
@Test
void authenticatedTenantMismatchIsRejected() throws Exception {
setLoginUser(1L, 10L);
TenantContextHolder.setTenantId(20L);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/app-api/education/context");
MockHttpServletResponse response = doFilter(request);
assertEquals(403, jsonCode(response));
}
@Test
void authenticatedTenantFillsMissingRequestTenant() throws Exception {
setLoginUser(1L, 10L);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/app-api/education/context");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
filter.doFilter(request, response, chain);
assertEquals(10L, TenantContextHolder.getTenantId());
verify(tenantFrameworkService).validTenant(10L);
assertEquals(request, chain.getRequest());
}
private MockHttpServletResponse doFilter(MockHttpServletRequest request) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, new MockFilterChain());
return response;
}
private static int jsonCode(MockHttpServletResponse response) throws Exception {
String content = response.getContentAsString();
int start = content.indexOf("\"code\":") + 7;
int end = content.indexOf(',', start);
return Integer.parseInt(content.substring(start, end));
}
private static void setLoginUser(Long userId, Long tenantId) {
LoginUser user = new LoginUser();
user.setId(userId);
user.setTenantId(tenantId);
user.setUserType(UserTypeEnum.MEMBER.getValue());
SecurityFrameworkUtils.setLoginUser(user, new MockHttpServletRequest());
}
}

View File

@@ -29,10 +29,6 @@
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
@@ -41,7 +37,6 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>

Some files were not shown because too many files have changed in this diff Show More