Преглед изворни кода

feat: 新增ECS外部课程系统模块

- 添加全新模块 `ruoyi-ecs`,包含课表、课程、学期和考勤管理
- 配置Nacos和API模块,更新需求文档
- 优化课表查询排序逻辑
- 增强部门管理功能,支持培训班查询
- 扩展后台管理功能,新增教室和用户账户远程服务
- 解耦循环依赖,引入统一的分页结果封装
- 更新项目文档和配置文件
yubo пре 1 месец
родитељ
комит
89169146a4
100 измењених фајлова са 10846 додато и 53 уклоњено
  1. 270 0
      .workbuddy/CMD.md
  2. 38 0
      .workbuddy/memory/2026-04-14.md
  3. 94 0
      .workbuddy/memory/2026-04-15.md
  4. 167 0
      .workbuddy/memory/2026-04-20.md
  5. 24 0
      .workbuddy/memory/2026-04-22.md
  6. 71 0
      .workbuddy/memory/MEMORY.md
  7. 84 0
      .workbuddy/plans/create-crud-command-for-ecs_7e7de224(未完成).md
  8. 83 0
      .workbuddy/plans/ecs-attend-requirements_5bc8e760.md
  9. 40 0
      .workbuddy/plans/ecs-course-requirements_9a7b4847.md
  10. 258 0
      .workbuddy/plans/require-dev-command-system_ad93f548.md
  11. 320 0
      .workbuddy/skills/require/SKILL.md
  12. 370 0
      .workbuddy/skills/require/references/requirements-template.md
  13. 720 0
      .workbuddy/skills/ruoyi-backend/SKILL.md
  14. 240 0
      .workbuddy/skills/ruoyi-backend/references/coding_standards.md
  15. 372 0
      .workbuddy/skills/ruoyi-backend/references/dubbo_integration.md
  16. 106 0
      .workbuddy/skills/ruoyi-backend/references/project_structure.md
  17. 275 0
      .workbuddy/skills/ruoyi-backend/references/requirements-template.md
  18. 31 0
      .workbuddy/skills/ruoyi-backend/templates/Bo.java.template
  19. 88 0
      .workbuddy/skills/ruoyi-backend/templates/ConsumerController.java.template
  20. 142 0
      .workbuddy/skills/ruoyi-backend/templates/Controller.java.template
  21. 39 0
      .workbuddy/skills/ruoyi-backend/templates/Domain.java.template
  22. 19 0
      .workbuddy/skills/ruoyi-backend/templates/Dto.java.template
  23. 102 0
      .workbuddy/skills/ruoyi-backend/templates/ImportListener.java.template
  24. 26 0
      .workbuddy/skills/ruoyi-backend/templates/ImportVo.java.template
  25. 15 0
      .workbuddy/skills/ruoyi-backend/templates/Mapper.java.template
  26. 29 0
      .workbuddy/skills/ruoyi-backend/templates/QueryDto.java.template
  27. 66 0
      .workbuddy/skills/ruoyi-backend/templates/RemoteService.java.template
  28. 113 0
      .workbuddy/skills/ruoyi-backend/templates/RemoteServiceImpl.java.template
  29. 49 0
      .workbuddy/skills/ruoyi-backend/templates/Service.java.template
  30. 119 0
      .workbuddy/skills/ruoyi-backend/templates/ServiceImpl.java.template
  31. 34 0
      .workbuddy/skills/ruoyi-backend/templates/Vo.java.template
  32. 41 37
      README.md
  33. 25 0
      config/nacos/ruoyi-ecs.yml
  34. 294 0
      doc/2026-04-21-变更日志.md
  35. 224 0
      doc/2026-04-27-班级同步新入参适配实现计划.md
  36. 576 0
      doc/JW系统Kafka数据同步接口文档.md
  37. 316 0
      doc/ecs-attenRule-requirements.md
  38. 493 0
      doc/ecs-attend-requirements.md
  39. 536 0
      doc/ecs-course-requirements.md
  40. 564 0
      doc/ecs-section-requirements.md
  41. 436 0
      doc/ecs-term-requirements.md
  42. 2 2
      pom.xml
  43. 1 0
      ruoyi-api/pom.xml
  44. 8 1
      ruoyi-api/ruoyi-api-backstage/pom.xml
  45. 31 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java
  46. 45 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteUserAccountService.java
  47. 41 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteClassroomDto.java
  48. 28 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteClassroomQueryDto.java
  49. 31 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTeacherDto.java
  50. 28 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTeacherQueryDto.java
  51. 43 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTraineeDto.java
  52. 31 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTraineeQueryDto.java
  53. 7 0
      ruoyi-api/ruoyi-api-bom/pom.xml
  54. 32 0
      ruoyi-api/ruoyi-api-ecs/pom.xml
  55. 66 0
      ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/RemoteCourseService.java
  56. 48 0
      ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteCourseDto.java
  57. 38 0
      ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteCourseQueryDto.java
  58. 0 1
      ruoyi-api/ruoyi-api-system/pom.xml
  59. 35 0
      ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java
  60. 62 0
      ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/PageResult.java
  61. 91 0
      ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/dto/RemoteClassDto.java
  62. 1 1
      ruoyi-auth/src/main/resources/application.yml
  63. 1 0
      ruoyi-modules/pom.xml
  64. 3 3
      ruoyi-modules/ruoyi-backstage/pom.xml
  65. 14 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/PtRoom.java
  66. 14 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/bo/PtRoomBo.java
  67. 14 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/vo/PtRoomVo.java
  68. 88 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java
  69. 1 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/SelfController.java
  70. 1 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/TeacherController.java
  71. 1 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/TraineeController.java
  72. 178 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/dubbo/RemoteUserAccountServiceImpl.java
  73. 6 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/mapper/PtUserAccountMapper.java
  74. 9 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/IPtUserAccountService.java
  75. 34 2
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtUserAccountServiceImpl.java
  76. 5 6
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/task/InitRunner.java
  77. 3 0
      ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/basics/room/PtRoomMapper.xml
  78. 22 0
      ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/payment/PtUserAccountMapper.xml
  79. 24 0
      ruoyi-modules/ruoyi-ecs/Dockerfile
  80. 117 0
      ruoyi-modules/ruoyi-ecs/pom.xml
  81. 22 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/RuoYiEcsApplication.java
  82. 107 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/AttendController.java
  83. 143 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/AttendRuleController.java
  84. 70 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/ClassController.java
  85. 48 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/ClassroomController.java
  86. 144 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/CourseController.java
  87. 69 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/SectionController.java
  88. 48 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/TeacherController.java
  89. 143 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/TermController.java
  90. 71 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/TraineeController.java
  91. 54 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/sync/SyncDataController.java
  92. 104 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsAttend.java
  93. 68 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsAttendRule.java
  94. 74 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsCourse.java
  95. 53 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsCourseTeacher.java
  96. 154 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsSection.java
  97. 103 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsTerm.java
  98. 96 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/bo/AttendForm.java
  99. 99 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/bo/EcsAttendBo.java
  100. 63 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/bo/EcsAttendRuleBo.java

+ 270 - 0
.workbuddy/CMD.md

@@ -0,0 +1,270 @@
+# 指令规范(CMD)
+
+> 本文件定义了 ykt_server 项目的业务功能开发指令,驱动从需求分析到代码生成的完整流程。
+
+---
+
+## 指令总览
+
+| 指令 | 用途 | 调用技能 | 产物 |
+|------|------|----------|------|
+| `/require` | 生成需求规格说明书 | require | `doc/{模块}-{功能名}-requirements.md` |
+| `/dev` | 基于需求规格说明书生成前后端代码 | ruoyi-backend + ykt-web-dev | 后端 Java 文件 + 前端 Vue/TS 文件 |
+
+---
+
+## 1. `/require` — 需求规格说明书生成
+
+### 语法
+
+```
+/require <模块> <功能名> [简要需求描述] [选项]
+```
+
+### 参数
+
+| 参数 | 必填 | 说明 | 示例 |
+|------|------|------|------|
+| 模块 | 是 | 模块名(不含 ruoyi- 前缀) | `ecs`、`backstage`、`system` |
+| 功能名 | 是 | 功能简写(英文,驼峰命名) | `course`、`student` |
+| 简要需求 | 否 | 自然语言描述字段和特殊要求 | `课程名称、课程类型(字典course_type)、讲师(多对多)` |
+
+### 选项
+
+| 标记 | 说明 | 示例 |
+|------|------|------|
+| `--table=表名` | 指定主表名(默认 `t_{模块}_{功能名}`) | `--table=t_ecs_course` |
+| `--sub-table=表名` | 指定从表名 | `--sub-table=t_ecs_course_teacher` |
+| `--no-tenant` | 非多租户表 | `--no-tenant` |
+| `--no-import` | 不需要导入导出 | `--no-import` |
+| `--dubbo` | 需要暴露 Dubbo 接口 | `--dubbo` |
+| `--db=数据库类型` | 指定数据库类型,影响建表语句生成(默认 `kingbase`) | `--db=mysql`、`--db=dm`、`--db=oceanbase` |
+
+### 流程
+
+```
+1. 解析指令参数(模块、功能名、简要需求、选项)
+2. 调用 require 技能
+3. 加载需求规格说明书模板(references/requirements-template.md)
+4. 分析简要需求,自动推断字段属性:
+   - 字段类型、查询方式、表单组件
+   - 字典字段(描述中含"字典"、"下拉")
+   - 关联字段(描述中含"关联"、"选择"、"多对多")
+   - 主从关系(描述中含"多对多"、"一对多",或指定了 --sub-table)
+5. 填充模板,生成初始需求规格说明书(含建表语句)
+6. 输出到 doc/{模块}-{功能名}-requirements.md
+7. 展示给用户,支持反复沟通修改
+8. 用户确认后形成最终需求规格说明书
+```
+
+### 需求推断规则
+
+| 用户描述 | 推断结果                                                                           |
+|----------|--------------------------------------------------------------------------------|
+| "名称" | fieldType=String, component=input, inQuery=true, queryType=like                |
+| "类型(字典xxx)" | fieldType=String, dictType=xxx, component=select                               |
+| "编码" | fieldType=String, component=input, inQuery=true, queryType=like, required=true |
+| "学分/分数" | fieldType=BigDecimal, component=inputNumber                                    |
+| "数量/人数" | fieldType=Integer, component=inputNumber                                       |
+| "时间/日期" | fieldType=Date, component=datetime                                             |
+| "备注/描述" | fieldType=String, component=textarea                                           |
+| "状态" | fieldType=String, dictType=status, component=select                            |
+| "讲师/教师(多对多)" | 生成从表 + relation 配置                                                             |
+| "排序" | fieldType=Integer, component=inputNumber                                       |
+| "来源" | fieldType=Integer, dictType=data_source, component=select                      |
+| "ID/标识" | fieldType=Long, inTable=false, inForm=false                                    |
+| `--db=mysql` | 数据库类型设为 MySQL,生成 MySQL 建表语句                                                    |
+| `--db=dm` | 数据库类型设为达梦,生成达盛建表语句                                                             |
+| `--db=oceanbase` | 数据库类型设为 OceanBase,生成 OceanBase 建表语句                                            |
+| (默认) | 数据库类型为人大金仓(kingbase)                                                           |
+
+### 示例
+
+**基础用法**:
+```
+/require ecs 课程 课程名称、课程编码、课程类型(字典course_type)、学分、学时、讲师(多对多关联) --sub-table=t_ecs_course_teacher
+```
+
+**仅指定模块和功能名,后续逐步补充需求**:
+```
+/require ecs 教室
+```
+
+**指定表名和选项**:
+```
+/require backstage 房间 房间名称、房间类型(字典FJLX)、容纳人数 --table=t_pt_room --no-import
+```
+
+**多行描述**:
+```
+/require ecs 课程
+字段:课程名称、课程编码、课程类型(字典course_type)、学分、学时
+特殊:教师为多对多关联,需要Dubbo暴露,需要导入导出
+```
+
+---
+
+## 2. `/dev` — 代码生成
+
+### 语法
+
+```
+/dev <模块> <功能名> [选项]
+```
+
+### 参数
+
+| 参数 | 必填 | 说明 | 示例 |
+|------|------|------|------|
+| 模块 | 是 | 模块名 | `ecs` |
+| 功能名 | 是 | 功能简写 | `course` |
+
+### 选项
+
+| 标记 | 说明 | 示例 |
+|------|------|------|
+| `--backend=路径` | 后端项目根路径(默认 `d:/dt_ykt/ykt_server`) | `--backend=d:/other/server` |
+| `--frontend=路径` | 前端项目根路径(默认 `d:/dt_ykt/ykt_web`) | `--frontend=d:/other/web` |
+| `--backend-only` | 仅生成后端代码 | `--backend-only` |
+| `--frontend-only` | 仅生成前端代码 | `--frontend-only` |
+
+### 流程
+
+```
+1. 解析指令参数(模块、功能名、选项)
+2. 读取需求规格说明书 doc/{模块}-{功能名}-requirements.md
+   - 如果文件不存在,提示用户先执行 /require
+3. 后端代码生成(调用 ruoyi-backend 技能):
+   a. 解析需求文档的字段清单、接口清单、特殊需求
+   b. 生成 Domain/Bo/Vo/Mapper/Service/ServiceImpl/Controller
+   c. 如有导入需求,生成 ImportVo/ImportListener
+   d. 如有 Dubbo 暴露需求,生成 RemoteService/RemoteServiceImpl
+   e. 如有从表,生成从表相关文件
+4. 前端代码生成(调用 ykt-web-dev 技能):
+   a. 解析需求文档的字段清单、接口清单、VO 结构
+   b. 生成 api/{模块}/{功能名}/index.ts + types.ts
+   c. 生成 views/{模块}/{功能名}/index.vue(列表页)
+   d. 生成 views/{模块}/{功能名}/form.vue(表单页)
+   e. 处理字典字段(useDict 引用)
+   f. 处理关联字段(关联选择组件)
+5. 展示生成结果清单
+6. 用户确认后写入文件
+```
+
+### 需求规格说明书 → 代码映射
+
+| 需求文档内容 | 后端生成 | 前端生成 |
+|-------------|----------|----------|
+| 基础信息.模块 | 包路径 `org.dromara.{module}` | 目录 `api/{module}/`、`views/{module}/` |
+| 基础信息.主表名 | `@TableName`、Domain 类 | types.ts 表单类型 |
+| 基础信息.从表名 | 从表 Domain/Bo/Vo/Mapper/Service | 从表类型定义、子表单组件 |
+| 基础信息.是否多租户 | `extends TenantEntity / BaseEntity` | — |
+| 字段(inDb=true) | Domain 字段 | — |
+| 字段(required=true) | Bo `@NotNull`/`@NotBlank` 校验 | types.ts 必填校验 |
+| 字段(inTable=true) | Vo `@ExcelProperty` | index.vue 表格列 |
+| 字段(inQuery=true) | Bo 查询字段 | index.vue 搜索栏 |
+| 字段(inForm=true) | Bo 表单字段 | form.vue 表单项 |
+| 字段(dictType) | Vo `@ExcelDictFormat` | `useDict()` + `el-select` |
+| 字段(relation) | — | 关联选择弹窗组件 |
+| 字段(inDb=false) | Vo 纯计算字段 | types.ts 只读展示 |
+| 字段(component) | — | 对应 Element Plus 组件 |
+| 接口清单 | Controller 方法 | api/index.ts 接口函数 |
+| VO 结构 | Vo 类定义 | types.ts 类型定义 |
+| 特殊需求.excelImport | ImportVo + ImportListener | — |
+| 特殊需求.excelExport | Vo `@ExcelProperty` | — |
+| 特殊需求.dubboExpose | RemoteService + RemoteServiceImpl | — |
+
+### 示例
+
+**基础用法**:
+```
+/dev ecs 课程
+```
+自动查找 `doc/ecs-course-requirements.md`,使用默认路径生成前后端代码。
+
+**仅生成后端**:
+```
+/dev ecs 课程 --backend-only
+```
+
+**指定自定义路径**:
+```
+/dev ecs 课程 --backend=d:/other/server --frontend=d:/other/web
+```
+
+**仅生成前端**:
+```
+/dev ecs 课程 --frontend-only
+```
+
+---
+
+## 3. 完整工作流示例
+
+### 示例:从零创建课程管理功能
+
+**Step 1:生成需求规格说明书**
+```
+/require ecs 课程 课程名称、课程编码、课程类型(字典course_type)、学分、学时、讲师(多对多关联) --sub-table=t_ecs_course_teacher --dubbo --db=kingbase
+```
+
+**Step 2:确认/修改需求规格说明书**
+
+AI 生成 `doc/ecs-course-requirements.md`,用户检查并修改(可多轮对话调整字段、增删接口等)。
+
+**Step 3:生成代码**
+```
+/dev ecs 课程
+```
+
+**Step 4:验证生成结果**
+
+后端文件:
+```
+ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/
+├── controller/CourseController.java
+├── domain/
+│   ├── Course.java
+│   ├── CourseTeacher.java
+│   ├── bo/CourseBo.java
+│   └── vo/CourseVo.java
+├── mapper/CourseMapper.java
+└── service/
+    ├── ICourseService.java
+    └── impl/CourseServiceImpl.java
+```
+
+前端文件:
+```
+d:/dt_ykt/ykt_web/src/
+├── api/ecs/course/
+│   ├── index.ts
+│   └── types.ts
+└── views/ecs/course/
+    ├── index.vue
+    └── form.vue
+```
+
+---
+
+## 4. 路径约定
+
+| 资源 | 默认路径 | 说明 |
+|------|----------|------|
+| 需求文档 | `d:/dt_ykt/ykt_server/doc/{模块}-{功能名}-requirements.md` | Markdown 格式 |
+| 后端代码 | `d:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-{模块}/` | Java 微服务模块 |
+| 前端代码 | `d:/dt_ykt/ykt_web/src/` | Vue 3 + TypeScript |
+| require 技能 | `d:/dt_ykt/ykt_server/.workbuddy/skills/require/` | 需求分析技能 |
+| 后端技能 | `d:/dt_ykt/ykt_server/.workbuddy/skills/ruoyi-backend/` | 后端代码生成技能 |
+| 前端技能 | `d:/dt_ykt/ykt_web/.workbuddy/skills/ykt-web-dev/` | 前端代码生成技能 |
+
+---
+
+## 5. 注意事项
+
+1. **必须先执行 `/require`**:`/dev` 指令依赖需求规格说明书,未找到时会提示先执行 `/require`
+2. **需求文档可手工编辑**:生成后用户可直接修改 Markdown 文件,`/dev` 以文件内容为准
+3. **增量开发**:如已有部分代码,`/dev` 会提示覆盖风险,由用户决定
+4. **字典命名规范**:字典类型(dictType)是全局的,不加模块前缀(如 `course_type` 而非 `ecs_course_type`)
+5. **DTO 命名规范**:Dubbo 传输对象统一使用 `Dto` 后缀(如 `RemoteCourseDto`)
+6. **多对多关联**:通过中间表实现,列表显示时 SQL 拼接关联字段(如讲师姓名拼接串)

+ 38 - 0
.workbuddy/memory/2026-04-14.md

@@ -0,0 +1,38 @@
+# 2026-04-14 工作日志
+
+## 完成的任务
+
+### 1. 创建 ruoyi-backend 后端技能
+
+在 `D:\dt_ykt\ykt_server\.workbuddy\skills\ruoyi-backend\` 目录下创建了完整的后端开发技能文件:
+
+**主文件:**
+- `SKILL.md` - 技能主文档,包含项目结构、分层对象定义规范、开发工作流程、关键转换模式等
+
+**参考文档(references/):**
+- `project_structure.md` - 项目结构详解
+- `coding_standards.md` - 编码规范
+- `dubbo_integration.md` - Dubbo 集成指南
+
+**代码模板(templates/):**
+- `Domain.java.template` - 实体类模板
+- `Bo.java.template` - 业务对象模板
+- `Vo.java.template` - 视图对象模板
+- `Dto.java.template` - 数据传输对象模板
+- `QueryDto.java.template` - 查询DTO模板
+- `Service.java.template` - Service接口模板
+- `ServiceImpl.java.template` - Service实现模板
+- `Controller.java.template` - 控制器模板
+- `ConsumerController.java.template` - 消费者控制器模板(跨服务调用)
+- `Mapper.java.template` - Mapper接口模板
+- `RemoteService.java.template` - Dubbo接口模板
+- `RemoteServiceImpl.java.template` - Dubbo实现模板
+
+**技能覆盖内容:**
+- RuoYi-Cloud-Plus 微服务框架开发规范
+- Domain/BO/VO/DTO 分层对象设计
+- MyBatis-Plus 数据访问层开发
+- Apache Dubbo 3.X 远程调用
+- Sa-Token 权限注解使用
+- 分页处理规范
+- 对象转换模式(MapstructUtils、Hutool BeanUtil)

+ 94 - 0
.workbuddy/memory/2026-04-15.md

@@ -0,0 +1,94 @@
+# 2026-04-15 工作日志
+
+## 通用字段规范化
+
+- 用户明确了所有表的通用字段定义,与实际 DDL 对齐
+- 修正了通用字段类型:`create_by`/`update_by` 为 BIGINT(非 varchar),`del_flag` 为 character(1 char)/char(1)(非 integer),新增 `create_dept` BIGINT
+- 多租户表额外包含 `tenant_id`(BIGINT),非多租户表不含
+- 更新了三个文件:需求模板、require SKILL.md、ecs-course-requirements.md
+- 建表 DDL 中从表也需包含完整的通用字段(之前缺失)
+
+## 课程管理前端代码生成(/dev ecs course --frontend-only)
+
+- 基于需求规格说明书 `doc/ecs-course-requirements.md` 生成前端代码
+- 替换已有旧版课程前端代码(旧版为单教师关联,新版为多教师关联从表)
+- 生成4个文件:
+  - `src/api/ecs/course/types.ts`:新增 CourseTeacherVO 从表类型、teacherNames 字段、courseType/dataSource 查询条件
+  - `src/api/ecs/course/index.ts`:新增导入导出接口(exportTemplate/importData/exportCourse)
+  - `src/views/ecs/course/index.vue`:完整列表页,含 course_type/data_source 双字典、导入导出按钮、dataSource 第三方数据锁定
+  - `src/views/ecs/course/form.vue`:新增表单页(替代旧 detail.vue),含从表教师多选表格、字典下拉、必填校验
+- 旧文件 detail.vue 需手工删除(在 ykt_web 项目下,非当前 workspace)
+
+## 课程管理前端导入导出Bug修复
+
+- **问题**:下载模板时报 `relativeURL.replace is not a function`
+- **原因**:`proxy?.download(url, params, fileName)` 的第一个参数期望是 URL 字符串,但生成代码把 axios response 对象传了进去
+  - 错误写法:`const res = await exportTemplate(); proxy?.download(res, 'xxx.xlsx')`
+  - RuoYi 框架的 `download` 函数(来自 `@/utils/request`)会内部调用 `service.post(url, ...)`,所以不需要先发请求再传结果
+- **修复**:改为直接传 URL 字符串给 `proxy?.download`
+  - 下载模板:`proxy?.download('ecs/course/exportTemplate', {}, '课程导入模板_xxx.xlsx')`
+  - 导出数据:`proxy?.download('ecs/course/export', { ...queryParams }, '课程数据_xxx.xlsx')`
+- **API清理**:删除 `api/ecs/course/index.ts` 中的 `exportTemplate`、`importData`、`exportCourse` 三个函数(导入用 el-upload 的 action 方式,导出/模板用 proxy.download 方式)
+## 后端技能 SKILL.md 从表 VO 映射修复
+
+- **问题**:A.4 代码生成模板节只列了主表 VO 映射,从表 VO 未列出,导致从表生成的 Vo 类缺少 `@AutoMapper` 注解
+- **修复**:A.4 节拆分为「主表」和「从表」两部分,明确从表复用 `Vo.java.template`(含 `@AutoMapper`),并加了 ⚠️ 强调说明
+- **模板本身无需修改**:`Vo.java.template` 第5行已有 `import io.github.linpeilie.annotations.AutoMapper`,第22行已有 `@AutoMapper(target = ${ClassName}.class)`,只需按正确类名填充即可
+
+## 从表关联字段命名统一(teacherList → courseTeacherList)
+
+- **问题**:后端生成 `EcsCourseBo`/`EcsCourseVo` 中从表字段名为 `teacherList`,前端 `types.ts` 中为 `courseTeacherList`,JSON 序列化/反序列化不一致
+- **原因**:SKILL.md A.5 字段映射表缺少从表关联字段的命名规则,导致生成时随意命名
+- **修复**:SKILL.md A.5 新增一行 `从表关联字段(Bo/Vo)→ private List<{SubName}Vo> {Name}{Sub}List;`
+- **已修改3个文件**:
+  - `EcsCourseVo.java`:`teacherList` → `courseTeacherList`
+  - `EcsCourseBo.java`:`teacherList` → `courseTeacherList`
+  - `EcsCourseServiceImpl.java`:5处引用全部更新为 `setCourseTeacherList`/`getCourseTeacherList`
+- **命名规则**:`{Name}{Sub}List`(主表名首字母小写 + 从表名 + List),例如 `EcsCourse` + `Teacher` = `courseTeacherList`
+
+## 后端代码验证(/dev ecs course)
+
+- **已修正** `EcsCourseTeacherVo.java`:第21行新增 `@AutoMapper(target = EcsCourseTeacher.class)`,第7行导入 `EcsCourseTeacher`
+- 对比主表 `EcsCourseVo.java` 第25行 `@AutoMapper(target = EcsCourse.class)` → 结构完全一致
+- **结论**:技能已修复,下次 `/dev` 生成从表 VO 时会用 `Vo.java.template` 正确填充 `@AutoMapper(target = {SubName}.class)`
+
+## 前端技能模板导入导出规范化
+
+- 按 ptRoom 模式统一了导入导出写法,更新了4个文件:
+  - `templates/view-list.vue`:新增"更多"下拉菜单(下载模板/导入数据/导出数据)+ upload reactive + 导入弹窗 + 所有处理方法
+  - `SKILL.md`:api/index.ts 示例去掉 export 函数,新增导入导出说明块
+  - `references/api-style.md`:导出改为 `proxy?.download()`,新增完整的导出导入规范章节
+  - `references/vue-style.md`:列表页工具栏增加"更多"下拉,script 增加 upload + 导入导出方法
+- 核心变更:api/index.ts 不再有 exportTemplate/importData/exportCourse 函数
+- 导出/下载模板:`proxy?.download(url, params, fileName)` 方式
+- 导入:`el-upload` action 属性 + `globalHeaders()` + `ImportOption` reactive
+
+## 需求模板增加 lockRule 操作锁定规则
+
+- 新增字段属性 `lockRule`:当某字段值决定行级操作权限时使用
+- lockRule 格式:`{ lockValue: 值, lockActions: [edit, delete], tip: 提示文案 }`
+- 典型场景:`dataSource=1`(第三方同步)时禁止编辑和删除
+- 更新了3个文件:
+  - `references/requirements-template.md`:字段属性速查表增加 lockRule 行 + lockRule 格式说明 + 示例字段
+  - `SKILL.md`:字段通用默认值增加 lockRule、解析规则"来源"增加 lockRule 推断、VO结构说明增加"有lockRule的字段必须加入VO"、注意事项第8条
+  - `doc/ecs-course-requirements.md`:dataSource 字段改为 `inTable=false`/`inQuery=false`,增加 lockRule 配置,备注增加锁定规则说明
+
+## 课程前端代码重新生成(对齐 lockRule)
+
+- 基于 lockRule 规范,dataSource 字段 `inQuery=false`/`inTable=false`
+- 修改3个前端文件对齐需求文档:
+  - `index.vue`:删除搜索栏 dataSource 下拉、删除 `data_source` 字典引用、删除 queryParams.dataSource
+  - `types.ts`:删除 CourseQuery.dataSource 字段
+  - `form.vue`:保留 dataSource 字段和 `data_source` 字典(表单仍需要)
+- 操作列的 lockRule 判断逻辑(v-if="scope.row.dataSource != '1'")保持不变
+
+## dataSource 字段 inForm=false 更新
+
+- 用户确认:dataSource 不需要在表单中录入和显示,默认值由后台处理
+- 需求文档:`dataSource.inForm=false`/`inAdd=false`/`inEdit=false`,删除 `required`/`dictType`/`component`
+- 前端代码变更:
+  - `form.vue`:删除 dataSource 表单项、删除 data_source 字典引用、删除 formData.dataSource、删除 formRules.dataSource
+  - `types.ts`:CourseForm 中删除 dataSource 字段(CourseVO 保留,供列表页操作列判断)
+- 前端技能模板更新:
+  - `SKILL.md`:types.ts 映射增加 lockRule 注释、注意事项第6条
+  - `references/vue-style.md`:操作列增加 lockRule 注释示例 + 新增完整的 lockRule 规范章节

+ 167 - 0
.workbuddy/memory/2026-04-20.md

@@ -0,0 +1,167 @@
+# 2026-04-20 工作日志
+
+## 后端代码生成模板修正
+
+### 问题
+用户反馈代码生成模板文件不对,对照 `ruoyi-backstage/basics/PtParameter` 实际代码分析发现多处差异。
+
+### 修正内容
+
+**模板文件修正(3个):**
+
+1. **ServiceImpl.java.template** — 最大改动
+   - ❌ 旧:`baseMapper.selectVoList(bo)` / `baseMapper.selectVoPageList(bo, pageQuery)` — 编译不通过的方法签名
+   - ✅ 新:`LambdaQueryWrapper` + `buildQueryWrapper` 私有方法
+   - 新增 `validEntityBeforeSave` 保存前校验方法
+   - `insertByBo` 增加主键回填 + `Objects.requireNonNull` 校验
+   - `updateByBo` 增加 null 检查 + `validEntityBeforeSave`
+   - `deleteWithValidByIds` 增加 `isValid` 校验预留位置
+   - 分页查询默认 `orderByDesc(createTime)`
+   - 新增模板变量 `${queryWrapperConditions}` 和 `${PkCapField}`
+
+2. **Mapper.java.template** — 删除多余的 `Bo` import(Mapper 不需要引入 Bo)
+
+3. **Service.java.template** — 删除多余的 `Domain` import(Service 接口不引入实体类)
+
+**已生成代码修正(3个 ServiceImpl + 3个 Service 接口 + 3个 Mapper):**
+
+- `EcsTermServiceImpl` — 完整 CRUD + buildQueryWrapper(termName like, termNumb like, termType eq, roomId eq)
+- `EcsAttendServiceImpl` — 完整 CRUD + 保留手工考勤特殊逻辑 + buildQueryWrapper
+- `EcsSectionServiceImpl` — 完整 CRUD + buildQueryWrapper(含日期范围查询)
+- `IEcsAttendService` — 补充 updateByBo / deleteWithValidByIds 接口
+- `IEcsSectionService` — 补充 insertByBo / updateByBo / deleteWithValidByIds 接口
+- 3个 Mapper 文件删除多余 Bo import
+
+**SKILL.md 更新:**
+- 新增「模板变量说明」表格
+- 新增「ServiceImpl 查询规范」章节,明确 LambdaQueryWrapper 模式
+
+**MEMORY.md 更新:**
+- 新增 ServiceImpl 查询规范和模板变量说明
+
+## 前端代码生成 — ECS 课表管理(/dev ecs section --frontend-only)
+
+### 需求特点
+- 只读模式:无新增/编辑/删除,只有列表查询和导出
+- lockRule:dataSource=1 时禁止编辑删除(当前接口无编辑删除,预留)
+- 日期范围查询:courseDate 支持 between 查询
+- 字典字段:`ecs_time_slot`(上课时段)
+
+### 生成文件
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| types.ts | `src/api/ecs/section/types.ts` | VO/Query 类型定义 |
+| index.ts | `src/api/ecs/section/index.ts` | 仅 listSection + getSection |
+| index.vue | `src/views/ecs/section/index.vue` | 列表页(搜索+表格+导出,无增删改form) |
+
+### 设计要点
+- 无 form.vue(只读模式不需要表单页)
+- 学期字典前端本地定义(非系统字典),`ecs_time_slot` 使用 useDict
+- 星期使用前端映射表 weekdayMap 转换显示
+- 日期范围使用 el-date-picker type="daterange",查询时拆分为 courseDateBegin/courseDateEnd
+- 是否考勤使用 el-tag 标签展示
+- 默认隐藏低频列(连课节次/占用节次/占用列表/开始时间/结束时间)
+
+## 前端代码生成 — ECS 设备管理(/dev ecs term --frontend-only)
+
+### 需求特点
+- 完整 CRUD:新增/编辑/删除/导入/导出
+- 字典字段:`term_type`(设备类型)、`term_brand`(设备品牌)
+- 教室关联选择:通过弹窗选择教室,选择后回填 roomId 和 roomName
+- 管理密码:不在列表显示,不在 Excel 导出,仅在表单中可编辑(show-password)
+- 双列表单布局:el-row + el-col :span="12" 排列
+
+### 生成文件
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| types.ts | `src/api/ecs/term/types.ts` | VO/Form/Query 类型定义 |
+| index.ts | `src/api/ecs/term/index.ts` | 完整 CRUD API |
+| index.vue | `src/views/ecs/term/index.vue` | 列表页(搜索+表格+增删改+导入导出) |
+| form.vue | `src/views/ecs/term/form.vue` | 表单页(双列布局+教室选择弹窗) |
+
+### 设计要点
+- 列表页使用 `min-width` 替代 `width`(遵循前端表格列宽规范)
+- 教室选择:点击输入框弹窗 → `listClassroom` 分页查询 → 单选高亮 → 确认回填 roomId/roomName
+- 新增时字典字段(termType)默认选中第一个选项
+- form.vue 使用双列布局提升空间利用率
+- 操作列固定 `width="120"`(包含编辑+删除两个按钮)
+
+### 修正记录
+- ClassroomVO 主键是 `roomId`(非 `ptRoomId`),已修正 form.vue 中的教室选择确认逻辑
+- ClassroomVO 无 `floorName` 字段,教室选择表格改为 roomCode 列
+
+## 前端代码生成 — ECS 考勤管理(/dev ecs attend --frontend-only)
+
+### 需求特点
+- 只读模式 + 手工考勤补签:无编辑/删除/导入,仅新增(手工考勤)+导出
+- 级联选择:教室(弹窗选择)→ 班级(下拉)→ 学员(下拉,依赖班级过滤)
+- 字典字段:`check_type`(考勤方式)、`push_status`(推送状态)
+- 冗余回填:选择学员后自动回填 realName/userNumb,选择班级后回填 className
+
+### 生成文件
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| types.ts | `src/api/ecs/attend/types.ts` | AttendVO / AttendForm / AttendQuery 类型定义 |
+| index.ts | `src/api/ecs/attend/index.ts` | listAttend / getAttend / addAttend(无 update/del) |
+| index.vue | `src/views/ecs/attend/index.vue` | 列表页(搜索+表格+手工考勤按钮+导出) |
+| form.vue | `src/views/ecs/attend/form.vue` | 手工考勤表单(级联选择+教室弹窗) |
+
+### 设计要点
+- 列表页无 selection 列、无编辑/删除/导入按钮,仅「手工考勤」和「导出」
+- form.vue 核心逻辑:教室(弹窗选择,同 term)→ 班级(getClassOptions 下拉)→ 学员(listTrainee 按 deptId 过滤,级联清空)
+- 选班级后自动清空已选学员并重新加载学员列表
+- 选学员后自动回填 realName 和 userNumb
+- 考勤方式默认选中字典第一项,考勤时间默认当前时间
+- 推送状态列使用 dict-tag 组件展示
+
+## 字典字段类型规范 — 全局更新
+
+### 规范内容
+数据字典(dictType)对应的数据库表字段类型为 `varchar(16)`,Java 实体/Bo/Vo 中对应类型为 `String`(而非 `Integer`)。字典值虽然常为数字,但存储和传输统一使用字符串。
+
+### 更新文件
+
+| 文件 | 更新内容 |
+|------|----------|
+| MEMORY.md | 技术规范新增「字典字段类型规范」条目 |
+| require/SKILL.md | 字段解析规则:`fieldType=Integer` → `fieldType=String`(4处);字段类型映射表新增 `String(字典字段)` 行 `varchar(16)`;DDL生成逻辑新增字典字段说明 |
+| ruoyi-backend/SKILL.md | A.5 映射表新增字典字段行 `private String xxxType`;注意事项新增第7条;buildQueryWrapper 规则拆分出字典字段的 `StringUtils.isNotBlank` 判断 |
+| ykt-web-dev/SKILL.md | A.4.1 类型定义注释中标注字典字段类型;注意事项新增第10条 |
+
+## 主键不导出 Excel — 规范更新
+
+### 规范内容
+主键字段(Long 类型)不加 `@ExcelProperty` 注解,不参与 Excel 导出。Vo 中主键仅作为标识字段,需求文档中主键默认 `excelExport: false`。
+
+### 更新文件
+
+| 文件 | 更新内容 |
+|------|----------|
+| Vo.java.template | 去掉主键字段的 `@ExcelProperty(value = "${pkComment}")` 注解 |
+| ruoyi-backend/SKILL.md | A.5 映射表主键行:`@ExcelProperty("ID")` → `不导出`;注意事项新增第8条 |
+| require/references/requirements-template.md | 主键字段模板增加 `excelExport: false` |
+| require/SKILL.md | 字段生成规则主键增加 `excelExport=false`;注意事项新增第9条 |
+| MEMORY.md | 技术规范新增「主键不导出 Excel」条目 |
+
+## 日期类型规范修正:LocalDateTime → Date
+
+### 规范内容
+日期和时间字段 Java 类型统一使用 `java.util.Date`(而非 `LocalDateTime`),与 BaseEntity 基类保持一致。ServiceImpl 中使用 `new Date()` 而非 `LocalDateTime.now()`。
+
+### 更新文件
+
+| 文件 | 更新内容 |
+|------|----------|
+| EcsAttend.java / Bo / Vo | `LocalDateTime` → `Date`(checkTime/uploadTime/pushTime) |
+| EcsAttendServiceImpl.java | `LocalDateTime.now()` → `new Date()`,import 改为 `java.util.Date` |
+| EcsSection.java / Bo / Vo | `LocalDateTime` → `Date`(courseDate/startTime/endTime + 范围字段) |
+| EcsCourseVo.java | `createTime` 从 `LocalDateTime` → `Date`,删除多余 import |
+| EcsCourseTeacherVo.java | `createTime` 从 `LocalDateTime` → `Date`,删除多余 import |
+| EcsTermVo.java | 删除多余的 `import java.time.LocalDateTime` |
+| require/SKILL.md | 日期映射 `fieldType=LocalDateTime` → `Date`;通用字段表 `LocalDateTime` → `Date`;字段类型映射表 `LocalDateTime` → `Date` |
+| ruoyi-backend/SKILL.md | A.5 映射表新增 `fieldType=Date` 行 |
+| 4个需求文档(term/attend/section/course) | 所有 `LocalDateTime` → `Date` |
+| MEMORY.md | 新增「日期类型规范」条目 |

+ 24 - 0
.workbuddy/memory/2026-04-22.md

@@ -0,0 +1,24 @@
+# 2026-04-22 工作日志
+
+## 完成的任务
+
+### ECS 考勤规则模块开发
+- ✅ 生成需求规格说明书 `doc/ecs-attenRule-requirements.md`
+- ✅ 生成后端代码 9 个文件(Domain/Bo/Vo/Mapper/Service/ServiceImpl/Controller/ImportVo/ImportListener)
+- 表名:`t_ecs_atten_rule`,多租户表
+
+### 规范修正
+- ⚠️ **重要发现**:多租户表 tenant_id 字段类型应为 `varchar(20)`,不是 `bigint`
+  - 原因:TenantEntity.java 中 `tenantId` 是 String 类型
+  - 已更新 MEMORY.md 中的通用字段规范
+
+### 前端代码生成
+- ✅ 生成前端代码 4 个文件(types.ts/index.ts/index.vue/form.vue)
+- 路径:`ykt_web/src/api/ecs/attenRule/` + `ykt_web/src/views/ecs/attenRule/`
+
+## 技术要点
+- ServiceImpl 使用 LambdaQueryWrapper + buildQueryWrapper 模式
+- status 字段使用 sys_normal_disable 字典(String 类型)
+- 主键不回显 Excel(无 @ExcelProperty 注解)
+- 前端使用 min-width 规范表格列宽
+- 新增时字典字段默认选中第一个选项

+ 71 - 0
.workbuddy/memory/MEMORY.md

@@ -0,0 +1,71 @@
+# MEMORY.md - 长期记忆
+
+## 项目背景
+
+- 用户在腾讯从事 WorkBuddy 需求分析和 Vue 前端设计与重构工作
+- 同时负责 ECS 电子班牌系统和 ykt_server 项目开发
+- ECS 模块位于 `ruoyi-modules/ruoyi-ecs` 下
+- 技术栈:RuoYi-Cloud-Plus 框架、Dubbo 微服务、多租户架构
+- 数据库:达梦、人大金仓、OceanBase 等国产方案
+- 开发工具:WebStorm、DataGrip、IDEA
+- 用户偏好轻量软件,使用中文交流,Windows 系统
+
+## 技术规范
+
+- **字典命名规范**:字典是全局的,命名不加模块前缀(如 `course_type` 而非 `ecs_course_type`)
+- **DTO 命名规范**:统一使用 `Dto` 后缀
+- **关联表处理**:多对多关系通过中间表实现,列表显示时需 SQL 拼接关联字段(如讲师姓名拼接串)
+- **数据库类型**:默认人大金仓(kingbase),支持 --db 选项指定 mysql/dm/oceanbase
+- **三个技能文件均已添加语言要求**:所有对话、说明、注释均使用中文
+- **通用字段规范**:所有表统一包含 del_flag(char(1))/create_dept(bigint)/create_by(bigint)/create_time(timestamp)/update_by(bigint)/update_time(timestamp),多租户表额外含 tenant_id(varchar(20))。建表DDL自动追加,字段清单中无需重复声明
+- **前端导入导出规范**:导出/下载模板用 `proxy?.download(url, params, fileName)` 方式,导入用 `el-upload` 的 `action` 属性 + `globalHeaders()` + `ImportOption` reactive。api/index.ts 中不需要 exportTemplate/importData/export 函数
+- **前端表格列宽规范**:`el-table-column` 必须使用 `min-width` 而非 `width`。固定 `width` 不会自动扩展,导致右侧空白;`min-width` 设最小宽度后自动分配剩余空间填满表格
+- **操作锁定规则 lockRule**:当某字段值决定行级操作权限时使用 lockRule。格式:`{ lockValue: 值, lockActions: [edit/delete], tip: 提示文案 }`。字段需 `inDb=true`,通常 `inTable=false`/`inQuery=false`,但必须包含在 VO 中供前端判断。典型场景:dataSource=1 时禁止编辑删除
+- **ServiceImpl 查询规范**:必须使用 `LambdaQueryWrapper` + `buildQueryWrapper` 私有方法,**禁止**直接传 Bo 给 Mapper。分页查询用 `baseMapper.selectVoPage(pageQuery.build(), lqw)` + `TableDataInfo.build(result)`。insertByBo 需回填主键 + `validEntityBeforeSave` 校验。Mapper 不引入 Bo,Service 接口不引入 Domain。
+- **模板变量**:`${queryWrapperConditions}` 用于 buildQueryWrapper 中的条件代码块,`${PkCapField}` 用于主键首字母大写的 getter/setter 调用
+- **字典字段类型规范**:数据字典(dictType)对应的数据库表字段类型为 `varchar(16)`,Java 实体/Bo/Vo 中对应类型为 `String`(而非 `Integer`)。字典值虽然常为数字,但存储和传输统一使用字符串
+- **主键不导出 Excel**:主键字段(Long 类型)不加 `@ExcelProperty` 注解,不参与 Excel 导出。Vo 中主键仅作为标识字段,需求文档中主键默认 `excelExport: false`
+- **日期类型规范**:日期和时间字段的 Java 类型统一使用 `java.util.Date`(而非 `LocalDateTime`),数据库类型为 `datetime`。与 BaseEntity 基类保持一致(createTime/updateTime 均为 Date)。ServiceImpl 中使用 `new Date()` 而非 `LocalDateTime.now()`
+
+## 技能体系
+
+### 指令规范
+- 指令规范文件:`D:\dt_ykt\ykt_server\.workbuddy\CMD.md`
+- 定义 `/require` 和 `/dev` 双指令系统
+
+### /require 指令 → require 技能
+- 触发:`/require [模块] [功能名] [简要需求] [选项]`
+- 技能位置:`D:\dt_ykt\ykt_server\.workbuddy\skills\require\`
+- 输出:`doc/[模块]-[功能名]-requirements.md`(需求规格说明书)
+- 流程:分析简要需求 → 加载需求模板 → 生成需求文档 → 用户确认
+- **`--db` 选项**:指定数据库类型(kingbase/mysql/dm/oceanbase),默认 kingbase,影响建表语句生成
+- 需求文档包含9个章节:基础信息、接口清单、字段属性速查表、字段清单、VO结构、特殊需求、字典项、建表语句、备注
+
+### /dev 指令 → ruoyi-backend + ykt-web-dev 技能
+- 触发:`/dev [模块] [功能名] [选项]`
+- 输入:读取 `doc/[模块]-[功能名]-requirements.md`
+- 后端技能:`D:\dt_ykt\ykt_server\.workbuddy\skills\ruoyi-backend\`(已新增需求驱动章节)
+- 前端技能:`D:\dt_ykt\ykt_web\.workbuddy\skills\ykt-web-dev\`(已新增需求驱动章节)
+- 输出:后端 Java 文件 + 前端 Vue/TS 文件
+
+### 路径约定
+- 需求文档:`d:/dt_ykt/ykt_server/doc/[模块]-[功能名]-requirements.md`
+- 后端代码:`d:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-[模块]/`
+- 前端代码:`d:/dt_ykt/ykt_web/src/`
+- 可通过 `--backend=` 和 `--frontend=` 参数手工指定项目路径
+
+### 完整开发流程
+```
+1. /require [模块] [功能名] [简要需求]  →  生成需求规格说明书
+2. 反复沟通/人工调整需求文档             →  确认最终需求规格说明书
+3. /dev [模块] [功能名]                  →  读取需求文档,生成前后端代码
+4. 用户确认代码                          →  写入文件
+```
+
+### 技能清单
+| 技能 | 位置 | 职责 |
+|------|------|------|
+| require | `ykt_server/.workbuddy/skills/require/` | 需求分析 + 规格说明书生成 |
+| ruoyi-backend | `ykt_server/.workbuddy/skills/ruoyi-backend/` | 后端代码生成(模板驱动 + 需求驱动) |
+| ykt-web-dev | `ykt_web/.workbuddy/skills/ykt-web-dev/` | 前端代码生成(模板驱动 + 需求驱动) |
+

+ 84 - 0
.workbuddy/plans/create-crud-command-for-ecs_7e7de224(未完成).md

@@ -0,0 +1,84 @@
+---
+name: create-crud-command-for-ecs
+overview: 设计并编写一个在指定模块下创建业务功能的标准化指令规范,触发 ruoyi-backend 技能自动生成 CRUD 代码
+todos:
+  - id: create-cmd-spec
+    content: 创建 CMD.md 指令规范文件,包含完整指令语法、参数说明、示例
+    status: pending
+  - id: update-skill-md
+    content: 更新 SKILL.md,在开发工作流程章节添加指令使用说明
+    status: pending
+    dependencies:
+      - create-cmd-spec
+---
+
+## 用户需求
+
+设计并创建一套在 RuoYi-Cloud-Plus 项目指定模块下创建业务功能的指令规范文件。
+
+## 功能概述
+
+- 创建 `CMD.md` 指令规范文件,定义标准化的代码生成指令格式
+- 支持多种触发方式:/生成、生成功能、创建功能、create
+- 支持两种生成模式:纯消费者模式(consumer)和完整 CRUD 模式(full)
+- 支持 YAML 格式字段定义内联
+- 更新 `SKILL.md` 添加指令使用说明
+
+## 核心特性
+
+1. **模块指定**:支持 `module`、`sub`、`name` 三级结构
+2. **生成模式**:
+
+- consumer:纯消费者,仅生成 Controller(ECS 现有模式)
+- full:完整 CRUD,生成 Domain/Bo/Vo/Mapper/Service/ServiceImpl/Controller
+
+3. **可选功能**:Dubbo 暴露、导入导出、多租户支持
+4. **字段定义**:YAML 格式内联,支持完整字段属性
+
+## 输出文件
+
+1. `d:/dt_ykt/ykt_server/.workbuddy/skills/ruoyi-backend/CMD.md`(新建)
+2. `d:/dt_ykt/ykt_server/.workbuddy/skills/ruoyi-backend/SKILL.md`(更新)
+
+## 技术方案
+
+### 1. 指令设计原则
+
+- **简洁优先**:最小化必填参数,提供合理的默认值
+- **灵活扩展**:支持内联 YAML 字段定义,也支持引用需求模板
+- **模式匹配**:根据 `mode` 参数自动选择生成策略
+- **一致性**:与现有 ruoyi-backend 技能模板保持完全一致
+
+### 2. 生成模式
+
+| 模式 | 描述 | 生成文件 |
+| --- | --- | --- |
+| consumer | 纯消费者,仅 Controller | Controller.java |
+| full | 完整 CRUD | Domain, Bo, Vo, Mapper, Service, ServiceImpl, Controller |
+| full+dubbo | 完整+暴露 | 上述 + RemoteService, RemoteServiceImpl |
+
+
+### 3. 指令参数规范
+
+| 参数 | 默认值 | 说明 |
+| --- | --- | --- |
+| `module` | 必填 | 模块名:ruoyi-ecs, ruoyi-backstage, ruoyi-system |
+| `sub` | 必填 | 子模块名:basics, course, student |
+| `name` | 必填 | 功能名:course, teacher, classroom |
+| `table` | t_{module}_{name} | 数据库表名 |
+| `mode` | full | 模式:consumer/full |
+| `dubbo` | false | 是否暴露 Dubbo 接口 |
+| `import` | false | 是否支持导入导出 |
+| `tenant` | true | 是否多租户 |
+| `fields` | - | YAML 字段定义 |
+
+
+### 4. ECS 模块特殊处理
+
+- ECS 模块默认使用 consumer 模式
+- Controller 路径:`org.dromara.ecs.controller.{sub}.{Name}Controller`
+- 权限前缀:`ecs:{name}:{operation}`
+
+# Agent Extensions
+
+本任务不涉及 Agent Extensions 使用。

+ 83 - 0
.workbuddy/plans/ecs-attend-requirements_5bc8e760.md

@@ -0,0 +1,83 @@
+---
+name: ecs-attend-requirements
+overview: 为 ECS 电子班牌模块生成「考勤管理」功能的需求规格说明书,包含字段定义、VO结构、字典项、建表DDL等完整规格。
+todos:
+  - id: generate-attend-requirements
+    content: 使用 [skill:require] 生成 ECS 考勤模块完整需求规格说明书到 doc/ecs-attend-requirements.md
+    status: completed
+---
+
+## 产品概述
+
+ECS 电子班牌系统考勤管理模块,用于记录和管理学员的刷卡/人脸识别考勤数据,支持考勤记录查询、详情查看、手工补录和 Excel 导出功能。
+
+## 核心功能
+
+### 数据结构(14个业务字段)
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| attendId | Long | 考勤ID(主键) |
+| userId | Long | 学员ID |
+| userNumb | String | 学号 |
+| realName | String | 学员姓名 |
+| classId | Long | 班级ID |
+| className | String | 班级名称 |
+| roomId | Long | 教室ID |
+| roomName | String | 教室名称 |
+| checkTime | LocalDateTime | 考勤时间 |
+| checkType | Integer | 考勤方式:0-刷卡 1-人脸(字典 check_type) |
+| uploadTime | LocalDateTime | 上传时间 |
+| pushStatus | Integer | 推送状态:0-未推送 1-成功 2-失败(字典 push_status) |
+| pushTime | LocalDateTime | 推送时间 |
+| pushRetry | Integer | 推送尝试次数 |
+
+
+### 列表显示字段(8列)
+
+班级名称、学员姓名、学号、教室名称、考勤时间、上传时间、推送状态、推送时间
+
+### 查询条件(3项)
+
+学员姓名(模糊)、班级名称(精确)、推送状态(字典下拉)
+
+### 业务操作
+
+- **导出**:Excel 导出考勤数据
+- **查看**:考勤记录详情
+- **手工考勤**:新增手工录入的考勤记录
+
+### 约束条件
+
+- 无编辑/删除接口(考勤记录生成后不可修改)
+- 多租户隔离
+- 数据库类型:kingbase(人大金仓)
+
+## 技术方案
+
+基于 require 技能的需求规格说明书模板,参照 `doc/ecs-course-requirements.md` 已有文档格式,生成 ECS 考勤模块的需求规格说明书。
+
+## 实现方式
+
+按照 require 技能的工作流程执行:
+
+1. **解析需求**:从用户 JSON 数据结构和业务描述中提取全部字段及属性
+2. **字段推断**:按技能规则推断每个字段的 inTable/inQuery/inForm/dictType/component 等属性
+3. **模板填充**:生成完整的9章节需求规格说明书 Markdown 文档
+4. **DDL 生成**:根据 kingbase 方言生成建表 SQL 语句
+
+## 关键设计决策
+
+1. **className/userNumb/roomName 为 VO 承载字段(inDb=false)**:这些是关联实体的名称冗余字段,实际存储在表中但由关联 ID 字段决定。根据用户给出的数据结构,这些字段直接存在于数据中(可能是设备上报时由系统填充),所以 `inDb=true`。
+2. **userId/classId/roomId 不在列表显示但在表单中使用**:作为隐藏关联字段存在。
+3. **无编辑/删除**:接口清单仅保留 list/query/add/export 三类操作接口。
+4. **checkType/pushStatus 使用字典组件**:分别对应 `check_type` 和 `push_status` 字典。
+5. **手工考勤 = 新增接口**:通过 POST /attend/ 实现,表单中选择学员、教室、考勤方式等。
+
+## Agent Extensions
+
+### Skill
+
+- **require**
+- Purpose: 根据需求模板和字段解析规则,生成结构化的需求规格说明书 Markdown 文档
+- Expected outcome: 在 `doc/ecs-attend-requirements.md` 产出完整的9章节需求规格说明书,包含基础信息、接口清单、字段清单(YAML)、VO 结构、特殊需求、字典项、Kingbase DDL 建表语句、备注

+ 40 - 0
.workbuddy/plans/ecs-course-requirements_9a7b4847.md

@@ -0,0 +1,40 @@
+---
+name: ecs-course-requirements
+overview: 根据需求规格模板,为ECS模块的课程管理(course)功能生成完整的需求规格说明书
+todos:
+  - id: generate-requirements-doc
+    content: 生成课程管理需求规格说明书并保存到 doc/ecs-course-requirements.md
+    status: completed
+---
+
+## 产品概述
+
+根据需求规格模板,为 ECS 模块生成课程管理功能的需求规格说明书。
+
+## 核心功能
+
+- 填写基础信息(模块名 ecs、功能简写 course、主表 t_ecs_course、多租户)
+- 定义标准 CRUD 接口清单(列表/详情/新增/编辑/删除/导入导出)
+- 根据字段表完成 YAML 字段清单(含查询方式、表单显示、字典、关联等属性)
+- 定义 VO 结构(列表 VO)
+- 配置特殊需求(导入导出、Dubbo 暴露)
+- 列出字典项(课程类型 course_type、数据来源 data_source)—— **字典是全局的,不加模块前缀**
+- 生成完整 Markdown 文件输出到工作区
+- 将"字典命名不加模块前缀"规则写入技能 SKILL.md
+
+## 技术栈
+
+- 文档格式:Markdown(YAML 内嵌)
+- 输出路径:d:/dt_ykt/ykt_server/doc/
+
+## 实现方案
+
+基于已读取的模板文件和用户提供的字段定义,逐章节填充生成完整的需求规格说明书。所有字段属性决策已在上下文中明确,无需额外探索。
+
+## 目录结构
+
+```
+d:/dt_ykt/ykt_server/
+└── doc/
+    └── ecs-course-requirements.md  # [NEW] 课程管理需求规格说明书
+```

+ 258 - 0
.workbuddy/plans/require-dev-command-system_ad93f548.md

@@ -0,0 +1,258 @@
+---
+name: require-dev-command-system
+overview: 设计 /require 和 /dev 双阶段指令系统及对应技能,实现从需求规格说明书到前后端代码生成的完整开发流程
+todos:
+  - id: create-cmd
+    content: 创建 .workbuddy/CMD.md 指令规范文件,定义 /require 和 /dev 指令语法、参数、流程、示例
+    status: completed
+  - id: create-require-skill
+    content: 创建 require 技能:SKILL.md + 迁移 requirements-template.md
+    status: completed
+    dependencies:
+      - create-cmd
+  - id: update-backend-skill
+    content: 增强 ruoyi-backend 技能:新增需求规格说明书驱动代码生成章节
+    status: completed
+    dependencies:
+      - create-require-skill
+  - id: update-frontend-skill
+    content: 增强 ykt-web-dev 技能:新增需求规格说明书驱动代码生成章节
+    status: completed
+    dependencies:
+      - create-require-skill
+  - id: update-memory
+    content: 更新 MEMORY.md,记录指令规范和技能体系
+    status: completed
+    dependencies:
+      - update-backend-skill
+      - update-frontend-skill
+---
+
+## 产品概述
+
+设计一套双阶段指令驱动的工作流,用于在 RuoYi-Cloud-Plus 项目中自动化生成业务功能的前后端代码。
+
+## 核心特性
+
+- **`/require` 指令**:输入简要需求,调用 require 技能生成结构化需求规格说明书(Markdown),支持反复沟通迭代
+- **`/dev` 指令**:基于已确认的需求规格说明书,调用后端技能 + 前端技能生成完整前后端代码
+- **指令规范文件**:`CMD.md` 统一定义指令语法、参数、流程、示例
+- **require 技能**:独立的技能,负责需求分析和文档生成
+- **dev 技能升级**:在现有 ruoyi-backend 和 ykt-web-dev 技能基础上,增加按需求规格说明书驱动代码生成的能力
+
+## 工作流程
+
+```
+用户输入 /require ecs 课程 [简要需求]
+    ↓
+require 技能 → 分析需求 → 加载需求模板 → 生成需求规格说明书
+    ↓
+用户反复沟通/人工调整 → 最终确认 doc/ecs-course-requirements.md
+    ↓
+用户输入 /dev ecs 课程
+    ↓
+dev 技能 → 读取需求规格说明书 → 后端生成 + 前端生成 → 输出代码文件
+```
+
+## 路径约定
+
+- 需求文档:`d:/dt_ykt/ykt_server/doc/[模块]-[功能名]-requirements.md`
+- 后端代码:`d:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-[模块]/`
+- 前端代码:`d:/dt_ykt/ykt_web/src/`
+- 可通过参数手工指定前后端项目位置
+
+## 技术方案
+
+### 整体架构
+
+```
+指令层(CMD.md)          ← 统一指令入口定义
+    │
+    ├── /require → require-skill     ← 需求分析技能(新建)
+    │                ├── SKILL.md
+    │                └── references/requirements-template.md(从 ruoyi-backend 迁移)
+    │
+    └── /dev     → ruoyi-backend + ykt-web-dev  ← 开发技能(现有,需增强)
+                     ├── 读取需求规格说明书
+                     ├── ruoyi-backend:生成后端代码
+                     └── ykt-web-dev:生成前端代码
+```
+
+### 1. 指令设计(CMD.md)
+
+**`/require` 指令**:
+
+```
+/require [模块] [功能名] [简要需求描述]
+```
+
+参数说明:
+
+| 参数 | 必填 | 说明 | 示例 |
+| --- | --- | --- | --- |
+| 模块 | 是 | 模块名(不含 ruoyi- 前缀) | ecs, backstage, system |
+| 功能名 | 是 | 功能简写(英文) | course, student |
+| 简要需求 | 否 | 自然语言描述,可多行 | 课程名称、课程类型(字典course_type)、学分、讲师(多对多) |
+
+
+可选参数(通过标记指定):
+
+| 标记 | 说明 | 示例 |
+| --- | --- | --- |
+| `--table=表名` | 指定主表名 | `--table=t_ecs_course` |
+| `--sub-table=表名` | 指定从表名 | `--sub-table=t_ecs_course_teacher` |
+| `--no-tenant` | 非多租户 | `--no-tenant` |
+| `--no-import` | 不需要导入导出 | `--no-import` |
+
+
+示例:
+
+```
+/require ecs 课程 课程名称、课程编码、课程类型(字典course_type)、学分、学时、讲师(多对多关联) --sub-table=t_ecs_course_teacher
+```
+
+**`/dev` 指令**:
+
+```
+/dev [模块] [功能名] [选项]
+```
+
+参数说明:
+
+| 参数 | 必填 | 说明 | 示例 |
+| --- | --- | --- | --- |
+| 模块 | 是 | 模块名 | ecs |
+| 功能名 | 是 | 功能简写 | course |
+| --backend=路径 | 否 | 后端项目路径,默认 d:/dt_ykt/ykt_server | --backend=d:/other/server |
+| --frontend=路径 | 否 | 前端项目路径,默认 d:/dt_ykt/ykt_web | --frontend=d:/other/web |
+| --backend-only | 否 | 仅生成后端 | --backend-only |
+| --frontend-only | 否 | 仅生成前端 | --frontend-only |
+
+
+示例:
+
+```
+/dev ecs 课程
+/dev ecs 课程 --frontend=d:/other/web
+/dev ecs 课程 --backend-only
+```
+
+### 2. require 技能设计
+
+**位置**:`d:/dt_ykt/ykt_server/.workbuddy/skills/require/`
+
+**职责**:
+
+1. 接收用户的简要需求描述
+2. 加载需求规格说明书模板(`references/requirements-template.md`)
+3. 分析简要需求,自动推断:
+
+- 字段类型、查询方式、表单组件
+- 字典字段识别(用户描述中带"字典"、"下拉"的)
+- 关联字段识别(用户描述中带"关联"、"选择XX"的)
+- 主从表关系(用户描述中带"多对多"、"一对多"的)
+
+4. 生成结构化的需求规格说明书
+5. 输出到 `doc/[模块]-[功能名]-requirements.md`
+
+**SKILL.md 核心流程**:
+
+```
+1. 解析指令参数(模块、功能名、简要需求)
+2. 加载 requirements-template.md 模板
+3. 分析简要需求,填充模板:
+   a. 基础信息:模块路径、表名推断(t_{模块}_{功能名})
+   b. 接口清单:默认 CRUD + 导入导出
+   c. 字段清单:逐字段解析,推断属性
+   d. VO 结构:根据 inTable/inForm 字段自动组装
+   e. 特殊需求:根据标记推断
+4. 输出需求规格说明书到 doc/ 目录
+5. 提示用户确认或修改
+```
+
+**需求分析推断规则**:
+
+| 用户描述 | 推断结果 |
+| --- | --- |
+| "名称" | fieldType=String, component=input, inQuery=true, queryType=like |
+| "类型(字典xxx)" | fieldType=Integer, dictType=xxx, component=select |
+| "编码" | fieldType=String, component=input, inQuery=true, queryType=like, required=true |
+| "学分/分数" | fieldType=BigDecimal, component=inputNumber |
+| "数量/人数" | fieldType=Integer, component=inputNumber |
+| "时间/日期" | fieldType=LocalDateTime, component=datetime |
+| "备注/描述" | fieldType=String, component=textarea |
+| "状态" | fieldType=Integer, dictType=status, component=select |
+| "讲师/教师(多对多)" | 生成从表,relation 配置 |
+| "排序" | fieldType=Integer, component=inputNumber |
+| "来源" | fieldType=Integer, dictType=data_source, component=select |
+
+
+### 3. dev 技能增强设计
+
+**后端技能增强**(ruoyi-backend):
+
+- 新增「从需求规格说明书生成代码」的工作流程
+- 读取 `doc/[模块]-[功能名]-requirements.md`
+- 按需求文档的字段清单生成 Domain/Bo/Vo/Mapper/Service/ServiceImpl/Controller
+- 按需求文档的特殊需求决定是否生成 ImportVo/ImportListener/Dubbo 接口
+
+**前端技能增强**(ykt-web-dev):
+
+- 新增「从需求规格说明书生成代码」的工作流程
+- 读取同一份需求规格说明书
+- 按 inTable 字段生成列表页(index.vue)
+- 按 inForm 字段生成表单页(form.vue / detail.vue)
+- 按 dictType 生成字典引用代码
+- 按 relation 生成关联选择组件
+- 生成 API 接口(index.ts)和类型定义(types.ts)
+
+### 4. 文件清单
+
+#### 新建文件
+
+| 文件路径 | 说明 |
+| --- | --- |
+| `d:/dt_ykt/ykt_server/.workbuddy/skills/require/SKILL.md` | require 技能主文件 |
+| `d:/dt_ykt/ykt_server/.workbuddy/skills/require/references/requirements-template.md` | 需求模板(从 ruoyi-backend/references/ 迁移) |
+| `d:/dt_ykt/ykt_server/.workbuddy/CMD.md` | 指令规范文件(全局,位于 .workbuddy 根目录) |
+
+
+#### 修改文件
+
+| 文件路径 | 修改内容 |
+| --- | --- |
+| `d:/dt_ykt/ykt_server/.workbuddy/skills/ruoyi-backend/SKILL.md` | 新增「从需求规格说明书驱动代码生成」章节 |
+| `d:/dt_ykt/ykt_web/.workbuddy/skills/ykt-web-dev/SKILL.md` | 新增「从需求规格说明书驱动代码生成」章节 |
+
+
+### 5. 需求规格说明书与代码生成的映射关系
+
+```
+需求规格说明书                →  后端生成文件
+─────────────────────────────────────────────────
+基础信息.模块                →  包路径 org.dromara.{module}
+基础信息.主表名              →  @TableName, Domain 类
+基础信息.从表名              →  从表 Domain/Bo/Vo/Mapper/Service
+基础信息.是否多租户          →  extends TenantEntity / BaseEntity
+字段清单(inDb=true)          →  Domain 字段
+字段清单(必填)               →  Bo 校验注解 @NotNull/@NotBlank
+字段清单(inTable=true)       →  Vo @ExcelProperty + 列表展示
+字段清单(dictType)           →  Vo @ExcelDictFormat, 前端字典下拉
+字段清单(relation)           →  前端关联选择组件
+字段清单(inDb=false)         →  Vo 纯计算字段(如 teacherNames)
+接口清单                     →  Controller 方法
+特殊需求.excelImport         →  ImportVo + ImportListener
+特殊需求.dubboExpose         →  RemoteService + RemoteServiceImpl
+
+需求规格说明书                →  前端生成文件
+─────────────────────────────────────────────────
+基础信息.功能简写             →  api/{module}/{name}/, views/{module}/{name}/
+字段清单(inTable=true)       →  index.vue 表格列
+字段清单(inQuery=true)       →  index.vue 搜索栏
+字段清单(inForm=true)        →  form.vue 表单项
+字段清单(dictType)           →  useDict() + el-select
+字段清单(relation)           →  关联选择弹窗
+字段清单(component)          →  对应 Element Plus 组件
+接口清单                     →  api/index.ts 接口函数
+VO 结构                      →  api/types.ts 类型定义
+```

+ 320 - 0
.workbuddy/skills/require/SKILL.md

@@ -0,0 +1,320 @@
+# require 技能 — 需求规格说明书生成
+
+> 本技能负责分析用户的简要需求描述,基于需求规格说明书模板,生成结构化的需求规格说明书文档。
+
+## 语言要求
+
+**所有对话、说明、注释均使用中文。** 与用户的交互全程中文,代码注释也使用中文。
+
+## 技能定位
+
+- **触发方式**:用户输入 `/require` 指令
+- **核心职责**:将自然语言需求转化为结构化需求规格说明书
+- **输出产物**:`doc/{模块}-{功能名}-requirements.md`
+- **下游依赖**:`/dev` 指令读取需求规格说明书生成代码
+
+---
+
+## 工作流程
+
+### 1. 解析指令
+
+从用户输入中提取:
+
+| 参数 | 提取规则 | 示例 |
+|------|----------|------|
+| 模块 | 第1个参数 | `ecs` → `ruoyi-modules/ruoyi-ecs` |
+| 功能名 | 第2个参数 | `course` |
+| 功能中文名 | 自动推断或用户补充 | `课程`、`课程管理` |
+| 简要需求 | 第3个参数及之后 | `课程名称、课程类型(字典course_type)、讲师(多对多)` |
+| 选项 | `--` 前缀标记 | `--table=t_ecs_course --dubbo --db=kingbase` |
+
+**模块路径映射**:
+
+| 模块名 | 后端路径 | 包名 |
+|--------|----------|------|
+| ecs | `ruoyi-modules/ruoyi-ecs` | `org.dromara.ecs` |
+| backstage | `ruoyi-modules/ruoyi-backstage` | `org.dromara.backstage` |
+| system | `ruoyi-modules/ruoyi-system` | `org.dromara.system` |
+
+### 2. 加载需求模板
+
+读取 `references/requirements-template.md`,模板包含以下章节:
+
+1. 基础信息(模块、表名、多租户、数据库类型等)
+2. 接口清单(CRUD + 导入导出)
+3. 字段属性速查表(字段元数据定义规范)
+4. 字段清单(YAML 格式,主表 + 从表)
+5. VO 结构(列表 VO、明细 VO、从表 VO)
+6. 特殊需求(导入导出、Dubbo 暴露等)
+7. 字典项
+8. 建表语句(根据数据库类型生成 DDL)
+9. 备注
+
+### 3. 分析简要需求
+
+将用户自然语言描述的字段逐个解析,推断字段属性。
+
+#### 字段解析规则
+
+| 用户描述模式 | 推断属性 |
+|-------------|----------|
+| "XX名称" / "XX名" | `fieldType=String`, `component=input`, `inQuery=true`, `queryType=like` |
+| "XX编码" / "XX代码" | `fieldType=String`, `component=input`, `inQuery=true`, `queryType=like`, `required=true` |
+| "XX类型(字典xxx)" | `fieldType=String`, `dictType=xxx`, `component=select`, `inQuery=true`, `queryType=eq` |
+| "XX状态" | `fieldType=String`, `dictType=status`, `component=select`, `inQuery=true`, `queryType=eq` |
+| "学分" / "分数" / "金额" | `fieldType=BigDecimal`, `component=inputNumber` |
+| "数量" / "人数" / "学时" | `fieldType=Integer`, `component=inputNumber` |
+| "时间" / "日期" | `fieldType=Date`, `component=datetime` |
+| "备注" / "描述" / "说明" | `fieldType=String`, `component=textarea`, `inTable=false` |
+| "排序" / "序号" | `fieldType=Integer`, `component=inputNumber` |
+| "来源" | `fieldType=String`, `dictType=data_source`, `component=select`, `inTable=false`, `inQuery=false`, `lockRule: {lockValue: 1, lockActions: [edit, delete], tip: '第三方数据不可操作'}` |
+| "XX(多对多)" / "XX(关联)" | 生成从表配置 + relation |
+| "XX(字典xxx)" | `fieldType=String`, `dictType=xxx`, `component=select` |
+| "第三方标识" / "otherId" | `fieldType=String`, `inTable=false`, `inForm=false` |
+
+#### 字段通用默认值
+
+| 属性 | 默认值 |
+|------|--------|
+| `inDb` | `true` |
+| `inTable` | `true`(除非备注/描述类) |
+| `inQuery` | `false`(除非名称/编码/类型/状态类) |
+| `queryType` | `eq` |
+| `inForm` | `true`(除非主键/公共字段) |
+| `inAdd` | `true` |
+| `inEdit` | `true` |
+| `required` | `false`(除非编码类) |
+| `component` | `input` |
+| `excelExport` | `true` |
+| `lockRule` | `null`(除非"来源"/"数据来源"类字段) |
+
+#### 主从关系推断
+
+当用户描述包含以下关键词时,自动生成从表:
+
+- **"多对多"**:创建中间表,从表字段包含 `id`、`{mainId}`、关联字段ID + 名称、`createTime`
+- **"一对多"**:创建从表,从表包含 `id`、`{mainId}`、业务字段
+- **`--sub-table=表名`**:显式指定从表名
+
+从表命名规则:
+- 多对多:`t_{模块}_{主表}_{关联实体}`(如 `t_ecs_course_teacher`)
+- 一对多:`t_{模块}_{从表实体}`(如 `t_ecs_course_schedule`)
+
+### 4. 填充模板
+
+按照以下顺序填充需求规格说明书:
+
+1. **基础信息**:模块路径、表名推断(`t_{模块}_{功能名}`)、多租户、数据库类型(默认 kingbase)、描述
+2. **接口清单**:默认 CRUD 7 个接口 + 导入导出(除非 `--no-import`)
+3. **字段清单**:
+   - 主键字段(`{功能名}Id`, Long, inTable=false, inForm=false, excelExport=false)
+   - 业务字段(按用户描述逐个推断)
+   - VO 承载字段(`inDb=false` 的计算字段,如 teacherNames)
+   - 第三方同步字段(otherId + dataSource,如有"来源"描述)
+   - 公共字段(自动追加,无需手动写,见下方通用字段定义)
+4. **VO 结构**:根据 `inTable=true` 字段组装列表 VO,有从表时增加明细 VO。**有 lockRule 的字段必须加入 VO**(前端需此字段判断操作权限)
+5. **特殊需求**:根据选项标记填充
+5. **特殊需求**:根据选项标记填充
+6. **字典项**:根据 `dictType` 字段收集
+7. **建表语句**:根据数据库类型和字段清单生成 DDL(见下方 DDL 生成规则)
+8. **备注**:特殊设计说明
+
+### 5. 输出文档
+
+将填充后的需求规格说明书写入:
+
+```
+d:/dt_ykt/ykt_server/doc/{模块}-{功能名}-requirements.md
+```
+
+### 6. 确认与迭代
+
+- 展示生成的需求规格说明书给用户
+- 用户可指出需要修改的部分
+- 修改后重新生成,覆盖原文件
+- 用户也可直接编辑 Markdown 文件
+
+---
+
+## 参考文档索引
+
+| 文档 | 说明 |
+|------|------|
+| `references/requirements-template.md` | 需求规格说明书模板(字段属性定义规范) |
+
+---
+
+## 注意事项
+
+1. **字典命名不加模块前缀**:`course_type` 而非 `ecs_course_type`
+2. **DTO 命名使用 Dto 后缀**:`RemoteCourseDto`
+3. **多对多列表展示**:通过 SQL 拼接关联字段(如 GROUP_CONCAT),主表增加 `inDb=false` 的 VO 承载字段
+4. **权限字符格式**:`{模块}:{功能}:{操作}`,如 `ecs:course:list`
+5. **接口路径格式**:`/{功能名}/{操作}`,如 `/course/list`
+6. **字段命名使用驼峰**:Java 端 `courseName`,数据库端 `course_name`
+7. **数据库类型选项**:`--db=kingbase`(默认)、`--db=mysql`、`--db=dm`、`--db=oceanbase`
+8. **操作锁定规则 lockRule**:当某字段值决定行级操作权限时使用。字段需 `inDb=true`,通常 `inTable=false`(不显示列)、`inQuery=false`(不作为搜索条件),但**必须包含在 VO 中**供前端判断。典型场景:`dataSource=1` 时禁止编辑和删除。前端根据 lockRule 配置,在操作列按字段值动态显示/隐藏编辑、删除按钮
+9. **主键不导出 Excel**:主键字段(Long 类型)默认 `excelExport=false`,Vo 中不加 `@ExcelProperty` 注解
+
+---
+
+## 通用字段定义
+
+所有表统一包含以下通用字段,建表 DDL 中自动追加,字段清单中无需重复声明:
+
+| 字段          | Java 类型       | 逻辑删除默认值 | 说明               |
+| ------------- | --------------- | -------------- | ------------------ |
+| del_flag      | Integer         | 0              | 逻辑删除:0-未删除,1-已删除 |
+| create_dept   | Long            | —              | 创建部门             |
+| create_by     | Long            | —              | 创建者              |
+| create_time   | Date            | —              | 创建时间             |
+| update_by     | Long            | —              | 最后修改者            |
+| update_time   | Date            | —              | 最后修改时间           |
+
+如果基础信息中"是否多租户"为 **是**,则额外包含:
+
+| 字段          | Java 类型       | 说明               |
+| ------------- | --------------- | ------------------ |
+| tenant_id     | Long            | 租户ID             |
+
+### 通用字段在各数据库中的 DDL 类型
+
+| 字段          | kingbase                              | mysql             | dm          | oceanbase         |
+| ------------- | ------------------------------------- | ----------------- | ----------- | ----------------- |
+| del_flag      | character(1 char) NOT NULL DEFAULT '0'::bpchar | char(1) NOT NULL DEFAULT '0' | CHAR(1) DEFAULT '0' | char(1) NOT NULL DEFAULT '0' |
+| create_dept   | bigint                                | bigint(20)        | BIGINT      | bigint(20)        |
+| create_by     | bigint                                | bigint(20)        | BIGINT      | bigint(20)        |
+| create_time   | timestamp                             | datetime          | TIMESTAMP   | datetime          |
+| update_by     | bigint                                | bigint(20)        | BIGINT      | bigint(20)        |
+| update_time   | timestamp                             | datetime          | TIMESTAMP   | datetime          |
+| tenant_id     | bigint                                | bigint(20)        | BIGINT      | bigint(20)        |
+
+---
+
+## DDL 生成规则
+
+### 数据库类型映射
+
+| 选项值 | 数据库 | 方言标识 |
+|--------|--------|----------|
+| `kingbase` | 人大金仓(默认) | KingbaseES |
+| `mysql` | MySQL | MySQL |
+| `dm` | 达梦 | DM |
+| `oceanbase` | OceanBase | OceanBase |
+
+### 字段类型映射
+
+| Java 类型 | kingbase | mysql | dm | oceanbase |
+|-----------|----------|-------|----|-----------|
+| Long | bigint | bigint(20) | BIGINT | bigint(20) |
+| Integer | integer | int(11) | INT | int(11) |
+| String(无长度) | character varying(255 char) | varchar(255) | VARCHAR(255) | varchar(255) |
+| **String(字典字段)** | **character varying(16 char)** | **varchar(16)** | **VARCHAR(16)** | **varchar(16)** |
+| BigDecimal | numeric(10,2) | decimal(10,2) | DECIMAL(10,2) | decimal(10,2) |
+| Date | timestamp | datetime | TIMESTAMP | datetime |
+
+> **⚠️ 字典字段类型规范**:数据字典(dictType)对应的数据库字段类型统一为 `varchar(16)`,Java 类型为 `String`。字典值虽然常为数字(如 "0"、"1"),但存储和传输统一使用字符串。
+
+### DDL 生成规范
+
+#### kingbase(人大金仓)
+
+```sql
+-- 建表
+CREATE TABLE "dbo"."t_xxx" (
+  "id" bigint NOT NULL,
+  "column_name" character varying(255 char) NOT NULL DEFAULT ''::varchar,
+  "status" integer NOT NULL DEFAULT '0'::bpchar,
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" timestamp,
+  "update_by" bigint,
+  "update_time" timestamp,
+  CONSTRAINT "t_xxx_pkey" PRIMARY KEY ("id")
+);
+-- 多租户表额外包含:
+-- "tenant_id" character varying(20 char) NOT NULL DEFAULT ''::varchar,
+
+-- 表注释
+ALTER TABLE "dbo"."t_xxx" COMMENT '表注释';
+
+-- 列注释
+ALTER TABLE "dbo"."t_xxx" MODIFY "column_name" COMMENT '列注释';
+```
+
+#### mysql
+
+```sql
+CREATE TABLE `t_xxx` (
+  `id` bigint(20) NOT NULL,
+  `column_name` varchar(255) NOT NULL DEFAULT '' COMMENT '列注释',
+  `status` int(11) NOT NULL DEFAULT '0' COMMENT '状态',
+  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0-未删除 1-已删除)',
+  `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
+  `create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  -- 多租户表额外包含:
+  -- `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户编号',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表注释';
+```
+
+#### dm(达梦)
+
+```sql
+CREATE TABLE "t_xxx" (
+  "id" BIGINT NOT NULL,
+  "column_name" VARCHAR(255) DEFAULT '',
+  "status" INT DEFAULT 0,
+  "del_flag" CHAR(1) DEFAULT '0',
+  "create_dept" BIGINT,
+  "create_by" BIGINT,
+  "create_time" TIMESTAMP,
+  "update_by" BIGINT,
+  "update_time" TIMESTAMP,
+  -- 多租户表额外包含:
+  -- "tenant_id" BIGINT,
+  PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "t_xxx" IS '表注释';
+COMMENT ON COLUMN "t_xxx"."column_name" IS '列注释';
+```
+
+#### oceanbase
+
+```sql
+CREATE TABLE `t_xxx` (
+  `id` bigint(20) NOT NULL,
+  `column_name` varchar(255) NOT NULL DEFAULT '' COMMENT '列注释',
+  `status` int(11) NOT NULL DEFAULT '0' COMMENT '状态',
+  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0-未删除 1-已删除)',
+  `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
+  `create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  -- 多租户表额外包含:
+  -- `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户编号',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表注释';
+```
+
+### DDL 生成逻辑
+
+1. 从字段清单中提取 `inDb=true` 的字段
+2. 跳过 `inDb=false` 的纯 VO 承载字段
+3. 根据数据库类型映射字段类型
+4. String 类型默认长度 255,varchar 默认值 `''`,Integer 默认值 `0`
+5. **字典字段**(有 dictType)使用 `varchar(16)` 而非 `integer`,Java 类型为 `String`
+5. **通用字段自动追加**(无需在字段清单中声明):
+   - 所有表:`del_flag`、`create_dept`、`create_by`、`create_time`、`update_by`、`update_time`
+   - 多租户表额外:`tenant_id`
+6. 通用字段类型按上方「通用字段在各数据库中的 DDL 类型」表映射
+7. 主键约束使用表名 + `_pkey` 命名(kingbase 风格)
+8. 从表 DDL 同理生成,额外包含主表关联字段(如 `course_id`)
+9. 从表也包含完整的通用字段

+ 370 - 0
.workbuddy/skills/require/references/requirements-template.md

@@ -0,0 +1,370 @@
+# 功能需求规格文档模板
+
+> 本模板用于描述业务功能需求,技能将根据此文档生成后端 + 前端代码。
+> 请按此格式填写,结构化描述越完整,代码生成质量越高。
+
+---
+
+## 1. 基础信息
+
+| 属性 | 值 |
+|------|-----|
+| 功能中文名 | {功能中文名} |
+| 功能简写 | {功能简写} |
+| 所属模块 | {ruoyi-modules/xxx} |
+| 主表名 | {t_xxx} |
+| 从表名 | {t_xxx}(如无主从结构则填"无") |
+| 是否多租户 | 是/否 |
+| 数据库类型 | {kingbase/mysql/dm/oceanbase}(默认 kingbase) |
+| 描述 | {一句话功能描述} |
+
+---
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| 列表 | GET | /{简写}/list | /{模块}/{简写}/list | {模块}:{简写}:list |
+| 详情 | GET | /{简写}/{id} | /{模块}/{简写}/{id} | {模块}:{简写}:query |
+| 新增 | POST | /{简写}/ | /{模块}/{简写}/ | {模块}:{简写}:add |
+| 编辑 | PUT | /{简写}/ | /{模块}/{简写}/ | {模块}:{简写}:edit |
+| 删除 | DELETE | /{简写}/{ids} | /{模块}/{简写}/{ids} | {模块}:{简写}:remove |
+| 导出模板 | POST | /{简写}/exportTemplate | /{模块}/{简写}/exportTemplate | {模块}:{简写}:export |
+| 导入数据 | POST | /{简写}/importData | /{模块}/{简写}/importData | {模块}:{简写}:import |
+| 导出数据 | POST | /{简写}/export | /{模块}/{简写}/export | {模块}:{简写}:export |
+
+### 2.2 补充接口(如有)
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| ... | ... | ... | ... | ... |
+
+---
+
+## 3. 字段属性速查表
+
+| 属性 | 含义 | 可选值 | 说明 |
+|------|------|--------|------|
+| `fieldName` | 字段名(驼峰) | — | 代码变量名 |
+| `columnName` | 列名(下划线) | — | 数据库列名 |
+| `fieldType` | Java类型 | String/Long/Integer/BigDecimal/LocalDateTime | |
+| `inDb` | 是否在表中存在 | true/false | false=纯前端计算字段 |
+| `inTable` | 列表是否显示 | true/false | 生成表格列 |
+| `inQuery` | 是否查询字段 | true/false | 生成搜索条件 |
+| `queryType` | 查询方式 | eq/like/between | 精确/模糊/范围 |
+| `inForm` | 是否表单字段 | true/false | 生成表单项 |
+| `inAdd` | 新增表单显示 | true/false | |
+| `inEdit` | 编辑表单显示 | true/false | |
+| `required` | 是否必填 | true/false | 生成校验注解 |
+| `dictType` | 字典类型 | 字典标识 | 有值=字典下拉,否则普通输入 |
+| `relation` | 关联选择配置 | 对象 | 有值=弹出选择框 |
+| `component` | 前端组件类型 | input/select/inputNumber/datetime/textarea | 默认根据类型推断 |
+| `width` | 表格列宽 | 数字(px) | 默认auto |
+| `sort` | 排序 | 数字 | 越小越靠前 |
+| `excelExport` | Excel导出 | true/false | 默认true |
+| `lockRule` | 操作锁定规则 | 对象 | 有值=按此字段值控制行级操作权限 |
+
+### 关联选择 relation 格式
+
+```yaml
+relation:
+  table: {关联表名}
+  idField: {回填ID字段}
+  nameField: {显示名称字段}
+  title: 选择{实体名}
+  path: /{模块}/{实体}/{路径}
+```
+
+### 操作锁定规则 lockRule 格式
+
+> 当某字段的值决定该行是否允许修改/删除时,使用 lockRule。字段需 `inDb=true`,通常 `inTable=false`(不显示列)、`inQuery=false`(不作为搜索条件),但必须包含在 VO 中供前端判断。
+
+```yaml
+lockRule:
+  lockValue: {锁定值}          # 当字段值等于此值时,行操作被锁定
+  lockActions:                  # 被锁定的操作列表
+    - edit                      # 禁止编辑
+    - delete                    # 禁止删除
+  tip: {锁定提示文案}           # 锁定时显示的提示(可选)
+```
+
+**典型场景**:`dataSource`(数据来源)字段,值为 `1`(第三方同步)时禁止修改和删除,值为 `0`(平台录入)时允许操作。
+
+---
+
+## 4. 字段清单
+
+### 4.1 主表 `{t_xxx}`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: {xxxId}
+    columnName: {xxx_id}
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    excelExport: false
+    remark: 主键,编辑时传递
+
+  # ---------- 业务字段 ----------
+  - fieldName: {fieldName}
+    columnName: {column_name}
+    fieldType: String
+    inDb: true
+    inTable: true           # 列表是否显示
+    inQuery: true           # 是否为查询条件
+    queryType: like         # eq/like/between
+    inForm: true            # 是否在表单中
+    inAdd: true             # 新增表单是否显示
+    inEdit: true            # 编辑表单是否显示
+    required: true          # 是否必填
+    dictType: {dict_type}   # 字典类型(有则下拉,否则输入)
+    relation:               # 关联弹出选择(如有)
+      table: t_xxx
+      idField: xxx_id
+      nameField: xxx_name
+      title: 选择xxx
+      path: /xxx/xxx/selectXxx
+    component: input       # 前端组件:input/select/inputNumber/datetime/textarea
+    width: 150              # 表格列宽
+    sort: 1                 # 排序
+    excelExport: true
+    remark: 字段说明
+
+  # ---------- VO承载字段(仅返回,不展示不维护) ----------
+  - fieldName: {fieldName}
+    columnName: {column_name}
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    excelExport: false
+    remark: VO承载字段,不展示不维护
+
+  # ---------- 操作锁定字段(存在于表,不显示列,不查询,但控制行级操作权限) ----------
+  - fieldName: dataSource
+    columnName: data_source
+    fieldType: Integer
+    inDb: true
+    inTable: false          # 不显示列
+    inQuery: false          # 不作为查询条件(如需查询可设为 true)
+    inForm: true            # 表单中可选
+    inAdd: true
+    inEdit: true
+    required: true
+    dictType: data_source
+    component: select
+    lockRule:               # 操作锁定规则
+      lockValue: 1          # dataSource=1 时锁定
+      lockActions:
+        - edit              # 禁止编辑
+        - delete            # 禁止删除
+      tip: 第三方数据不可操作
+    excelExport: true
+    remark: 数据来源:0-平台录入,1-第三方同步
+
+  # ---------- 公共字段(所有表统一包含,多租户表增加 tenant_id) ----------
+  # 通用字段定义(无需在字段清单中重复写,建表时自动追加):
+  # | 字段          | Java类型        | 说明               |
+  # | ------------- | --------------- | ------------------ |
+  # | del_flag      | Integer         | 逻辑删除:0-未删除,1-已删除 |
+  # | create_dept   | Long            | 创建部门             |
+  # | create_by     | Long            | 创建者              |
+  # | create_time   | LocalDateTime   | 创建时间             |
+  # | update_by     | Long            | 最后修改者            |
+  # | update_time   | LocalDateTime   | 最后修改时间           |
+  # | tenant_id     | Long            | 租户ID(仅多租户表包含)    |
+
+  - fieldName: delFlag
+    columnName: del_flag
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: LocalDateTime
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+    excelExport: true
+```
+
+### 4.2 从表 `{t_xxx}`
+
+> 仅主从结构时填写。
+
+```yaml
+fields:
+  - fieldName: id
+    columnName: id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: {mainId}
+    columnName: {main_id}
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: {fieldName}
+    columnName: {column_name}
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false           # 编辑时是否允许修改
+    required: true
+    relation:
+      table: t_xxx
+      idField: xxx_id
+      nameField: xxx_name
+      title: 选择xxx
+      path: /xxx/xxx/selectXxx
+    component: xxxSelect
+    sort: 1
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: LocalDateTime
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+
+  # 从表同样包含通用字段(del_flag, create_dept, create_by, create_time, update_by, update_time)
+  # 多租户从表也包含 tenant_id
+```
+
+---
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+{xxx}Vo:
+  - {xxx}Id
+  - {field1}
+  - {field2}
+  # ... 按字段清单中 inTable=true 的字段填写
+  - createTime
+```
+
+### 5.2 主表明细 VO(如有从表)
+
+```yaml
+{xxx}DetailVo:
+  includes: {xxx}Vo
+  additional:
+    - {subList}: List<{xxx}{Sub}Vo>   # 从表列表
+```
+
+### 5.3 从表 VO
+
+```yaml
+{xxx}{Sub}Vo:
+  - id
+  - {mainId}
+  - {field1}
+  - {field2}
+  - createTime
+```
+
+---
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: true/false
+  excelExport: true/false
+
+  importRules:
+    - fieldName: {fieldName}
+      required: true/false
+      unique: true/false    # 唯一性校验
+      min: 数值
+      max: 数值
+
+  # 从表是否参与导入导出
+  subTableImport: true/false
+  subTableExport: true/false
+
+  dubboExpose: true/false
+  dubboServiceName: Remote{xxx}Service
+```
+
+---
+
+## 7. 字典项(如有)
+
+| 字典类型 | 枚举值 | 说明 |
+|----------|--------|------|
+| `{dict_type}` | 0=选项1, 1=选项2 | {说明} |
+
+---
+
+## 8. 建表语句
+
+> 根据基础信息中的"数据库类型"和字段清单自动生成建表 DDL。
+> 支持的数据库类型:kingbase(人大金仓)、mysql、dm(达梦)、oceanbase。
+> 默认为 kingbase。
+>
+> **通用字段**:所有表统一包含以下字段(建表语句自动追加,无需在字段清单中重复):
+>
+> | 字段          | 类型                 | 说明               |
+> | ------------- | -------------------- | ------------------ |
+> | del_flag      | TINYINT/SMALLINT     | 逻辑删除:0-未删除,1-已删除 |
+> | create_dept   | BIGINT               | 创建部门             |
+> | create_by     | BIGINT               | 创建者              |
+> | create_time   | DATETIME/TIMESTAMP   | 创建时间             |
+> | update_by     | BIGINT               | 最后修改者            |
+> | update_time   | DATETIME/TIMESTAMP   | 最后修改时间           |
+>
+> 如果指明了多租户,则额外包含:
+>
+> | 字段          | 类型                 | 说明               |
+> | ------------- | -------------------- | ------------------ |
+> | tenant_id     | BIGINT               | 租户ID             |
+
+### 8.1 主表 `{t_xxx}`
+
+```sql
+-- 根据数据库类型生成对应的 DDL
+-- kingbase 示例格式:
+-- CREATE TABLE "dbo"."t_xxx" ( ... );
+-- ALTER TABLE "dbo"."t_xxx" COMMENT '表注释';
+-- ALTER TABLE "dbo"."t_xxx" MODIFY "col" COMMENT '列注释';
+```
+
+### 8.2 从表 `{t_xxx}`(如有)
+
+```sql
+-- 同上格式
+```
+
+---
+
+## 9. 备注
+
+1. {补充说明1}
+2. {补充说明2}

+ 720 - 0
.workbuddy/skills/ruoyi-backend/SKILL.md

@@ -0,0 +1,720 @@
+# RuoYi-Cloud-Plus 后端开发技能
+
+## 语言要求
+
+**所有对话、说明、注释均使用中文。** 与用户的交互全程中文,代码注释也使用中文。
+
+## 简介
+
+本技能指导基于 RuoYi-Cloud-Plus 微服务框架的 Java 后端开发,涵盖代码结构、分层设计、Dubbo 远程调用等核心开发规范。
+
+## 项目结构
+
+```
+ykt_server/
+├── ruoyi-api/                    # API接口定义层
+│   ├── ruoyi-api-backstage/      # backstage模块对外暴露的Dubbo接口
+│   ├── ruoyi-api-system/         # system模块对外暴露的Dubbo接口
+│   └── ...
+├── ruoyi-modules/                # 业务模块实现层
+│   ├── ruoyi-backstage/          # 后台管理模块
+│   ├── ruoyi-ecs/                # ECS电子班牌模块
+│   ├── ruoyi-system/             # 系统管理模块
+│   └── ...
+├── ruoyi-common/                 # 公共组件
+│   ├── ruoyi-common-core/        # 核心工具类
+│   ├── ruoyi-common-mybatis/     # MyBatis-Plus封装
+│   ├── ruoyi-common-dubbo/       # Dubbo公共配置
+│   └── ...
+├── ruoyi-gateway/                # 网关服务
+├── ruoyi-auth/                   # 认证中心
+└── pom.xml                       # 父POM
+```
+
+## 核心框架栈
+
+| 组件 | 技术 | 版本 |
+|------|------|------|
+| 基础框架 | Spring Boot | 3.1+ |
+| 微服务框架 | Spring Cloud Alibaba | - |
+| RPC框架 | Apache Dubbo | 3.X |
+| ORM框架 | MyBatis-Plus | - |
+| 权限认证 | Sa-Token + JWT | - |
+| 注册中心 | Nacos | - |
+| 数据库连接池 | HikariCP | - |
+| 主键生成 | 雪花ID | - |
+
+## 分层对象定义规范
+
+### 1. Domain(实体类)
+
+- 位置:`ruoyi-modules/{module}/domain/`
+- 继承:`TenantEntity`(多租户)或 `BaseEntity`
+- 注解:`@TableName` 指定表名,`@TableId` 指定主键
+- 职责:与数据库表一一对应
+
+```java
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_pt_room")
+public class PtRoom extends TenantEntity {
+    @TableId(value = "room_id")
+    private Long roomId;
+    private String roomName;
+    // ...
+}
+```
+
+### 2. BO(业务对象)
+
+- 位置:`ruoyi-modules/{module}/domain/bo/`
+- 继承:`${parentEntity}`(主表为 `TenantEntity`,从表为 `BaseEntity`)
+- 注解:**必须有** `@AutoMapper(target = Domain.class, reverseConvertGenerate = false)` 配置与Domain的映射(主表、从表都一样)
+- 职责:用于Service层参数传递,包含校验注解
+
+```java
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = PtRoom.class, reverseConvertGenerate = false)
+public class PtRoomBo extends TenantEntity {
+    @NotNull(message = "房间Id不能为空", groups = { EditGroup.class })
+    private Long roomId;
+    
+    @NotBlank(message = "房间名称不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String roomName;
+    // ...
+}
+```
+
+### 3. VO(视图对象)
+
+- 位置:`ruoyi-modules/{module}/domain/vo/`
+- 实现:`Serializable`
+- 注解:**必须有** `@AutoMapper(target = Domain.class)` 配置与Domain的映射(主表、从表都一样)
+- 职责:用于Controller层返回给前端的数据
+
+```java
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = PtRoom.class)
+public class PtRoomVo implements Serializable {
+    @ExcelProperty(value = "房间名称")
+    private String roomName;
+    
+    @Translation(type = TransConstant.XXX, mapper = "fieldName")
+    private String fieldName;
+    // ...
+}
+```
+
+### 4. DTO(数据传输对象)
+
+- 位置:`ruoyi-api/{api-module}/domain/dto/`
+- 实现:`Serializable`
+- 职责:用于跨服务(Dubbo)传输的数据对象
+
+```java
+@Data
+public class RemoteRoomDto implements Serializable {
+    private Long roomId;
+    private String roomName;
+    // ...
+}
+```
+
+## 模块生成场景
+
+### 1. 创建模块
+
+**位置**:`ruoyi-modules/ruoyi-{模块名}/`
+
+**生成内容**:
+- Maven 模块目录结构
+- `pom.xml`(引用父POM、添加依赖)
+- 基础包结构(直接在模块包下):
+  ```
+  src/main/java/org/dromara/{模块名}/
+  ├── controller/          # 控制器
+  ├── domain/              # 实体类
+  │   ├── bo/              # 业务对象
+  │   └── vo/              # 视图对象
+  ├── mapper/              # Mapper接口
+  ├── service/             # Service接口
+  │   └── impl/            # Service实现
+  ├── dubbo/               # Dubbo服务实现(可选)
+  └── listener/            # Excel导入监听器(可选)
+  ```
+- 如需Dubbo暴露,同步创建 `ruoyi-api/ruoyi-api-{模块名}/`
+- **重要**:在 `ruoyi-api/ruoyi-api-bom/pom.xml` 的 `<dependencyManagement>` 中添加 API 模块依赖:
+  ```xml
+  <dependency>
+      <groupId>org.dromara</groupId>
+      <artifactId>ruoyi-api-{模块名}</artifactId>
+      <version>${revision}</version>
+  </dependency>
+  ```
+
+### 2. 创建子模块
+
+**位置**:`controller/{子模块名}/`
+
+**生成内容**:
+- 子模块包目录(仅一层,不再嵌套basics等额外目录)
+
+### 3. 创建功能
+
+功能创建有两个路径:
+
+**路径1:模块下直接创建功能**
+- `controller/{功能名}Controller.java`
+
+**路径2:子模块下创建功能**
+- `controller/{子模块名}/{功能名}Controller.java`
+
+**生成具体文件**:
+Domain、Bo、Vo、Mapper、Service、ServiceImpl、Controller
+(如有Dubbo:RemoteService接口 + RemoteServiceImpl实现)
+
+**Controller 层规范**:
+
+1. **请求路径格式**:`/{子模块名}/{功能名}` 或 `/{功能名}`
+   - 示例:`/basicParameter/ptParameter`
+
+2. **权限字符格式**:`{模块名}:{功能名}:{操作名}`
+   - 模块名:对应子模块目录名(如 `basicParameter`)
+   - 功能名:对应功能Controller名(如 `ptParameter`)
+   - 操作名:`list`(列表)、`query`(详情)、`add`(新增)、`edit`(修改)、`remove`(删除)、`export`(导出)
+   - 示例:`basicParameter:ptParameter:add`
+
+3. **标准方法模板**:
+   ```java
+   /**
+    * 查询列表
+    */
+   @SaCheckPermission("{模块名}:{功能名}:list")
+   @GetMapping("/list")
+   public TableDataInfo<Vo> list(Bo bo, PageQuery pageQuery) {
+       return service.queryPageList(bo, pageQuery);
+   }
+
+   /**
+    * 导出数据
+    */
+   @SaCheckPermission("{模块名}:{功能名}:export")
+   @Log(title = "功能标题", businessType = BusinessType.EXPORT)
+   @PostMapping("/export")
+   public void export(Bo bo, HttpServletResponse response) {
+       List<Vo> list = service.queryList(bo);
+       ExcelUtil.exportExcel(list, "导出标题", Vo.class, response);
+   }
+
+   /**
+    * 获取详情
+    */
+   @SaCheckPermission("{模块名}:{功能名}:query")
+   @GetMapping("/{id}")
+   public R<Vo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+       return R.ok(service.queryById(id));
+   }
+
+   /**
+    * 新增数据
+    */
+   @SaCheckPermission("{模块名}:{功能名}:add")
+   @Log(title = "功能标题", businessType = BusinessType.INSERT)
+   @RepeatSubmit()
+   @PostMapping()
+   public R<Void> add(@Validated(AddGroup.class) @RequestBody Bo bo) {
+       return toAjax(service.insertByBo(bo));
+   }
+
+   /**
+    * 修改数据
+    */
+   @SaCheckPermission("{模块名}:{功能名}:edit")
+   @Log(title = "功能标题", businessType = BusinessType.UPDATE)
+   @RepeatSubmit()
+   @PutMapping()
+   public R<Void> edit(@Validated(EditGroup.class) @RequestBody Bo bo) {
+       return toAjax(service.updateByBo(bo));
+   }
+
+   /**
+    * 删除数据
+    */
+   @SaCheckPermission("{模块名}:{功能名}:remove")
+   @Log(title = "功能标题", businessType = BusinessType.DELETE)
+   @DeleteMapping("/{ids}")
+   public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
+       return toAjax(service.deleteWithValidByIds(List.of(ids), true));
+   }
+
+   /**
+    * 导出导入模板
+    */
+   @SaCheckPermission("{模块名}:{功能名}:export")
+   @Log(title = "导入模板", businessType = BusinessType.EXPORT)
+   @PostMapping("/exportTemplate")
+   public void exportTemplate(HttpServletResponse response) {
+       List<ImportVo> list = new ArrayList<>();
+       ExcelUtil.exportExcel(list, "导入模板", ImportVo.class, response);
+   }
+
+   /**
+    * 导入数据
+    */
+   @SaCheckPermission("{模块名}:{功能名}:import")
+   @Log(title = "数据导入", businessType = BusinessType.IMPORT)
+   @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+   public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
+       ExcelResult<ImportVo> result = ExcelUtil.importExcel(
+           file.getInputStream(), 
+           ImportVo.class, 
+           new ImportListener(updateSupport)
+       );
+       return R.ok(result.getAnalysis());
+   }
+   ```
+
+4. **导入功能相关文件**:
+   - **ImportVo**:导入专用的VO类,使用 `@ExcelProperty` 注解标记Excel列
+   - **ImportListener**:继承 `AnalysisEventListener<ImportVo>` 实现 `ExcelListener<ImportVo>` 接口
+     - 位置:`listener/{功能名}ImportListener.java`
+     - 通过 `SpringUtils.getBean()` 获取Service
+     - 在 `invoke()` 方法中处理每行数据
+     - 在 `getExcelResult()` 中返回导入结果
+
+## 开发工作流程
+
+### 场景1:基础数据模块开发(CRUD)
+
+1. **创建 Domain 实体类**
+   - 继承 `TenantEntity` 或 `BaseEntity`
+   - 使用 `@TableName` 和 `@TableId` 注解
+
+2. **创建 BO 业务对象**
+   - 继承 `${parentEntity}`(主表 `TenantEntity`,从表 `BaseEntity`)
+   - **必须有** `@AutoMapper(target = Domain.class, reverseConvertGenerate = false)` 注解(主表、从表都一样)
+   - 添加校验注解(`@NotNull`, `@NotBlank` 等)
+
+3. **创建 VO 视图对象**
+   - 实现 `Serializable`
+   - **必须有** `@AutoMapper(target = Domain.class)` 注解(主表、从表都一样)
+   - 添加 Excel 导出注解(可选)
+
+4. **创建 Mapper 接口**
+   - 继承 `BaseMapper<Domain>`
+
+5. **创建 Service 接口和实现**
+   - 接口继承 `IService<Domain>`
+   - 实现类使用 `@RequiredArgsConstructor` 注入依赖
+
+6. **创建 Controller**
+   - 使用 `@RestController` 和 `@RequestMapping`
+   - 使用 `@SaCheckPermission` 配置权限
+   - 继承 `BaseController` 获取通用方法
+
+### 场景2:Dubbo 接口暴露
+
+1. **在 ruoyi-api 模块定义接口**
+   ```java
+   public interface RemoteXxxService {
+       R<RemoteXxxDto> getById(Long id);
+       TableDataInfo<RemoteXxxDto> selectPage(RemoteXxxQueryDto query);
+   }
+   ```
+
+2. **创建 DTO 和 QueryDTO**
+   - DTO 用于数据传输
+   - QueryDTO 用于查询条件(包含分页参数)
+
+3. **在 ruoyi-modules 模块实现接口**
+   ```java
+   @DubboService
+   public class RemoteXxxServiceImpl implements RemoteXxxService {
+       // 实现方法
+   }
+   ```
+
+4. **注册 Dubbo 服务**
+   - 实现类添加 `@DubboService` 注解
+   - 框架会自动暴露服务
+
+### 场景3:跨服务调用 Dubbo 接口
+
+1. **在 Consumer 端注入远程服务**
+   ```java
+   @DubboReference
+   private RemoteXxxService remoteXxxService;
+   ```
+
+2. **直接调用远程方法**
+   ```java
+   R<RemoteXxxDto> result = remoteXxxService.getById(id);
+   ```
+
+## 关键转换模式
+
+### DTO → BO 转换(Dubbo接口实现层)
+
+```java
+@Override
+public TableDataInfo<RemoteTraineeDto> selectTraineePage(RemoteTraineeQueryDto queryDto) {
+    // 1. DTO → BO(手动设置查询条件)
+    PtUserAccountBo bo = new PtUserAccountBo();
+    bo.setCategory(TRAINEE_CATEGORY);
+    if (queryDto.getRealName() != null) {
+        bo.setRealName(queryDto.getRealName());
+    }
+    
+    // 2. 构建分页参数
+    PageQuery pageQuery = new PageQuery();
+    pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+    pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+    
+    // 3. 调用本地Service
+    TableDataInfo<PtUserAccountVo> result = userAccountService.queryPageList(bo, pageQuery);
+    
+    // 4. VO → DTO 转换
+    List<RemoteTraineeDto> dtoList = result.getRows().stream()
+        .map(this::convertToTraineeDto)
+        .toList();
+    
+    // 5. 构建返回结果
+    TableDataInfo<RemoteTraineeDto> pageData = TableDataInfo.build();
+    pageData.setRows(dtoList);
+    pageData.setTotal(result.getTotal());
+    return pageData;
+}
+
+private RemoteTraineeDto convertToTraineeDto(PtUserAccountVo vo) {
+    if (vo == null) return null;
+    RemoteTraineeDto dto = new RemoteTraineeDto();
+    dto.setUserId(vo.getUserId());
+    dto.setRealName(vo.getRealName());
+    // 手动映射字段...
+    return dto;
+}
+```
+
+### VO → DTO 转换(使用 MapstructUtils)
+
+```java
+// 简单对象转换
+RemoteUserAccountVo vo = userAccountService.queryById(userId);
+RemoteUserAccountVo result = MapstructUtils.convert(vo, RemoteUserAccountVo.class);
+
+// 列表转换
+List<RemoteUserAccountVo> voList = userAccountService.queryList(bo);
+return MapstructUtils.convert(voList, RemoteUserAccountVo.class);
+```
+
+### Bean 拷贝(使用 Hutool)
+
+```java
+// BO → Domain 转换(在Service层)
+PtUserAccountBo ptUserAccountBo = BeanUtil.copyProperties(bo, PtUserAccountBo.class);
+```
+
+## 分页处理规范
+
+### 分页查询参数
+
+```java
+@Data
+public class RemoteXxxQueryDto implements Serializable {
+    private Integer pageNum = 1;    // 页码,默认1
+    private Integer pageSize = 10;  // 每页条数,默认10
+    private String keyword;         // 查询关键字
+    // ... 其他查询条件
+}
+```
+
+### 分页查询实现
+
+```java
+@Override
+public TableDataInfo<RemoteXxxDto> selectPage(RemoteXxxQueryDto queryDto) {
+    // 1. 构建分页参数
+    PageQuery pageQuery = new PageQuery();
+    pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+    pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+    
+    // 2. 构建查询条件
+    XxxBo bo = new XxxBo();
+    // 设置查询条件...
+    
+    // 3. 执行查询
+    TableDataInfo<XxxVo> result = xxxService.queryPageList(bo, pageQuery);
+    
+    // 4. 转换并返回
+    List<RemoteXxxDto> dtoList = MapstructUtils.convert(result.getRows(), RemoteXxxDto.class);
+    TableDataInfo<RemoteXxxDto> pageData = TableDataInfo.build();
+    pageData.setRows(dtoList);
+    pageData.setTotal(result.getTotal());
+    return pageData;
+}
+```
+
+## 常用注解速查
+
+### 实体类注解
+
+| 注解 | 用途 | 位置 |
+|------|------|------|
+| `@TableName` | 指定数据库表名 | Domain |
+| `@TableId` | 指定主键字段 | Domain |
+| `@TableLogic` | 逻辑删除标记 | Domain |
+| `@AutoMapper` | 配置对象映射(Bo/Vo **都必须有**) | BO/VO |
+| `@ExcelProperty` | Excel导出配置 | VO |
+| `@Translation` | 数据翻译(字典/关联) | VO |
+
+### 校验注解
+
+| 注解 | 用途 |
+|------|------|
+| `@NotNull` | 非空校验(允许空字符串) |
+| `@NotBlank` | 非空白校验(不允许null、空字符串、空白字符) |
+| `@NotEmpty` | 非空校验(用于集合、字符串) |
+| `@Size` | 长度/大小范围校验 |
+| `@Pattern` | 正则表达式校验 |
+
+### Dubbo 注解
+
+| 注解 | 用途 |
+|------|------|
+| `@DubboService` | 暴露 Dubbo 服务 |
+| `@DubboReference` | 引用 Dubbo 服务 |
+
+### 权限注解
+
+| 注解 | 用途 |
+|------|------|
+| `@SaCheckPermission` | 检查权限标识 |
+| `@SaCheckRole` | 检查角色 |
+| `@SaCheckLogin` | 检查登录状态 |
+
+## 代码模板
+
+技能目录下 `templates/` 文件夹包含以下代码模板:
+
+- `Domain.java.template` - 实体类模板
+- `Bo.java.template` - 业务对象模板
+- `Vo.java.template` - 视图对象模板
+- `Dto.java.template` - 数据传输对象模板
+- `ImportVo.java.template` - 导入专用VO模板
+- `Service.java.template` - Service接口模板
+- `ServiceImpl.java.template` - Service实现模板
+- `Controller.java.template` - 控制器模板
+- `Mapper.java.template` - Mapper接口模板
+- `RemoteService.java.template` - Dubbo接口模板
+- `RemoteServiceImpl.java.template` - Dubbo实现模板
+- `ImportListener.java.template` - Excel导入监听器模板
+
+### 模板变量说明
+
+| 变量 | 说明 | 示例 |
+|------|------|------|
+| `${module}` | 模块名(小写) | `ecs` |
+| `${submodule}` | 子模块包名 | `term` |
+| `${ClassName}` | 类名(首字母大写) | `EcsTerm` |
+| `${tableComment}` | 表注释 | `设备信息` |
+| `${pkField}` | 主键字段名(驼峰) | `termId` |
+| `${pkType}` | 主键类型 | `Long` |
+| `${PkCapField}` | 主键字段名(首字母大写,用于getter/setter) | `TermId` |
+| `${author}` | 作者 | `ruoyi` |
+| `${date}` | 日期 | `2026-04-20` |
+| `${parentEntity}` | 父实体类 | `TenantEntity` / `BaseEntity` |
+| `${queryWrapperConditions}` | 查询条件代码块(由生成器根据字段自动生成) | 见下方说明 |
+
+### ServiceImpl 查询规范(重要)
+
+ServiceImpl **必须**使用 `LambdaQueryWrapper` 模式,**禁止**直接传递 Bo 对象给 Mapper。
+
+```java
+// ❌ 错误写法(BaseMapperPlus 没有此方法签名)
+return baseMapper.selectVoList(bo);
+return baseMapper.selectVoPageList(bo, pageQuery);
+
+// ✅ 正确写法
+LambdaQueryWrapper<EcsTerm> lqw = buildQueryWrapper(bo);
+return baseMapper.selectVoList(lqw);                                    // 列表查询
+Page<EcsTermVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);  // 分页查询
+return TableDataInfo.build(result);
+```
+
+**buildQueryWrapper 生成规则**:
+- String 类型字段 → `lqw.like(StringUtils.isNotBlank(bo.getXxx()), Entity::getXxx, bo.getXxx())`
+- 字典字段(dictType,String 类型) → `lqw.eq(StringUtils.isNotBlank(bo.getXxx()), Entity::getXxx, bo.getXxx())`
+- Integer/Long 类型字段(非字典的精确匹配) → `lqw.eq(bo.getXxx() != null, Entity::getXxx, bo.getXxx())`
+- 日期范围字段 → `lqw.ge(bo.getBeginTime() != null, Entity::getXxx, bo.getBeginTime())` + `lqw.le(...)`
+- 分页查询默认按 `createTime` 降序排列:`lqw.orderByDesc(Entity::getCreateTime)`
+
+**insertByBo 规范**:
+- 转换后需调用 `validEntityBeforeSave(Objects.requireNonNull(add))` 进行保存前校验
+- 插入成功后回填主键:`bo.set${PkCapField}(add.get${PkCapField}())`
+
+**updateByBo 规范**:
+- 转换后需 null 检查 + 调用 `validEntityBeforeSave`
+
+**deleteWithValidByIds 规范**:
+- 当 `isValid=true` 时预留校验逻辑位置
+
+## 注意事项
+
+1. **对象转换**:DTO/VO/BO 之间的转换优先使用 `MapstructUtils`,复杂场景手动转换
+2. **分页参数**:pageSize 需要限制最大值(通常500),防止内存溢出
+3. **校验分组**:新增和编辑使用不同的校验分组(`AddGroup`, `EditGroup`)
+4. **Dubbo 调用**:远程接口返回类型建议使用 `R<T>` 包装,便于错误处理
+5. **租户隔离**:多租户表必须继承 `TenantEntity`,单表继承 `BaseEntity`
+6. **字典命名**:字典类型(dictType)是全局的,命名不加模块前缀。例如用 `course_type` 而非 `ecs_course_type`,用 `data_source` 而非 `ecs_data_source`
+7. **字典字段类型**:数据字典(dictType)对应的数据库表字段类型为 `varchar(16)`,Java 实体/Bo/Vo 中对应类型为 `String`(而非 `Integer`)。字典值虽然常为数字,但存储和传输统一使用字符串。建表语句中字典字段用 `character varying(16 char)` 或 `varchar(16)`
+8. **主键不导出**:主键字段(Long 类型)不加 `@ExcelProperty` 注解,不参与 Excel 导出。Vo 中主键仅作为标识,不需要在导出文件中展示
+
+---
+
+## 附录:从需求规格说明书生成代码
+
+> 本章节定义基于需求规格说明书自动生成后端代码的完整流程。由 `/dev` 指令触发。
+
+### A.1 输入文件
+
+需求规格说明书位于:`d:/dt_ykt/ykt_server/doc/{模块}-{功能名}-requirements.md`
+
+文件不存在时,提示用户先执行 `/require {模块} {功能名}`。
+
+### A.2 读取与解析
+
+依次读取需求规格说明书的以下章节:
+
+| 章节 | 用途 |
+|------|------|
+| 基础信息 | 确定包路径、表名、多租户标记 |
+| 字段清单 | 生成 Domain/Bo/Vo 各字段 |
+| VO 结构 | 生成列表VO、明细VO的类型定义 |
+| 接口清单 | 生成 Controller 方法 |
+| 特殊需求 | 判断是否生成 ImportVo/Listener、Dubbo接口 |
+| 字典项 | 生成 VO 的 `@ExcelDictFormat` 注解 |
+
+### A.3 后端文件生成清单
+
+根据需求文档内容,按需生成以下文件:
+
+#### A.3.1 主表文件(必有)
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| Domain | `domain/{Name}.java` | 表名=基础信息.主表名,字段=字段清单中 inDb=true |
+| Bo | `domain/bo/{Name}Bo.java` | 字段=字段清单中 inForm=true,含校验注解 |
+| Vo | `domain/vo/{Name}Vo.java` | 字段=字段清单中 inTable/inForm=true,含Excel注解 |
+| Mapper | `mapper/{Name}Mapper.java` | 继承 `BaseMapper<Domain>` |
+| Service | `service/I{Name}Service.java` | 继承 `IService<Domain>` |
+| ServiceImpl | `service/impl/{Name}ServiceImpl.java` | 实现 Service,含 CRUD + 导入导出逻辑 |
+| Controller | `controller/{Name}Controller.java` | REST API,含权限注解 |
+
+#### A.3.2 从表文件(如有 relation 字段)
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| Domain(从表) | `domain/{SubName}.java` | 表名=基础信息.从表名 |
+| Bo(从表) | `domain/bo/{SubName}Bo.java` | **必须有** `@AutoMapper(target = {SubName}.class, reverseConvertGenerate = false)` + 主表ID外键字段,继承 `BaseEntity` |
+| Vo(从表) | `domain/vo/{SubName}Vo.java` | **必须有** `@AutoMapper(target = {SubName}.class)` + 主表ID、关联字段 |
+| Mapper(从表) | `mapper/{SubName}Mapper.java` | — |
+| Service(从表) | `service/I{SubName}Service.java` | 含批量插入方法 |
+| ServiceImpl(从表) | `service/impl/{SubName}ServiceImpl.java` | — |
+
+#### A.3.3 导入导出文件(特殊需求.导入=true)
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| ImportVo | `domain/vo/{Name}ImportVo.java` | 导入专用VO,含 `@ExcelProperty` |
+| ImportListener | `listener/{Name}ImportListener.java` | 继承 `AnalysisEventListener<ImportVo>` |
+
+#### A.3.4 Dubbo 接口文件(特殊需求.Dubbo暴露=true)
+
+| 文件 | 路径 | 说明 |
+|------|------|------|
+| RemoteService | `dubbo/Remote{Name}Service.java` | Dubbo 接口定义 |
+| RemoteDto | `dto/Remote{Name}Dto.java` | Dubbo 传输对象 |
+| RemoteServiceImpl | `dubbo/Remote{Name}ServiceImpl.java` | `@DubboService` 实现 |
+
+### A.4 代码生成模板
+
+使用技能目录下 `templates/` 中的模板文件填充:
+
+**主表:**
+
+```
+templates/
+├── Domain.java.template      → domain/{Name}.java
+├── Bo.java.template           → domain/bo/{Name}Bo.java
+├── Vo.java.template           → domain/vo/{Name}Vo.java
+├── Mapper.java.template       → mapper/{Name}Mapper.java
+├── Service.java.template      → service/I{Name}Service.java
+├── ServiceImpl.java.template  → service/impl/{Name}ServiceImpl.java
+├── Controller.java.template   → controller/{Name}Controller.java
+├── ImportVo.java.template     → domain/vo/{Name}ImportVo.java
+├── ImportListener.java.template → listener/{Name}ImportListener.java
+├── RemoteService.java.template → dubbo/Remote{Name}Service.java
+├── RemoteServiceImpl.java.template → dubbo/Remote{Name}ServiceImpl.java
+└── Dto.java.template          → dto/Remote{Name}Dto.java
+```
+
+**从表(复用主表模板,替换 {Name} → {SubName}):**
+
+> ⚠️ 从表模板与主表使用同一套模板,关键差异:
+> - `${parentEntity}` 变量:主表为 `TenantEntity`,从表为 `BaseEntity`
+> - 从表 Bo **必须**包含 `@AutoMapper(target = {SubName}.class, reverseConvertGenerate = false)` 注解
+> - 从表 Vo **必须**包含 `@AutoMapper(target = {SubName}.class)` 注解
+> - **不管主表还是从表,Bo 和 Vo 都必须有 `@AutoMapper` 映射注解,这是框架对象转换(MapstructUtils.convert)的前提**
+
+```
+Domain.java.template      → domain/{SubName}.java
+Bo.java.template           → domain/bo/{SubName}Bo.java
+Vo.java.template           → domain/vo/{SubName}Vo.java          ← 必须!含 @AutoMapper
+Mapper.java.template       → mapper/{SubName}Mapper.java
+Service.java.template      → service/I{SubName}Service.java
+└── ServiceImpl.java.template  → service/impl/{SubName}ServiceImpl.java
+```
+
+### A.5 字段到代码属性的映射
+
+| 需求字段属性 | Domain 字段 | Bo 校验注解 | Vo Excel注解 |
+|-------------|------------|------------|-------------|
+| fieldType=Long(主键) | `private Long xxxId;` | `@NotNull(groups=EditGroup.class)` | **不导出**(不加 @ExcelProperty) |
+| fieldType=String, required=true | `private String xxxName;` | `@NotBlank` | `@ExcelProperty("名称")` |
+| fieldType=Integer, dictType=xxx | `private Integer xxxType;` | — | `@ExcelProperty("类型"), @ExcelDictFormat(dictType="xxx")` |
+| **dictType=xxx(字典字段)** | **`private String xxxType;`** | — | **`@ExcelProperty("类型"), @ExcelDictFormat(dictType="xxx")`** |
+| fieldType=BigDecimal | `private BigDecimal xxxAmount;` | — | `@ExcelProperty("金额")` |
+| fieldType=Date | `private Date xxxTime;` | — | `@ExcelProperty("时间")` |
+| inDb=false | —(不出现在 Domain) | — | `private String teacherNames;`(纯计算字段) |
+| inTable=false, inForm=true | —(不出现在 Domain) | — | `private String remark;`(仅表单) |
+| **从表关联字段(Bo/Vo)** | **`private List<{SubName}Vo> {Name}{Sub}List;`** | — | — |
+
+### A.6 多对多关联处理
+
+主表 Domain 中不直接存储讲师ID,而是:
+1. 从表 `{Name}Teacher.java` 存储 `{xxxId}`、`{teacherId}`、`{teacherName}`
+2. 主表列表查询时,通过 SQL `GROUP_CONCAT` 拼接 `{teacherNames}`,返回到 `Vo` 的 `inDb=false` 字段
+3. 列表页面通过 teacherNames 直接展示讲师列表
+
+### A.7 输出路径约定
+
+| 资源 | 路径 |
+|------|------|
+| 后端项目根 | `d:/dt_ykt/ykt_server`(可通过 `--backend=` 参数覆盖) |
+| 主表代码 | `d:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-{模块}/src/main/java/org/dromara/{模块}/` |
+| Dubbo API | `d:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-{模块}/src/main/java/org/dromara/{模块}/api/` |
+
+### A.8 权限字符生成规则
+
+格式:`{子模块名(如basics)}:{功能名}:{操作名}`
+
+示例:对于 `ruoyi-backstage/basics/room/CourseController.java` → `basics:course:list`
+
+### A.9 注意事项
+
+1. **覆盖确认**:文件已存在时,提示用户确认是否覆盖
+2. **子模块路径**:优先使用 `controller/{子模块}/{功能名}Controller.java`
+3. **多租户**:多租户表继承 `TenantEntity`,单表继承 `BaseEntity`
+4. **包名大小写**:类名首字母大写,包路径全小写

+ 240 - 0
.workbuddy/skills/ruoyi-backend/references/coding_standards.md

@@ -0,0 +1,240 @@
+# RuoYi-Cloud-Plus 编码规范
+
+## 命名规范
+
+### 类命名
+
+- **实体类**:使用表名驼峰命名,如 `t_pt_room` → `PtRoom`
+- **业务对象**:实体名 + `Bo`,如 `PtRoomBo`
+- **视图对象**:实体名 + `Vo`,如 `PtRoomVo`
+- **传输对象**:`Remote` + 实体名 + `Dto`,如 `RemoteRoomDto`
+- **服务接口**:`I` + 实体名 + `Service`,如 `IPtRoomService`
+- **服务实现**:实体名 + `ServiceImpl`,如 `PtRoomServiceImpl`
+- **Mapper**:实体名 + `Mapper`,如 `PtRoomMapper`
+- **Controller**:实体名 + `Controller`,如 `PtRoomController`
+- **Dubbo接口**:`Remote` + 功能 + `Service`,如 `RemotePtRoomService`
+
+### 方法命名
+
+| 操作 | 前缀 | 示例 |
+|------|------|------|
+| 查询单个 | queryById / getById | `queryById(Long id)` |
+| 查询列表 | queryList / selectList | `queryList(Bo bo)` |
+| 分页查询 | queryPageList / selectPage | `queryPageList(Bo bo, PageQuery query)` |
+| 新增 | insertByBo / save | `insertByBo(Bo bo)` |
+| 修改 | updateByBo / update | `updateByBo(Bo bo)` |
+| 删除 | deleteByIds / remove | `deleteByIds(Collection<Long> ids)` |
+| 校验 | validateXxx | `validateUnique(Bo bo)` |
+
+### 变量命名
+
+- 使用驼峰命名法
+- 布尔类型避免使用 `is` 开头(避免与 getter 冲突)
+- 常量使用全大写下划线分隔
+
+```java
+// 推荐
+private String userName;
+private Boolean enabled;
+private static final String DEFAULT_STATUS = "0";
+
+// 避免
+private String user_name;
+private Boolean isEnabled;
+```
+
+## 注解使用规范
+
+### 类级别注解顺序
+
+```java
+@Slf4j                                    // 日志
+@RequiredArgsConstructor                  // 构造器注入
+@Service                                  // Spring Bean
+@DubboService                             // Dubbo服务(如需要)
+public class XxxServiceImpl implements IXxxService {
+}
+```
+
+### Controller 注解
+
+```java
+@Validated                                // 参数校验
+@RequiredArgsConstructor                  // 构造器注入
+@RestController                           // REST控制器
+@RequestMapping("/room")                  // 请求路径
+public class PtRoomController extends BaseController {
+}
+```
+
+### 字段注解顺序
+
+```java
+@NotBlank(message = "名称不能为空", groups = {AddGroup.class, EditGroup.class})
+@ExcelProperty(value = "名称")
+private String name;
+```
+
+## 依赖注入规范
+
+### 推荐:构造器注入
+
+```java
+@Service
+@RequiredArgsConstructor
+public class PtRoomServiceImpl implements IPtRoomService {
+    private final PtRoomMapper baseMapper;
+    private final IPtAreaService areaService;
+}
+```
+
+### 避免:字段注入
+
+```java
+// 不推荐
+@Autowired
+private PtRoomMapper baseMapper;
+```
+
+## 注释规范
+
+### 类注释
+
+```java
+/**
+ * 房间定义对象 t_pt_room
+ *
+ * @author bing
+ * @date 2024-08-09
+ */
+```
+
+### 方法注释
+
+```java
+/**
+ * 查询房间列表
+ *
+ * @param bo 查询条件
+ * @return 房间列表
+ */
+List<PtRoomVo> queryList(PtRoomBo bo);
+```
+
+### 复杂逻辑注释
+
+```java
+// 1. 构建查询条件
+PtUserAccountBo bo = new PtUserAccountBo();
+bo.setCategory(TRAINEE_CATEGORY);
+
+// 2. 设置分页参数
+PageQuery pageQuery = new PageQuery();
+pageQuery.setPageNum(Math.max(queryDto.getPageNum(), 1));
+
+// 3. 执行查询并转换结果
+TableDataInfo<PtUserAccountVo> result = userAccountService.queryPageList(bo, pageQuery);
+```
+
+## 异常处理规范
+
+### 使用 R 对象包装返回
+
+```java
+public R<RemoteTeacherDto> selectTeacherById(Long userId) {
+    PtUserAccountVo vo = userAccountService.queryById(userId);
+    if (vo == null || !TEACHER_CATEGORY.equals(vo.getCategory())) {
+        return R.fail("未找到对应的教师信息");
+    }
+    return R.ok(convertToTeacherDto(vo));
+}
+```
+
+### 抛出自定义异常
+
+```java
+if (room == null) {
+    throw new ServiceException("房间不存在");
+}
+```
+
+## 分页参数处理
+
+### 安全分页
+
+```java
+PageQuery pageQuery = new PageQuery();
+pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+```
+
+### 分页返回值构建
+
+```java
+TableDataInfo<RemoteXxxDto> pageData = TableDataInfo.build();
+pageData.setRows(dtoList);
+pageData.setTotal(result.getTotal());
+return pageData;
+```
+
+## 空值处理
+
+### 集合判空
+
+```java
+import cn.hutool.core.collection.CollectionUtil;
+
+if (CollectionUtil.isEmpty(list)) {
+    return Collections.emptyList();
+}
+```
+
+### 对象判空
+
+```java
+if (vo == null) {
+    return null;
+}
+```
+
+## 常量定义
+
+### 分类常量
+
+```java
+public class XxxConstants {
+    /** 教师类型编码 */
+    public static final String TEACHER_CATEGORY = "1";
+    
+    /** 学员类型编码 */
+    public static final String TRAINEE_CATEGORY = "2";
+}
+```
+
+### 枚举类
+
+```java
+@Getter
+@AllArgsConstructor
+public enum StatusEnum {
+    NORMAL("0", "正常"),
+    DISABLED("1", "停用");
+    
+    private final String code;
+    private final String desc;
+}
+```
+
+## 日志使用
+
+```java
+@Slf4j
+@Service
+public class XxxServiceImpl {
+    public void doSomething() {
+        log.info("处理开始,参数:{}", param);
+        log.debug("调试信息:{}", detail);
+        log.error("处理失败", e);
+    }
+}
+```

+ 372 - 0
.workbuddy/skills/ruoyi-backend/references/dubbo_integration.md

@@ -0,0 +1,372 @@
+# Dubbo 集成指南
+
+## 概述
+
+RuoYi-Cloud-Plus 使用 Apache Dubbo 3.X 作为 RPC 远程调用框架,替代传统的 Feign 调用。
+
+## 核心概念
+
+| 概念 | 说明 |
+|------|------|
+| Provider | 服务提供者,暴露服务 |
+| Consumer | 服务消费者,调用服务 |
+| Interface | 服务接口定义 |
+| Registry | 注册中心(Nacos) |
+
+## 服务提供者(Provider)
+
+### 1. 定义接口(ruoyi-api 模块)
+
+```java
+package org.dromara.backstage.api;
+
+import org.dromara.backstage.api.domain.dto.RemoteRoomDto;
+import org.dromara.backstage.api.domain.dto.RemoteRoomQueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+import java.util.List;
+
+/**
+ * 房间信息远程调用接口
+ */
+public interface RemotePtRoomService {
+    
+    /**
+     * 根据ID查询房间
+     */
+    R<RemoteRoomDto> selectRoomById(Long roomId);
+    
+    /**
+     * 查询房间列表
+     */
+    List<RemoteRoomDto> selectRoomList();
+    
+    /**
+     * 分页查询房间列表
+     */
+    TableDataInfo<RemoteRoomDto> selectRoomPage(RemoteRoomQueryDto queryDto);
+    
+    /**
+     * 新增房间
+     */
+    R<Void> insertRoom(RemoteRoomDto dto);
+    
+    /**
+     * 修改房间
+     */
+    R<Void> updateRoom(RemoteRoomDto dto);
+    
+    /**
+     * 删除房间
+     */
+    R<Void> deleteRoomByIds(Long[] ids);
+}
+```
+
+### 2. 定义 DTO(ruoyi-api 模块)
+
+```java
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 房间信息DTO
+ */
+@Data
+public class RemoteRoomDto implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /** 房间ID */
+    private Long roomId;
+    
+    /** 房间名称 */
+    private String roomName;
+    
+    /** 房间编码 */
+    private String roomCode;
+    
+    /** 所属区域ID */
+    private Long areaId;
+    
+    /** 房间类型 */
+    private String roomType;
+    
+    /** 容纳人数 */
+    private Integer capacity;
+}
+```
+
+### 3. 定义 QueryDTO(ruoyi-api 模块)
+
+```java
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 房间查询DTO
+ */
+@Data
+public class RemoteRoomQueryDto implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /** 页码 */
+    private Integer pageNum = 1;
+    
+    /** 每页条数 */
+    private Integer pageSize = 10;
+    
+    /** 房间名称(模糊查询) */
+    private String roomName;
+    
+    /** 房间编码(精确查询) */
+    private String roomCode;
+    
+    /** 所属区域ID */
+    private Long areaId;
+    
+    /** 房间类型 */
+    private String roomType;
+}
+```
+
+### 4. 实现服务(ruoyi-modules 模块)
+
+```java
+package org.dromara.backstage.basics.dubbo;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.dromara.backstage.api.RemotePtRoomService;
+import org.dromara.backstage.api.domain.dto.RemoteRoomDto;
+import org.dromara.backstage.api.domain.dto.RemoteRoomQueryDto;
+import org.dromara.backstage.basics.domain.bo.PtRoomBo;
+import org.dromara.backstage.basics.domain.vo.PtRoomVo;
+import org.dromara.backstage.basics.service.IPtRoomService;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 房间信息远程服务实现
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+@DubboService
+public class RemotePtRoomServiceImpl implements RemotePtRoomService {
+    
+    private final IPtRoomService roomService;
+    
+    @Override
+    public R<RemoteRoomDto> selectRoomById(Long roomId) {
+        PtRoomVo vo = roomService.queryById(roomId);
+        if (vo == null) {
+            return R.fail("房间不存在");
+        }
+        return R.ok(convertToDto(vo));
+    }
+    
+    @Override
+    public List<RemoteRoomDto> selectRoomList() {
+        PtRoomBo bo = new PtRoomBo();
+        List<PtRoomVo> voList = roomService.queryList(bo);
+        return MapstructUtils.convert(voList, RemoteRoomDto.class);
+    }
+    
+    @Override
+    public TableDataInfo<RemoteRoomDto> selectRoomPage(RemoteRoomQueryDto queryDto) {
+        // 1. 构建分页参数
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+        pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+        
+        // 2. 构建查询条件
+        PtRoomBo bo = new PtRoomBo();
+        bo.setRoomName(queryDto.getRoomName());
+        bo.setRoomCode(queryDto.getRoomCode());
+        bo.setAreaId(queryDto.getAreaId());
+        bo.setRoomType(queryDto.getRoomType());
+        
+        // 3. 执行查询
+        TableDataInfo<PtRoomVo> result = roomService.queryPageList(bo, pageQuery);
+        
+        // 4. 转换结果
+        List<RemoteRoomDto> dtoList = MapstructUtils.convert(result.getRows(), RemoteRoomDto.class);
+        
+        // 5. 构建返回
+        TableDataInfo<RemoteRoomDto> pageData = TableDataInfo.build();
+        pageData.setRows(dtoList);
+        pageData.setTotal(result.getTotal());
+        return pageData;
+    }
+    
+    @Override
+    public R<Void> insertRoom(RemoteRoomDto dto) {
+        PtRoomBo bo = MapstructUtils.convert(dto, PtRoomBo.class);
+        roomService.insertByBo(bo);
+        return R.ok();
+    }
+    
+    @Override
+    public R<Void> updateRoom(RemoteRoomDto dto) {
+        PtRoomBo bo = MapstructUtils.convert(dto, PtRoomBo.class);
+        roomService.updateByBo(bo);
+        return R.ok();
+    }
+    
+    @Override
+    public R<Void> deleteRoomByIds(Long[] ids) {
+        roomService.deleteWithValidByIds(List.of(ids), false);
+        return R.ok();
+    }
+    
+    /**
+     * 手动转换方法(复杂场景使用)
+     */
+    private RemoteRoomDto convertToDto(PtRoomVo vo) {
+        if (vo == null) {
+            return null;
+        }
+        RemoteRoomDto dto = new RemoteRoomDto();
+        dto.setRoomId(vo.getRoomId());
+        dto.setRoomName(vo.getRoomName());
+        dto.setRoomCode(vo.getRoomCode());
+        dto.setAreaId(vo.getAreaId());
+        dto.setRoomType(vo.getRoomType());
+        dto.setCapacity(vo.getCapacity());
+        return dto;
+    }
+}
+```
+
+## 服务消费者(Consumer)
+
+### 1. 注入远程服务
+
+```java
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemotePtRoomService;
+import org.dromara.backstage.api.domain.dto.RemoteRoomDto;
+import org.dromara.backstage.api.domain.dto.RemoteRoomQueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 房间信息控制器
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/ecs/room")
+public class RoomController extends BaseController {
+    
+    @DubboReference
+    private RemotePtRoomService remotePtRoomService;
+    
+    /**
+     * 查询房间列表
+     */
+    @SaCheckPermission("ecs:room:list")
+    @GetMapping("/list")
+    public TableDataInfo<RemoteRoomDto> list(RemoteRoomQueryDto queryDto) {
+        return remotePtRoomService.selectRoomPage(queryDto);
+    }
+    
+    /**
+     * 获取房间详细信息
+     */
+    @SaCheckPermission("ecs:room:list")
+    @GetMapping("/{roomId}")
+    public R<RemoteRoomDto> getInfo(@PathVariable Long roomId) {
+        return remotePtRoomService.selectRoomById(roomId);
+    }
+    
+    /**
+     * 获取所有房间列表(不分页)
+     */
+    @SaCheckPermission("ecs:room:list")
+    @GetMapping("/all")
+    public R<List<RemoteRoomDto>> all() {
+        return R.ok(remotePtRoomService.selectRoomList());
+    }
+}
+```
+
+## 异步调用
+
+### 消费端异步调用
+
+```java
+@DubboReference(async = true)
+private RemotePtRoomService remotePtRoomService;
+
+public void asyncCall() {
+    // 发起异步调用
+    remotePtRoomService.selectRoomById(1L);
+    
+    // 获取异步结果
+    CompletableFuture<RemoteRoomDto> future = RpcContext.getContext().getCompletableFuture();
+    future.whenComplete((result, exception) -> {
+        if (exception != null) {
+            log.error("调用失败", exception);
+        } else {
+            log.info("调用成功:{}", result);
+        }
+    });
+}
+```
+
+## 常见问题
+
+### 1. 服务注册失败
+
+检查 `@DubboService` 注解是否正确添加,以及 Nacos 注册中心是否正常运行。
+
+### 2. 服务调用超时
+
+在 `application.yml` 中配置超时时间:
+
+```yaml
+dubbo:
+  consumer:
+    timeout: 5000  # 5秒
+```
+
+### 3. 循环依赖问题
+
+避免模块间的循环依赖,通过接口层解耦。
+
+### 4. 版本兼容性
+
+确保所有服务的 Dubbo 版本一致。
+
+## 最佳实践
+
+1. **接口粒度**:一个 Dubbo 接口对应一个业务领域
+2. **参数设计**:使用 DTO 包装参数,避免过多参数
+3. **返回值**:使用 `R<T>` 包装,统一错误处理
+4. **异常处理**:在 Provider 端捕获并转换为业务异常
+5. **版本控制**:接口变更时考虑版本兼容性

+ 106 - 0
.workbuddy/skills/ruoyi-backend/references/project_structure.md

@@ -0,0 +1,106 @@
+# RuoYi-Cloud-Plus 项目结构详解
+
+## 模块职责说明
+
+### ruoyi-api 模块
+
+存放对外暴露的 Dubbo 接口定义,供其他微服务调用。
+
+```
+ruoyi-api-backstage/
+├── src/main/java/org/dromara/backstage/api/
+│   ├── RemoteXxxService.java              # Dubbo接口定义
+│   └── domain/
+│       ├── bo/                            # 远程调用BO对象
+│       ├── dto/                           # 远程调用DTO对象
+│       └── vo/                            # 远程调用VO对象
+```
+
+**规范**:
+- 接口命名以 `Remote` 开头
+- DTO/VO 命名以 `Remote` 开头
+- 所有传输对象必须实现 `Serializable`
+
+### ruoyi-modules 模块
+
+业务模块实现,包含具体的业务逻辑。
+
+```
+ruoyi-backstage/
+├── src/main/java/org/dromara/backstage/
+│   ├── basics/                            # 基础数据模块
+│   │   ├── domain/
+│   │   │   ├── PtRoom.java                # 实体类
+│   │   │   ├── bo/PtRoomBo.java           # 业务对象
+│   │   │   └── vo/PtRoomVo.java           # 视图对象
+│   │   ├── mapper/PtRoomMapper.java       # 数据访问层
+│   │   └── service/
+│   │       ├── IPtRoomService.java        # 服务接口
+│   │       └── impl/PtRoomServiceImpl.java # 服务实现
+│   ├── payment/                           # 支付模块
+│   │   └── dubbo/
+│   │       └── RemoteXxxServiceImpl.java  # Dubbo服务实现
+│   └── controller/                        # 控制器(如需要HTTP接口)
+```
+
+### ruoyi-common 模块
+
+公共组件,所有模块共享。
+
+| 子模块 | 职责 |
+|--------|------|
+| ruoyi-common-core | 核心工具类、常量、异常 |
+| ruoyi-common-mybatis | MyBatis-Plus配置、分页、数据权限 |
+| ruoyi-common-dubbo | Dubbo公共配置 |
+| ruoyi-common-tenant | 多租户支持 |
+| ruoyi-common-security | 安全相关(Sa-Token封装) |
+| ruoyi-common-excel | Excel导入导出 |
+| ruoyi-common-translation | 数据翻译 |
+
+## 包命名规范
+
+```
+org.dromara.{module}.{submodule}.{layer}
+
+示例:
+- org.dromara.backstage.basics.domain        # backstage模块-基础数据-实体
+- org.dromara.backstage.payment.dubbo        # backstage模块-支付-dubbo实现
+- org.dromara.ecs.controller                 # ecs模块-控制器
+```
+
+## 类命名规范
+
+| 类型 | 命名规则 | 示例 |
+|------|----------|------|
+| 实体类 | 表名驼峰 | PtRoom |
+| BO | 实体名+Bo | PtRoomBo |
+| VO | 实体名+Vo | PtRoomVo |
+| DTO | Remote+实体名+Dto | RemoteRoomDto |
+| QueryDTO | Remote+实体名+QueryDto | RemoteRoomQueryDto |
+| Mapper | 实体名+Mapper | PtRoomMapper |
+| Service | I+实体名+Service | IPtRoomService |
+| ServiceImpl | 实体名+ServiceImpl | PtRoomServiceImpl |
+| Controller | 实体名+Controller | PtRoomController |
+| Dubbo接口 | Remote+功能+Service | RemotePtRoomService |
+| Dubbo实现 | Remote+功能+ServiceImpl | RemotePtRoomServiceImpl |
+
+## 依赖关系
+
+```
+ruoyi-api (接口定义)
+    ↑
+ruoyi-modules (业务实现) ← 依赖 ruoyi-api 暴露服务
+    ↑
+ruoyi-common (公共组件) ← 所有模块依赖
+```
+
+## 配置文件位置
+
+```
+ruoyi-modules/{module}/src/main/resources/
+├── application.yml                        # 应用配置
+├── application-dev.yml                    # 开发环境
+├── application-prod.yml                   # 生产环境
+└── mapper/                                # MyBatis XML(如需要)
+    └── basics/PtRoomMapper.xml
+```

+ 275 - 0
.workbuddy/skills/ruoyi-backend/references/requirements-template.md

@@ -0,0 +1,275 @@
+# 功能需求规格文档模板
+
+> 本模板用于描述业务功能需求,技能将根据此文档生成后端 + 前端代码。
+> 请按此格式填写,结构化描述越完整,代码生成质量越高。
+
+---
+
+## 1. 基础信息
+
+| 属性 | 值 |
+|------|-----|
+| 功能中文名 | {功能中文名} |
+| 功能简写 | {功能简写} |
+| 所属模块 | {ruoyi-modules/xxx} |
+| 主表名 | {t_xxx} |
+| 从表名 | {t_xxx}(如无主从结构则填"无") |
+| 是否多租户 | 是/否 |
+| 描述 | {一句话功能描述} |
+
+---
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| 列表 | GET | /{简写}/list | /{模块}/{简写}/list | {模块}:{简写}:list |
+| 详情 | GET | /{简写}/{id} | /{模块}/{简写}/{id} | {模块}:{简写}:query |
+| 新增 | POST | /{简写}/ | /{模块}/{简写}/ | {模块}:{简写}:add |
+| 编辑 | PUT | /{简写}/ | /{模块}/{简写}/ | {模块}:{简写}:edit |
+| 删除 | DELETE | /{简写}/{ids} | /{模块}/{简写}/{ids} | {模块}:{简写}:remove |
+| 导出模板 | POST | /{简写}/exportTemplate | /{模块}/{简写}/exportTemplate | {模块}:{简写}:export |
+| 导入数据 | POST | /{简写}/importData | /{模块}/{简写}/importData | {模块}:{简写}:import |
+| 导出数据 | POST | /{简写}/export | /{模块}/{简写}/export | {模块}:{简写}:export |
+
+### 2.2 补充接口(如有)
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| ... | ... | ... | ... | ... |
+
+---
+
+## 3. 字段属性速查表
+
+| 属性 | 含义 | 可选值 | 说明 |
+|------|------|--------|------|
+| `fieldName` | 字段名(驼峰) | — | 代码变量名 |
+| `columnName` | 列名(下划线) | — | 数据库列名 |
+| `fieldType` | Java类型 | String/Long/Integer/BigDecimal/LocalDateTime | |
+| `inDb` | 是否在表中存在 | true/false | false=纯前端计算字段 |
+| `inTable` | 列表是否显示 | true/false | 生成表格列 |
+| `inQuery` | 是否查询字段 | true/false | 生成搜索条件 |
+| `queryType` | 查询方式 | eq/like/between | 精确/模糊/范围 |
+| `inForm` | 是否表单字段 | true/false | 生成表单项 |
+| `inAdd` | 新增表单显示 | true/false | |
+| `inEdit` | 编辑表单显示 | true/false | |
+| `required` | 是否必填 | true/false | 生成校验注解 |
+| `dictType` | 字典类型 | 字典标识 | 有值=字典下拉,否则普通输入 |
+| `relation` | 关联选择配置 | 对象 | 有值=弹出选择框 |
+| `component` | 前端组件类型 | input/select/inputNumber/datetime/textarea | 默认根据类型推断 |
+| `width` | 表格列宽 | 数字(px) | 默认auto |
+| `sort` | 排序 | 数字 | 越小越靠前 |
+| `excelExport` | Excel导出 | true/false | 默认true |
+
+### 关联选择 relation 格式
+
+```yaml
+relation:
+  table: {关联表名}
+  idField: {回填ID字段}
+  nameField: {显示名称字段}
+  title: 选择{实体名}
+  path: /{模块}/{实体}/{路径}
+```
+
+---
+
+## 4. 字段清单
+
+### 4.1 主表 `{t_xxx}`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: {xxxId}
+    columnName: {xxx_id}
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 主键,编辑时传递
+
+  # ---------- 业务字段 ----------
+  - fieldName: {fieldName}
+    columnName: {column_name}
+    fieldType: String
+    inDb: true
+    inTable: true           # 列表是否显示
+    inQuery: true           # 是否为查询条件
+    queryType: like         # eq/like/between
+    inForm: true            # 是否在表单中
+    inAdd: true             # 新增表单是否显示
+    inEdit: true            # 编辑表单是否显示
+    required: true          # 是否必填
+    dictType: {dict_type}   # 字典类型(有则下拉,否则输入)
+    relation:               # 关联弹出选择(如有)
+      table: t_xxx
+      idField: xxx_id
+      nameField: xxx_name
+      title: 选择xxx
+      path: /xxx/xxx/selectXxx
+    component: input       # 前端组件:input/select/inputNumber/datetime/textarea
+    width: 150              # 表格列宽
+    sort: 1                 # 排序
+    excelExport: true
+    remark: 字段说明
+
+  # ---------- VO承载字段(仅返回,不展示不维护) ----------
+  - fieldName: {fieldName}
+    columnName: {column_name}
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    excelExport: false
+    remark: VO承载字段,不展示不维护
+
+  # ---------- 公共字段 ----------
+  - fieldName: delFlag
+    columnName: del_flag
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: LocalDateTime
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+    excelExport: true
+```
+
+### 4.2 从表 `{t_xxx}`
+
+> 仅主从结构时填写。
+
+```yaml
+fields:
+  - fieldName: id
+    columnName: id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: {mainId}
+    columnName: {main_id}
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: {fieldName}
+    columnName: {column_name}
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false           # 编辑时是否允许修改
+    required: true
+    relation:
+      table: t_xxx
+      idField: xxx_id
+      nameField: xxx_name
+      title: 选择xxx
+      path: /xxx/xxx/selectXxx
+    component: xxxSelect
+    sort: 1
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: LocalDateTime
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+```
+
+---
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+{xxx}Vo:
+  - {xxx}Id
+  - {field1}
+  - {field2}
+  # ... 按字段清单中 inTable=true 的字段填写
+  - createTime
+```
+
+### 5.2 主表明细 VO(如有从表)
+
+```yaml
+{xxx}DetailVo:
+  includes: {xxx}Vo
+  additional:
+    - {subList}: List<{xxx}{Sub}Vo>   # 从表列表
+```
+
+### 5.3 从表 VO
+
+```yaml
+{xxx}{Sub}Vo:
+  - id
+  - {mainId}
+  - {field1}
+  - {field2}
+  - createTime
+```
+
+---
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: true/false
+  excelExport: true/false
+
+  importRules:
+    - fieldName: {fieldName}
+      required: true/false
+      unique: true/false    # 唯一性校验
+      min: 数值
+      max: 数值
+
+  # 从表是否参与导入导出
+  subTableImport: true/false
+  subTableExport: true/false
+
+  dubboExpose: true/false
+  dubboServiceName: Remote{xxx}Service
+```
+
+---
+
+## 7. 字典项(如有)
+
+| 字典类型 | 枚举值 | 说明 |
+|----------|--------|------|
+| `{dict_type}` | 0=选项1, 1=选项2 | {说明} |
+
+---
+
+## 8. 备注
+
+1. {补充说明1}
+2. {补充说明2}

+ 31 - 0
.workbuddy/skills/ruoyi-backend/templates/Bo.java.template

@@ -0,0 +1,31 @@
+package org.dromara.${module}.${submodule}.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.${module}.${submodule}.domain.${ClassName};
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.${parentEntity};
+
+/**
+ * ${tableComment}业务对象 ${tableName}
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = ${ClassName}.class, reverseConvertGenerate = false)
+public class ${ClassName}Bo extends ${parentEntity} {
+
+    /**
+     * ${pkComment}
+     */
+    @NotNull(message = "${pkComment}不能为空", groups = { EditGroup.class })
+    private Long ${pkField};
+
+${fields}
+}

+ 88 - 0
.workbuddy/skills/ruoyi-backend/templates/ConsumerController.java.template

@@ -0,0 +1,88 @@
+package org.dromara.${consumerModule}.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.${providerModule}.api.Remote${ClassName}Service;
+import org.dromara.${providerModule}.api.domain.dto.Remote${ClassName}Dto;
+import org.dromara.${providerModule}.api.domain.dto.Remote${ClassName}QueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * ${tableComment}控制器
+ * 通过Dubbo调用${providerModule}模块服务
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/${consumerModule}/${businessName}")
+public class ${ClassName}Controller extends BaseController {
+
+    @DubboReference
+    private Remote${ClassName}Service remote${ClassName}Service;
+
+    /**
+     * 查询${tableComment}列表
+     */
+    @SaCheckPermission("${consumerModule}:${businessName}:list")
+    @GetMapping("/list")
+    public TableDataInfo<Remote${ClassName}Dto> list(Remote${ClassName}QueryDto queryDto) {
+        return remote${ClassName}Service.select${ClassName}Page(queryDto);
+    }
+
+    /**
+     * 获取${tableComment}详细信息
+     */
+    @SaCheckPermission("${consumerModule}:${businessName}:query")
+    @GetMapping("/{${pkField}}")
+    public R<Remote${ClassName}Dto> getInfo(@PathVariable ${pkType} ${pkField}) {
+        return remote${ClassName}Service.select${ClassName}ById(${pkField});
+    }
+
+    /**
+     * 获取${tableComment}列表(不分页)
+     */
+    @SaCheckPermission("${consumerModule}:${businessName}:list")
+    @GetMapping("/all")
+    public R<List<Remote${ClassName}Dto>> all() {
+        Remote${ClassName}QueryDto queryDto = new Remote${ClassName}QueryDto();
+        return R.ok(remote${ClassName}Service.select${ClassName}List(queryDto));
+    }
+
+    /**
+     * 新增${tableComment}
+     */
+    @SaCheckPermission("${consumerModule}:${businessName}:add")
+    @PostMapping()
+    public R<Void> add(@RequestBody Remote${ClassName}Dto dto) {
+        return remote${ClassName}Service.insert${ClassName}(dto);
+    }
+
+    /**
+     * 修改${tableComment}
+     */
+    @SaCheckPermission("${consumerModule}:${businessName}:edit")
+    @PutMapping()
+    public R<Void> edit(@RequestBody Remote${ClassName}Dto dto) {
+        return remote${ClassName}Service.update${ClassName}(dto);
+    }
+
+    /**
+     * 删除${tableComment}
+     */
+    @SaCheckPermission("${consumerModule}:${businessName}:remove")
+    @DeleteMapping("/{${pkField}s}")
+    public R<Void> remove(@PathVariable ${pkType}[] ${pkField}s) {
+        return remote${ClassName}Service.delete${ClassName}ByIds(${pkField}s);
+    }
+
+}

+ 142 - 0
.workbuddy/skills/ruoyi-backend/templates/Controller.java.template

@@ -0,0 +1,142 @@
+package org.dromara.${module}.${submodule}.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.${module}.${submodule}.domain.bo.${ClassName}Bo;
+import org.dromara.${module}.${submodule}.domain.vo.${ClassName}Vo;
+import org.dromara.${module}.${submodule}.listener.${ClassName}ImportListener;
+import org.dromara.${module}.${submodule}.service.I${ClassName}Service;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * ${tableComment}
+ * 前端访问路由地址为:/${submodule}/${businessName}
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/${submodule}/${businessName}")
+public class ${ClassName}Controller extends BaseController {
+
+    private final I${ClassName}Service ${className}Service;
+
+    /**
+     * 查询${tableComment}列表
+     */
+    @SaCheckPermission("${submodule}:${businessName}:list")
+    @GetMapping("/list")
+    public TableDataInfo<${ClassName}Vo> list(${ClassName}Bo bo, PageQuery pageQuery) {
+        return ${className}Service.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出${tableComment}列表
+     */
+    @SaCheckPermission("${submodule}:${businessName}:export")
+    @Log(title = "${tableComment}", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(${ClassName}Bo bo, HttpServletResponse response) {
+        List<${ClassName}Vo> list = ${className}Service.queryList(bo);
+        ExcelUtil.exportExcel(list, "${tableComment}", ${ClassName}Vo.class, response);
+    }
+
+    /**
+     * 获取${tableComment}详细信息
+     *
+     * @param ${pkField} 主键
+     */
+    @SaCheckPermission("${submodule}:${businessName}:query")
+    @GetMapping("/{${pkField}}")
+    public R<${ClassName}Vo> getInfo(@NotNull(message = "主键不能为空")
+                                     @PathVariable ${pkType} ${pkField}) {
+        return R.ok(${className}Service.queryById(${pkField}));
+    }
+
+    /**
+     * 新增${tableComment}
+     */
+    @SaCheckPermission("${submodule}:${businessName}:add")
+    @Log(title = "${tableComment}", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody ${ClassName}Bo bo) {
+        return toAjax(${className}Service.insertByBo(bo));
+    }
+
+    /**
+     * 修改${tableComment}
+     */
+    @SaCheckPermission("${submodule}:${businessName}:edit")
+    @Log(title = "${tableComment}", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody ${ClassName}Bo bo) {
+        return toAjax(${className}Service.updateByBo(bo));
+    }
+
+    /**
+     * 删除${tableComment}
+     *
+     * @param ${pkField}s 主键串
+     */
+    @SaCheckPermission("${submodule}:${businessName}:remove")
+    @Log(title = "${tableComment}", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{${pkField}s}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable ${pkType}[] ${pkField}s) {
+        return toAjax(${className}Service.deleteWithValidByIds(List.of(${pkField}s), true));
+    }
+
+    /**
+     * 导出${tableComment}导入模板
+     */
+    @SaCheckPermission("${submodule}:${businessName}:export")
+    @Log(title = "${tableComment}导入模板", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportTemplate")
+    public void exportTemplate(HttpServletResponse response) {
+        List<${ClassName}ImportVo> list = new ArrayList<>();
+        ExcelUtil.exportExcel(list, "${tableComment}导入模板", ${ClassName}ImportVo.class, response);
+    }
+
+    /**
+     * 导入${tableComment}数据
+     *
+     * @param file Excel文件
+     * @param updateSupport 是否更新已存在数据
+     */
+    @SaCheckPermission("${submodule}:${businessName}:import")
+    @Log(title = "${tableComment}导入", businessType = BusinessType.IMPORT)
+    @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
+        ExcelResult<${ClassName}ImportVo> result = ExcelUtil.importExcel(
+            file.getInputStream(),
+            ${ClassName}ImportVo.class,
+            new ${ClassName}ImportListener(updateSupport)
+        );
+        return R.ok(result.getAnalysis());
+    }
+
+}

+ 39 - 0
.workbuddy/skills/ruoyi-backend/templates/Domain.java.template

@@ -0,0 +1,39 @@
+package org.dromara.${module}.${submodule}.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.${parentEntity};
+
+import java.io.Serial;
+
+/**
+ * ${tableComment}对象 ${tableName}
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("${tableName}")
+public class ${ClassName} extends ${parentEntity} {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * ${pkComment}
+     */
+    @TableId(value = "${pkColumn}")
+    private Long ${pkField};
+
+${fields}
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 19 - 0
.workbuddy/skills/ruoyi-backend/templates/Dto.java.template

@@ -0,0 +1,19 @@
+package org.dromara.${module}.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * ${tableComment}DTO
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Data
+public class Remote${ClassName}Dto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+${fields}
+}

+ 102 - 0
.workbuddy/skills/ruoyi-backend/templates/ImportListener.java.template

@@ -0,0 +1,102 @@
+package org.dromara.${module}.${submodule}.listener;
+
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.${module}.${submodule}.domain.bo.${ClassName}Bo;
+import org.dromara.${module}.${submodule}.domain.vo.${ClassName}ImportVo;
+import org.dromara.${module}.${submodule}.service.I${ClassName}Service;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.excel.core.ExcelListener;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.satoken.utils.LoginHelper;
+
+import java.util.List;
+
+/**
+ * ${tableComment}导入监听器
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Slf4j
+public class ${ClassName}ImportListener extends AnalysisEventListener<${ClassName}ImportVo> implements ExcelListener<${ClassName}ImportVo> {
+
+    private final I${ClassName}Service ${className}Service;
+
+    private final Boolean isUpdateSupport;
+
+    private final Long operUserId;
+
+    private int successNum = 0;
+    private int failureNum = 0;
+    private final StringBuilder successMsg = new StringBuilder();
+    private final StringBuilder failureMsg = new StringBuilder();
+
+    public ${ClassName}ImportListener(Boolean isUpdateSupport) {
+        this.${className}Service = SpringUtils.getBean(I${ClassName}Service.class);
+        this.operUserId = LoginHelper.getUserId();
+        this.isUpdateSupport = isUpdateSupport;
+    }
+
+    @Override
+    public ExcelResult<${ClassName}ImportVo> getExcelResult() {
+        return new ExcelResult<>() {
+
+            @Override
+            public List<${ClassName}ImportVo> getList() {
+                return null;
+            }
+
+            @Override
+            public List<String> getErrorList() {
+                return null;
+            }
+
+            @Override
+            public String getAnalysis() {
+                if (failureNum > 0) {
+                    failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+                    throw new ServiceException(failureMsg.toString());
+                } else {
+                    successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+                }
+                return successMsg.toString();
+            }
+        };
+    }
+
+    @Override
+    public void invoke(${ClassName}ImportVo importVo, AnalysisContext analysisContext) {
+        Integer rowIndex = analysisContext.readRowHolder().getRowIndex() + 1;
+        try {
+            // 1. 验证数据
+            ValidatorUtils.validate(importVo);
+
+            // 2. 转换为BO对象
+            ${ClassName}Bo bo = new ${ClassName}Bo();
+            // TODO: 设置BO属性
+
+            // 3. 检查数据是否存在
+            // TODO: 根据业务逻辑检查重复数据
+
+            // 4. 插入或更新数据
+            ${className}Service.insertByBo(bo);
+            successNum++;
+            successMsg.append("<br/>").append(successNum).append("、第 ").append(rowIndex).append("行,导入成功");
+
+        } catch (Exception e) {
+            failureNum++;
+            String msg = "<br/>第 " + rowIndex + "行,导入失败:";
+            failureMsg.append(msg).append(e.getMessage());
+            log.error(msg, e);
+        }
+    }
+
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+        // 解析完成后的操作
+    }
+}

+ 26 - 0
.workbuddy/skills/ruoyi-backend/templates/ImportVo.java.template

@@ -0,0 +1,26 @@
+package org.dromara.${module}.${submodule}.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * ${tableComment}导入VO
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Data
+public class ${ClassName}ImportVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "字段1")
+    private String field1;
+
+    @ExcelProperty(value = "字段2")
+    private String field2;
+
+    // 添加其他导入字段...
+}

+ 15 - 0
.workbuddy/skills/ruoyi-backend/templates/Mapper.java.template

@@ -0,0 +1,15 @@
+package org.dromara.${module}.${submodule}.mapper;
+
+import org.dromara.${module}.${submodule}.domain.${ClassName};
+import org.dromara.${module}.${submodule}.domain.vo.${ClassName}Vo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * ${tableComment}Mapper接口
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}, ${ClassName}Vo> {
+
+}

+ 29 - 0
.workbuddy/skills/ruoyi-backend/templates/QueryDto.java.template

@@ -0,0 +1,29 @@
+package org.dromara.${module}.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * ${tableComment}查询DTO
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Data
+public class Remote${ClassName}QueryDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 页码
+     */
+    private Integer pageNum = 1;
+
+    /**
+     * 每页条数
+     */
+    private Integer pageSize = 10;
+
+${fields}
+}

+ 66 - 0
.workbuddy/skills/ruoyi-backend/templates/RemoteService.java.template

@@ -0,0 +1,66 @@
+package org.dromara.${module}.api;
+
+import org.dromara.${module}.api.domain.dto.Remote${ClassName}Dto;
+import org.dromara.${module}.api.domain.dto.Remote${ClassName}QueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+import java.util.List;
+
+/**
+ * ${tableComment}远程调用接口
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+public interface Remote${ClassName}Service {
+
+    /**
+     * 根据ID查询${tableComment}
+     *
+     * @param ${pkField} ${pkComment}
+     * @return ${tableComment}信息
+     */
+    R<Remote${ClassName}Dto> select${ClassName}ById(${pkType} ${pkField});
+
+    /**
+     * 查询${tableComment}列表(不分页)
+     *
+     * @param queryDto 查询条件
+     * @return ${tableComment}列表
+     */
+    List<Remote${ClassName}Dto> select${ClassName}List(Remote${ClassName}QueryDto queryDto);
+
+    /**
+     * 分页查询${tableComment}列表
+     *
+     * @param queryDto 查询条件
+     * @return 分页结果
+     */
+    TableDataInfo<Remote${ClassName}Dto> select${ClassName}Page(Remote${ClassName}QueryDto queryDto);
+
+    /**
+     * 新增${tableComment}
+     *
+     * @param dto ${tableComment}信息
+     * @return 操作结果
+     */
+    R<Void> insert${ClassName}(Remote${ClassName}Dto dto);
+
+    /**
+     * 修改${tableComment}
+     *
+     * @param dto ${tableComment}信息
+     * @return 操作结果
+     */
+    R<Void> update${ClassName}(Remote${ClassName}Dto dto);
+
+    /**
+     * 删除${tableComment}
+     *
+     * @param ids ${pkComment}数组
+     * @return 操作结果
+     */
+    R<Void> delete${ClassName}ByIds(${pkType}[] ids);
+
+}

+ 113 - 0
.workbuddy/skills/ruoyi-backend/templates/RemoteServiceImpl.java.template

@@ -0,0 +1,113 @@
+package org.dromara.${module}.${submodule}.dubbo;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.dromara.${module}.api.Remote${ClassName}Service;
+import org.dromara.${module}.api.domain.dto.Remote${ClassName}Dto;
+import org.dromara.${module}.api.domain.dto.Remote${ClassName}QueryDto;
+import org.dromara.${module}.${submodule}.domain.bo.${ClassName}Bo;
+import org.dromara.${module}.${submodule}.domain.vo.${ClassName}Vo;
+import org.dromara.${module}.${submodule}.service.I${ClassName}Service;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * ${tableComment}远程服务实现
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+@DubboService
+public class Remote${ClassName}ServiceImpl implements Remote${ClassName}Service {
+
+    private final I${ClassName}Service ${className}Service;
+
+    /**
+     * 根据ID查询${tableComment}
+     */
+    @Override
+    public R<Remote${ClassName}Dto> select${ClassName}ById(${pkType} ${pkField}) {
+        ${ClassName}Vo vo = ${className}Service.queryById(${pkField});
+        if (vo == null) {
+            return R.fail("${tableComment}不存在");
+        }
+        return R.ok(MapstructUtils.convert(vo, Remote${ClassName}Dto.class));
+    }
+
+    /**
+     * 查询${tableComment}列表(不分页)
+     */
+    @Override
+    public List<Remote${ClassName}Dto> select${ClassName}List(Remote${ClassName}QueryDto queryDto) {
+        ${ClassName}Bo bo = new ${ClassName}Bo();
+        // 设置查询条件
+        List<${ClassName}Vo> voList = ${className}Service.queryList(bo);
+        return MapstructUtils.convert(voList, Remote${ClassName}Dto.class);
+    }
+
+    /**
+     * 分页查询${tableComment}列表
+     */
+    @Override
+    public TableDataInfo<Remote${ClassName}Dto> select${ClassName}Page(Remote${ClassName}QueryDto queryDto) {
+        // 1. 构建分页参数
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+        pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+
+        // 2. 构建查询条件
+        ${ClassName}Bo bo = new ${ClassName}Bo();
+        // TODO: 设置查询条件
+
+        // 3. 执行查询
+        TableDataInfo<${ClassName}Vo> result = ${className}Service.queryPageList(bo, pageQuery);
+
+        // 4. 转换结果
+        List<Remote${ClassName}Dto> dtoList = MapstructUtils.convert(result.getRows(), Remote${ClassName}Dto.class);
+
+        // 5. 构建返回
+        TableDataInfo<Remote${ClassName}Dto> pageData = TableDataInfo.build();
+        pageData.setRows(dtoList);
+        pageData.setTotal(result.getTotal());
+        return pageData;
+    }
+
+    /**
+     * 新增${tableComment}
+     */
+    @Override
+    public R<Void> insert${ClassName}(Remote${ClassName}Dto dto) {
+        ${ClassName}Bo bo = MapstructUtils.convert(dto, ${ClassName}Bo.class);
+        ${className}Service.insertByBo(bo);
+        return R.ok();
+    }
+
+    /**
+     * 修改${tableComment}
+     */
+    @Override
+    public R<Void> update${ClassName}(Remote${ClassName}Dto dto) {
+        ${ClassName}Bo bo = MapstructUtils.convert(dto, ${ClassName}Bo.class);
+        ${className}Service.updateByBo(bo);
+        return R.ok();
+    }
+
+    /**
+     * 删除${tableComment}
+     */
+    @Override
+    public R<Void> delete${ClassName}ByIds(${pkType}[] ids) {
+        ${className}Service.deleteWithValidByIds(List.of(ids), false);
+        return R.ok();
+    }
+
+}

+ 49 - 0
.workbuddy/skills/ruoyi-backend/templates/Service.java.template

@@ -0,0 +1,49 @@
+package org.dromara.${module}.${submodule}.service;
+
+import org.dromara.${module}.${submodule}.domain.bo.${ClassName}Bo;
+import org.dromara.${module}.${submodule}.domain.vo.${ClassName}Vo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ${tableComment}Service接口
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+public interface I${ClassName}Service {
+
+    /**
+     * 查询${tableComment}
+     */
+    ${ClassName}Vo queryById(${pkType} ${pkField});
+
+    /**
+     * 查询${tableComment}列表
+     */
+    List<${ClassName}Vo> queryList(${ClassName}Bo bo);
+
+    /**
+     * 分页查询${tableComment}列表
+     */
+    TableDataInfo<${ClassName}Vo> queryPageList(${ClassName}Bo bo, PageQuery pageQuery);
+
+    /**
+     * 新增${tableComment}
+     */
+    Boolean insertByBo(${ClassName}Bo bo);
+
+    /**
+     * 修改${tableComment}
+     */
+    Boolean updateByBo(${ClassName}Bo bo);
+
+    /**
+     * 校验并批量删除${tableComment}
+     */
+    Boolean deleteWithValidByIds(Collection<${pkType}> ids, Boolean isValid);
+
+}

+ 119 - 0
.workbuddy/skills/ruoyi-backend/templates/ServiceImpl.java.template

@@ -0,0 +1,119 @@
+package org.dromara.${module}.${submodule}.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.${module}.${submodule}.domain.${ClassName};
+import org.dromara.${module}.${submodule}.domain.bo.${ClassName}Bo;
+import org.dromara.${module}.${submodule}.domain.vo.${ClassName}Vo;
+import org.dromara.${module}.${submodule}.mapper.${ClassName}Mapper;
+import org.dromara.${module}.${submodule}.service.I${ClassName}Service;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * ${tableComment}Service业务层处理
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ${ClassName}ServiceImpl implements I${ClassName}Service {
+
+    private final ${ClassName}Mapper baseMapper;
+
+    /**
+     * 查询${tableComment}
+     */
+    @Override
+    public ${ClassName}Vo queryById(${pkType} ${pkField}) {
+        return baseMapper.selectVoById(${pkField});
+    }
+
+    /**
+     * 查询${tableComment}列表
+     */
+    @Override
+    public List<${ClassName}Vo> queryList(${ClassName}Bo bo) {
+        LambdaQueryWrapper<${ClassName}> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    /**
+     * 分页查询${tableComment}列表
+     */
+    @Override
+    public TableDataInfo<${ClassName}Vo> queryPageList(${ClassName}Bo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<${ClassName}> lqw = buildQueryWrapper(bo);
+        lqw.orderByDesc(${ClassName}::getCreateTime);
+        Page<${ClassName}Vo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 新增${tableComment}
+     */
+    @Override
+    public Boolean insertByBo(${ClassName}Bo bo) {
+        ${ClassName} add = MapstructUtils.convert(bo, ${ClassName}.class);
+        validEntityBeforeSave(Objects.requireNonNull(add));
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.set${PkCapField}(add.get${PkCapField}());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改${tableComment}
+     */
+    @Override
+    public Boolean updateByBo(${ClassName}Bo bo) {
+        ${ClassName} update = MapstructUtils.convert(bo, ${ClassName}.class);
+        if (update != null) {
+            validEntityBeforeSave(update);
+        }
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 校验并批量删除${tableComment}
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<${pkType}> ids, Boolean isValid) {
+        if (isValid) {
+            // 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(${ClassName} entity) {
+        // TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 构建查询条件
+     */
+    private LambdaQueryWrapper<${ClassName}> buildQueryWrapper(${ClassName}Bo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<${ClassName}> lqw = Wrappers.lambdaQuery();
+${queryWrapperConditions}
+        return lqw;
+    }
+
+}

+ 34 - 0
.workbuddy/skills/ruoyi-backend/templates/Vo.java.template

@@ -0,0 +1,34 @@
+package org.dromara.${module}.${submodule}.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.${module}.${submodule}.domain.${ClassName};
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.convert.ExcelDictConvert;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * ${tableComment}视图对象 ${tableName}
+ *
+ * @author ${author}
+ * @date ${date}
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = ${ClassName}.class)
+public class ${ClassName}Vo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * ${pkComment}
+     */
+    private Long ${pkField};
+
+${fields}
+}

+ 41 - 37
README.md

@@ -2,6 +2,7 @@
 <div style="height: 10px; clear: both;"></div>
 
 - - -
+
 ## 平台简介
 
 [![码云Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
@@ -17,7 +18,7 @@
 > RuoYi-Cloud-Plus `微服务通用权限管理系统` 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
 
 > 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可<br>
-活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源
+> 活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源
 
 > 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
 
@@ -36,16 +37,16 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 # 本框架与RuoYi的功能差异
 
 | 功能          | 本框架                                                                                                               | RuoYi                                                                              |
-|-------------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
-| 前端项目        | 采用 Vue3 + TS + ElementPlus 重写                                                                                     | 基于Vue2/Vue3 + JS                                                                   | 
-| 后端项目结构      | 采用插件化 + 扩展包形式 结构解耦 易于扩展                                                                                           | 模块相互注入耦合严重难以扩展                                                                     | 
+| ----------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
+| 前端项目        | 采用 Vue3 + TS + ElementPlus 重写                                                                                     | 基于Vue2/Vue3 + JS                                                                   |
+| 后端项目结构      | 采用插件化 + 扩展包形式 结构解耦 易于扩展                                                                                           | 模块相互注入耦合严重难以扩展                                                                     |
 | 后端代码风格      | 严格遵守Alibaba规范与项目统一配置的代码格式化                                                                                        | 代码书写与常规结构不同阅读障碍大                                                                   |
-| 分布式注册中心     | 采用 Alibaba Nacos 源码集成便于调试扩展与二次开发 框架还为其增加了各种监控                                                                     | 采用 Alibaba Nacos 自行搭建纯官方版本不可靠                                                      | 
-| 分布式配置中心     | 采用 Alibaba Nacos 源码集成便于调试扩展与二次开发 框架还为其增加了各种监控                                                                     | 采用 Alibaba Nacos 自行搭建纯官方版本不可靠                                                      | 
-| 服务网关        | 采用 SpringCloud Gateway 框架扩展了多种功能<br/>例如:内网鉴权、请求体缓存、跨域配置、请求响应日志等                                                   | 采用 SpringCloud Gateway 功能单一                                                        | 
+| 分布式注册中心     | 采用 Alibaba Nacos 源码集成便于调试扩展与二次开发 框架还为其增加了各种监控                                                                     | 采用 Alibaba Nacos 自行搭建纯官方版本不可靠                                                      |
+| 分布式配置中心     | 采用 Alibaba Nacos 源码集成便于调试扩展与二次开发 框架还为其增加了各种监控                                                                     | 采用 Alibaba Nacos 自行搭建纯官方版本不可靠                                                      |
+| 服务网关        | 采用 SpringCloud Gateway 框架扩展了多种功能<br/>例如:内网鉴权、请求体缓存、跨域配置、请求响应日志等                                                   | 采用 SpringCloud Gateway 功能单一                                                        |
 | 负载均衡        | 采用 SpringCloud Loadbalancer 扩展支持了开发团队路由 便于多团队开发调试                                                                 | 采用 SpringCloud Loadbalancer 功能单一                                                   |
-| RPC远程调用     | 采用 全新 Apache Dubbo 3.X 历史悠远不用多说                                                                                   | 采用 feign 功能有限编写方式 网络波动大 不稳定                                                        | 
-| 分布式限流熔断     | 采用 Alibaba Sentinel 源码集成便于调试扩展与二次开发 框架还为其增加了各种监控                                                                  | 采用 Alibaba Sentinel 自行搭建纯官方版本不可靠                                                   | 
+| RPC远程调用     | 采用 全新 Apache Dubbo 3.X 历史悠远不用多说                                                                                   | 采用 feign 功能有限编写方式 网络波动大 不稳定                                                        |
+| 分布式限流熔断     | 采用 Alibaba Sentinel 源码集成便于调试扩展与二次开发 框架还为其增加了各种监控                                                                  | 采用 Alibaba Sentinel 自行搭建纯官方版本不可靠                                                   |
 | 分布式事务       | 采用 Alibaba Seata 源码集成对接了Nacos与各种监控 简化了搭建部署流程                                                                      | 采用 Alibaba Seata 自行搭建纯官方版本 搭建繁琐与Nacos不挂钩 代码内使用方式怪异等                                |
 | Web容器       | 采用 Undertow 基于 XNIO 的高性能容器                                                                                        | 采用 Tomcat                                                                          |
 | 权限认证        | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展                                                                                  | Spring Security 配置繁琐扩展性极差                                                          |
@@ -66,26 +67,26 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 | 数据库连接池      | 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下                                                                        | 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般                                               |
 | 数据库主键       | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁                                                                  | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一                                                     |
 | WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物                                                         | 无                                                                                  |
-| 序列化         | 采用 Jackson Spring官方内置序列化 靠谱!!!                                                                                    | 采用 fastjson bugjson 远近闻名                                                           | 
+| 序列化         | 采用 Jackson Spring官方内置序列化 靠谱!!!                                                                                    | 采用 fastjson bugjson 远近闻名                                                           |
 | 分布式幂等       | 参考美团GTIS防重系统简化实现(细节可看文档)                                                                                          | 手动编写注解基于aop实现                                                                      |
-| 分布式任务调度     | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等                                                                 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造                                                   | 
-| 分布式日志中心     | 采用 ELK 业界成熟解决方案 实时收集所有服务的运行日志 快速发现定位问题                                                                            | 无                                                                                  | 
-| 分布式搜索引擎     | 采用 ElasticSearch、Easy-Es 以 Mybatis-Plus 方式操作 ElasticSearch                                                        | 无                                                                                  | 
-| 分布式消息队列     | 采用 支持 Kafka、RocketMQ、RabbitMQ 各种 延迟消息 事务消息 流消息                                                                    | 无                                                                                  | 
+| 分布式任务调度     | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等                                                                 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造                                                   |
+| 分布式日志中心     | 采用 ELK 业界成熟解决方案 实时收集所有服务的运行日志 快速发现定位问题                                                                            | 无                                                                                  |
+| 分布式搜索引擎     | 采用 ElasticSearch、Easy-Es 以 Mybatis-Plus 方式操作 ElasticSearch                                                        | 无                                                                                  |
+| 分布式消息队列     | 采用 支持 Kafka、RocketMQ、RabbitMQ 各种 延迟消息 事务消息 流消息                                                                    | 无                                                                                  |
 | 分库分表功能      | 采用 Apache Sharding-Proxy 代理服务无入侵支持分库分表 只需编写分库分表规则即可                                                               | 无                                                                                  |
 | 文件存储        | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储                                                     | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应                                                    |
 | 云存储         | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家                                                                          | 不支持                                                                                |
 | 短信          | 支持 阿里、腾讯 只需在yml配置好厂家密钥即可使用 接口化支持扩展其他厂家                                                                            | 不支持                                                                                |
 | 邮件          | 采用 mail-api 通用协议支持大部分邮件厂商                                                                                         | 不支持                                                                                |
-| 接口文档        | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了                                                     | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成                                                | 
+| 接口文档        | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了                                                     | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成                                                |
 | 校验框架        | 采用 Validation 支持注解与工具类校验 注解支持国际化                                                                                  | 仅支持注解 且注解不支持国际化                                                                    |
 | Excel框架     | 采用 Alibaba EasyExcel 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等                                               | 基于 POI 手写实现 功能有限 复杂 扩展性差                                                           |
-| 工具类框架       | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码                                                       | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等                                            | 
-| 服务监控框架      | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控                                    | 无                                                                                  | 
-| 全方位监控报警     | 采用 Prometheus、Grafana 多样化采集 多模板大屏展示 实时报警监控 提供详细的搭建文档                                                              | 无                                                                                  | 
+| 工具类框架       | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码                                                       | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等                                            |
+| 服务监控框架      | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控                                    | 无                                                                                  |
+| 全方位监控报警     | 采用 Prometheus、Grafana 多样化采集 多模板大屏展示 实时报警监控 提供详细的搭建文档                                                              | 无                                                                                  |
 | 链路追踪        | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点                                            | 无                                                                                  |
 | 代码生成器       | 只需设计好表结构 一键生成所有crud代码与页面<br/>降低80%的开发量 把精力都投入到业务设计上<br/>框架为其适配MP、SpringDoc规范化代码 同时支持动态多数据源代码生成                    | 代码生成原生结构 只支持单数据源生成                                                                 |
-| 部署方式        | 支持 Docker 编排 一键搭建所有环境 让开发人员从此不再为搭建环境而烦恼                                                                           | 原生jar部署 其他环境需手动下载安装 自行搭建                                                           | 
+| 部署方式        | 支持 Docker 编排 一键搭建所有环境 让开发人员从此不再为搭建环境而烦恼                                                                           | 原生jar部署 其他环境需手动下载安装 自行搭建                                                           |
 | 项目路径修改      | 提供详细的修改方案文档 并为其做了一些改动 非常简单即可修改成自己想要的                                                                              | 需要做很多改造 文档说明有限                                                                     |
 | 国际化         | 基于请求头动态返回不同语种的文本内容 开发难度低 有对应的工具类 支持大部分注解内容国际化                                                                     | 只提供基础功能 其他需自行编写扩展                                                                  |
 | 代码单例测试      | 提供单例测试 使用方式编写方法与maven多环境单测插件                                                                                      | 只提供基础功能 其他需自行编写扩展                                                                  |
@@ -94,7 +95,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 ## 本框架与RuoYi的业务差异
 
 | 业务     | 功能说明                                    | 本框架 | RuoYi            |
-|--------|-----------------------------------------|-----|------------------|
+| ------ | --------------------------------------- | --- | ---------------- |
 | 租户管理   | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等         | 支持  | 无                |
 | 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等             | 支持  | 无                |
 | 用户管理   | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等          | 支持  | 支持               |
@@ -122,21 +123,26 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 
 使用框架前请仔细阅读文档重点注意事项
 <br>
->[初始化项目 必看](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/init)
->>[https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/init](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/init)
->
->[专栏与视频 入门必看](https://plus-doc.dromara.org/#/common/column)
->>[https://plus-doc.dromara.org/#/common/column](https://plus-doc.dromara.org/#/common/column)
->
->[部署项目 必看](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/deploy)
->>[https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/deploy](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/deploy)
->
->[如何加群](https://plus-doc.dromara.org/#/common/add_group)
->>[https://plus-doc.dromara.org/#/common/add_group](https://plus-doc.dromara.org/#/common/add_group)
->
->[参考文档 Wiki](https://plus-doc.dromara.org)
->>[https://plus-doc.dromara.org](https://plus-doc.dromara.org)
 
+> [初始化项目 必看](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/init)
+> 
+> > [https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/init](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/init)
+> 
+> [专栏与视频 入门必看](https://plus-doc.dromara.org/#/common/column)
+> 
+> > [https://plus-doc.dromara.org/#/common/column](https://plus-doc.dromara.org/#/common/column)
+> 
+> [部署项目 必看](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/deploy)
+> 
+> > [https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/deploy](https://plus-doc.dromara.org/#/ruoyi-cloud-plus/quickstart/deploy)
+> 
+> [如何加群](https://plus-doc.dromara.org/#/common/add_group)
+> 
+> > [https://plus-doc.dromara.org/#/common/add_group](https://plus-doc.dromara.org/#/common/add_group)
+> 
+> [参考文档 Wiki](https://plus-doc.dromara.org)
+> 
+> > [https://plus-doc.dromara.org](https://plus-doc.dromara.org)
 
 ## 软件架构图
 
@@ -155,7 +161,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 ## 演示图例
 
 |                                                                                            |                                                                                            |
-|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
+| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
 | ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png "屏幕截图") |
@@ -175,5 +181,3 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 | ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png "屏幕截图") |
 | ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png "屏幕截图") |
-
-

+ 25 - 0
config/nacos/ruoyi-ecs.yml

@@ -0,0 +1,25 @@
+spring:
+  datasource:
+    dynamic:
+      # 设置默认的数据源或者数据源组,默认值即为 master
+      primary: master
+      datasource:
+        # 主库数据源
+        master:
+          type: ${spring.datasource.type}
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: ${datasource.system-master.url}
+          username: ${datasource.system-master.username}
+          password: ${datasource.system-master.password}
+#        oracle:
+#          type: ${spring.datasource.type}
+#          driverClassName: oracle.jdbc.OracleDriver
+#          url: ${datasource.system-oracle.url}
+#          username: ${datasource.system-oracle.username}
+#          password: ${datasource.system-oracle.password}
+#        postgres:
+#          type: ${spring.datasource.type}
+#          driverClassName: org.postgresql.Driver
+#          url: ${datasource.system-postgres.url}
+#          username: ${datasource.system-postgres.username}
+#          password: ${datasource.system-postgres.password}

+ 294 - 0
doc/2026-04-21-变更日志.md

@@ -0,0 +1,294 @@
+# 项目变更日志
+
+## 2026-04-21
+
+### 📊 变更统计
+- **修改文件**: 32个
+- **新增代码**: +828行
+- **删除代码**: -48行
+- **净增代码**: +780行
+- **新增模块**: 1个(ECS外部课程系统)
+
+---
+
+### 🎯 变更详情
+
+#### 1. 新增 ECS 模块(外部课程系统)⭐
+
+**模块路径**: `ruoyi-modules/ruoyi-ecs`  
+**状态**: 全新模块(未提交)
+
+##### 1.1 模块结构
+```
+ruoyi-modules/ruoyi-ecs/
+├── src/main/java/org/dromara/ecs/
+│   ├── RuoYiEcsApplication.java          # 应用启动类
+│   ├── controller/                        # 控制器层(9个)
+│   ├── domain/                           # 领域模型(8个)
+│   │   ├── EcsSection.java              # 课表实体
+│   │   ├── bo/                          # 业务对象
+│   │   └── vo/                          # 视图对象
+│   ├── dubbo/                            # Dubbo远程服务(1个)
+│   ├── job/                              # 定时任务(1个)
+│   ├── listener/                         # 消息监听(2个)
+│   ├── mapper/                           # 数据访问层(5个)
+│   └── service/                          # 业务逻辑层(7个)
+│       └── impl/
+│           └── EcsSectionServiceImpl.java  # 课表服务实现
+└── src/main/resources/
+    └── mapper/                           # MyBatis映射文件
+```
+
+##### 1.2 配套资源
+- **API模块**: `ruoyi-api/ruoyi-api-ecs/`
+- **Nacos配置**: `config/nacos/ruoyi-ecs.yml`
+- **需求文档**: `doc/` 目录下包含:
+  - `ecs-section-requirements.md` - 课表需求文档
+  - `ecs-course-requirements.md` - 课程需求文档
+  - `ecs-term-requirements.md` - 学期需求文档
+  - `ecs-attend-requirements.md` - 考勤需求文档
+
+##### 1.3 今日优化
+**文件**: [EcsSectionServiceImpl.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/service/impl/EcsSectionServiceImpl.java)
+
+**优化内容**: 课表查询排序逻辑
+- 修改 `buildQueryWrapper()` 方法
+- 新增排序规则:
+  - 按上课日期降序排列(`orderByDesc(CourseDate)`)
+  - 按课程节次升序排列(`orderByAsc(SectionIndex)`)
+- 移除分页查询中的旧排序逻辑(`CreateTime`)
+
+**影响范围**:
+- `queryList()` - 列表查询
+- `queryPageList()` - 分页查询
+
+---
+
+#### 2. ruoyi-system 模块 - 部门管理增强
+
+**变更文件**: 6个文件,+265行
+
+##### 2.1 部门实体扩展
+**文件**: [SysDept.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysDept.java)
+
+**新增字段**:
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| `managerId` | Long | 班主任用户ID |
+| `planCount` | Integer | 计划人数 |
+| `dataSource` | Integer | 数据来源(0-本地,1-同步) |
+| `roomId` | Long | 教室ID |
+| `roomName` | String | 教室名称 |
+
+**配套BO/VO更新**:
+- [SysDeptBo.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysDeptBo.java) - +15行
+- [SysDeptVo.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysDeptVo.java) - +25行
+
+##### 2.2 培训班查询功能
+**文件**: [RemoteDeptServiceImpl.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteDeptServiceImpl.java)
+
+**新增内容**:
+- 实现培训班查询Dubbo接口(+87行)
+- 支持按 `deptType=05` 筛选培训班
+- 返回培训班列表及详细信息
+
+**服务接口**:
+- [ISysDeptService.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysDeptService.java) - +35行
+- [SysDeptServiceImpl.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java) - +80行
+
+##### 2.3 远程服务API
+**文件**: [RemoteDeptService.java](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java)
+
+**新增接口**:
+- 培训班查询方法定义(+35行)
+- 支持跨模块调用
+
+---
+
+#### 3. ruoyi-backstage 模块 - 后台管理增强
+
+**变更文件**: 13个文件,+389行
+
+##### 3.1 教室管理
+**实体扩展**:
+- [PtRoom.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/PtRoom.java) - +14行
+- [PtRoomBo.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/bo/PtRoomBo.java) - +14行
+- [PtRoomVo.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/vo/PtRoomVo.java) - +14行
+
+**远程服务**:
+- [RemotePtRoomServiceImpl.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java) - +88行
+  - 实现教室查询Dubbo接口
+  - 支持跨模块调用教室信息
+
+**数据访问**:
+- [PtRoomMapper.xml](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/basics/room/PtRoomMapper.xml) - +3行
+
+##### 3.2 用户账户管理
+**远程服务实现**:
+- [RemoteUserAccountServiceImpl.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/dubbo/RemoteUserAccountServiceImpl.java) - +178行
+  - 实现用户账户查询Dubbo接口
+  - 支持账户余额查询
+  - 支持账户流水查询
+
+**服务层**:
+- [IPtUserAccountService.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/IPtUserAccountService.java) - +9行
+- [PtUserAccountServiceImpl.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtUserAccountServiceImpl.java) - +34行
+
+**数据访问**:
+- [PtUserAccountMapper.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/mapper/PtUserAccountMapper.java) - +6行
+- [PtUserAccountMapper.xml](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/payment/PtUserAccountMapper.xml) - +22行
+
+##### 3.3 个人中心接口
+**控制器更新**:
+- [SelfController.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/SelfController.java) - +1行
+- [TeacherController.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/TeacherController.java) - +1行
+- [TraineeController.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/TraineeController.java) - +1行
+
+**学员VO扩展**:
+- [YcTraineeVo.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/domain/vo/yc/YcTraineeVo.java) - +2行
+
+##### 3.4 其他优化
+**初始化任务**:
+- [InitRunner.java](file:///D:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/task/InitRunner.java) - +10行/-10行
+  - 优化启动初始化逻辑
+
+---
+
+#### 4. ruoyi-api 模块 - 远程服务接口扩展
+
+**变更文件**: 5个文件,+121行
+
+##### 4.1 新增远程服务接口
+**教室服务**:
+- [RemotePtRoomService.java](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java) - +31行
+  - 定义教室查询接口
+  - 支持按条件查询教室信息
+
+**用户账户服务**:
+- [RemoteUserAccountService.java](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteUserAccountService.java) - +45行
+  - 定义用户账户查询接口
+  - 支持余额查询和流水查询
+
+##### 4.2 新增DTO
+**后台DTO**: `ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/`
+- 教室相关DTO
+- 用户账户相关DTO
+
+**系统DTO**: `ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/dto/`
+- 培训班相关DTO
+- 部门相关DTO
+
+##### 4.3 分页结果封装
+**文件**: [PageResult.java](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/PageResult.java)
+
+**作用**: 
+- 解耦 `ruoyi-common-satoken` 与 `ruoyi-api-system` 的循环依赖
+- 提供统一的分页返回格式
+- 替代 `TableDataInfo` 在API层的使用
+
+---
+
+#### 5. 配置和文档
+
+##### 5.1 POM配置调整
+**根POM**: [pom.xml](file:///D:/dt_ykt/ykt_server/pom.xml) - +4行/-1行
+- 调整模块依赖关系
+- 优化构建配置
+
+**API POM**: 
+- [ruoyi-api/pom.xml](file:///D:/dt_ykt/ykt_server/ruoyi-api/pom.xml) - +1行(新增ECS API模块)
+- [ruoyi-api-backstage/pom.xml](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-backstage/pom.xml) - +9行
+- [ruoyi-api-bom/pom.xml](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-bom/pom.xml) - +7行
+- [ruoyi-api-system/pom.xml](file:///D:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-system/pom.xml) - -1行(移除循环依赖)
+
+**Modules POM**: 
+- [ruoyi-modules/pom.xml](file:///D:/dt_ykt/ykt_server/ruoyi-modules/pom.xml) - +1行(新增ECS模块)
+
+##### 5.2 应用配置
+**认证服务**: [application.yml](file:///D:/dt_ykt/ykt_server/ruoyi-auth/src/main/resources/application.yml) - +2行/-1行
+- 调整认证配置
+
+##### 5.3 项目文档
+**README**: [README.md](file:///D:/dt_ykt/ykt_server/README.md) - +78行/-48行
+- 更新项目说明
+- 补充新功能介绍
+
+---
+
+### 🔍 关键技术点
+
+#### 1. Dubbo远程服务扩展
+- 新增3个远程服务接口及实现
+- 支持跨模块调用:教室、用户账户、培训班
+- 统一使用DTO进行数据传输
+
+#### 2. 循环依赖解耦
+- **问题**: `ruoyi-common-satoken` 依赖 `ruoyi-api-system` 导致循环依赖
+- **方案**: 引入 `PageResult` 替代 `TableDataInfo`
+- **影响**: API层接口返回类型调整
+
+#### 3. 微服务模块新增
+- 完整的ECS外部课程系统模块
+- 支持课表、课程、学期、考勤管理
+- 独立Nacos配置
+
+#### 4. 培训班功能增强
+- 部门表扩展支持培训班属性
+- 新增班主任、计划人数、教室等字段
+- 提供专门的培训班查询接口
+
+#### 5. 支付账户管理
+- 用户账户远程服务支持跨模块查询
+- 支持余额查询和流水查询
+- 为消费模块提供数据支撑
+
+---
+
+### ⚠️ 注意事项
+
+1. **未提交内容**: 当前所有更改均未提交到Git
+2. **ECS模块**: 需确认Nacos配置已同步
+3. **数据库变更**: SysDept和PtRoom新增字段需执行DDL
+4. **循环依赖**: 确认PageResult替换后无编译错误
+5. **接口兼容**: 远程服务接口需向后兼容
+
+---
+
+### 📝 建议提交策略
+
+```bash
+# 1. 提交ECS模块
+git add ruoyi-modules/ruoyi-ecs/
+git add ruoyi-api/ruoyi-api-ecs/
+git add config/nacos/ruoyi-ecs.yml
+git add doc/ecs-*.md
+git commit -m "feat: 新增ECS外部课程系统模块"
+
+# 2. 提交System模块
+git add ruoyi-modules/ruoyi-system/
+git add ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java
+git add ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/
+git commit -m "feat: 部门管理增强,新增培训班查询功能"
+
+# 3. 提交Backstage模块
+git add ruoyi-modules/ruoyi-backstage/
+git add ruoyi-api/ruoyi-api-backstage/
+git commit -m "feat: 后台管理增强,新增教室和用户账户远程服务"
+
+# 4. 提交公共模块
+git add ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/PageResult.java
+git add ruoyi-api/pom.xml
+git add ruoyi-api/ruoyi-api-bom/pom.xml
+git add pom.xml
+git commit -m "refactor: 解耦循环依赖,新增PageResult分页封装"
+
+# 5. 提交配置和文档
+git add ruoyi-auth/src/main/resources/application.yml
+git add README.md
+git commit -m "docs: 更新项目文档和配置文件"
+```
+
+---
+
+**生成时间**: 2026-04-21  
+**生成工具**: Git + AI分析

+ 224 - 0
doc/2026-04-27-班级同步新入参适配实现计划.md

@@ -0,0 +1,224 @@
+# 统一 records 批量同步体系实现计划(不改 SyncHand、不改策略层)
+
+## 1. 背景与目标
+
+你已明确:
+1. 不在现有 `SyncHand` 上继续加方法;
+2. 直接新增独立的 controller、service 等;
+3. 后续还会接入学员、课表、教室等,入参结构统一为:
+
+```json
+{
+  "records": [
+    {
+      "业务对象字段": "..."
+    }
+  ]
+}
+```
+
+本方案目标:
+- 新建一套“统一 records 批量同步入口体系”;
+- 先落地培训班级场景;
+- 为学员/课表/教室预留同构扩展位;
+- 保持**策略层不改**(`ISyncDeptStrategy` / `TrainClassStrategyImpl` 等现有策略实现不调整)。
+
+---
+
+## 2. 设计原则
+
+1. **隔离旧链路**:`SyncHand` 与历史全量接口保持原样,避免影响现网。
+2. **统一请求壳**:所有新同步接口统一 `records` 批量模型。
+3. **业务分层清晰**:
+   - Controller:接收请求与基础校验;
+   - Application Service(新增):分流、编排;
+   - Adapter/Mapper(新增):DTO -> 领域对象(如 `ResourceDept`);
+   - Strategy(复用):落库与远程副作用。
+4. **先复用后扩展**:优先复用现有策略与常量,避免重复实现。
+
+---
+
+## 3. 总体架构(新增)
+
+新增一条并行链路(与 `SyncHand` 并存):
+
+`BatchSyncController`  
+-> `TrainBatchSyncService`(培训班级)  
+-> `TrainClassRecordMapper`(records -> `ResourceDept`)  
+-> 复用策略 `ISyncDeptStrategy(TRAIN_CLASS)`  
+-> `syncDept(...)` / `syncDelDept(...)`
+
+未来扩展:
+- `TraineeBatchSyncService`
+- `CourseBatchSyncService`
+- `ClassroomBatchSyncService`
+
+它们共享同一 `BatchSyncRequest<T>` 请求壳与统一处理约定。
+
+---
+
+## 4. 需新增内容清单
+
+## 4.1 Controller(新增,不改 SyncHand)
+
+建议新增文件:
+- `ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/controller/BatchSyncController.java`
+
+建议路由:
+- `POST /batch-sync/train/class`
+- 未来可扩展:
+  - `POST /batch-sync/train/trainee`
+  - `POST /batch-sync/train/course`
+  - `POST /batch-sync/train/classroom`
+
+职责:
+- 接收 `BatchSyncRequest<TrainClassRecordRequest>`;
+- 调用对应 service;
+- 返回 `R<Void>` 或后续统一结果对象。
+
+## 4.2 请求模型(新增)
+
+建议新增:
+1. `BatchSyncRequest<T>`:统一壳,字段 `List<T> records`
+2. `TrainClassRecordRequest`:培训班级记录 DTO
+
+培训班级字段:
+- `classId`
+- `className`
+- `year`
+- `senester`(兼容你给的外部字段拼写)
+- `checkinTime`
+- `startTime`
+- `endTime`
+- `studentNum`
+- `headTeacherName`
+- `delFlag`
+- `tenantId`
+
+说明:
+- `headTeacherName` 当前策略链未使用,先保留在请求层。
+
+## 4.3 Service(新增应用服务)
+
+建议新增文件:
+- `ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/TrainBatchSyncService.java`
+
+职责:
+1. 校验批次与记录;
+2. DTO 转 `ResourceDept`;
+3. 按 `delFlag` 分流:
+   - 删除集合 -> `syncDelDept(...)`
+   - 新增/更新集合 -> `syncDept(...)`
+4. 记录处理统计日志(总数、成功分流数、删除数、映射失败数)。
+
+## 4.4 记录映射器(新增)
+
+建议新增文件:
+- `ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/TrainClassRecordMapper.java`
+
+职责:
+- 将 `TrainClassRecordRequest` 映射为 `ResourceDept`,复用现有策略所需字段结构。
+
+字段映射(与现有策略兼容):
+- `classId -> dept_id`
+- `className -> dept_name`
+- `year -> year(Integer)`
+- `senester(1/2) -> semester(上学期/下学期)`
+- `checkinTime -> checkDate + payBegin`
+- `startTime -> beginDate`
+- `endTime -> endDate + payEnd`
+- `studentNum -> planCount`(为空设默认值,防止 `planCount.longValue()` NPE)
+- `tenantId -> tenantId`(为空回退默认租户)
+- 固定值:`chooseRoom=0`,`canEat=1`,`operatorId=FULL_SYNC_ADMIN`
+
+---
+
+## 5. 兼容与扩展策略
+
+## 5.1 与现有系统兼容
+
+1. `SyncHand` 不改。
+2. 现有 `SyncTrainService.syncTrainClass()` 不改。
+3. 现有策略层不改。
+4. 新旧接口可并行使用。
+
+## 5.2 面向未来扩展
+
+后续新增学员/课表/教室时,仅需:
+1. 新增对应 `RecordRequest` DTO;
+2. 新增对应 `*BatchSyncService`;
+3. 新增对应 mapper(转换为现有策略入参对象);
+4. 在 `BatchSyncController` 增加一个路由方法。
+
+统一约束:
+- 全部使用 `BatchSyncRequest<T>.records`。
+- 都采用“分流 + 复用策略层”的执行模型。
+
+---
+
+## 6. 校验与错误处理约定
+
+入口/服务层最小校验:
+1. `records` 非空;
+2. 必填字段非空(`classId/className/year/senester/checkinTime/startTime/endTime`);
+3. `senester` 仅允许 `1/2`;
+4. 时间格式统一(建议 `yyyy-MM-dd HH:mm:ss`,兼容日期字符串);
+5. `studentNum` 为空时默认 100。
+
+错误处理:
+- 记录级映射失败:记日志并计数;
+- 策略执行异常:抛 `ServiceException`;
+- 返回层先维持 `R<Void>`,后续可扩展批次结果对象。
+
+---
+
+## 7. 实施步骤(合并更新版)
+
+1. 新增统一请求壳 `BatchSyncRequest<T>`。
+2. 新增培训班级 DTO:`TrainClassRecordRequest`。
+3. 新增 `BatchSyncController` 与 `POST /batch-sync/train/class`。
+4. 新增 `TrainBatchSyncService`。
+5. 新增 `TrainClassRecordMapper` 并完成字段映射。
+6. 在 service 内实现 `delFlag` 分流并复用策略层调用。
+7. 增加统计日志与异常日志。
+8. 联调验证新增/更新/删除与混合批次。
+9. 回归验证旧接口(`SyncHand`)完全不受影响。
+
+---
+
+## 8. 验证计划
+
+## 8.1 功能验证
+1. `delFlag=0`:验证 `sys_dept` 新增/更新 + 团客新增/更新。
+2. `delFlag=1`:验证 `sys_dept` 删除 + 团客删除。
+3. 混合批次:验证分流正确。
+
+## 8.2 异常验证
+1. 必填字段缺失。
+2. `senester` 值非法。
+3. 时间格式异常。
+4. `studentNum` 为空默认值生效。
+5. `tenantId` 缺失时默认租户生效。
+
+## 8.3 回归验证
+1. `GET /hand/sync/train/class` 行为不变。
+2. 原策略层行为不变(年份/学期/班级层级、团客同步逻辑不变)。
+
+---
+
+## 9. 风险与注意事项
+
+1. 策略层当前是“记录级失败日志,不一定导致批次失败”,会存在“接口成功但部分失败”。
+2. `headTeacherName` 暂无落库目标字段,本期仅接收保留。
+3. 外部字段 `senester` 拼写建议在 DTO 层做兼容映射,内部命名可标准化。
+4. 分流执行顺序建议固定(先删后增改或先增改后删),并在文档中明确,避免同 `classId` 混合批次歧义。
+
+---
+
+## 10. 交付物
+
+1. 新增独立批量同步入口(controller)。
+2. 新增培训班级批量同步 service 与 mapper。
+3. 新增统一 `records` 请求壳与班级 DTO。
+4. 在不改 `SyncHand`、不改策略层前提下实现等效业务闭环。
+5. 验证记录与回归结果。

+ 576 - 0
doc/JW系统Kafka数据同步接口文档.md

@@ -0,0 +1,576 @@
+# JW(教务)系统Kafka数据同步接口文档
+
+## 一、概述
+
+本文档整理了从Kafka获取的与JW(教务)系统相关的数据同步逻辑,以及相应的接口响应JSON格式。
+
+## 二、Kafka消息架构
+
+### 2.1 消息主题(Topic)
+
+| Topic名称 | 常量定义 | 说明 |
+|----------|---------|------|
+| eventBus | `OLD_SYNC_TOPIC` | 第三方对接主题,用于与旧系统(包括教务系统)数据同步 |
+| sync_data_topic | `SYNC_DATA_TOPIC` | 一卡通内部操作双向同步主题 |
+| sync_to_cloud | `TO_CLOUD_TOPIC` | 云平台和本地部署同步主题 |
+
+### 2.2 消息发送者(Sender)
+
+| 发送者代码 | 常量名称 | 说明 |
+|----------|---------|------|
+| 002 | `TRAIN` | 教务系统 |
+| 003 | `HR` | 人事系统 |
+| 005 | `YKT` | 一卡通系统 |
+
+### 2.3 事件类型(EventType) - 教务系统相关
+
+| 事件类型代码 | 常量名称 | 说明 |
+|------------|---------|------|
+| 00200001 | `TRAIN_CLASS_ADD` | 班级增加 |
+| 00200002 | `TRAIN_CLASS_EDIT` | 班级修改 |
+| 00200003 | `TRAIN_CLASS_DEL` | 班级删除 |
+| 00200004 | `TRAINEE_ADD` | 学员增加 |
+| 00200005 | `TRAINEE_EDIT` | 学员修改 |
+| 00200006 | `TRAINEE_DEL` | 学员删除 |
+| 00500001 | `CONSUME_RECORD` | 消费记录(推送给教务) |
+
+## 三、Kafka消息格式
+
+### 3.1 标准Kafka消息结构
+
+```json
+{
+  "header": {
+    "eventId": "事件唯一标识",
+    "sender": "发送者代码(002-教务, 003-人事, 005-一卡通)",
+    "eventType": "事件类型代码",
+    "tenantId": "租户ID"
+  },
+  "body": {
+    // 具体业务数据
+  }
+}
+```
+
+## 四、从Kafka接收的教务系统数据接口
+
+### 4.1 教务系统 → 云端数据同步
+
+**处理类**: `TrainEventStrategyImpl`  
+**Kafka Consumer**: `KafkaCloudConsumer.kafkaEventBusHandler()`  
+**Topic**: `eventBus`  
+**Sender**: `002`
+
+#### 4.1.1 班级同步接口 (增加/修改/删除)
+
+**事件类型**: 
+- `TRAIN_CLASS_ADD` (00200001) - 班级增加
+- `TRAIN_CLASS_EDIT` (00200002) - 班级修改
+- `TRAIN_CLASS_DEL` (00200003) - 班级删除
+
+**调用服务**: 
+- 增加/修改: `remoteKafkaSyncService.syncTrainClass()`
+- 删除: `remoteKafkaSyncService.syncDelTrainClass()`
+
+**数据处理**: 
+- 增加/修改: `TrainUtils.getSycClass()`
+- 删除: `TrainUtils.getSycDeleteClass()`
+
+**说明**: 通过 `del_flag` 字段区分操作类型
+- `del_flag = 0` 或无此字段: 增加或修改(根据班级ID是否存在判断)
+- `del_flag = 1`: 删除
+
+**Kafka消息Body格式 (增加/修改)**:
+```json
+{
+  "id": "班级ID",
+  "name": "班级名称",
+  "year": 2025,
+  "xq": "学期(0-上学期, 1-下学期)",
+  "bmStarttime": "报名开始时间",
+  "bdTime": "报到时间",
+  "kbTime": "开班时间(开课时间)",
+  "byTime": "毕业时间(结业时间)",
+  "studentNum": 100,
+  "noPayAllow": "是否允许未缴费报到(0-不允许, 1-允许)",
+  "tenantId": "租户ID"
+}
+```
+
+**字段说明 (增加/修改)**:
+
+| 字段名 | 类型 | 必填 | 说明 | 示例 | 映射到内部字段 |
+|-------|------|------|------|------|---------------|
+| id | String | 是 | 班级ID | "CLASS001" | dept_id |
+| name | String | 是 | 班级名称 | "2025春季计算机培训班" | dept_name |
+| year | Integer | 是 | 年份 | 2025 | year |
+| xq | String | 是 | 学期(0-上学期, 1-下学期) | "0" | semester(转换为"上学期"或"下学期") |
+| bmStarttime | String | 是 | 报名开始时间/缴费开始时间 | "2025-01-15 08:00:00" | payBegin |
+| bdTime | String | 是 | 报到时间 | "2025-02-20 09:00:00" | checkDate |
+| kbTime | String | 是 | 开班时间(开课时间) | "2025-02-21 08:00:00" | beginDate |
+| byTime | String | 是 | 毕业时间(结业时间) | "2025-07-20 17:00:00" | endDate, payEnd |
+| studentNum | Integer | 否 | 计划人数(默认100) | 100 | planCount |
+| noPayAllow | String | 否 | 是否允许未缴费报到(0-不允许, 1-允许) | "0" | payCheck |
+| tenantId | String | 否 | 租户ID(无则使用默认) | "000000" | tenantId |
+
+**固定值 (增加/修改)**:
+- `chooseRoom`: "0" (是否自主选房)
+- `canEat`: "1" (是否就餐)
+- `operatorId`: KAFKA_SYNC_ADMIN常量值
+
+**Kafka消息Body格式 (删除)**:
+```json
+{
+  "id": "班级ID",
+  "del_flag": "1",
+  "tenantId": "租户ID"
+}
+```
+
+**字段说明 (删除)**:
+
+| 字段名 | 类型 | 必填 | 说明 | 示例 | 映射到内部字段 |
+|-------|------|------|------|------|---------------|
+| id | String | 是 | 班级ID | "CLASS001" | dept_id |
+| del_flag | String | 是 | 删除标志: "1"表示删除 | "1" | delFlag |
+| tenantId | String | 否 | 租户ID(无则使用默认) | "000000" | tenantId |
+
+**业务逻辑**:
+1. **增加/修改**: 根据班级ID查询,存在则更新,不存在则新增
+2. **删除**: 根据班级ID标记删除
+3. **班级结构**: 校本部 → 年份(2025年) → 学期(上学期/下学期) → 班级名称
+
+#### 4.1.2 学员同步接口 (增加/修改/删除)
+
+**事件类型**: 
+- `TRAINEE_ADD` (00200004) - 学员增加
+- `TRAINEE_EDIT` (00200005) - 学员修改
+- `TRAINEE_DEL` (00200006) - 学员删除
+
+**调用服务**: 
+- 增加/修改: `remoteKafkaSyncService.syncTrainee()`
+- 删除: `remoteKafkaSyncService.syncDelTrainee()`
+
+**数据处理**: 
+- 增加/修改: `TrainUtils.getSyncTrainee()`
+- 删除: `TrainUtils.getSyncDeleteTrainee()`
+
+**说明**: 通过事件类型区分操作,删除时数据结构不同
+
+**Kafka消息Body格式 (增加/修改)**:
+```json
+{
+  "student": {
+    "id": "学员ID",
+    "name": "学员姓名",
+    "sex": "性别(1-男, 2-女)",
+    "phone": "联系电话",
+    "idCard": "身份证号",
+    "currentClassId": "当前班级ID"
+  },
+  "trainClassStudent": {
+    "studentId": "学员ID",
+    "classId": "班级ID",
+    "status": "学员状态(1-已报名, 2-已报到)"
+  },
+  "tenantId": "租户ID"
+}
+```
+
+**字段说明 (增加/修改)**:
+
+**student对象**:
+
+| 字段名 | 类型 | 必填 | 说明 | 示例 | 映射到内部字段 |
+|-------|------|------|------|------|---------------|
+| id | String | 是 | 学员ID | "STU001" | userId |
+| name | String | 是 | 学员姓名 | "张三" | realName |
+| sex | String | 是 | 性别(1-男, 2-女) | "1" | sex |
+| phone | String | 是 | 联系电话 | "13800138000" | phone |
+| idCard | String | 否 | 身份证号 | "110101199001011234" | idNumber |
+| currentClassId | String | 是 | 当前班级ID | "CLASS001" | deptId |
+
+**trainClassStudent对象**:
+
+| 字段名 | 类型 | 必填 | 说明 | 示例 | 映射到内部字段 |
+|-------|------|------|------|------|---------------|
+| studentId | String | 是 | 学员ID | "STU001" | userId |
+| classId | String | 是 | 班级ID | "CLASS001" | deptId |
+| status | String | 是 | 学员状态 | "1" | delFlag(1或2为"0"表示有效, 其他为"2"表示删除) |
+
+**固定值 (增加/修改)**:
+- `category`: CATEGORY_TRAINEE常量值(学员类别)
+- `postCode`: TRAINEE_CODE常量值(学员岗位编码)
+- `operatorId`: KAFKA_SYNC_ADMIN常量值
+
+**Kafka消息Body格式 (删除)**:
+```json
+{
+  "id": "学员ID",
+  "classId": "班级ID",
+  "del_flag": "1",
+  "tenantId": "租户ID"
+}
+```
+
+**字段说明 (删除)**:
+
+| 字段名 | 类型 | 必填 | 说明 | 示例 | 映射到内部字段 |
+|-------|------|------|------|------|---------------|
+| id | String | 是 | 学员ID | "STU001" | userId |
+| classId | String | 是 | 班级ID | "CLASS001" | deptId |
+| del_flag | String | 是 | 删除标志: "1"表示删除 | "1" | - |
+| tenantId | String | 否 | 租户ID(无则使用默认) | "000000" | tenantId |
+
+**业务逻辑**:
+1. **增加/修改**: 根据学员ID和班级ID建立关联关系
+2. **删除**: 根据学员ID和班级ID删除关联关系
+3. **状态处理**: status为"1"(已报名)或"2"(已报到)时,delFlag="0"(有效);其他状态delFlag="2"(删除)
+4. **租户校验**: 无tenantId的学员数据将被忽略
+
+## 五、向教务系统推送的数据接口
+
+### 5.1 消费记录推送 → 教务系统
+
+**处理类**: `BaseBusiness.sendConsumeToKafka()`  
+**Kafka Producer**: `kafkaNormalProducer.sendKafkaMessage()`  
+**Topic**: `eventBus`  
+**Sender**: `005`(一卡通系统)  
+**EventType**: `CONSUME_RECORD` (00500001)
+
+#### 5.1.1 接口说明
+
+当消费记录上传完成后,系统会将就餐打卡信息推送到Kafka,教务系统消费此消息实现就餐打卡功能。
+
+**API接口**:
+```
+POST /v1/Consumes/Consume/kafka/{date}
+POST /v1/Consumes/Consume/kafka/{beginDate}/{endDate}
+```
+
+#### 5.1.2 推送数据格式 (YcPushConsumeInfoVo)
+
+**Kafka消息Body格式**:
+```json
+{
+  "recordId": "消费记录ID",
+  "userId": "人员ID",
+  "userNumb": "学号",
+  "xm": "姓名",
+  "deptId": "部门ID",
+  "deptName": "部门名称",
+  "roomId": "消费地点ID",
+  "roomName": "地点名称",
+  "cardNo": "卡流水号",
+  "factoryFixId": "物理卡号",
+  "consumeValue": "消费金额",
+  "cardValue": "卡余额",
+  "consumeDate": "消费时间",
+  "mealTypeId": "餐类ID",
+  "mealName": "餐类名称",
+  "termNo": "机号",
+  "termName": "机器名称",
+  "category": "身份类别(0-系统内置 1-教师 2-学生 3-家长)",
+  "otherSysId": "其他业务系统人员ID(教务或人事的人员ID)",
+  "classId": "班级ID",
+  "termRecordID": "机器流水号",
+  "posRecordState": "消费记录标识",
+  "tenantId": "租户ID"
+}
+```
+
+#### 5.1.3 字段说明
+
+| 字段名 | 类型 | 说明 | 示例 |
+|-------|------|------|------|
+| recordId | String | 消费记录ID | "123456" |
+| userId | String | 人员ID | "789" |
+| userNumb | String | 学号 | "2024001" |
+| xm | String | 姓名 | "张三" |
+| deptId | String | 部门ID | "100" |
+| deptName | String | 部门名称 | "计算机学院" |
+| roomId | String | 消费地点ID | "" |
+| roomName | String | 地点名称 | "" |
+| cardNo | String | 卡流水号 | "35193" |
+| factoryFixId | String | 物理卡号 | "3656457030" |
+| consumeValue | String | 消费金额 | "15.50" |
+| cardValue | String | 卡余额 | "184.50" |
+| consumeDate | String | 消费时间 | "2025-02-19 19:32:30" |
+| mealTypeId | String | 餐类ID | "1" |
+| mealName | String | 餐类名称 | "晚餐" |
+| termNo | String | 机号 | "100" |
+| termName | String | 机器名称 | "食堂1号窗口" |
+| category | String | 身份类别 | "2" (学生) |
+| otherSysId | String | 教务系统人员ID | "JW2024001" |
+| classId | String | 班级ID | "CLASS001" |
+| termRecordID | Long | 机器流水号 | 47309 |
+| posRecordState | Integer | 消费记录标识 | 364 |
+
+#### 5.1.4 餐类名称映射
+
+```java
+mealNameMap:
+  "1" -> "早餐"
+  "2" -> "午餐"
+  "3" -> "晚餐"
+  其他 -> "未知餐类"
+```
+
+## 六、Kafka消费者处理逻辑
+
+### 6.1 云端消费者 (KafkaCloudConsumer)
+
+**条件**: `locationFlag = 'cloud'`
+
+#### 6.1.1 eventBus主题监听
+
+```java
+@KafkaListener(topics = KafkaTopicConstants.OLD_SYNC_TOPIC, groupId = "old-to-cloud-group")
+public void kafkaEventBusHandler(ConsumerRecord<String, String> record)
+```
+
+**处理逻辑**:
+1. 检查offset避免重复消费
+2. 解析Kafka消息
+3. 判断sender,如果不是005或006则处理业务
+4. 根据sender获取对应的事件策略处理器
+5. 执行事件处理
+6. 记录消息消费状态
+
+#### 6.1.2 sync_to_cloud主题监听
+
+```java
+@KafkaListener(topics = KafkaTopicConstants.TO_CLOUD_TOPIC, groupId = "local-to-cloud-group")
+public void kafkaToCloudHandler(ConsumerRecord<String, String> record)
+```
+
+### 6.2 本地消费者 (KafkaLocalConsumer)
+
+**条件**: `locationFlag = 'local'`
+
+```java
+@KafkaListener(topics = SYNC_DATA_TOPIC, groupId = "YTK_${spring.system.tenantId}")
+public void cloudOperationSync(ConsumerRecord<String, String> record)
+```
+
+**处理逻辑**:
+1. 检查offset避免重复消费
+2. 解析Kafka消息并验证tenantId
+3. 根据sender获取事件策略处理器
+4. 执行事件处理
+5. 记录消息发送记录
+
+## 七、事件策略处理器
+
+### 7.1 教务系统事件处理 (TrainEventStrategyImpl)
+
+**Service名称**: `002`  
+**实现接口**: `IYktEventStrategy`
+
+```java
+@Service(EventSenderConstants.TRAIN)  // "002"
+public class TrainEventStrategyImpl implements IYktEventStrategy
+```
+
+**处理的事件类型**:
+- TRAIN_CLASS_ADD (00200001) → `syncTrainClass()` - 班级增加/修改
+- TRAIN_CLASS_EDIT (00200002) → `syncTrainClass()` - 班级增加/修改
+- TRAIN_CLASS_DEL (00200003) → `syncDelTrainClass()` - 班级删除
+- TRAINEE_ADD (00200004) → `syncTrainee()` - 学员增加/修改
+- TRAINEE_EDIT (00200005) → `syncTrainee()` - 学员增加/修改
+- TRAINEE_DEL (00200006) → `syncDelTrainee()` - 学员删除
+
+**说明**: 
+- 班级增加和修改使用相同接口,通过班级ID判断是新增还是更新
+- 学员增加和修改使用相同接口,通过学员ID判断是新增还是更新
+- 删除操作使用单独的接口
+
+### 7.2 人事系统事件处理 (TeacherEventStrategyImpl)
+
+**Service名称**: `003`  
+**实现接口**: `IYktEventStrategy`
+
+```java
+@Service(EventSenderConstants.HR)  // "003"
+public class TeacherEventStrategyImpl implements IYktEventStrategy
+```
+
+**处理的事件类型**:
+- DEPT_ADD (00300001) → `syncTeacherDept()`
+- DEPT_EDIT (00300002) → `syncTeacherDept()`
+- DEPT_DEL (00300003) → `syncDelTeacherDept()`
+- TEACHER_ADD (00300004) → `syncTeacher()`
+- TEACHER_EDIT (00300005) → `syncTeacher()`
+- TEACHER_DEL (00300006) → `syncDelTeacher()`
+
+### 7.3 消费系统事件处理 (ConsumeEventStrategyImpl)
+
+**Service名称**: `120`  
+**实现接口**: `IYktEventStrategy`
+
+```java
+@Service(EventSenderConstants.CONSUME)  // "120"
+public class ConsumeEventStrategyImpl implements IYktEventStrategy
+```
+
+**处理的事件类型**:
+- CONSUME (12000001) → `remoteConsumeService.dealKafkaConsumeData()`
+
+## 八、业务流程
+
+### 8.1 教务系统同步数据到云端
+
+```
+教务系统 → Kafka(eventBus) → KafkaCloudConsumer 
+  → TrainEventStrategyImpl → RemoteKafkaSyncService 
+  → 同步到云端数据库
+```
+
+### 8.2 消费记录推送到教务系统
+
+```
+消费机上传消费记录 → ConsumeController.uploadRecord()
+  → ConsumeBusiness.postOrderAsync()
+  → BaseBusiness.completeUploadRecord()
+  → BaseBusiness.sendConsumeToKafka()
+  → Kafka(eventBus) → 教务系统消费
+```
+
+### 8.3 手动推送消费记录到教务系统
+
+```
+API调用: POST /v1/Consumes/Consume/kafka/{date}
+  → ConsumeController.consumeKafka()
+  → BaseBusiness.sendToJwKafkaTest()
+  → 查询未发送的消费记录
+  → BaseBusiness.sendConsumeToKafka()
+  → Kafka(eventBus) → 教务系统消费
+```
+
+## 九、数据转换示例
+
+### 9.1 消费记录转换为教务系统格式
+
+**输入数据 (ConsumptionBo)**:
+```json
+{
+  "recordId": 123456,
+  "userId": 789,
+  "userNumb": "2024001",
+  "realName": "张三",
+  "deptName": "计算机学院",
+  "cardNo": 35193,
+  "factoryId": 3656457030,
+  "consumeMoney": 15.50,
+  "balance": 184.50,
+  "consumeDate": "2025-02-19 19:32:30",
+  "mealType": 3,
+  "termNo": 100,
+  "termName": "食堂1号窗口",
+  "termRecordId": 47309,
+  "recordStatus": 364
+}
+```
+
+**输出数据 (YcPushConsumeInfoVo)**:
+```json
+{
+  "recordId": "123456",
+  "userId": "789",
+  "userNumb": "2024001",
+  "xm": "张三",
+  "deptId": "100",
+  "deptName": "计算机学院",
+  "roomId": "",
+  "roomName": "",
+  "cardNo": "35193",
+  "factoryFixId": "3656457030",
+  "consumeValue": "15.50",
+  "cardValue": "184.50",
+  "consumeDate": "2025-02-19 19:32:30",
+  "mealTypeId": "3",
+  "mealName": "晚餐",
+  "termNo": "100",
+  "termName": "食堂1号窗口",
+  "category": "2",
+  "otherSysId": "JW2024001",
+  "classId": "CLASS001",
+  "termRecordID": 47309,
+  "posRecordState": 364
+}
+```
+
+## 十、相关配置
+
+### 10.1 Kafka Topic配置
+
+```yaml
+# 应用配置
+locationFlag: cloud  # 或 local
+
+# 租户配置
+spring:
+  system:
+    tenantId: "000000"
+```
+
+### 10.2 常量定义位置
+
+| 常量类 | 路径 |
+|-------|------|
+| KafkaTopicConstants | `ruoyi-common-message/kafka/constant/KafkaTopicConstants.java` |
+| EventSenderConstants | `ruoyi-common-message/kafka/constant/EventSenderConstants.java` |
+| EventTypeConstants | `ruoyi-common-message/kafka/constant/EventTypeConstants.java` |
+
+## 十一、错误处理
+
+### 11.1 消息消费记录
+
+系统会记录每条Kafka消息的消费状态:
+- **Y**: 消费成功
+- **N**: 消费失败
+
+记录表: `RemoteSendMessageRecordBo`
+
+### 11.2 Offset管理
+
+使用`IXfOffsetService`管理消费offset,防止重复消费:
+- 记录每条消息的offset、topic、groupId、partition
+- 消费前判断是否已处理过
+
+## 十二、注意事项
+
+1. **消息去重**: 通过offset管理机制确保消息不被重复消费
+2. **异步处理**: 消费记录推送采用异步任务提交,不阻塞主流程
+3. **租户隔离**: 消息中包含tenantId,确保多租户数据隔离
+4. **异常处理**: 消息处理失败时会记录失败状态,便于后续排查
+5. **数据格式**: 推送到教务系统的数据全部转换为String类型,确保兼容性
+
+## 十三、测试接口
+
+### 13.1 推送指定日期消费记录
+
+```bash
+POST /v1/Consumes/Consume/kafka/2025-02-19
+```
+
+### 13.2 推送日期范围消费记录
+
+```bash
+POST /v1/Consumes/Consume/kafka/2025-02-19/2025-02-20
+```
+
+**响应**:
+```json
+{
+  "code": 200,
+  "msg": "操作成功",
+  "data": null
+}
+```
+
+---
+
+**文档版本**: v1.0  
+**生成日期**: 2025-04-09  
+**维护人员**: 系统开发团队

+ 316 - 0
doc/ecs-attenRule-requirements.md

@@ -0,0 +1,316 @@
+# 功能需求规格文档:ECS-考勤规则
+
+> 本文档用于描述考勤规则(attenRule)业务功能需求,作为代码生成的输入依据。
+
+***
+
+## 1. 基础信息
+
+| 属性    | 值                          |
+| ----- | -------------------------- |
+| 功能中文名 | 考勤规则                       |
+| 功能简写  | attenRule                  |
+| 所属模块  | ruoyi-modules/ruoyi-ecs    |
+| 主表名   | t\_ecs\_atten\_rule        |
+| 从表名   | 无                          |
+| 是否多租户 | 是                          |
+| 数据库类型 | kingbase(默认)               |
+| 描述    | 考勤规则管理,用于配置提前打卡、迟到、旷课等时间阈值 |
+
+***
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口   | HTTP   | 路径(后端)                    | 前端路径                          | 权限字符                 |
+| ---- | ------ | ------------------------- | ----------------------------- | -------------------- |
+| 列表   | GET    | /attenRule/list           | /ecs/attenRule/list           | ecs:attenRule:list   |
+| 详情   | GET    | /attenRule/{id}           | /ecs/attenRule/{id}           | ecs:attenRule:query  |
+| 新增   | POST   | /attenRule/               | /ecs/attenRule/               | ecs:attenRule:add    |
+| 编辑   | PUT    | /attenRule/               | /ecs/attenRule/               | ecs:attenRule:edit   |
+| 删除   | DELETE | /attenRule/{ids}          | /ecs/attenRule/{ids}          | ecs:attenRule:remove |
+| 导出模板 | POST   | /attenRule/exportTemplate | /ecs/attenRule/exportTemplate | ecs:attenRule:export |
+| 导入数据 | POST   | /attenRule/importData     | /ecs/attenRule/importData     | ecs:attenRule:import |
+| 导出数据 | POST   | /attenRule/export         | /ecs/attenRule/export         | ecs:attenRule:export |
+
+***
+
+## 3. 字段属性速查表
+
+| 属性            | 含义      | 可选值                                          | 说明               |
+| ------------- | ------- | -------------------------------------------- | ---------------- |
+| `fieldName`   | 字段名(驼峰) | —                                            | 代码变量名            |
+| `columnName`  | 列名(下划线) | —                                            | 数据库列名            |
+| `fieldType`   | Java类型  | String/Long/Integer/BigDecimal/LocalDateTime | <br />           |
+| `inDb`        | 是否在表中存在 | true/false                                   | false=纯前端计算字段    |
+| `inTable`     | 列表是否显示  | true/false                                   | 生成表格列            |
+| `inQuery`     | 是否查询字段  | true/false                                   | 生成搜索条件           |
+| `queryType`   | 查询方式    | eq/like/between                              | 精确/模糊/范围         |
+| `inForm`      | 是否表单字段  | true/false                                   | 生成表单项            |
+| `inAdd`       | 新增表单显示  | true/false                                   | <br />           |
+| `inEdit`      | 编辑表单显示  | true/false                                   | <br />           |
+| `required`    | 是否必填    | true/false                                   | 生成校验注解           |
+| `dictType`    | 字典类型    | 字典标识                                         | 有值=字典下拉,否则普通输入   |
+| `component`   | 前端组件类型  | input/select/inputNumber/datetime/textarea   | 默认根据类型推断         |
+| `width`       | 表格列宽    | 数字(px)                                       | 默认auto           |
+| `sort`        | 排序      | 数字                                           | 越小越靠前            |
+| `excelExport` | Excel导出 | true/false                                   | 默认true           |
+| `lockRule`    | 操作锁定规则  | 对象                                           | 有值=按此字段值控制行级操作权限 |
+
+***
+
+## 4. 字段清单
+
+### 4.1 主表 `t_ecs_atten_rule`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: attendRuleId
+    columnName: attend_rule_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    excelExport: false
+    remark: 考勤规则Id,雪花ID
+
+  # ---------- 业务字段 ----------
+  - fieldName: ruleName
+    columnName: rule_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: true
+    component: input
+    width: 150
+    sort: 1
+    excelExport: true
+    remark: 规则名称
+
+  - fieldName: ruleNumb
+    columnName: rule_numb
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: true
+    component: input
+    width: 150
+    sort: 2
+    excelExport: true
+    remark: 规则编码
+
+  - fieldName: advanceStart
+    columnName: advance_start
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    queryType: eq
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: inputNumber
+    width: 150
+    sort: 3
+    excelExport: true
+    remark: 提前打卡分钟数
+
+  - fieldName: lateAfter
+    columnName: late_after
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    queryType: eq
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: inputNumber
+    width: 150
+    sort: 4
+    excelExport: true
+    remark: 迟到延后分钟数
+
+  - fieldName: absentAfter
+    columnName: absent_after
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    queryType: eq
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: inputNumber
+    width: 150
+    sort: 5
+    excelExport: true
+    remark: 旷课延后分钟数
+
+  - fieldName: status
+    columnName: status
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: eq
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: true
+    dictType: sys_normal_disable
+    component: select
+    width: 100
+    sort: 6
+    excelExport: true
+    remark: 状态:0-正常,1-停用
+
+  # ---------- 公共字段(所有表统一包含) ----------
+  - fieldName: delFlag
+    columnName: del_flag
+    fieldType: String
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+    excelExport: true
+```
+
+***
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+EcsAttendRuleVo:
+  - attendRuleId
+  - ruleName
+  - ruleNumb
+  - advanceStart
+  - lateAfter
+  - absentAfter
+  - status
+  - createTime
+```
+
+***
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: true
+  excelExport: true
+
+  importRules:
+    - fieldName: ruleName
+      required: true
+      unique: false
+    - fieldName: ruleNumb
+      required: true
+      unique: true
+      max: 32
+    - fieldName: advanceStart
+      required: false
+      min: 0
+      max: 999
+    - fieldName: lateAfter
+      required: false
+      min: 0
+      max: 999
+    - fieldName: absentAfter
+      required: false
+      min: 0
+      max: 999
+    - fieldName: status
+      required: true
+
+  subTableImport: false
+  subTableExport: false
+
+  dubboExpose: false
+```
+
+***
+
+## 7. 字典项
+
+| 字典类型                 | 枚举值        | 说明     |
+| -------------------- | ---------- | ------ |
+| `sys_normal_disable` | 0=正常, 1=停用 | 通用状态字典 |
+
+***
+
+## 8. 建表语句
+
+> 数据库类型:kingbase(人大金仓)
+
+### 8.1 主表 `t_ecs_attend_rule`
+
+```sql
+-- 考勤规则表
+CREATE TABLE "dbo"."t_ecs_attend_rule" (
+  "attend_rule_id" bigint NOT NULL,
+  "rule_name" character varying(255 char) NOT NULL DEFAULT ''::varchar,
+  "rule_numb" character varying(32 char) NOT NULL DEFAULT ''::varchar,
+  "advance_start" integer,
+  "late_after" integer,
+  "absent_after" integer,
+  "status" character varying(16 char) NOT NULL DEFAULT '0'::varchar,
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "tenant_id" character varying(20 char) NOT NULL DEFAULT ''::varchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" timestamp,
+  "update_by" bigint,
+  "update_time" timestamp,
+  CONSTRAINT "t_ecs_attend_rule_pkey" PRIMARY KEY ("attend_rule_id")
+);
+
+-- 表注释
+COMMENT ON TABLE "dbo"."t_ecs_attend_rule" IS '考勤规则表';
+
+-- 列注释
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."attend_rule_id" IS '考勤规则Id';
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."rule_name" IS '规则名称';
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."rule_numb" IS '规则编码';
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."advance_start" IS '提前打卡分钟数';
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."late_after" IS '迟到延后分钟数';
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."absent_after" IS '旷课延后分钟数';
+COMMENT ON COLUMN "dbo"."t_ecs_attend_rule"."status" IS '状态:0-正常,1-停用';
+```
+
+***
+
+## 9. 备注
+
+1. 考勤规则是单表结构,无需关联查询
+2. 提前打卡/迟到延后/旷课延后三个时间字段单位为分钟,取值范围建议 0-999
+3. 规则编码(ruleNumb)建议唯一,用于系统内部标识
+4. ECS 模块默认启用租户隔离(tenant\_id 必填)

+ 493 - 0
doc/ecs-attend-requirements.md

@@ -0,0 +1,493 @@
+# 功能需求规格文档 — ECS 考勤管理
+
+> 本文档描述 ECS 电子班牌模块「考勤管理」功能的完整需求规格。
+> 技能将根据此文档生成后端 + 前端代码。
+
+---
+
+## 1. 基础信息
+
+| 属性 | 值 |
+|------|-----|
+| 功能中文名 | 考勤管理 |
+| 功能简写 | attend |
+| 所属模块 | ruoyi-modules/ruoyi-ecs |
+| 主表名 | t_ecs_attend |
+| 从表名 | 无 |
+| 是否多租户 | 是 |
+| 数据库类型 | kingbase(人大金仓) |
+| 描述 | 管理学员考勤记录,数据由考勤设备上传,支持查看、查询、导出;同时支持手工考勤补签(选择教室→班级→班级下学员) |
+
+---
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| 列表 | GET | /attend/list | /ecs/attend/list | ecs:attend:list |
+| 详情 | GET | /attend/{id} | /ecs/attend/{id} | ecs:attend:query |
+| 新增(手工考勤) | POST | /attend/ | /ecs/attend/ | ecs:attend:add |
+| 导出数据 | POST | /attend/export | /ecs/attend/export | ecs:attend:export |
+
+> **说明**:考勤记录主要来自设备上传,本模块提供查看、查询和导出功能。手工考勤为补签场景,需关联选择教室、班级、班级下学员。不支持编辑、删除、导入操作。
+
+### 2.2 补充接口
+
+无
+
+---
+
+## 3. 字段属性速查表
+
+| 属性 | 含义 | 可选值 | 说明 |
+|------|------|--------|------|
+| `fieldName` | 字段名(驼峰) | — | 代码变量名 |
+| `columnName` | 列名(下划线) | — | 数据库列名 |
+| `fieldType` | Java类型 | String/Long/Integer/BigDecimal/Date | |
+| `inDb` | 是否在表中存在 | true/false | false=纯前端计算字段 |
+| `inTable` | 列表是否显示 | true/false | 生成表格列 |
+| `inQuery` | 是否查询字段 | true/false | 生成搜索条件 |
+| `queryType` | 查询方式 | eq/like/between | 精确/模糊/范围 |
+| `inForm` | 是否表单字段 | true/false | 生成表单项 |
+| `inAdd` | 新增表单显示 | true/false | |
+| `inEdit` | 编辑表单显示 | true/false | |
+| `required` | 是否必填 | true/false | 生成校验注解 |
+| `dictType` | 字典类型 | 字典标识 | 有值=字典下拉,否则普通输入 |
+| `relation` | 关联选择配置 | 对象 | 有值=弹出选择框 |
+| `component` | 前端组件类型 | input/select/inputNumber/datetime/textarea | 默认根据类型推断 |
+| `width` | 表格列宽 | 数字(px) | 默认auto |
+| `sort` | 排序 | 数字 | 越小越靠前 |
+| `excelExport` | Excel导出 | true/false | 默认true |
+| `lockRule` | 操作锁定规则 | 对象 | 有值=按此字段值控制行级操作权限 |
+
+---
+
+## 4. 字段清单
+
+### 4.1 主表 `t_ecs_attend`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: attendId
+    columnName: attend_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 主键,详情时传递
+
+  # ---------- 学员关联 ----------
+  - fieldName: userId
+    columnName: user_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false
+    required: true
+    relation:
+      table: t_pt_user_account
+      idField: pt_user_id
+      nameField: real_name
+      title: 选择学员
+      path: /backstage/userAccount/selectUserAccount
+      cascade:
+        dependsOn: classId
+        description: 选择班级后,只能选择该班级下的学员
+    component: userSelect
+    remark: 学员Id(关联学员表,级联依赖班级)
+
+  - fieldName: userNumb
+    column_name: user_numb
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: false
+    component: input
+    width: 120
+    sort: 4
+    excelExport: true
+    remark: 学号(冗余存储)
+
+  - fieldName: realName
+    columnName: real_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: false
+    component: input
+    width: 120
+    sort: 3
+    excelExport: true
+    remark: 学员姓名(冗余存储)
+
+  # ---------- 班级关联 ----------
+  - fieldName: classId
+    columnName: class_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false
+    required: true
+    relation:
+      table: t_ecs_class
+      idField: class_id
+      nameField: class_name
+      title: 选择班级
+      path: /ecs/class/selectClass
+    component: classSelect
+    remark: 班级Id(关联班级表)
+
+  - fieldName: className
+    columnName: class_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: false
+    component: input
+    width: 150
+    sort: 1
+    excelExport: true
+    remark: 班级名称(冗余存储)
+
+  # ---------- 教室关联 ----------
+  - fieldName: roomId
+    columnName: room_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false
+    required: true
+    relation:
+      table: t_pt_room
+      idField: room_id
+      nameField: room_name
+      title: 选择教室
+      path: /backstage/room/selectRoom
+    component: roomSelect
+    remark: 教室Id(关联教室表)
+
+  - fieldName: roomName
+    columnName: room_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: input
+    width: 150
+    sort: 5
+    excelExport: true
+    remark: 教室名称(冗余存储)
+
+  # ---------- 考勤信息 ----------
+  - fieldName: checkTime
+    columnName: check_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false
+    required: true
+    component: datetime
+    width: 180
+    sort: 6
+    excelExport: true
+    remark: 考勤时间
+
+  - fieldName: checkType
+    columnName: check_type
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: false
+    required: true
+    dictType: check_type
+    component: select
+    excelExport: true
+    remark: 考勤方式:0-刷卡,1-人脸
+
+  - fieldName: uploadTime
+    columnName: upload_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 7
+    excelExport: true
+    remark: 上传时间
+
+  # ---------- 推送状态 ----------
+  - fieldName: pushStatus
+    columnName: push_status
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: eq
+    inForm: false
+    required: false
+    dictType: push_status
+    component: select
+    width: 100
+    sort: 8
+    excelExport: true
+    remark: 推送状态:0-未推送,1-推送成功,2-推送失败
+
+  - fieldName: pushTime
+    columnName: push_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 9
+    excelExport: true
+    remark: 推送时间
+
+  - fieldName: pushRetry
+    columnName: push_retry
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    component: inputNumber
+    width: 80
+    excelExport: false
+    remark: 推送尝试次数
+
+  # ---------- 公共字段(所有表统一包含,多租户表增加 tenant_id) ----------
+  # 通用字段定义(无需在字段清单中重复写,建表时自动追加):
+  # | 字段          | Java类型        | 说明               |
+  # | ------------- | --------------- | ------------------ |
+  # | del_flag      | Integer         | 逻辑删除:0-未删除,1-已删除 |
+  # | create_dept   | Long            | 创建部门             |
+  # | create_by     | Long            | 创建者              |
+  # | create_time   | Date            | 创建时间             |
+  # | update_by     | Long            | 最后修改者            |
+  # | update_time   | Date            | 最后修改时间           |
+  # | tenant_id     | Long            | 租户ID(多租户表包含)    |
+
+  - fieldName: delFlag
+    columnName: del_flag
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: Date
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    component: datetime
+```
+
+---
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+AttendVo:
+  - attendId          # 主键
+  - className         # 班级名称
+  - realName          # 学员姓名
+  - userNumb          # 学号
+  - roomName          # 教室名称
+  - checkTime         # 考勤时间
+  - uploadTime        # 上传时间
+  - pushStatus        # 推送状态
+  - pushTime          # 推送时间
+```
+
+### 5.2 主表明细 VO
+
+```yaml
+AttendDetailVo:
+  includes: AttendVo
+  additional:
+    - userId           # 学员Id
+    - userNumb         # 学号
+    - realName         # 学员姓名
+    - classId          # 班级Id
+    - className        # 班级名称
+    - roomId           # 教室Id
+    - roomName         # 教室名称
+    - checkTime        # 考勤时间
+    - checkType        # 考勤方式
+    - uploadTime       # 上传时间
+    - pushStatus       # 推送状态
+    - pushTime         # 推送时间
+    - pushRetry        # 推送尝试次数
+```
+
+---
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: false       # 不支持导入
+  excelExport: true        # 支持导出
+
+  # 手工考勤新增时的特殊规则
+  addRules:
+    - fieldName: roomId
+      required: true
+      message: 请选择教室
+    - fieldName: classId
+      required: true
+      message: 请选择班级
+    - fieldName: userId
+      required: true
+      message: 请选择学员
+      cascade: true
+      dependsOn: classId
+      description: 先选班级,再选该班级下的学员
+    - fieldName: checkTime
+      required: true
+      message: 请填写考勤时间
+    - fieldName: checkType
+      required: true
+      message: 请选择考勤方式
+
+  # 级联选择规则
+  cascadeRules:
+    - field: userId
+      dependsOn: classId
+      description: 选择班级后,学员列表过滤为该班级下的学员
+      apiPath: /backstage/userAccount/selectUserAccount
+      apiParams:
+        classId: ${classId}
+
+  dubboExpose: false       # 不暴露 Dubbo 服务
+```
+
+---
+
+## 7. 字典项
+
+| 字典类型 | 枚举值 | 说明 |
+|----------|--------|------|
+| `check_type` | 0=刷卡, 1=人脸 | 考勤方式 |
+| `push_status` | 0=未推送, 1=推送成功, 2=推送失败 | 推送状态 |
+
+---
+
+## 8. 建表语句
+
+> **通用字段**:所有表统一包含以下字段(建表语句自动追加,无需在字段清单中重复):
+>
+> | 字段          | 类型 | 说明               |
+> | ------------- | -------------------- | ------------------ |
+> | del_flag      | character(1 char) | 逻辑删除:0-未删除,1-已删除 |
+> | create_dept   | bigint | 创建部门             |
+> | create_by     | bigint | 创建者              |
+> | create_time   | timestamp | 创建时间             |
+> | update_by     | bigint | 更新者              |
+> | update_time   | timestamp | 更新时间           |
+> | tenant_id     | bigint | 租户ID(多租户表) |
+
+### 8.1 主表 `t_ecs_attend`
+
+```sql
+-- ============================================
+-- ECS 考勤记录表(kingbase/人大金仓)
+-- ============================================
+CREATE TABLE "dbo"."t_ecs_attend" (
+  "attend_id" bigint NOT NULL,
+  "user_id" bigint NOT NULL,
+  "user_numb" character varying(50 char) NOT NULL DEFAULT ''::varchar,
+  "real_name" character varying(100 char) NOT NULL DEFAULT ''::varchar,
+  "class_id" bigint NOT NULL,
+  "class_name" character varying(100 char) NOT NULL DEFAULT ''::varchar,
+  "room_id" bigint NOT NULL,
+  "room_name" character varying(100 char) NOT NULL DEFAULT ''::varchar,
+  "check_time" timestamp NOT NULL,
+  "check_type" integer NOT NULL DEFAULT 0,
+  "upload_time" timestamp,
+  "push_status" integer NOT NULL DEFAULT 0,
+  "push_time" timestamp,
+  "push_retry" integer NOT NULL DEFAULT 0,
+  -- 通用字段(自动追加)
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" timestamp,
+  "update_by" bigint,
+  "update_time" timestamp,
+  "tenant_id" bigint,
+  CONSTRAINT "t_ecs_attend_pkey" PRIMARY KEY ("attend_id")
+);
+
+-- 表注释
+ALTER TABLE "dbo"."t_ecs_attend" COMMENT 'ECS考勤记录表';
+
+-- 列注释
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "attend_id" COMMENT '主键';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "user_id" COMMENT '学员Id';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "user_numb" COMMENT '学号';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "real_name" COMMENT '学员姓名';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "class_id" COMMENT '班级Id';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "class_name" COMMENT '班级名称';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "room_id" COMMENT '教室Id';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "room_name" COMMENT '教室名称';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "check_time" COMMENT '考勤时间';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "check_type" COMMENT '考勤方式:0-刷卡,1-人脸';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "upload_time" COMMENT '上传时间';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "push_status" COMMENT '推送状态:0-未推送,1-推送成功,2-推送失败';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "push_time" COMMENT '推送时间';
+ALTER TABLE "dbo"."t_ecs_attend" MODIFY "push_retry" COMMENT '推送尝试次数';
+
+-- 索引
+CREATE INDEX "idx_ecs_attend_class_id" ON "dbo"."t_ecs_attend" ("class_id");
+CREATE INDEX "idx_ecs_attend_user_id" ON "dbo"."t_ecs_attend" ("user_id");
+CREATE INDEX "idx_ecs_attend_check_time" ON "dbo"."t_ecs_attend" ("check_time");
+CREATE INDEX "idx_ecs_attend_push_status" ON "dbo"."t_ecs_attend" ("push_status");
+```
+
+---
+
+## 9. 备注
+
+1. **数据冗余设计**:学员姓名(`realName`)、学号(`userNumb`)、班级名称(`className`)、教室名称(`roomName`)采用冗余存储策略,避免关联查询性能问题。当学员/班级/教室信息变更时需考虑是否同步更新历史记录。
+2. **只读模型**:考勤记录主要由设备自动上传,不支持编辑和删除操作,确保数据的不可篡改性。本模块提供查看、查询、导出功能。
+3. **推送机制**:`pushStatus` 记录推送状态,`pushRetry` 记录重试次数,支持后续定时任务扫描推送失败记录进行重试。
+4. **手工考勤**:新增操作即「手工考勤」,用于补签场景。表单填写顺序为:选择教室 → 选择班级 → 选择学员(级联:选完班级后只能选该班级下的学员) → 选择考勤方式 → 填写考勤时间。
+5. **关联实体说明**:
+   - 教室 → 关联 `t_pt_room` 表(通过 backstage 模块的 RemotePtRoomService)
+   - 班级 → 关联 `t_ecs_class` 表(ECS 模块内部)
+   - 学员 → 关联 `t_pt_user_account` 表(通过 backstage 模块的 RemoteUserAccountService),**级联依赖班级**,只显示所选班级下的学员
+6. **级联选择**:userId 级联依赖 classId,前端实现时班级选择变更后需清空学员已选值,并按新班级过滤学员列表。

+ 536 - 0
doc/ecs-course-requirements.md

@@ -0,0 +1,536 @@
+# 功能需求规格文档:课程管理
+
+> 本模板用于描述业务功能需求,技能将根据此文档生成后端 + 前端代码。
+> 请按此格式填写,结构化描述越完整,代码生成质量越高。
+
+---
+
+## 1. 基础信息
+
+| 属性 | 值 |
+|------|-----|
+| 功能中文名 | 课程管理 |
+| 功能简写 | course |
+| 所属模块 | ruoyi-modules/ruoyi-ecs |
+| 主表名 | t_ecs_course |
+| 从表名 | t_ecs_course_teacher |
+| 是否多租户 | 是 |
+| 数据库类型 | kingbase |
+| 描述 | 管理课程信息,支持多教师关联 |
+
+---
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| 列表 | GET | /course/list | /ecs/course/list | ecs:course:list |
+| 详情 | GET | /course/{id} | /ecs/course/{id} | ecs:course:query |
+| 新增 | POST | /course/ | /ecs/course/ | ecs:course:add |
+| 编辑 | PUT | /course/ | /ecs/course/ | ecs:course:edit |
+| 删除 | DELETE | /course/{ids} | /ecs/course/{ids} | ecs:course:remove |
+| 导出模板 | POST | /course/exportTemplate | /ecs/course/exportTemplate | ecs:course:export |
+| 导入数据 | POST | /course/importData | /ecs/course/importData | ecs:course:import |
+| 导出数据 | POST | /course/export | /ecs/course/export | ecs:course:export |
+
+### 2.2 补充接口(如有)
+
+> 无
+
+---
+
+## 3. 字段属性速查表
+
+| 属性 | 含义 | 可选值 | 说明 |
+|------|------|--------|------|
+| `fieldName` | 字段名(驼峰) | — | 代码变量名 |
+| `columnName` | 列名(下划线) | — | 数据库列名 |
+| `fieldType` | Java类型 | String/Long/Integer/BigDecimal/Date | |
+| `inDb` | 是否在表中存在 | true/false | false=纯前端计算字段 |
+| `inTable` | 列表是否显示 | true/false | 生成表格列 |
+| `inQuery` | 是否查询字段 | true/false | 生成搜索条件 |
+| `queryType` | 查询方式 | eq/like/between | 精确/模糊/范围 |
+| `inForm` | 是否表单字段 | true/false | 生成表单项 |
+| `inAdd` | 新增表单显示 | true/false | |
+| `inEdit` | 编辑表单显示 | true/false | |
+| `required` | 是否必填 | true/false | 生成校验注解 |
+| `dictType` | 字典类型 | 字典标识 | 有值=字典下拉,否则普通输入 |
+| `relation` | 关联选择配置 | 对象 | 有值=弹出选择框 |
+| `component` | 前端组件类型 | input/select/inputNumber/datetime/textarea | 默认根据类型推断 |
+| `width` | 表格列宽 | 数字(px) | 默认auto |
+| `sort` | 排序 | 数字 | 越小越靠前 |
+| `excelExport` | Excel导出 | true/false | 默认true |
+
+### 关联选择 relation 格式
+
+```yaml
+relation:
+  table: {关联表名}
+  idField: {回填ID字段}
+  nameField: {显示名称字段}
+  title: 选择{实体名}
+  path: /{模块}/{实体}/{路径}
+```
+
+---
+
+## 4. 字段清单
+
+### 4.1 主表 `t_ecs_course`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: courseId
+    columnName: course_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 主键,编辑时传递
+
+  # ---------- 业务字段 ----------
+  - fieldName: courseName
+    columnName: course_name
+    fieldType: String
+    inDb: true
+    inTable: true           # 列表显示
+    inQuery: true           # 作为查询条件
+    queryType: like         # 模糊查询
+    inForm: true            # 表单显示
+    inAdd: true
+    inEdit: true
+    required: true          # 必填
+    component: input
+    width: 200
+    sort: 1
+    excelExport: true
+    remark: 课程名称
+
+  - fieldName: courseCode
+    columnName: course_code
+    fieldType: String
+    inDb: true
+    inTable: true           # 列表显示
+    inQuery: true           # 作为查询条件
+    queryType: like         # 模糊查询
+    inForm: true            # 表单显示
+    inAdd: true
+    inEdit: true
+    required: true          # 必填
+    component: input
+    width: 150
+    sort: 2
+    excelExport: true
+    remark: 课程编码
+
+  - fieldName: courseType
+    columnName: course_type
+    fieldType: Integer
+    inDb: true
+    inTable: true           # 列表显示
+    inQuery: true           # 作为查询条件
+    queryType: eq           # 精确查询
+    inForm: true            # 表单显示
+    inAdd: true
+    inEdit: true
+    required: true          # 必填
+    dictType: course_type   # 字典下拉
+    component: select
+    width: 100
+    sort: 3
+    excelExport: true
+    remark: 课程类型:0-选修课,1-必修课
+
+  - fieldName: credit
+    columnName: credit
+    fieldType: BigDecimal
+    inDb: true
+    inTable: true           # 列表显示
+    inQuery: false
+    inForm: true            # 表单显示
+    inAdd: true
+    inEdit: true
+    required: true          # 必填
+    component: inputNumber
+    width: 100
+    sort: 4
+    excelExport: true
+    remark: 学分
+
+  - fieldName: hours
+    columnName: hours
+    fieldType: Integer
+    inDb: true
+    inTable: true           # 列表显示
+    inQuery: false
+    inForm: true            # 表单显示
+    inAdd: true
+    inEdit: true
+    required: true          # 必填
+    component: inputNumber
+    width: 100
+    sort: 5
+    excelExport: true
+    remark: 学时
+
+  # ---------- VO承载字段(讲师姓名拼接串) ----------
+  - fieldName: teacherNames
+    columnName: teacher_names
+    fieldType: String
+    inDb: false             # 纯VO字段,数据库无此列
+    inTable: true            # 列表显示
+    inQuery: false
+    inForm: false
+    excelExport: true
+    remark: 讲师姓名拼接串(如:张三、李四、王五),由关联表查询拼接
+
+  # ---------- 第三方同步字段 ----------
+  - fieldName: otherId
+    columnName: other_id
+    fieldType: String
+    inDb: true
+    inTable: false          # 列表不显示
+    inQuery: false
+    inForm: false            # 表单不显示
+    excelExport: false
+    remark: 第三方标识符,仅同步用
+
+  - fieldName: dataSource
+    columnName: data_source
+    fieldType: Integer
+    inDb: true
+    inTable: false          # 不显示列
+    inQuery: false          # 不作为查询条件
+    inForm: false           # 不在表单中显示,默认值由后台处理
+    inAdd: false
+    inEdit: false
+    lockRule:               # 操作锁定规则:dataSource=1 时禁止编辑和删除
+      lockValue: 1
+      lockActions:
+        - edit
+        - delete
+      tip: 第三方数据不可操作
+    width: 120
+    sort: 6
+    excelExport: true
+    remark: 数据来源:0-平台录入,1-第三方同步
+
+  # ---------- 公共字段(所有表统一包含,建表时自动追加) ----------
+  # 通用字段定义:
+  # | 字段          | Java类型        | 说明               |
+  # | ------------- | --------------- | ------------------ |
+  # | del_flag      | Integer         | 逻辑删除:0-未删除,1-已删除 |
+  # | create_dept   | Long            | 创建部门             |
+  # | create_by     | Long            | 创建者              |
+  # | create_time   | Date            | 创建时间             |
+  # | update_by     | Long            | 最后修改者            |
+  # | update_time   | Date            | 最后修改时间           |
+  # | tenant_id     | Long            | 租户ID(仅多租户表包含)    |
+
+  - fieldName: delFlag
+    columnName: del_flag
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: tenantId
+    columnName: tenant_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: Date
+    inDb: true
+    inTable: true           # 列表显示
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+    excelExport: true
+```
+
+### 4.2 从表 `t_ecs_course_teacher`
+
+> 课程-教师关联表,支持一门课程关联多个教师
+
+```yaml
+fields:
+  - fieldName: id
+    columnName: id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 主键
+
+  - fieldName: courseId
+    columnName: course_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 课程ID,关联主表
+
+  - fieldName: teacherId
+    columnName: teacher_id
+    fieldType: Long
+    inDb: true
+    inTable: false          # 列表不显示ID
+    inQuery: false
+    inForm: true            # 表单显示
+    inAdd: true
+    inEdit: true
+    required: true
+    relation:
+      table: t_ecs_teacher
+      idField: teacher_id
+      nameField: teacher_name
+      title: 选择教师
+      path: /ecs/teacher/selectTeacher
+    component: teacherSelect
+    sort: 1
+    remark: 教师ID,关联教师信息表
+
+  - fieldName: teacherName
+    columnName: teacher_name
+    fieldType: String
+    inDb: true
+    inTable: true            # 列表显示教师姓名
+    inQuery: false
+    inForm: false            # 表单不直接编辑,通过关联选择回填
+    sort: 2
+    excelExport: true
+    remark: 教师姓名(冗余),由关联选择回填
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: Date
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+```
+
+---
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+CourseVo:
+  - courseId
+  - courseName
+  - courseCode
+  - courseType
+  - credit
+  - hours
+  - teacherNames           # 讲师姓名拼接串
+  - dataSource             # 操作锁定判断(lockRule)
+  - createTime
+```
+
+### 5.2 主表明细 VO(包含从表)
+
+```yaml
+CourseDetailVo:
+  includes: CourseVo
+  additional:
+    - teacherList: List<CourseTeacherVo>   # 课程教师关联列表
+```
+
+### 5.3 从表 VO
+
+```yaml
+CourseTeacherVo:
+  - id
+  - courseId
+  - teacherId
+  - teacherName
+  - createTime
+```
+
+---
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: true
+  excelExport: true
+
+  importRules:
+    - fieldName: courseName
+      required: true
+      unique: false
+    - fieldName: courseCode
+      required: true
+      unique: true              # 课程编码唯一校验
+    - fieldName: courseType
+      required: true
+      unique: false
+    - fieldName: credit
+      required: true
+      unique: false
+      min: 0.5
+      max: 20
+    - fieldName: hours
+      required: true
+      unique: false
+      min: 1
+      max: 9999
+    - fieldName: dataSource
+      required: true
+      unique: false
+
+  # 从表参与导入(教师信息单独导入)
+  subTableImport: true
+  subTableExport: true
+
+  dubboExpose: true
+  dubboServiceName: RemoteCourseService
+```
+
+---
+
+## 7. 字典项(如有)
+
+| 字典类型 | 枚举值 | 说明 |
+|----------|--------|------|
+| `course_type` | 0=选修课, 1=必修课 | 课程类型 |
+| `data_source` | 0=平台录入, 1=第三方同步 | 数据来源 |
+
+---
+
+## 8. 建表语句
+
+> 数据库类型:kingbase(人大金仓)
+>
+> **通用字段**:所有表统一包含以下字段(建表语句自动追加,无需在字段清单中重复):
+>
+> | 字段          | 类型                 | 说明               |
+> | ------------- | -------------------- | ------------------ |
+> | del_flag      | character(1 char)    | 逻辑删除:0-未删除,1-已删除 |
+> | create_dept   | bigint               | 创建部门             |
+> | create_by     | bigint               | 创建者              |
+> | create_time   | timestamp            | 创建时间             |
+> | update_by     | bigint               | 最后修改者            |
+> | update_time   | timestamp            | 最后修改时间           |
+>
+> 多租户表额外包含:
+>
+> | 字段          | 类型                 | 说明               |
+> | ------------- | -------------------- | ------------------ |
+> | tenant_id     | bigint               | 租户ID             |
+
+### 8.1 主表 `t_ecs_course`
+
+```sql
+-- 课程信息表
+CREATE TABLE "dbo"."t_ecs_course" (
+  "course_id" bigint NOT NULL,
+  "course_name" character varying(255 char) NOT NULL DEFAULT ''::varchar,
+  "course_code" character varying(255 char) NOT NULL DEFAULT ''::varchar,
+  "course_type" integer NOT NULL DEFAULT '0'::bpchar,
+  "credit" numeric(10,2) NOT NULL DEFAULT 0,
+  "hours" integer NOT NULL DEFAULT 0,
+  "other_id" character varying(255 char) DEFAULT ''::varchar,
+  "data_source" integer NOT NULL DEFAULT '0'::bpchar,
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" timestamp,
+  "update_by" bigint,
+  "update_time" timestamp,
+  "tenant_id" bigint,
+  CONSTRAINT "t_ecs_course_pkey" PRIMARY KEY ("course_id")
+);
+
+-- 表注释
+ALTER TABLE "dbo"."t_ecs_course" COMMENT '课程信息表';
+
+-- 列注释
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "course_id" COMMENT '课程ID,主键';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "course_name" COMMENT '课程名称';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "course_code" COMMENT '课程编码';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "course_type" COMMENT '课程类型:0-选修课,1-必修课';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "credit" COMMENT '学分';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "hours" COMMENT '学时';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "other_id" COMMENT '第三方标识符,仅同步用';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "data_source" COMMENT '数据来源:0-平台录入,1-第三方同步';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "del_flag" COMMENT '删除标志:0-正常,1-删除';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "create_dept" COMMENT '创建部门';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "create_by" COMMENT '创建者';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "create_time" COMMENT '创建时间';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "update_by" COMMENT '更新者';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "update_time" COMMENT '更新时间';
+ALTER TABLE "dbo"."t_ecs_course" MODIFY "tenant_id" COMMENT '租户编号';
+```
+
+### 8.2 从表 `t_ecs_course_teacher`
+
+```sql
+-- 课程-教师关联表
+CREATE TABLE "dbo"."t_ecs_course_teacher" (
+  "id" bigint NOT NULL,
+  "course_id" bigint NOT NULL,
+  "teacher_id" bigint NOT NULL,
+  "teacher_name" character varying(255 char) DEFAULT ''::varchar,
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" timestamp,
+  "update_by" bigint,
+  "update_time" timestamp,
+  "tenant_id" bigint,
+  CONSTRAINT "t_ecs_course_teacher_pkey" PRIMARY KEY ("id")
+);
+
+-- 表注释
+ALTER TABLE "dbo"."t_ecs_course_teacher" COMMENT '课程-教师关联表';
+
+-- 列注释
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "id" COMMENT '主键';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "course_id" COMMENT '课程ID,关联主表';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "teacher_id" COMMENT '教师ID,关联教师信息表';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "teacher_name" COMMENT '教师姓名(冗余),由关联选择回填';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "del_flag" COMMENT '删除标志:0-正常,1-删除';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "create_dept" COMMENT '创建部门';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "create_by" COMMENT '创建者';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "create_time" COMMENT '创建时间';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "update_by" COMMENT '更新者';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "update_time" COMMENT '更新时间';
+ALTER TABLE "dbo"."t_ecs_course_teacher" MODIFY "tenant_id" COMMENT '租户编号';
+```
+
+---
+
+## 9. 备注
+
+1. **教师关联设计**:课程与教师为多对多关系,通过中间表 `t_ecs_course_teacher` 关联,课程列表中 `teacherNames` 字段由 SQL 拼接关联表的教师姓名得到。
+
+2. **讲师姓名拼接逻辑**(后端实现):
+   ```sql
+   SELECT c.*,
+          GROUP_CONCAT(ct.teacher_name ORDER BY ct.id SEPARATOR '、') AS teacher_names
+   FROM t_ecs_course c
+   LEFT JOIN t_ecs_course_teacher ct ON c.course_id = ct.course_id AND ct.del_flag = 0
+   WHERE c.del_flag = 0
+   GROUP BY c.course_id
+   ```
+
+3. **表单教师选择**:新增/编辑课程时,通过关联弹出框选择教师,选中后回填 `teacherId` 到中间表,`teacherName` 冗余存储。
+
+4. **删除关联**:删除课程时,中间表关联数据随主表一起逻辑删除(`del_flag=1`)。
+
+5. **导入说明**:Excel 导入时,教师信息单独处理,需先导入课程基础信息,再导入教师关联关系。
+
+6. **操作锁定规则**:`dataSource` 字段值为 `1`(第三方同步)时,前端操作列隐藏编辑和删除按钮,显示锁定图标和提示"第三方数据不可操作";值为 `0`(平台录入)时正常显示操作按钮。

+ 564 - 0
doc/ecs-section-requirements.md

@@ -0,0 +1,564 @@
+# 功能需求规格文档 — ECS 课表管理
+
+> 本文档描述 ECS 电子班牌模块「课表管理」功能的完整需求规格。
+> 技能将根据此文档生成后端 + 前端代码。
+
+---
+
+## 1. 基础信息
+
+| 属性 | 值 |
+|------|-----|
+| 功能中文名 | 课表管理 |
+| 功能简写 | section |
+| 所属模块 | ruoyi-modules/ruoyi-ecs |
+| 主表名 | t_ecs_section |
+| 从表名 | 无 |
+| 是否多租户 | 是 |
+| 数据库类型 | kingbase(人大金仓) |
+| 描述 | 管理课表信息,数据由第三方同步或平台录入,支持查看、查询、导出,第三方数据禁止编辑删除 |
+
+---
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| 列表 | GET | /section/list | /ecs/section/list | ecs:section:list |
+| 详情 | GET | /section/{id} | /ecs/section/{id} | ecs:section:query |
+| 导出数据 | POST | /section/export | /ecs/section/export | ecs:section:export |
+
+> **说明**:课表数据由第三方同步或平台录入,本模块提供查看、查询、导出功能。暂不提供新增、编辑、删除接口。
+
+### 2.2 补充接口
+
+无
+
+---
+
+## 3. 字段属性速查表
+
+| 属性 | 含义 | 可选值 | 说明 |
+|------|------|--------|------|
+| `fieldName` | 字段名(驼峰) | — | 代码变量名 |
+| `columnName` | 列名(下划线) | — | 数据库列名 |
+| `fieldType` | Java类型 | String/Long/Integer/BigDecimal/Date | |
+| `inDb` | 是否在表中存在 | true/false | false=纯前端计算字段 |
+| `inTable` | 列表是否显示 | true/false | 生成表格列 |
+| `inQuery` | 是否查询字段 | true/false | 生成搜索条件 |
+| `queryType` | 查询方式 | eq/like/between | 精确/模糊/范围 |
+| `inForm` | 是否表单字段 | true/false | 生成表单项 |
+| `inAdd` | 新增表单显示 | true/false | |
+| `inEdit` | 编辑表单显示 | true/false | |
+| `required` | 是否必填 | true/false | 生成校验注解 |
+| `dictType` | 字典类型 | 字典标识 | 有值=字典下拉,否则普通输入 |
+| `relation` | 关联选择配置 | 对象 | 有值=弹出选择框 |
+| `component` | 前端组件类型 | input/select/inputNumber/datetime/textarea | 默认根据类型推断 |
+| `width` | 表格列宽 | 数字(px) | 默认auto |
+| `sort` | 排序 | 数字 | 越小越靠前 |
+| `excelExport` | Excel导出 | true/false | 默认true |
+| `lockRule` | 操作锁定规则 | 对象 | 有值=按此字段值控制行级操作权限 |
+
+---
+
+## 4. 字段清单
+
+### 4.1 主表 `t_ecs_section`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: sectionId
+    columnName: section_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 主键,详情时传递
+
+  # ---------- 课程关联 ----------
+  - fieldName: courseId
+    columnName: course_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    remark: 课程Id
+
+  - fieldName: courseName
+    columnName: course_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: input
+    width: 150
+    sort: 2
+    excelExport: true
+    remark: 课程名称(冗余存储)
+
+  # ---------- 班级关联 ----------
+  - fieldName: classId
+    columnName: class_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    remark: 班级Id
+
+  - fieldName: className
+    columnName: class_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: false
+    component: input
+    width: 150
+    sort: 1
+    excelExport: true
+    remark: 班级名称(冗余存储)
+
+  # ---------- 教室关联 ----------
+  - fieldName: roomId
+    columnName: room_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    remark: 教室Id
+
+  - fieldName: roomName
+    columnName: room_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: false
+    component: input
+    width: 150
+    sort: 3
+    excelExport: true
+    remark: 教室名称(冗余存储)
+
+  # ---------- 学期信息 ----------
+  - fieldName: academicYear
+    columnName: academic_year
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: eq
+    inForm: false
+    component: input
+    width: 120
+    sort: 4
+    excelExport: true
+    remark: 学年,如 2025-2026
+
+  - fieldName: semester
+    columnName: semester
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: eq
+    inForm: false
+    component: select
+    width: 80
+    sort: 5
+    excelExport: true
+    remark: 学期:1-第一学期,2-第二学期
+
+  # ---------- 排课信息 ----------
+  - fieldName: courseDate
+    columnName: course_date
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: between
+    inForm: false
+    component: datetime
+    width: 120
+    sort: 6
+    excelExport: true
+    remark: 上课日期
+
+  - fieldName: weekday
+    columnName: weekday
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: inputNumber
+    width: 80
+    sort: 7
+    excelExport: true
+    remark: 星期 1-7(周一至周日)
+
+  - fieldName: timeSlot
+    columnName: time_slot
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    dictType: ecs_time_slot
+    component: select
+    width: 100
+    sort: 8
+    excelExport: true
+    remark: 上课时段:0-上午,1-下午,2-晚上
+
+  - fieldName: sectionIndex
+    columnName: section_index
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: inputNumber
+    width: 80
+    sort: 9
+    excelExport: true
+    remark: 课程节次,一天的第几节课
+
+  - fieldName: startSection
+    columnName: start_section
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: inputNumber
+    width: 80
+    sort: 10
+    excelExport: true
+    remark: 连课节次
+
+  - fieldName: sectionCount
+    columnName: section_count
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: inputNumber
+    width: 80
+    sort: 11
+    excelExport: true
+    remark: 占用节次,默认1
+
+  - fieldName: sectionLList
+    columnName: section_l_list
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: input
+    width: 100
+    sort: 12
+    excelExport: true
+    remark: 占用列表,JSON格式如[2,3]
+
+  - fieldName: startTime
+    columnName: start_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 100
+    sort: 13
+    excelExport: true
+    remark: 开始时间
+
+  - fieldName: endTime
+    columnName: end_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 100
+    sort: 14
+    excelExport: true
+    remark: 结束时间
+
+  # ---------- 考勤标记 ----------
+  - fieldName: isAttend
+    columnName: is_attend
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: select
+    width: 80
+    sort: 15
+    excelExport: true
+    remark: 是否考勤:0-否,1-是
+
+  # ---------- 第三方同步字段 ----------
+  - fieldName: otherId
+    columnName: other_id
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    excelExport: false
+    remark: 第三方课表Id
+
+  - fieldName: dataSource
+    columnName: data_source
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    dictType: data_source
+    component: select
+    lockRule:
+      lockValue: 1
+      lockActions:
+        - edit
+        - delete
+      tip: 第三方数据不可操作
+    excelExport: true
+    remark: 数据来源:0-平台录入,1-第三方同步
+
+  - fieldName: courseOtherId
+    columnName: course_other_id
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    excelExport: false
+    remark: 第三方课程Id
+
+  - fieldName: classOtherId
+    columnName: class_other_id
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    excelExport: false
+    remark: 第三方班级Id
+
+  - fieldName: roomOtherId
+    columnName: room_other_id
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    excelExport: false
+    remark: 第三方教室Id
+
+  # ---------- 公共字段(所有表统一包含,多租户表增加 tenant_id) ----------
+  # 通用字段定义(无需在字段清单中重复写,建表时自动追加):
+  # | 字段          | Java类型        | 说明               |
+  # | ------------- | --------------- | ------------------ |
+  # | del_flag      | Integer         | 逻辑删除:0-未删除,1-已删除 |
+  # | create_dept   | Long            | 创建部门             |
+  # | create_by     | Long            | 创建者              |
+  # | create_time   | Date            | 创建时间             |
+  # | update_by     | Long            | 最后修改者            |
+  # | update_time   | Date            | 最后修改时间           |
+  # | tenant_id     | Long            | 租户ID(多租户表包含)    |
+
+  - fieldName: delFlag
+    columnName: del_flag
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inForm: false
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: Date
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: false
+    component: datetime
+```
+
+---
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+EcsSectionVo:
+  - sectionId          # 主键
+  - className          # 班级名称
+  - courseName         # 课程名称
+  - roomName           # 教室名称
+  - courseDate         # 上课日期
+  - timeSlot           # 上课时段
+  - sectionIndex       # 课程节次
+  - startSection       # 连课节次
+  - sectionCount       # 占用节次
+  - sectionLList       # 占用列表
+  - startTime          # 开始时间
+  - endTime            # 结束时间
+  - isAttend           # 是否考勤
+  - dataSource         # 数据来源(lockRule需要,前端判断操作权限)
+```
+
+---
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: false       # 不支持导入
+  excelExport: true        # 支持导出
+
+  # 本模块仅提供查看、查询、导出功能,暂不提供新增、编辑、删除
+  # data_source=1 时第三方数据禁止编辑和删除(预留 lockRule,后续开放编辑删除接口时生效)
+  lockRule:
+    field: dataSource
+    lockValue: 1
+    lockActions:
+      - edit
+      - delete
+    tip: 第三方数据不可操作
+
+  dubboExpose: false       # 不暴露 Dubbo 服务
+```
+
+---
+
+## 7. 字典项
+
+| 字典类型 | 枚举值 | 说明 |
+|----------|--------|------|
+| `ecs_time_slot` | 0=上午, 1=下午, 2=晚上 | 上课时段 |
+| `data_source` | 0=平台录入, 1=第三方同步 | 数据来源 |
+
+---
+
+## 8. 建表语句
+
+> **通用字段**:所有表统一包含以下字段(建表语句自动追加,无需在字段清单中重复):
+>
+> | 字段          | 类型 | 说明               |
+> | ------------- | -------------------- | ------------------ |
+> | del_flag      | character(1 char) | 逻辑删除:0-未删除,1-已删除 |
+> | create_dept   | bigint | 创建部门             |
+> | create_by     | bigint | 创建者              |
+> | create_time   | timestamp | 创建时间             |
+> | update_by     | bigint | 更新者              |
+> | update_time   | timestamp | 更新时间           |
+> | tenant_id     | bigint | 租户ID(多租户表) |
+
+### 8.1 主表 `t_ecs_section`
+
+```sql
+-- ============================================
+-- ECS 课表信息表(kingbase/人大金仓)
+-- ============================================
+CREATE TABLE "dbo"."t_ecs_section" (
+  "section_id" bigint NOT NULL,
+  "course_id" bigint NOT NULL,
+  "class_id" bigint NOT NULL,
+  "room_id" bigint NOT NULL,
+  "course_name" character varying(200 char) NOT NULL DEFAULT ''::varchar,
+  "class_name" character varying(200 char) NOT NULL DEFAULT ''::varchar,
+  "room_name" character varying(200 char) NOT NULL DEFAULT ''::varchar,
+  "academic_year" character varying(20 char) NOT NULL DEFAULT ''::varchar,
+  "semester" integer NOT NULL DEFAULT 1,
+  "course_date" timestamp NOT NULL,
+  "weekday" integer NOT NULL DEFAULT 1,
+  "time_slot" integer NOT NULL DEFAULT 0,
+  "section_index" integer NOT NULL DEFAULT 1,
+  "start_section" integer NOT NULL DEFAULT 1,
+  "section_count" integer NOT NULL DEFAULT 1,
+  "section_l_list" character varying(500 char) DEFAULT ''::varchar,
+  "start_time" timestamp,
+  "end_time" timestamp,
+  "is_attend" integer NOT NULL DEFAULT 0,
+  "other_id" character varying(100 char) DEFAULT ''::varchar,
+  "data_source" integer NOT NULL DEFAULT 0,
+  "course_other_id" character varying(100 char) DEFAULT ''::varchar,
+  "class_other_id" character varying(100 char) DEFAULT ''::varchar,
+  "room_other_id" character varying(100 char) DEFAULT ''::varchar,
+  -- 通用字段(自动追加)
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" timestamp,
+  "update_by" bigint,
+  "update_time" timestamp,
+  "tenant_id" bigint,
+  CONSTRAINT "t_ecs_section_pkey" PRIMARY KEY ("section_id")
+);
+
+-- 表注释
+ALTER TABLE "dbo"."t_ecs_section" COMMENT 'ECS课表信息表';
+
+-- 列注释
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "section_id" COMMENT '主键';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "course_id" COMMENT '课程Id';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "class_id" COMMENT '班级Id';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "room_id" COMMENT '教室Id';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "course_name" COMMENT '课程名称';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "class_name" COMMENT '班级名称';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "room_name" COMMENT '教室名称';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "academic_year" COMMENT '学年';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "semester" COMMENT '学期:1-第一学期,2-第二学期';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "course_date" COMMENT '上课日期';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "weekday" COMMENT '星期 1-7(周一至周日)';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "time_slot" COMMENT '上课时段:0-上午,1-下午,2-晚上';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "section_index" COMMENT '课程节次';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "start_section" COMMENT '连课节次';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "section_count" COMMENT '占用节次';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "section_l_list" COMMENT '占用列表,JSON格式如[2,3]';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "start_time" COMMENT '开始时间';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "end_time" COMMENT '结束时间';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "is_attend" COMMENT '是否考勤:0-否,1-是';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "other_id" COMMENT '第三方课表Id';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "data_source" COMMENT '数据来源:0-平台录入,1-第三方同步';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "course_other_id" COMMENT '第三方课程Id';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "class_other_id" COMMENT '第三方班级Id';
+ALTER TABLE "dbo"."t_ecs_section" MODIFY "room_other_id" COMMENT '第三方教室Id';
+
+-- 索引
+CREATE INDEX "idx_ecs_section_class_id" ON "dbo"."t_ecs_section" ("class_id");
+CREATE INDEX "idx_ecs_section_course_id" ON "dbo"."t_ecs_section" ("course_id");
+CREATE INDEX "idx_ecs_section_room_id" ON "dbo"."t_ecs_section" ("room_id");
+CREATE INDEX "idx_ecs_section_course_date" ON "dbo"."t_ecs_section" ("course_date");
+CREATE INDEX "idx_ecs_section_academic_year" ON "dbo"."t_ecs_section" ("academic_year", "semester");
+CREATE INDEX "idx_ecs_section_data_source" ON "dbo"."t_ecs_section" ("data_source");
+```
+
+---
+
+## 9. 备注
+
+1. **数据冗余设计**:课程名称(`courseName`)、班级名称(`className`)、教室名称(`roomName`)采用冗余存储策略,单表无关联查询,提升查询性能。当课程/班级/教室信息变更时需考虑是否同步更新历史记录。
+2. **只读模型**:课表数据主要由第三方同步或平台录入,当前版本仅提供查看、查询、导出功能,不支持新增、编辑、删除操作。
+3. **lockRule 预留**:`dataSource=1` 时锁定编辑和删除操作。当前版本虽无编辑删除接口,但 lockRule 配置已预留,后续开放增删改接口时即可生效。
+4. **JSON字段**:`sectionLList` 存储占用列表,格式为 JSON 数组(如 `[2,3]`),Java 端使用 String 类型存储,前端负责解析展示。
+5. **单表设计**:`courseId`、`classId`、`roomId` 仅存储 ID 值,不配置关联选择器,名称字段冗余存储。
+6. **第三方字段**:`otherId`、`courseOtherId`、`classOtherId`、`roomOtherId` 用于第三方系统数据映射,不显示在列表和表单中。
+7. **原始需求中列表字段"推送状态"不在数据结构中**,已从显示字段中移除。如需推送状态字段请后续补充。

+ 436 - 0
doc/ecs-term-requirements.md

@@ -0,0 +1,436 @@
+# ECS 设备信息管理 — 需求规格说明书
+
+## 1. 基础信息
+
+| 属性 | 值 |
+|------|-----|
+| 功能中文名 | 设备信息 |
+| 功能简写 | term |
+| 所属模块 | ruoyi-modules/ruoyi-ecs |
+| 主表名 | t_ecs_term |
+| 从表名 | 无 |
+| 是否多租户 | 是 |
+| 数据库类型 | kingbase |
+| 描述 | 管理电子班牌设备信息,包括设备名称、编码、类型、网络配置、管理账号及所属教室,支持增删改查和导入导出 |
+
+---
+
+## 2. 接口清单
+
+### 2.1 主表接口
+
+| 接口 | HTTP | 路径(后端) | 前端路径 | 权限字符 |
+|------|------|-------------|----------|----------|
+| 列表 | GET | /term/list | /ecs/term/list | ecs:term:list |
+| 详情 | GET | /term/{termId} | /ecs/term/{termId} | ecs:term:query |
+| 新增 | POST | /term/ | /ecs/term/ | ecs:term:add |
+| 编辑 | PUT | /term/ | /ecs/term/ | ecs:term:edit |
+| 删除 | DELETE | /term/{termIds} | /ecs/term/{termIds} | ecs:term:remove |
+| 导出模板 | POST | /term/exportTemplate | /ecs/term/exportTemplate | ecs:term:export |
+| 导入数据 | POST | /term/importData | /ecs/term/importData | ecs:term:import |
+| 导出数据 | POST | /term/export | /ecs/term/export | ecs:term:export |
+
+---
+
+## 3. 字段属性速查表
+
+| 属性 | 含义 | 可选值 | 说明 |
+|------|------|--------|------|
+| `fieldName` | 字段名(驼峰) | — | 代码变量名 |
+| `columnName` | 列名(下划线) | — | 数据库列名 |
+| `fieldType` | Java类型 | String/Long/Integer/BigDecimal/Date | |
+| `inDb` | 是否在表中存在 | true/false | false=纯前端计算字段 |
+| `inTable` | 列表是否显示 | true/false | 生成表格列 |
+| `inQuery` | 是否查询字段 | true/false | 生成搜索条件 |
+| `queryType` | 查询方式 | eq/like/between | 精确/模糊/范围 |
+| `inForm` | 是否表单字段 | true/false | 生成表单项 |
+| `inAdd` | 新增表单显示 | true/false | |
+| `inEdit` | 编辑表单显示 | true/false | |
+| `required` | 是否必填 | true/false | 生成校验注解 |
+| `dictType` | 字典类型 | 字典标识 | 有值=字典下拉,否则普通输入 |
+| `relation` | 关联选择配置 | 对象 | 有值=弹出选择框 |
+| `component` | 前端组件类型 | input/select/inputNumber/datetime/textarea | 默认根据类型推断 |
+| `width` | 表格列宽 | 数字(px) | 默认auto |
+| `sort` | 排序 | 数字 | 越小越靠前 |
+| `excelExport` | Excel导出 | true/false | 默认true |
+| `lockRule` | 操作锁定规则 | 对象 | 有值=按此字段值控制行级操作权限 |
+
+---
+
+## 4. 字段清单
+
+### 4.1 主表 `t_ecs_term`
+
+```yaml
+fields:
+  # ---------- 主键 ----------
+  - fieldName: termId
+    columnName: term_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inForm: false
+    remark: 设备Id
+
+  # ---------- 业务字段 ----------
+  - fieldName: termName
+    columnName: term_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: true
+    component: input
+    width: 150
+    sort: 1
+    excelExport: true
+    remark: 设备名称
+
+  - fieldName: termNumb
+    columnName: term_numb
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: true
+    component: input
+    width: 150
+    sort: 2
+    excelExport: true
+    remark: 设备编码
+
+  - fieldName: termType
+    columnName: term_type
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: eq
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: true
+    dictType: term_type
+    component: select
+    width: 100
+    sort: 3
+    excelExport: true
+    remark: 设备类型
+
+  - fieldName: termIP
+    columnName: term_ip
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: input
+    width: 140
+    sort: 4
+    excelExport: true
+    remark: 设备IP
+
+  - fieldName: termMac
+    columnName: term_mac
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: input
+    width: 150
+    sort: 5
+    excelExport: true
+    remark: MAC地址
+
+  - fieldName: termPort
+    columnName: term_port
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: inputNumber
+    sort: 6
+    excelExport: true
+    remark: 设备端口
+
+  - fieldName: termBrand
+    columnName: term_brand
+    fieldType: Integer
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    dictType: term_brand
+    component: select
+    sort: 7
+    excelExport: true
+    remark: 设备品牌
+
+  - fieldName: adminName
+    columnName: admin_name
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: input
+    sort: 8
+    excelExport: true
+    remark: 管理账号
+
+  - fieldName: adminPwd
+    columnName: admin_pwd
+    fieldType: String
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: input
+    excelExport: false
+    sort: 9
+    remark: 管理密码
+
+  - fieldName: serverIp
+    columnName: server_ip
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: input
+    width: 140
+    sort: 10
+    excelExport: true
+    remark: 服务器IP
+
+  - fieldName: serverPort
+    columnName: server_port
+    fieldType: Integer
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    component: inputNumber
+    width: 100
+    sort: 11
+    excelExport: true
+    remark: 服务器端口
+
+  # ---------- 教室关联(冗余存储名称) ----------
+  - fieldName: roomId
+    columnName: room_id
+    fieldType: Long
+    inDb: true
+    inTable: false
+    inQuery: false
+    inForm: true
+    inAdd: true
+    inEdit: true
+    required: false
+    relation:
+      table: t_pt_room
+      idField: ptRoomId
+      nameField: roomName
+      title: 选择教室
+      path: /backstage/room/selectRoom
+    component: roomSelect
+    sort: 12
+    remark: 教室Id(关联教室表)
+
+  - fieldName: roomName
+    columnName: room_name
+    fieldType: String
+    inDb: true
+    inTable: true
+    inQuery: true
+    queryType: like
+    inForm: false
+    inAdd: false
+    inEdit: false
+    width: 150
+    sort: 13
+    excelExport: true
+    remark: 教室名称(冗余存储,选择教室时自动回填)
+
+  # ---------- 公共字段 ----------
+  # 通用字段定义(无需在字段清单中重复写,建表时自动追加):
+  # | 字段          | Java类型        | 说明               |
+  # | ------------- | --------------- | ------------------ |
+  # | del_flag      | Integer         | 逻辑删除:0-未删除,1-已删除 |
+  # | create_dept   | Long            | 创建部门             |
+  # | create_by     | Long            | 创建者              |
+  # | create_time   | Date            | 创建时间             |
+  # | update_by     | Long            | 最后修改者            |
+  # | update_time   | Date            | 最后修改时间           |
+  # | tenant_id     | Long            | 租户ID(多租户表包含)      |
+
+  - fieldName: createTime
+    columnName: create_time
+    fieldType: Date
+    inDb: true
+    inTable: true
+    inQuery: false
+    inForm: false
+    component: datetime
+    width: 180
+    sort: 99
+    excelExport: true
+```
+
+---
+
+## 5. VO 结构
+
+### 5.1 主表列表 VO
+
+```yaml
+EcsTermVo:
+  - termId
+  - termName
+  - termNumb
+  - termType
+  - termIP
+  - termMac
+  - serverIp
+  - serverPort
+  - roomId
+  - roomName
+  - createTime
+```
+
+---
+
+## 6. 特殊需求
+
+```yaml
+special:
+  excelImport: true
+  excelExport: true
+
+  importRules:
+    - fieldName: termName
+      required: true
+    - fieldName: termNumb
+      required: true
+      unique: true
+    - fieldName: termType
+      required: true
+
+  dubboExpose: false
+```
+
+---
+
+## 7. 字典项
+
+| 字典类型 | 枚举值 | 说明 |
+|----------|--------|------|
+| `term_type` | 0=电子班牌, 1=考勤机, 2=门禁机 | 设备类型 |
+| `term_brand` | 0=希沃, 1=鸿合, 2=其他 | 设备品牌 |
+
+> **注意**:字典值仅为示例,实际值由业务方确定。
+
+---
+
+## 8. 建表语句
+
+### 8.1 主表 `t_ecs_term`
+
+```sql
+-- 人大金仓(KingbaseES)
+CREATE TABLE "dbo"."t_ecs_term" (
+  "term_id" bigint NOT NULL,
+  "term_name" character varying(255 char) NOT NULL DEFAULT ''::varchar,
+  "term_numb" character varying(255 char) NOT NULL DEFAULT ''::varchar,
+  "term_type"  character varying(16 char) NOT NULL DEFAULT '0'::varchar,
+  "term_ip" character varying(255 char) DEFAULT ''::varchar,
+  "term_mac" character varying(255 char) DEFAULT ''::varchar,
+  "term_port" integer,
+  "term_brand"  character varying(16 char) NOT NULL DEFAULT 'dt'::varchar,
+  "admin_name" character varying(255 char) DEFAULT ''::varchar,
+  "admin_pwd" character varying(255 char) DEFAULT ''::varchar,
+  "server_ip" character varying(255 char) DEFAULT ''::varchar,
+  "server_port" integer,
+  "room_id" bigint,
+  "room_name" character varying(255 char) DEFAULT ''::varchar,
+  "tenant_id" character varying(20 char) NOT NULL DEFAULT ''::varchar,
+  "del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+  "create_dept" bigint,
+  "create_by" bigint,
+  "create_time" datetime,
+  "update_by" bigint,
+  "update_time" datetime,
+  CONSTRAINT "t_ecs_term_pkey" PRIMARY KEY ("term_id")
+);
+
+-- 表注释
+ALTER TABLE "dbo"."t_ecs_term" COMMENT '设备信息表';
+
+-- 列注释
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_id" COMMENT '设备Id';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_name" COMMENT '设备名称';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_numb" COMMENT '设备编码';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_type" COMMENT '设备类型:0-电子班牌,1-考勤机,2-门禁机';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_ip" COMMENT '设备IP';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_mac" COMMENT 'MAC地址';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_port" COMMENT '设备端口';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "term_brand" COMMENT '设备品牌';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "admin_name" COMMENT '管理账号';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "admin_pwd" COMMENT '管理密码';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "server_ip" COMMENT '服务器IP';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "server_port" COMMENT '服务器端口';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "room_id" COMMENT '教室Id';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "room_name" COMMENT '教室名称';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "tenant_id" COMMENT '租户编号';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "del_flag" COMMENT '删除标志(0-未删除 1-已删除)';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "create_dept" COMMENT '创建部门';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "create_by" COMMENT '创建者';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "create_time" COMMENT '创建时间';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "update_by" COMMENT '更新者';
+ALTER TABLE "dbo"."t_ecs_term" MODIFY "update_time" COMMENT '更新时间';
+```
+
+---
+
+## 9. 备注
+
+1. **冗余存储教室名称**:`roomName` 冗余存储教室名称,选择教室时自动回填,支持按教室名称模糊查询,避免关联查询性能问题。当教室信息变更时需考虑是否同步更新历史记录。
+2. **管理密码**:`adminPwd` 采用普通文本存储,不在列表中显示,不参与 Excel 导出,仅在表单中可编辑。
+3. **设备编码唯一性**:`termNumb` 在导入时做唯一性校验,确保设备编码不重复。
+4. **教室关联选择**:`roomId` 通过关联选择器从 `t_pt_room` 表选择,选择后自动回填 `roomName`。
+5. **关联实体说明**:
+   - 教室 → 关联 `t_pt_room` 表(通过 backstage 模块的 RemotePtRoomService)

+ 2 - 2
pom.xml

@@ -80,12 +80,12 @@
                 <!-- 环境标识,需要与配置文件的名称相对应 -->
                 <profiles.active>dev</profiles.active>
                 <!--<nacos.server>172.23.63.213:8848,172.23.63.213:8838,172.23.63.213:8828</nacos.server>-->
-                <nacos.server>127.0.0.1:8848</nacos.server>
+                <nacos.server>192.168.5.240:8858</nacos.server>
                 <nacos.discovery.group>DEFAULT_GROUP</nacos.discovery.group>
                 <nacos.config.group>DEFAULT_GROUP</nacos.config.group>
                 <nacos.username>nacos</nacos.username>
                 <nacos.password>datu@XR#2025!!</nacos.password>
-                <logstash.address>127.0.0.1:4560</logstash.address>
+                <logstash.address>192.168.5.18:4560</logstash.address>
             </properties>
             <activation>
                 <!-- 默认环境 -->

+ 1 - 0
ruoyi-api/pom.xml

@@ -18,6 +18,7 @@
         <module>ruoyi-api-hotel</module>
         <module>ruoyi-api-sync</module>
         <module>ruoyi-api-hik</module>
+        <module>ruoyi-api-ecs</module>
     </modules>
 
     <artifactId>ruoyi-api</artifactId>

+ 8 - 1
ruoyi-api/ruoyi-api-backstage/pom.xml

@@ -27,7 +27,14 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-excel</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mybatis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-encrypt</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 31 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java

@@ -2,7 +2,10 @@ package org.dromara.backstage.api;
 
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBatchSetBo;
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBo;
+import org.dromara.backstage.api.domain.dto.RemoteClassroomDto;
+import org.dromara.backstage.api.domain.dto.RemoteClassroomQueryDto;
 import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 
 import java.util.Collection;
 import java.util.List;
@@ -38,4 +41,32 @@ public interface RemotePtRoomService {
      * @return 包含房间信息的视图对象列表,每个对象代表一个房间的详细信息
      */
     List<RemotePtRoomVo> selectRoomList();
+
+    // region ==================== 教室查询方法 ====================
+
+    /**
+     * 查询教室列表(不分页)
+     *
+     * @param queryDto 查询条件(roomCode精确, roomName模糊, areaId精确)
+     * @return 教室列表
+     */
+    List<RemoteClassroomDto> selectClassroomList(RemoteClassroomQueryDto queryDto);
+
+    /**
+     * 分页查询教室列表
+     *
+     * @param queryDto 查询条件
+     * @return 教室分页列表
+     */
+    TableDataInfo<RemoteClassroomDto> selectClassroomPage(RemoteClassroomQueryDto queryDto);
+
+    /**
+     * 根据ID查询教室
+     *
+     * @param roomId 房间ID
+     * @return 教室信息
+     */
+    RemoteClassroomDto selectClassroomById(Long roomId);
+
+    // endregion ==================== 教室查询方法 ====================
 }

+ 45 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteUserAccountService.java

@@ -1,8 +1,13 @@
 package org.dromara.backstage.api;
 
 import org.dromara.backstage.api.domain.bo.RemoteUserAccountBo;
+import org.dromara.backstage.api.domain.dto.RemoteTeacherDto;
+import org.dromara.backstage.api.domain.dto.RemoteTeacherQueryDto;
+import org.dromara.backstage.api.domain.dto.RemoteTraineeDto;
+import org.dromara.backstage.api.domain.dto.RemoteTraineeQueryDto;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 
 import java.util.Date;
 import java.util.List;
@@ -106,4 +111,44 @@ public interface RemoteUserAccountService {
      * @return 账户信息
      */
     List<RemoteUserAccountVo> getExpireAccount(Date startDate, Date endDate);
+
+    /**
+     * 分页查询教师列表
+     *
+     * @param queryDto 查询条件
+     * @return 教师分页数据
+     */
+    TableDataInfo<RemoteTeacherDto> selectTeacherPage(RemoteTeacherQueryDto queryDto);
+
+    /**
+     * 根据用户ID查询教师信息
+     *
+     * @param userId 用户ID
+     * @return 教师信息
+     */
+    R<RemoteTeacherDto> selectTeacherById(Long userId);
+
+    /**
+     * 分页查询学员列表
+     *
+     * @param queryDto 查询条件
+     * @return 学员分页数据
+     */
+    TableDataInfo<RemoteTraineeDto> selectTraineePage(RemoteTraineeQueryDto queryDto);
+
+    /**
+     * 根据培训班ID通过t_user_dept关系表分页查询学员列表
+     *
+     * @param queryDto 查询条件(必须包含deptId)
+     * @return 学员分页数据
+     */
+    TableDataInfo<RemoteTraineeDto> selectTraineePageByDeptId(RemoteTraineeQueryDto queryDto);
+
+    /**
+     * 根据用户ID查询学员信息
+     *
+     * @param userId 用户ID
+     * @return 学员信息
+     */
+    R<RemoteTraineeDto> selectTraineeById(Long userId);
 }

+ 41 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteClassroomDto.java

@@ -0,0 +1,41 @@
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 教室信息返回对象
+ */
+@Data
+public class RemoteClassroomDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 房间ID */
+    private Long roomId;
+
+    /** 所属区域ID */
+    private Long areaId;
+
+    /** 所属区域名称 */
+    private String areaName;
+
+    /** 房间编码 */
+    private String roomCode;
+
+    /** 房间名称 */
+    private String roomName;
+
+    /** 容纳人数 */
+    private Integer capacity;
+
+    /** 门牌号 */
+    private String codeOne;
+
+    /** 第三方Id */
+    private String otherId;
+
+}

+ 28 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteClassroomQueryDto.java

@@ -0,0 +1,28 @@
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 教室查询条件
+ */
+@Data
+public class RemoteClassroomQueryDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 房间编码(精确查询) */
+    private String roomCode;
+
+    /** 房间名称(模糊查询) */
+    private String roomName;
+
+    /** 页码,默认1 */
+    private Integer pageNum;
+
+    /** 每页条数,默认10 */
+    private Integer pageSize;
+}

+ 31 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTeacherDto.java

@@ -0,0 +1,31 @@
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 教师信息DTO(ECS模块使用)
+ */
+@Data
+public class RemoteTeacherDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 账户ID */
+    private Long userId;
+
+    /** 账户姓名 */
+    private String realName;
+
+    /** 所属部门名称 */
+    private String deptName;
+
+    /** 性别编码 */
+    private String sex;
+
+    /** 手机号码 */
+    private String phone;
+}

+ 28 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTeacherQueryDto.java

@@ -0,0 +1,28 @@
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 教师信息查询DTO(ECS模块使用,不依赖mybatis模块)
+ */
+@Data
+public class RemoteTeacherQueryDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 姓名(模糊查询) */
+    private String realName;
+
+    /** 手机号(精确查询) */
+    private String phone;
+
+    /** 页码 */
+    private Integer pageNum;
+
+    /** 每页条数 */
+    private Integer pageSize;
+}

+ 43 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTraineeDto.java

@@ -0,0 +1,43 @@
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 学员信息DTO(ECS模块使用)
+ */
+@Data
+public class RemoteTraineeDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 账户ID */
+    private Long userId;
+
+    /** 账户姓名 */
+    private String realName;
+
+    /** 所属培训班名称 */
+    private String deptName;
+
+    /** 所属培训班ID */
+    private Long deptId;
+
+    /** 性别编码 */
+    private String sex;
+
+    /** 性别中文 */
+    private String sexName;
+
+    /** 手机号码 */
+    private String phone;
+
+    /** 第三方标识 */
+    private String otherId;
+
+    /** 身份证号码 */
+    private String idCard;
+}

+ 31 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/dto/RemoteTraineeQueryDto.java

@@ -0,0 +1,31 @@
+package org.dromara.backstage.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 学员信息查询DTO(ECS模块使用,不依赖mybatis模块)
+ */
+@Data
+public class RemoteTraineeQueryDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 姓名(模糊查询) */
+    private String realName;
+
+    /** 手机号(精确查询) */
+    private String phone;
+
+    /** 培训班ID(精确查询) */
+    private Long deptId;
+
+    /** 页码 */
+    private Integer pageNum;
+
+    /** 每页条数 */
+    private Integer pageSize;
+}

+ 7 - 0
ruoyi-api/ruoyi-api-bom/pom.xml

@@ -45,6 +45,13 @@
                 <artifactId>ruoyi-api-backstage</artifactId>
                 <version>${revision}</version>
             </dependency>
+
+            <!-- 电子班牌系统接口 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-api-ecs</artifactId>
+                <version>${revision}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 </project>

+ 32 - 0
ruoyi-api/ruoyi-api-ecs/pom.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.dromara</groupId>
+        <artifactId>ruoyi-api</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-api-ecs</artifactId>
+
+    <description>
+        ruoyi-api-ecs 电子班牌系统接口服务模块
+    </description>
+
+    <dependencies>
+
+        <!-- RuoYi Common Core-->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mybatis</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 66 - 0
ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/RemoteCourseService.java

@@ -0,0 +1,66 @@
+package org.dromara.ecs.api;
+
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.ecs.api.domain.dto.RemoteCourseDto;
+import org.dromara.ecs.api.domain.dto.RemoteCourseQueryDto;
+
+import java.util.List;
+
+/**
+ * 课程管理远程调用接口
+ *
+ * @author ruoyi
+ * @date 2026-04-15
+ */
+public interface RemoteCourseService {
+
+    /**
+     * 根据ID查询课程
+     *
+     * @param courseId 课程ID
+     * @return 课程信息
+     */
+    R<RemoteCourseDto> selectCourseById(Long courseId);
+
+    /**
+     * 查询课程列表(不分页)
+     *
+     * @param queryDto 查询条件
+     * @return 课程列表
+     */
+    List<RemoteCourseDto> selectCourseList(RemoteCourseQueryDto queryDto);
+
+    /**
+     * 分页查询课程列表
+     *
+     * @param queryDto 查询条件
+     * @return 分页结果
+     */
+    TableDataInfo<RemoteCourseDto> selectCoursePage(RemoteCourseQueryDto queryDto);
+
+    /**
+     * 新增课程
+     *
+     * @param dto 课程信息
+     * @return 操作结果
+     */
+    R<Void> insertCourse(RemoteCourseDto dto);
+
+    /**
+     * 修改课程
+     *
+     * @param dto 课程信息
+     * @return 操作结果
+     */
+    R<Void> updateCourse(RemoteCourseDto dto);
+
+    /**
+     * 删除课程
+     *
+     * @param ids 课程ID数组
+     * @return 操作结果
+     */
+    R<Void> deleteCourseByIds(Long[] ids);
+
+}

+ 48 - 0
ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteCourseDto.java

@@ -0,0 +1,48 @@
+package org.dromara.ecs.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程管理DTO
+ *
+ * @author ruoyi
+ * @date 2026-04-15
+ */
+@Data
+public class RemoteCourseDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 课程ID */
+    private Long courseId;
+
+    /** 课程名称 */
+    private String courseName;
+
+    /** 课程编码 */
+    private String courseCode;
+
+    /** 课程类型:0-选修课,1-必修课 */
+    private Integer courseType;
+
+    /** 学分 */
+    private BigDecimal credit;
+
+    /** 学时 */
+    private Integer hours;
+
+    /** 讲师姓名拼接串 */
+    private String teacherNames;
+
+    /** 第三方标识符 */
+    private String otherId;
+
+    /** 数据来源:0-平台录入,1-第三方同步 */
+    private Integer dataSource;
+
+}

+ 38 - 0
ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteCourseQueryDto.java

@@ -0,0 +1,38 @@
+package org.dromara.ecs.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 课程查询条件DTO
+ *
+ * @author ruoyi
+ * @date 2026-04-15
+ */
+@Data
+public class RemoteCourseQueryDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 课程名称(模糊查询) */
+    private String courseName;
+
+    /** 课程编码(模糊查询) */
+    private String courseCode;
+
+    /** 课程类型(精确查询) */
+    private Integer courseType;
+
+    /** 数据来源(精确查询) */
+    private Integer dataSource;
+
+    /** 页码,默认1 */
+    private Integer pageNum;
+
+    /** 每页条数,默认10 */
+    private Integer pageSize;
+
+}

+ 0 - 1
ruoyi-api/ruoyi-api-system/pom.xml

@@ -27,7 +27,6 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-excel</artifactId>
         </dependency>
-
     </dependencies>
 
 </project>

+ 35 - 0
ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java

@@ -2,7 +2,9 @@ package org.dromara.system.api;
 
 import cn.hutool.core.lang.tree.Tree;
 import org.dromara.common.core.domain.R;
+import org.dromara.system.api.domain.PageResult;
 import org.dromara.system.api.domain.bo.RemoteDeptBo;
+import org.dromara.system.api.domain.dto.RemoteClassDto;
 import org.dromara.system.api.domain.vo.RemoteDeptVo;
 
 import java.util.Date;
@@ -125,4 +127,37 @@ public interface RemoteDeptService {
     Boolean deleteLocalDept(Long deptId);
 
     List<RemoteDeptVo> selectDeptList();
+
+    /**
+     * 查询班级导航树
+     *
+     * @return 导航树
+     */
+    List<Tree<Long>> selectClassNavTree();
+
+    /**
+     * 按祖先节点分页查询班级列表
+     *
+     * @param ancestorId 选中的导航树节点ID
+     * @param deptName   班级名称(模糊搜索,可选)
+     * @param pageNum    页码
+     * @param pageSize   每页条数
+     * @return 班级分页数据
+     */
+    PageResult<RemoteClassDto> selectClassPage(Long ancestorId, String deptName, Integer pageNum, Integer pageSize);
+
+    /**
+     * 根据部门ID查询班级详情
+     *
+     * @param deptId 部门ID
+     * @return 班级信息
+     */
+    RemoteClassDto selectClassById(Long deptId);
+
+    /**
+     * 查询所有培训班列表
+     *
+     * @return 培训班列表
+     */
+    List<RemoteClassDto> selectAllClassList();
 }

+ 62 - 0
ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/PageResult.java

@@ -0,0 +1,62 @@
+package org.dromara.system.api.domain;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页结果封装(API层专用,不依赖MyBatis)
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class PageResult<T> implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 总记录数
+     */
+    private long total;
+
+    /**
+     * 列表数据
+     */
+    private List<T> rows;
+
+    /**
+     * 消息状态码
+     */
+    private int code;
+
+    /**
+     * 消息内容
+     */
+    private String msg;
+
+    /**
+     * 构造分页结果
+     *
+     * @param rows  列表数据
+     * @param total 总记录数
+     */
+    public PageResult(List<T> rows, long total) {
+        this.rows = rows;
+        this.total = total;
+        this.code = 200;
+        this.msg = "查询成功";
+    }
+
+    /**
+     * 静态工厂方法
+     */
+    public static <T> PageResult<T> build(List<T> rows, long total) {
+        return new PageResult<>(rows, total);
+    }
+
+}

+ 91 - 0
ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/dto/RemoteClassDto.java

@@ -0,0 +1,91 @@
+package org.dromara.system.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 班级信息DTO
+ * 用于Dubbo远程调用返回班级信息(包含新增字段)
+ *
+ * @author luoyibo
+ */
+@Data
+public class RemoteClassDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 部门id
+     */
+    private Long deptId;
+
+    /**
+     * 父部门id
+     */
+    private Long parentId;
+
+     /**
+     * 部门名称(班级名称)
+     */
+    private String deptName;
+
+
+    /**
+     * 唯一标识
+     */
+    private String otherId;
+
+    /**
+     * 开班日期
+     */
+    private Date beginDate;
+
+    /**
+     * 结业日期
+     */
+    private Date endDate;
+
+    /**
+     * 报到日期
+     */
+    private Date checkDate;
+
+    /**
+     * 班主任用户ID
+     */
+    private Long managerId;
+
+    /**
+     * 班主任用户名称
+     */
+    private String managerName;
+
+    /**
+     * 计划人数
+     */
+    private Integer planCount;
+
+    /**
+     * 教室ID
+     */
+    private Long roomId;
+
+    /**
+     * 教室名称
+     */
+    private String roomName;
+
+    /**
+     * 数据来源:0-本地,1-同步
+     */
+    private Integer dataSource;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+}

+ 1 - 1
ruoyi-auth/src/main/resources/application.yml

@@ -1,6 +1,6 @@
 # Tomcat
 server:
-  port: 9210
+  port: 9211
 
 # Spring
 spring:

+ 1 - 0
ruoyi-modules/pom.xml

@@ -16,6 +16,7 @@
         <module>ruoyi-job</module>
         <module>ruoyi-resource</module>
         <module>ruoyi-workflow</module>
+        <module>ruoyi-ecs</module>
     </modules>
 
     <artifactId>ruoyi-modules</artifactId>

+ 3 - 3
ruoyi-modules/ruoyi-backstage/pom.xml

@@ -162,11 +162,11 @@
         </dependency>
 
         <!-- 人脸识别 -->
-        <!--<dependency>
+        <dependency>
             <groupId>com.arcsoft.face</groupId>
             <artifactId>arcsoft-sdk-face</artifactId>
-            <version>1.1.1.0</version>
-        </dependency>-->
+            <version>4.1.1.0</version>
+        </dependency>
         <dependency>
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-api-consume</artifactId>

+ 14 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/PtRoom.java

@@ -101,5 +101,19 @@ public class PtRoom extends TenantEntity {
      */
     private String status;
 
+    /**
+     * 数据来源:0-本地,1-第三方同步
+     */
+    private String dataSource;
+
+    /**
+     * 第三方系统原始ID
+     */
+    private String otherId;
+
+    /**
+     * 容纳人数
+     */
+    private Integer capacity;
 
 }

+ 14 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/bo/PtRoomBo.java

@@ -97,5 +97,19 @@ public class PtRoomBo extends TenantEntity {
      */
     private String status;
 
+    /**
+     * 数据来源:0-本地,1-第三方同步
+     */
+    private String dataSource;
+
+    /**
+     * 第三方系统原始ID
+     */
+    private String otherId;
+
+    /**
+     * 容纳人数
+     */
+    private Integer capacity;
 
 }

+ 14 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/vo/PtRoomVo.java

@@ -102,5 +102,19 @@ public class PtRoomVo implements Serializable {
      */
     private String lockPassword;
 
+    /**
+     * 数据来源:0-本地,1-第三方同步
+     */
+    private String dataSource;
+
+    /**
+     * 第三方系统原始ID
+     */
+    private String otherId;
+
+    /**
+     * 容纳人数
+     */
+    private Integer capacity;
 
 }

+ 88 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java

@@ -6,12 +6,17 @@ import org.apache.dubbo.config.annotation.DubboService;
 import org.dromara.backstage.api.RemotePtRoomService;
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBatchSetBo;
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBo;
+import org.dromara.backstage.api.domain.dto.RemoteClassroomDto;
+import org.dromara.backstage.api.domain.dto.RemoteClassroomQueryDto;
 import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
 import org.dromara.backstage.basics.domain.bo.PtRoomBatchSetBo;
 import org.dromara.backstage.basics.domain.bo.PtRoomBo;
 import org.dromara.backstage.basics.domain.vo.PtRoomVo;
 import org.dromara.backstage.basics.service.IPtRoomService;
+import org.dromara.common.core.enums.FJLXEnum;
 import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.springframework.stereotype.Service;
 
@@ -27,6 +32,9 @@ import java.util.List;
 public class RemotePtRoomServiceImpl implements RemotePtRoomService {
 
     private final  IPtRoomService roomService;
+
+    /** 教室类型编码 */
+    private static final String CLASSROOM_TYPE = FJLXEnum.JS.code();
     @Override
     public Boolean insertByBo(RemotePtRoomBo bo) throws Exception {
         return roomService.insertByBo(BeanUtil.toBean(bo, PtRoomBo.class));
@@ -71,4 +79,84 @@ public class RemotePtRoomServiceImpl implements RemotePtRoomService {
         List<PtRoomVo> list = roomService.queryList();
         return MapstructUtils.convert(list,RemotePtRoomVo.class);
     }
+
+    // ==================== 教室查询方法 ====================
+
+    /**
+     * 查询教室列表(不分页)
+     */
+    @Override
+    public List<RemoteClassroomDto> selectClassroomList(RemoteClassroomQueryDto queryDto) {
+        PtRoomBo bo = convertToBo(queryDto);
+        bo.setRoomType(CLASSROOM_TYPE);
+        List<PtRoomVo> list = roomService.selectList(bo);
+        return list.stream().map(this::convertToDto).toList();
+    }
+
+    /**
+     * 分页查询教室列表
+     */
+    @Override
+    public TableDataInfo<RemoteClassroomDto> selectClassroomPage(RemoteClassroomQueryDto queryDto) {
+        PtRoomBo bo = convertToBo(queryDto);
+        bo.setRoomType(CLASSROOM_TYPE);
+
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+        pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+
+        TableDataInfo<PtRoomVo> result = roomService.queryPageList(bo, pageQuery);
+
+        List<RemoteClassroomDto> dtoList = result.getRows().stream().map(this::convertToDto).toList();
+        TableDataInfo<RemoteClassroomDto> pageData = TableDataInfo.build();
+        pageData.setRows(dtoList);
+        pageData.setTotal(result.getTotal());
+        return pageData;
+    }
+
+    /**
+     * 根据ID查询教室
+     */
+    @Override
+    public RemoteClassroomDto selectClassroomById(Long roomId) {
+        PtRoomVo vo = roomService.queryById(roomId);
+        if (vo == null) {
+            return null;
+        }
+        if (!CLASSROOM_TYPE.equals(vo.getRoomType())) {
+            return null;
+        }
+        return convertToDto(vo);
+    }
+
+    // ==================== 转换方法 ====================
+
+    /**
+     * QueryDto → Bo 转换
+     */
+    private PtRoomBo convertToBo(RemoteClassroomQueryDto queryDto) {
+        PtRoomBo bo = new PtRoomBo();
+        bo.setRoomCode(queryDto.getRoomCode());
+        bo.setRoomName(queryDto.getRoomName());
+        return bo;
+    }
+
+    /**
+     * PtRoomVo → RemoteClassroomDto 转换
+     */
+    private RemoteClassroomDto convertToDto(PtRoomVo vo) {
+        if (vo == null) {
+            return null;
+        }
+        RemoteClassroomDto dto = new RemoteClassroomDto();
+        dto.setRoomId(vo.getRoomId());
+        dto.setAreaId(vo.getAreaId());
+        dto.setAreaName(vo.getAreaName());
+        dto.setRoomCode(vo.getRoomCode());
+        dto.setRoomName(vo.getRoomName());
+        dto.setCapacity(vo.getCapacity());
+        dto.setCodeOne(vo.getCodeOne());
+        dto.setOtherId(vo.getOtherId());
+        return dto;
+    }
 }

+ 1 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/SelfController.java

@@ -20,6 +20,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
+ * 自助终端服务公共控制器
  * @ClassName SelfController
  * @Description TODO
  * @Author luoyibo

+ 1 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/TeacherController.java

@@ -22,6 +22,7 @@ import java.text.MessageFormat;
 import java.util.Map;
 
 /**
+ * 自助终端-教职工服务控制器
  * @ClassName TeacherController
  * @Description 教职工自助控制器
  * @Author luoyibo

+ 1 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/TraineeController.java

@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.Map;
 
 /**
+ * 自助终端-学员服务控制器
  * @ClassName TraineeController
  * @Description 学员自助控制器
  * @Author luoyibo

+ 178 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/dubbo/RemoteUserAccountServiceImpl.java

@@ -6,6 +6,10 @@ import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.dromara.backstage.api.RemoteUserAccountService;
 import org.dromara.backstage.api.domain.bo.RemoteUserAccountBo;
+import org.dromara.backstage.api.domain.dto.RemoteTeacherDto;
+import org.dromara.backstage.api.domain.dto.RemoteTeacherQueryDto;
+import org.dromara.backstage.api.domain.dto.RemoteTraineeDto;
+import org.dromara.backstage.api.domain.dto.RemoteTraineeQueryDto;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.backstage.business.accouunt.UserAccountBusiness;
 import org.dromara.backstage.domain.vo.card.PtCardVo;
@@ -15,6 +19,8 @@ import org.dromara.backstage.payment.service.IPtUserAccountService;
 import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
@@ -230,5 +236,177 @@ public class RemoteUserAccountServiceImpl implements RemoteUserAccountService {
         return MapstructUtils.convert(list, RemoteUserAccountVo.class);
     }
 
+    // ==================== 教师查询方法 ====================
+
+    /** 教师类型编码 */
+    private static final String TEACHER_CATEGORY = "1";
+
+    /**
+     * 分页查询教师信息列表
+     */
+    @Override
+    public TableDataInfo<RemoteTeacherDto> selectTeacherPage(RemoteTeacherQueryDto queryDto) {
+        PtUserAccountBo bo = new PtUserAccountBo();
+        bo.setCategory(TEACHER_CATEGORY);
+        if (queryDto.getRealName() != null) {
+            bo.setRealName(queryDto.getRealName());
+        }
+        if (queryDto.getPhone() != null) {
+            bo.setPhone(queryDto.getPhone());
+        }
+
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+        pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+
+        TableDataInfo<PtUserAccountVo> result = userAccountService.queryPageList(bo, pageQuery);
+
+        List<RemoteTeacherDto> dtoList = result.getRows().stream().map(this::convertToTeacherDto).toList();
+        TableDataInfo<RemoteTeacherDto> pageData = TableDataInfo.build();
+        pageData.setRows(dtoList);
+        pageData.setTotal(result.getTotal());
+        return pageData;
+    }
+
+    /**
+     * 根据ID查询教师信息
+     */
+    @Override
+    public R<RemoteTeacherDto> selectTeacherById(Long userId) {
+        PtUserAccountVo vo = userAccountService.queryById(userId);
+        if (vo == null || !TEACHER_CATEGORY.equals(vo.getCategory())) {
+            return R.fail("未找到对应的教师信息");
+        }
+        return R.ok(convertToTeacherDto(vo));
+    }
+
+    /**
+     * PtUserAccountVo → RemoteTeacherDto 转换
+     */
+    private RemoteTeacherDto convertToTeacherDto(PtUserAccountVo vo) {
+        if (vo == null) {
+            return null;
+        }
+        RemoteTeacherDto dto = new RemoteTeacherDto();
+        dto.setUserId(vo.getUserId());
+        dto.setRealName(vo.getRealName());
+        dto.setDeptName(vo.getDeptName());
+        dto.setSex(vo.getSex());
+        dto.setPhone(vo.getPhone());
+        return dto;
+    }
+
+    // ==================== 学员查询方法 ====================
+
+    /** 学员类型编码 */
+    private static final String TRAINEE_CATEGORY = "2";
+
+    /**
+     * 分页查询学员信息列表
+     */
+    @Override
+    public TableDataInfo<RemoteTraineeDto> selectTraineePage(RemoteTraineeQueryDto queryDto) {
+        PtUserAccountBo bo = new PtUserAccountBo();
+        bo.setCategory(TRAINEE_CATEGORY);
+        if (queryDto.getRealName() != null) {
+            bo.setRealName(queryDto.getRealName());
+        }
+        if (queryDto.getPhone() != null) {
+            bo.setPhone(queryDto.getPhone());
+        }
+        if (queryDto.getDeptId() != null) {
+            bo.setDeptId(queryDto.getDeptId());
+        }
+
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+        pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+
+        TableDataInfo<PtUserAccountVo> result = userAccountService.queryPageList(bo, pageQuery);
+
+        List<RemoteTraineeDto> dtoList = result.getRows().stream().map(this::convertToTraineeDto).toList();
+        TableDataInfo<RemoteTraineeDto> pageData = TableDataInfo.build();
+        pageData.setRows(dtoList);
+        pageData.setTotal(result.getTotal());
+        return pageData;
+    }
+
+    /**
+     * 根据培训班ID通过t_user_dept关系表分页查询学员列表
+     */
+    @Override
+    public TableDataInfo<RemoteTraineeDto> selectTraineePageByDeptId(RemoteTraineeQueryDto queryDto) {
+        PtUserAccountBo bo = new PtUserAccountBo();
+        bo.setCategory(TRAINEE_CATEGORY);
+        if (queryDto.getDeptId() != null) {
+            bo.setDeptId(queryDto.getDeptId());
+        }
+        if (queryDto.getRealName() != null) {
+            bo.setRealName(queryDto.getRealName());
+        }
+        if (queryDto.getPhone() != null) {
+            bo.setPhone(queryDto.getPhone());
+        }
+
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1);
+        pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10);
+
+        TableDataInfo<PtUserAccountVo> result = userAccountService.queryTraineePageByDeptId(bo, pageQuery);
+        TableDataInfo<RemoteTraineeDto> pageData = TableDataInfo.build();
+        pageData.setTotal(result.getTotal());
+        if (result.getRows() != null) {
+            List<RemoteTraineeDto> dtoList = result.getRows().stream().map(this::convertToTraineeDto).toList();
+            pageData.setRows(dtoList);
+        } else {
+            pageData.setRows(null);
+        }
+
+        return pageData;
+    }
+
+    /**
+     * 根据ID查询学员信息
+     */
+    @Override
+    public R<RemoteTraineeDto> selectTraineeById(Long userId) {
+        PtUserAccountVo vo = userAccountService.queryById(userId);
+        if (vo == null || !TRAINEE_CATEGORY.equals(vo.getCategory())) {
+            return R.fail("未找到对应的学员信息");
+        }
+        return R.ok(convertToTraineeDto(vo));
+    }
+
+    /**
+     * PtUserAccountVo → RemoteTraineeDto 转换
+     */
+    private RemoteTraineeDto convertToTraineeDto(PtUserAccountVo vo) {
+        if (vo == null) {
+            return null;
+        }
+        RemoteTraineeDto dto = new RemoteTraineeDto();
+        dto.setUserId(vo.getUserId());
+        dto.setRealName(vo.getRealName());
+        dto.setDeptName(vo.getDeptName());
+        dto.setDeptId(vo.getDeptId());
+        dto.setSex(vo.getSex());
+        dto.setSexName(convertSexName(vo.getSex()));
+        dto.setPhone(vo.getPhone());
+        dto.setOtherId(vo.getOtherId());
+        dto.setIdCard(vo.getIdNumber() != null ? vo.getIdNumber() : "");
+        return dto;
+    }
+
+    /**
+     * 性别编码转中文
+     */
+    private String convertSexName(String sex) {
+        if ("1".equals(sex)) {
+            return "男";
+        } else if ("2".equals(sex)) {
+            return "女";
+        }
+        return "未知";
+    }
 
 }

+ 6 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/mapper/PtUserAccountMapper.java

@@ -46,6 +46,12 @@ public interface PtUserAccountMapper extends BaseMapperPlus<PtUserAccount, PtUse
 
     YcTraineeVo selectTraineeByBo(@Param("bo") PtUserAccountBo bo, @Param("doingDate") Date doingDate);
 
+    /**
+     * 根据部门ID通过t_user_dept关系表查询学员userId列表
+     */
+    List<Long> selectUserIdsByDeptId(@Param("deptId") Long deptId, @Param("realName") String realName,
+                                     @Param("phone") String phone, @Param("idNumber") String idNumber);
+
     List<PtUserAccount4SelectVo> getCardInfoByFactoryId (@Param("factoryId") String factoryId);
 
     @Select("select * from t_pt_userAccount where del_flag = #{delFlag} and update_time between #{startDate} and #{endDate}")

+ 9 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/IPtUserAccountService.java

@@ -183,6 +183,15 @@ public interface IPtUserAccountService {
 
     YcTraineeVo selectTraineeByBo(PtUserAccountBo bo, Date doingDate);
 
+    /**
+     * 根据部门ID通过t_user_dept关系表分页查询学员列表
+     *
+     * @param bo        查询条件(必须包含deptId)
+     * @param pageQuery 分页参数
+     * @return 学员分页列表
+     */
+    TableDataInfo<PtUserAccountVo> queryTraineePageByDeptId(PtUserAccountBo bo, PageQuery pageQuery);
+
     /**
      * 删除指定部门下的一卡通账户信息
      * @param deptId 部门Id

+ 34 - 2
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtUserAccountServiceImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.backstage.payment.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.lang.UUID;
@@ -14,7 +15,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
-import org.dromara.backstage.basics.domain.bo.PtCardtypeBo;
 import org.dromara.backstage.basics.domain.vo.PtCardtypeVo;
 import org.dromara.backstage.basics.service.IPtCardtypeService;
 import org.dromara.backstage.cardCenter.domain.bo.PtCardBo;
@@ -32,7 +32,6 @@ import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.constant.DefaultConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.enums.CardStatusEnum;
-import org.dromara.common.core.enums.UserAccountStatusEnum;
 import org.dromara.common.core.service.DictService;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.SpringUtils;
@@ -556,9 +555,42 @@ public class PtUserAccountServiceImpl implements IPtUserAccountService {
 
     @Override
     public YcTraineeVo selectTraineeByBo(PtUserAccountBo bo, Date doingDate) {
+
+        YcTraineeVo vo = baseMapper.selectTraineeByBo(bo, doingDate);
         return TenantHelper.ignore(() -> baseMapper.selectTraineeByBo(bo, doingDate));
     }
 
+    /**
+     * 根据部门ID通过t_user_dept关系表分页查询学员列表
+     * 第一步:通过t_user_dept查出该班级下的userId列表
+     * 第二步:用MyBatis-Plus内置selectVoPage查询,@EncryptField解密注解自动生效
+     */
+    @Override
+    public TableDataInfo<PtUserAccountVo> queryTraineePageByDeptId(PtUserAccountBo bo, PageQuery pageQuery) {
+        // 加密字段处理(自定义SQL不走buildQueryWrapper,需手动加密)
+        MybatisEncryptInterceptor encryptInterceptor = SpringUtils.getBean(MybatisEncryptInterceptor.class);
+        String encryptedPhone = encryptInterceptor.encrypt(bo.getPhone());
+        String encryptedIdNumber = encryptInterceptor.encrypt(bo.getIdNumber());
+        // 第一步:通过t_user_dept查出该班级下的userId列表
+        List<Long> userIds = TenantHelper.ignore(() ->
+            baseMapper.selectUserIdsByDeptId(bo.getDeptId(), bo.getRealName(), encryptedPhone, encryptedIdNumber)
+        );
+        if (CollUtil.isEmpty(userIds)) {
+            return TableDataInfo.build();
+        }
+        // 第二步:用selectVoPage查询,解密注解生效
+        LambdaQueryWrapper<PtUserAccount> lqw = Wrappers.lambdaQuery();
+        lqw.in(PtUserAccount::getUserId, userIds)
+            .eq(PtUserAccount::getCategory, bo.getCategory())
+            .isNotNull(PtUserAccount::getLifespan)
+            .orderByAsc(PtUserAccount::getRealName);
+        Page<PtUserAccountVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        List<PtUserAccountVo> list = result.getRecords();
+        addOtherInfoForList(list);
+        result.setRecords(list);
+        return TableDataInfo.build(result);
+    }
+
     /**
      * 删除指定部门下的一卡通账户信息
      *

+ 5 - 6
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/task/InitRunner.java

@@ -5,7 +5,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.dromara.backstage.business.InitServiceBusiness;
 import org.springframework.boot.ApplicationArguments;
 import org.springframework.boot.ApplicationRunner;
-import org.springframework.boot.CommandLineRunner;
 import org.springframework.stereotype.Component;
 
 /**
@@ -26,10 +25,10 @@ public class InitRunner implements ApplicationRunner {
 
     @Override
     public void run(ApplicationArguments args) throws Exception {
-        initServiceBusiness.initGlobalData();
-        initServiceBusiness.initMealTypeInfo();
-        initServiceBusiness.initDiscountAndOther();
-        initServiceBusiness.initUserCard();
-        initServiceBusiness.initUserAccount();
+        // initServiceBusiness.initGlobalData();
+        // initServiceBusiness.initMealTypeInfo();
+        // initServiceBusiness.initDiscountAndOther();
+        // initServiceBusiness.initUserCard();
+        // initServiceBusiness.initUserAccount();
     }
 }

+ 3 - 0
ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/basics/room/PtRoomMapper.xml

@@ -20,6 +20,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <result property="createTime"    column="create_time"    />
             <result property="updateBy"    column="update_by"    />
             <result property="updateTime"    column="update_time"    />
+            <result property="dataSource"    column="data_source"    />
+            <result property="otherId"       column="other_id"       />
+            <result property="capacity"      column="capacity"       />
     </resultMap>
 
     <select id="selectRoomLockList" resultType="org.dromara.backstage.basics.domain.vo.PtRoomVo">

+ 22 - 0
ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/payment/PtUserAccountMapper.xml

@@ -121,6 +121,28 @@
 
     </select>
 
+    <select id="selectUserIdsByDeptId" resultType="java.lang.Long">
+        SELECT DISTINCT tpuA.user_id
+        FROM t_pt_userAccount tpuA
+        WHERE tpuA.del_flag = '0'
+          AND tpuA.category = '2'
+          AND EXISTS (
+            SELECT 1 FROM t_user_dept ud
+            WHERE ud.user_id = tpuA.user_id
+              AND ud.dept_id = #{deptId}
+              AND ud.del_flag = '0'
+          )
+          <if test="realName != null and realName != ''">
+            AND tpuA.real_name LIKE CONCAT('%', #{realName}, '%')
+          </if>
+          <if test="phone != null and phone != ''">
+            AND tpuA.phone = #{phone}
+          </if>
+          <if test="idNumber != null and idNumber != ''">
+            AND tpuA.id_number = #{idNumber}
+          </if>
+    </select>
+
     <select id="getCardInfoByFactoryId" resultType="org.dromara.backstage.payment.domain.PtUserAccount4SelectVo">
         SELECT a.real_name as realName, a.phone, b.dept_name as deptName, a.lifespan,c.create_time as createTime, c.status as cardStatus
         FROM t_pt_userAccount a

+ 24 - 0
ruoyi-modules/ruoyi-ecs/Dockerfile

@@ -0,0 +1,24 @@
+# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
+FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
+#FROM bellsoft/liberica-openjdk-debian:21.0.3-cds
+#FROM findepi/graalvm:java17-native
+
+LABEL maintainer="Lion Li"
+
+RUN mkdir -p /ruoyi/ecs/logs \
+    /ruoyi/ecs/temp \
+    /ruoyi/skywalking/agent
+
+WORKDIR /ruoyi/ecs
+
+ENV SERVER_PORT=9210 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
+
+EXPOSE ${SERVER_PORT}
+
+ADD ./target/ruoyi-ecs.jar ./app.jar
+
+ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
+           #-Dskywalking.agent.service_name=ruoyi-ecs \
+           #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
+           -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
+           -jar app.jar

+ 117 - 0
ruoyi-modules/ruoyi-ecs/pom.xml

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.dromara</groupId>
+        <artifactId>ruoyi-modules</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-ecs</artifactId>
+
+    <description>
+        ruoyi-ecs 电子班牌管理系统
+    </description>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-nacos</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-sentinel</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-log</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-dict</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-doc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-dubbo</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-tenant</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-translation</artifactId>
+        </dependency>
+
+        <!-- ECS API -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-ecs</artifactId>
+        </dependency>
+
+        <!-- Backstage API(Dubbo消费) -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-backstage</artifactId>
+        </dependency>
+
+        <!-- System API -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-system</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-idempotent</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 22 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/RuoYiEcsApplication.java

@@ -0,0 +1,22 @@
+package org.dromara.ecs;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
+
+/**
+ * 电子班牌管理系统
+ *
+ * @author ruoyi
+ */
+@EnableDubbo
+@SpringBootApplication
+public class RuoYiEcsApplication {
+    public static void main(String[] args) {
+        SpringApplication application = new SpringApplication(RuoYiEcsApplication.class);
+        application.setApplicationStartup(new BufferingApplicationStartup(2048));
+        application.run(args);
+        System.out.println("(♥◠‿◠)ノ゙  电子班牌管理模块启动成功   ლ(´ڡ`ლ)゙  ");
+    }
+}

+ 107 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/AttendController.java

@@ -0,0 +1,107 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.ecs.domain.bo.AttendForm;
+import org.dromara.ecs.domain.bo.EcsAttendBo;
+import org.dromara.ecs.domain.vo.EcsAttendVo;
+import org.dromara.ecs.service.IEcsAttendService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 考勤管理
+ * 前端访问路由地址为:/ecs/attend
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/attend")
+public class AttendController extends BaseController {
+
+    private final IEcsAttendService ecsAttendService;
+
+    /**
+     * 查询考勤记录列表
+     */
+    @SaCheckPermission("ecs:attend:list")
+    @GetMapping("/list")
+    public TableDataInfo<EcsAttendVo> list(EcsAttendBo bo, PageQuery pageQuery) {
+        return ecsAttendService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出考勤记录列表
+     */
+    @SaCheckPermission("ecs:attend:export")
+    @Log(title = "考勤管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(EcsAttendBo bo, HttpServletResponse response) {
+        List<EcsAttendVo> list = ecsAttendService.queryList(bo);
+        ExcelUtil.exportExcel(list, "考勤记录", EcsAttendVo.class, response);
+    }
+
+    /**
+     * 获取考勤记录详细信息
+     *
+     * @param attendId 主键
+     */
+    @SaCheckPermission("ecs:attend:query")
+    @GetMapping("/{attendId}")
+    public R<EcsAttendVo> getInfo(@NotNull(message = "主键不能为空")
+                                   @PathVariable Long attendId) {
+        return R.ok(ecsAttendService.queryById(attendId));
+    }
+
+    /**
+     * 新增考勤记录(手工考勤)
+     */
+    @SaCheckPermission("ecs:attend:add")
+    @Log(title = "手工考勤", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody EcsAttendBo bo) {
+        return toAjax(ecsAttendService.insertByBo(bo));
+    }
+
+    /**
+     * 手工批量考勤(支持多人同时考勤)
+     */
+    @SaCheckPermission("ecs:attend:add")
+    @Log(title = "手工批量考勤", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/batch")
+    public R<Void> batchAdd(@Validated(AddGroup.class) @RequestBody AttendForm form) {
+        return toAjax(ecsAttendService.batchInsertByForm(form));
+    }
+
+    /**
+     * 推送考勤记录到第三方系统
+     *
+     * @param attendIds 考勤记录ID列表
+     * @return 本次成功推送数
+     */
+    @SaCheckPermission("ecs:attend:add")
+    @Log(title = "推送考勤记录", businessType = BusinessType.UPDATE)
+    @PostMapping("/push")
+    public R<Integer> push(@RequestBody List<Long> attendIds) {
+        return ecsAttendService.pushByIds(attendIds);
+    }
+
+}

+ 143 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/AttendRuleController.java

@@ -0,0 +1,143 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.ecs.domain.bo.EcsAttendRuleBo;
+import org.dromara.ecs.domain.vo.EcsAttendRuleImportVo;
+import org.dromara.ecs.domain.vo.EcsAttendRuleVo;
+import org.dromara.ecs.listener.EcsAttendRuleImportListener;
+import org.dromara.ecs.service.IEcsAttendRuleService;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 考勤规则
+ * 前端访问路由地址为:/ecs/attenRule
+ *
+ * @author ruoyi
+ * @date 2026-04-22
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/attenRule")
+public class AttendRuleController extends BaseController {
+
+    private final IEcsAttendRuleService ecsAttendRuleService;
+
+    /**
+     * 查询考勤规则列表
+     */
+    @SaCheckPermission("ecs:attendRules:list")
+    @GetMapping("/list")
+    public TableDataInfo<EcsAttendRuleVo> list(EcsAttendRuleBo bo, PageQuery pageQuery) {
+        return ecsAttendRuleService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出考勤规则列表
+     */
+    @SaCheckPermission("ecs:attendRules:export")
+    @Log(title = "考勤规则", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(EcsAttendRuleBo bo, HttpServletResponse response) {
+        List<EcsAttendRuleVo> list = ecsAttendRuleService.queryList(bo);
+        ExcelUtil.exportExcel(list, "考勤规则", EcsAttendRuleVo.class, response);
+    }
+
+    /**
+     * 获取考勤规则详细信息
+     *
+     * @param attendRuleId 主键
+     */
+    @SaCheckPermission("ecs:attendRules:query")
+    @GetMapping("/{attendRuleId}")
+    public R<EcsAttendRuleVo> getInfo(@NotNull(message = "主键不能为空")
+                                     @PathVariable Long attendRuleId) {
+        return R.ok(ecsAttendRuleService.queryById(attendRuleId));
+    }
+
+    /**
+     * 新增考勤规则
+     */
+    @SaCheckPermission("ecs:attendRules:add")
+    @Log(title = "考勤规则", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody EcsAttendRuleBo bo) {
+        return toAjax(ecsAttendRuleService.insertByBo(bo));
+    }
+
+    /**
+     * 修改考勤规则
+     */
+    @SaCheckPermission("ecs:attendRules:edit")
+    @Log(title = "考勤规则", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody EcsAttendRuleBo bo) {
+        return toAjax(ecsAttendRuleService.updateByBo(bo));
+    }
+
+    /**
+     * 删除考勤规则
+     *
+     * @param attendRuleIds 主键串
+     */
+    @SaCheckPermission("ecs:attendRules:remove")
+    @Log(title = "考勤规则", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{attendRuleIds}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] attendRuleIds) {
+        return toAjax(ecsAttendRuleService.deleteWithValidByIds(List.of(attendRuleIds), true));
+    }
+
+    /**
+     * 导出考勤规则导入模板
+     */
+    @SaCheckPermission("ecs:attendRules:export")
+    @Log(title = "考勤规则导入模板", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportTemplate")
+    public void exportTemplate(HttpServletResponse response) {
+        List<EcsAttendRuleImportVo> list = new ArrayList<>();
+        ExcelUtil.exportExcel(list, "考勤规则导入模板", EcsAttendRuleImportVo.class, response);
+    }
+
+    /**
+     * 导入考勤规则数据
+     *
+     * @param file Excel文件
+     * @param updateSupport 是否更新已存在数据
+     */
+    @SaCheckPermission("ecs:attendRules:import")
+    @Log(title = "考勤规则导入", businessType = BusinessType.IMPORT)
+    @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
+        ExcelResult<EcsAttendRuleImportVo> result = ExcelUtil.importExcel(
+            file.getInputStream(),
+            EcsAttendRuleImportVo.class,
+            new EcsAttendRuleImportListener(updateSupport)
+        );
+        return R.ok(result.getAnalysis());
+    }
+
+}

+ 70 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/ClassController.java

@@ -0,0 +1,70 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.lang.tree.Tree;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.system.api.RemoteDeptService;
+import org.dromara.system.api.domain.PageResult;
+import org.dromara.system.api.domain.dto.RemoteClassDto;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 班级信息
+ * 前端访问路由地址为:/ecs/class
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/class")
+public class ClassController extends BaseController {
+
+    @DubboReference
+    private RemoteDeptService remoteDeptService;
+
+    /**
+     * 查询班级导航树
+     */
+    @SaCheckPermission("ecs:class:list")
+    @GetMapping("/nav-tree")
+    public R<List<Tree<Long>>> navTree() {
+        return R.ok(remoteDeptService.selectClassNavTree());
+    }
+
+    /**
+     * 查询班级信息列表(分页)
+     */
+    @SaCheckPermission("ecs:class:list")
+    @GetMapping("/list")
+    public PageResult<RemoteClassDto> list(
+        @RequestParam(required = false) Long ancestorId,
+        @RequestParam(required = false) String deptName,
+        @RequestParam(defaultValue = "1") Integer pageNum,
+        @RequestParam(defaultValue = "10") Integer pageSize) {
+        return remoteDeptService.selectClassPage(ancestorId, deptName, pageNum, pageSize);
+    }
+
+    /**
+     * 获取班级详细信息
+     */
+    @SaCheckPermission("ecs:class:list")
+    @GetMapping("/{deptId}")
+    public R<RemoteClassDto> getInfo(@PathVariable Long deptId) {
+        return R.ok(remoteDeptService.selectClassById(deptId));
+    }
+
+    /**
+     * 获取班级下拉选项
+     */
+    @SaCheckPermission("ecs:class:list")
+    @GetMapping("/class-options")
+    public R<List<RemoteClassDto>> classOptions() {
+        List<RemoteClassDto> list = remoteDeptService.selectAllClassList();
+        return R.ok(list);
+    }
+}

+ 48 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/ClassroomController.java

@@ -0,0 +1,48 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemotePtRoomService;
+import org.dromara.backstage.api.domain.dto.RemoteClassroomDto;
+import org.dromara.backstage.api.domain.dto.RemoteClassroomQueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 教室信息
+ * 前端访问路由地址为:/ecs/room
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/room")
+public class ClassroomController extends BaseController {
+
+    @DubboReference
+    private RemotePtRoomService remotePtRoomService;
+
+    /**
+     * 查询教室信息列表
+     */
+    @SaCheckPermission("ecs:room:list")
+    @GetMapping("/list")
+    public TableDataInfo<RemoteClassroomDto> list(RemoteClassroomQueryDto queryDto) {
+        return remotePtRoomService.selectClassroomPage(queryDto);
+    }
+
+    /**
+     * 获取教室详细信息
+     */
+    @SaCheckPermission("ecs:room:list")
+    @GetMapping("/{roomId}")
+    public R<RemoteClassroomDto> getInfo(@PathVariable Long roomId) {
+        return R.ok(remotePtRoomService.selectClassroomById(roomId));
+    }
+}

+ 144 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/CourseController.java

@@ -0,0 +1,144 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.ecs.domain.bo.EcsCourseBo;
+import org.dromara.ecs.domain.vo.EcsCourseImportVo;
+import org.dromara.ecs.domain.vo.EcsCourseVo;
+import org.dromara.ecs.listener.EcsCourseImportListener;
+import org.dromara.ecs.service.IEcsCourseService;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 课程管理
+ * 前端访问路由地址为:/ecs/course
+ *
+ * @author ruoyi
+ * @date 2026-04-15
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/course")
+public class CourseController extends BaseController {
+
+    private final IEcsCourseService courseService;
+
+    /**
+     * 查询课程列表
+     */
+    @SaCheckPermission("ecs:course:list")
+    @GetMapping("/list")
+    public TableDataInfo<EcsCourseVo> list(EcsCourseBo bo, PageQuery pageQuery) {
+        return courseService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出课程列表
+     */
+    @SaCheckPermission("ecs:course:export")
+    @Log(title = "课程管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(EcsCourseBo bo, HttpServletResponse response) {
+        List<EcsCourseVo> list = courseService.queryList(bo);
+        ExcelUtil.exportExcel(list, "课程管理", EcsCourseVo.class, response);
+    }
+
+    /**
+     * 获取课程详细信息
+     *
+     * @param courseId 主键
+     */
+    @SaCheckPermission("ecs:course:query")
+    @GetMapping("/{courseId}")
+    public R<EcsCourseVo> getInfo(@NotNull(message = "主键不能为空")
+                                   @PathVariable Long courseId) {
+        return R.ok(courseService.queryById(courseId));
+    }
+
+    /**
+     * 新增课程
+     */
+    @SaCheckPermission("ecs:course:add")
+    @Log(title = "课程管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody EcsCourseBo bo) {
+        bo.setDataSource(0);
+        return toAjax(courseService.insertByBo(bo));
+    }
+
+    /**
+     * 修改课程
+     */
+    @SaCheckPermission("ecs:course:edit")
+    @Log(title = "课程管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody EcsCourseBo bo) {
+        return toAjax(courseService.updateByBo(bo));
+    }
+
+    /**
+     * 删除课程
+     *
+     * @param courseIds 主键串
+     */
+    @SaCheckPermission("ecs:course:remove")
+    @Log(title = "课程管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{courseIds}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                           @PathVariable Long[] courseIds) {
+        return toAjax(courseService.deleteWithValidByIds(List.of(courseIds), true));
+    }
+
+    /**
+     * 导出课程导入模板
+     */
+    @SaCheckPermission("ecs:course:export")
+    @Log(title = "课程导入模板", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportTemplate")
+    public void exportTemplate(HttpServletResponse response) {
+        List<EcsCourseImportVo> list = new ArrayList<>();
+        ExcelUtil.exportExcel(list, "课程导入模板", EcsCourseImportVo.class, response);
+    }
+
+    /**
+     * 导入课程数据
+     *
+     * @param file Excel文件
+     * @param updateSupport 是否更新已存在数据
+     */
+    @SaCheckPermission("ecs:course:import")
+    @Log(title = "课程导入", businessType = BusinessType.IMPORT)
+    @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
+        ExcelResult<EcsCourseImportVo> result = ExcelUtil.importExcel(
+            file.getInputStream(),
+            EcsCourseImportVo.class,
+            new EcsCourseImportListener(updateSupport)
+        );
+        return R.ok(result.getAnalysis());
+    }
+
+}

+ 69 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/SectionController.java

@@ -0,0 +1,69 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.ecs.domain.bo.EcsSectionBo;
+import org.dromara.ecs.domain.vo.EcsSectionVo;
+import org.dromara.ecs.service.IEcsSectionService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 课表管理
+ * 前端访问路由地址为:/ecs/section
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/section")
+public class SectionController extends BaseController {
+
+    private final IEcsSectionService ecsSectionService;
+
+    /**
+     * 查询课表记录列表
+     */
+    @SaCheckPermission("ecs:section:list")
+    @GetMapping("/list")
+    public TableDataInfo<EcsSectionVo> list(EcsSectionBo bo, PageQuery pageQuery) {
+        return ecsSectionService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出课表记录列表
+     */
+    @SaCheckPermission("ecs:section:export")
+    @Log(title = "课表管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(EcsSectionBo bo, HttpServletResponse response) {
+        List<EcsSectionVo> list = ecsSectionService.queryList(bo);
+        ExcelUtil.exportExcel(list, "课表记录", EcsSectionVo.class, response);
+    }
+
+    /**
+     * 获取课表记录详细信息
+     *
+     * @param sectionId 主键
+     */
+    @SaCheckPermission("ecs:section:query")
+    @GetMapping("/{sectionId}")
+    public R<EcsSectionVo> getInfo(@NotNull(message = "主键不能为空")
+                                    @PathVariable Long sectionId) {
+        return R.ok(ecsSectionService.queryById(sectionId));
+    }
+
+}

+ 48 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/TeacherController.java

@@ -0,0 +1,48 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemoteUserAccountService;
+import org.dromara.backstage.api.domain.dto.RemoteTeacherDto;
+import org.dromara.backstage.api.domain.dto.RemoteTeacherQueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 教师信息
+ * 前端访问路由地址为:/ecs/teacher
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/teacher")
+public class TeacherController extends BaseController {
+
+    @DubboReference
+    private RemoteUserAccountService remoteUserAccountService;
+
+    /**
+     * 查询教师信息列表
+     */
+    @SaCheckPermission("ecs:teacher:list")
+    @GetMapping("/list")
+    public TableDataInfo<RemoteTeacherDto> list(RemoteTeacherQueryDto queryDto) {
+        return remoteUserAccountService.selectTeacherPage(queryDto);
+    }
+
+    /**
+     * 获取教师详细信息
+     */
+    @SaCheckPermission("ecs:teacher:list")
+    @GetMapping("/{userId}")
+    public R<RemoteTeacherDto> getInfo(@PathVariable Long userId) {
+        return remoteUserAccountService.selectTeacherById(userId);
+    }
+}

+ 143 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/TermController.java

@@ -0,0 +1,143 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.ecs.domain.bo.EcsTermBo;
+import org.dromara.ecs.domain.vo.EcsTermImportVo;
+import org.dromara.ecs.domain.vo.EcsTermVo;
+import org.dromara.ecs.listener.EcsTermImportListener;
+import org.dromara.ecs.service.IEcsTermService;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 设备信息
+ * 前端访问路由地址为:/ecs/term
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/term")
+public class TermController extends BaseController {
+
+    private final IEcsTermService ecsTermService;
+
+    /**
+     * 查询设备信息列表
+     */
+    @SaCheckPermission("ecs:term:list")
+    @GetMapping("/list")
+    public TableDataInfo<EcsTermVo> list(EcsTermBo bo, PageQuery pageQuery) {
+        return ecsTermService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出设备信息列表
+     */
+    @SaCheckPermission("ecs:term:export")
+    @Log(title = "设备信息", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(EcsTermBo bo, HttpServletResponse response) {
+        List<EcsTermVo> list = ecsTermService.queryList(bo);
+        ExcelUtil.exportExcel(list, "设备信息", EcsTermVo.class, response);
+    }
+
+    /**
+     * 获取设备信息详细信息
+     *
+     * @param termId 主键
+     */
+    @SaCheckPermission("ecs:term:query")
+    @GetMapping("/{termId}")
+    public R<EcsTermVo> getInfo(@NotNull(message = "主键不能为空")
+                                @PathVariable Long termId) {
+        return R.ok(ecsTermService.queryById(termId));
+    }
+
+    /**
+     * 新增设备信息
+     */
+    @SaCheckPermission("ecs:term:add")
+    @Log(title = "设备信息", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody EcsTermBo bo) {
+        return toAjax(ecsTermService.insertByBo(bo));
+    }
+
+    /**
+     * 修改设备信息
+     */
+    @SaCheckPermission("ecs:term:edit")
+    @Log(title = "设备信息", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody EcsTermBo bo) {
+        return toAjax(ecsTermService.updateByBo(bo));
+    }
+
+    /**
+     * 删除设备信息
+     *
+     * @param termIds 主键串
+     */
+    @SaCheckPermission("ecs:term:remove")
+    @Log(title = "设备信息", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{termIds}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] termIds) {
+        return toAjax(ecsTermService.deleteWithValidByIds(List.of(termIds), true));
+    }
+
+    /**
+     * 导出设备信息导入模板
+     */
+    @SaCheckPermission("ecs:term:export")
+    @Log(title = "设备信息导入模板", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportTemplate")
+    public void exportTemplate(HttpServletResponse response) {
+        List<EcsTermImportVo> list = new ArrayList<>();
+        ExcelUtil.exportExcel(list, "设备信息导入模板", EcsTermImportVo.class, response);
+    }
+
+    /**
+     * 导入设备信息数据
+     *
+     * @param file Excel文件
+     * @param updateSupport 是否更新已存在数据
+     */
+    @SaCheckPermission("ecs:term:import")
+    @Log(title = "设备信息导入", businessType = BusinessType.IMPORT)
+    @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
+        ExcelResult<EcsTermImportVo> result = ExcelUtil.importExcel(
+            file.getInputStream(),
+            EcsTermImportVo.class,
+            new EcsTermImportListener(updateSupport)
+        );
+        return R.ok(result.getAnalysis());
+    }
+
+}

+ 71 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/TraineeController.java

@@ -0,0 +1,71 @@
+package org.dromara.ecs.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemoteUserAccountService;
+import org.dromara.backstage.api.domain.dto.RemoteTraineeDto;
+import org.dromara.backstage.api.domain.dto.RemoteTraineeQueryDto;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.system.api.RemoteDeptService;
+import org.dromara.system.api.domain.vo.RemoteDeptVo;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 学员信息
+ * 前端访问路由地址为:/ecs/trainee
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/trainee")
+public class TraineeController extends BaseController {
+
+    @DubboReference
+    private RemoteUserAccountService remoteUserAccountService;
+
+    @DubboReference
+    private RemoteDeptService remoteDeptService;
+
+    /**
+     * 查询学员信息列表
+     * 当deptId不为空时,通过t_user_dept关系表查询该班级下的学员(支持一个学员在多个班级的场景)
+     * 当deptId为空时,走原有的查询逻辑
+     */
+    @SaCheckPermission("ecs:trainee:list")
+    @GetMapping("/list")
+    public TableDataInfo<RemoteTraineeDto> list(RemoteTraineeQueryDto queryDto) {
+        if (queryDto.getDeptId() != null) {
+            return remoteUserAccountService.selectTraineePageByDeptId(queryDto);
+        }
+        return remoteUserAccountService.selectTraineePage(queryDto);
+    }
+
+    /**
+     * 获取学员详细信息
+     */
+    @SaCheckPermission("ecs:trainee:list")
+    @GetMapping("/{userId}")
+    public R<RemoteTraineeDto> getInfo(@PathVariable Long userId) {
+        return remoteUserAccountService.selectTraineeById(userId);
+    }
+
+    /**
+     * 获取培训班下拉选项(正在进行的培训班)
+     */
+    @SaCheckPermission("ecs:trainee:list")
+    @GetMapping("/class-options")
+    public R<List<RemoteDeptVo>> classOptions() {
+        List<RemoteDeptVo> list = remoteDeptService.selectDoingClass(new Date());
+        return R.ok(list);
+    }
+}

+ 54 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/controller/sync/SyncDataController.java

@@ -0,0 +1,54 @@
+package org.dromara.ecs.controller.sync;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.ecs.domain.dto.sync.SyncSectionDTO;
+import org.dromara.ecs.domain.dto.wrapper.SyncDataWrapper;
+import org.dromara.ecs.service.sync.ISyncSectionService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 电子班牌数据同步接口
+ * 提供给外部系统调用,同步电子班牌数据到本平台
+ * 统一使用 {"records": [...]} 格式
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/sync")
+@SaIgnore
+public class SyncDataController {
+
+    private final ISyncSectionService syncSectionService;
+
+    /**
+     * 接收外部系统同步的课表数据
+     * 接口地址:POST /ecs/sync/section/push
+     * 请求格式:{"records": [{...}, {...}]}
+     */
+    @PostMapping("/section/push")
+    public R<String> pushSections(@RequestBody SyncDataWrapper<SyncSectionDTO> wrapper) {
+        if (wrapper == null || wrapper.getRecords() == null || wrapper.getRecords().isEmpty()) {
+            return R.fail("同步数据不能为空");
+        }
+
+        Integer successCount = syncSectionService.receiveSections(wrapper.getRecords());
+
+        return R.ok("成功处理 " + successCount + " 条课表数据");
+    }
+
+    /**
+     * 接收外部系统同步的单条课表数据
+     * 接口地址:POST /ecs/sync/section/push-one
+     */
+    @PostMapping("/section/push-one")
+    public R<String> pushSection(@RequestBody SyncSectionDTO dto) {
+        Boolean success = syncSectionService.receiveSection(dto);
+        return Boolean.TRUE.equals(success) ? R.ok("课表数据同步成功") : R.fail("课表数据同步失败");
+    }
+}

+ 104 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsAttend.java

@@ -0,0 +1,104 @@
+package org.dromara.ecs.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.util.Date;
+
+/**
+ * 考勤管理对象 t_ecs_attend
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_ecs_attend")
+public class EcsAttend extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 考勤Id
+     */
+    @TableId(value = "attend_id")
+    private Long attendId;
+
+    /**
+     * 学员Id
+     */
+    private Long userId;
+
+    /**
+     * 学号
+     */
+    private String userNumb;
+
+    /**
+     * 学员姓名
+     */
+    private String realName;
+
+    /**
+     * 班级Id
+     */
+    private Long classId;
+
+    /**
+     * 班级名称
+     */
+    private String className;
+
+    /**
+     * 教室Id
+     */
+    private Long roomId;
+
+    /**
+     * 教室名称
+     */
+    private String roomName;
+
+    /**
+     * 考勤时间
+     */
+    private Date checkTime;
+
+    /**
+     * 考勤方式:0-刷卡,1-人脸
+     */
+    private String checkType;
+
+    /**
+     * 上传时间
+     */
+    private Date uploadTime;
+
+    /**
+     * 推送状态:0-未推送,1-推送成功,2-推送失败
+     */
+    private Integer pushStatus;
+
+    /**
+     * 推送时间
+     */
+    private Date pushTime;
+
+    /**
+     * 推送尝试次数
+     */
+    private Integer pushRetry;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 68 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsAttendRule.java

@@ -0,0 +1,68 @@
+package org.dromara.ecs.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+
+/**
+ * 考勤规则对象 t_ecs_atten_rule
+ *
+ * @author ruoyi
+ * @date 2026-04-22
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_ecs_attend_rule")
+public class EcsAttendRule extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 考勤规则Id
+     */
+    @TableId(value = "attend_rule_id")
+    private Long attendRuleId;
+
+    /**
+     * 规则名称
+     */
+    private String ruleName;
+
+    /**
+     * 规则编码
+     */
+    private String ruleNumb;
+
+    /**
+     * 提前打卡分钟数
+     */
+    private Integer advanceStart;
+
+    /**
+     * 迟到延后分钟数
+     */
+    private Integer lateAfter;
+
+    /**
+     * 旷课延后分钟数
+     */
+    private Integer absentAfter;
+
+    /**
+     * 状态:0-正常,1-停用
+     */
+    private String status;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 74 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsCourse.java

@@ -0,0 +1,74 @@
+package org.dromara.ecs.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+
+/**
+ * 课程管理对象 t_ecs_course
+ *
+ * @author ruoyi
+ * @date 2026-04-15
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_ecs_course")
+public class EcsCourse extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 课程ID
+     */
+    @TableId(value = "course_id")
+    private Long courseId;
+
+    /**
+     * 课程名称
+     */
+    private String courseName;
+
+    /**
+     * 课程编码
+     */
+    private String courseCode;
+
+    /**
+     * 课程类型:0-选修课,1-必修课
+     */
+    private Integer courseType;
+
+    /**
+     * 学分
+     */
+    private BigDecimal credit;
+
+    /**
+     * 学时
+     */
+    private Integer hours;
+
+    /**
+     * 第三方标识符
+     */
+    private String otherId;
+
+    /**
+     * 数据来源:0-平台录入,1-第三方同步
+     */
+    private Integer dataSource;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 53 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsCourseTeacher.java

@@ -0,0 +1,53 @@
+package org.dromara.ecs.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+
+/**
+ * 课程-教师关联对象 t_ecs_course_teacher
+ *
+ * @author ruoyi
+ * @date 2026-04-15
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_ecs_course_teacher")
+public class EcsCourseTeacher extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 课程ID,关联主表
+     */
+    private Long courseId;
+
+    /**
+     * 教师ID,关联教师信息表
+     */
+    private Long teacherId;
+
+    /**
+     * 教师姓名(冗余)
+     */
+    private String teacherName;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 154 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsSection.java

@@ -0,0 +1,154 @@
+package org.dromara.ecs.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.util.Date;
+
+/**
+ * 课表管理对象 t_ecs_section
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_ecs_section")
+public class EcsSection extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "section_id")
+    private Long sectionId;
+
+    /**
+     * 课程Id
+     */
+    private Long courseId;
+
+    /**
+     * 班级Id
+     */
+    private Long classId;
+
+    /**
+     * 教室Id
+     */
+    private Long roomId;
+
+    /**
+     * 课程名称
+     */
+    private String courseName;
+
+    /**
+     * 班级名称
+     */
+    private String className;
+
+    /**
+     * 教室名称
+     */
+    private String roomName;
+
+    /**
+     * 学年,如 2025-2026
+     */
+    private String academicYear;
+
+    /**
+     * 学期:1-第一学期,2-第二学期
+     */
+    private Integer semester;
+
+    /**
+     * 上课日期
+     */
+    private Date courseDate;
+
+    /**
+     * 星期 1-7(周一至周日)
+     */
+    private Integer weekday;
+
+    /**
+     * 上课时段:0-上午,1-下午,2-晚上
+     */
+    private Integer timeSlot;
+
+    /**
+     * 课程节次,一天的第几节课
+     */
+    private Integer sectionIndex;
+
+    /**
+     * 连课节次
+     */
+    private Integer startSection;
+
+    /**
+     * 占用节次,默认1
+     */
+    private Integer sectionCount;
+
+    /**
+     * 占用列表,JSON格式如[2,3]
+     */
+    private String sectionLList;
+
+    /**
+     * 开始时间
+     */
+    private Date startTime;
+
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 是否考勤:0-否,1-是
+     */
+    private Integer isAttend;
+
+    /**
+     * 第三方课表Id
+     */
+    private String otherId;
+
+    /**
+     * 数据来源:0-平台录入,1-第三方同步
+     */
+    private Integer dataSource;
+
+    /**
+     * 第三方课程Id
+     */
+    private String courseOtherId;
+
+    /**
+     * 第三方班级Id
+     */
+    private String classOtherId;
+
+    /**
+     * 第三方教室Id
+     */
+    private String roomOtherId;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 103 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/EcsTerm.java

@@ -0,0 +1,103 @@
+package org.dromara.ecs.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+
+/**
+ * 设备信息对象 t_ecs_term
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_ecs_term")
+public class EcsTerm extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 设备Id
+     */
+    @TableId(value = "term_id")
+    private Long termId;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+
+    /**
+     * 设备编码
+     */
+    private String termNumb;
+
+    /**
+     * 设备类型:0-电子班牌,1-考勤机,2-门禁机
+     */
+    private String termType;
+
+    /**
+     * 设备IP
+     */
+    private String termIp;
+
+    /**
+     * MAC地址
+     */
+    private String termMac;
+
+    /**
+     * 设备端口
+     */
+    private Integer termPort;
+
+    /**
+     * 设备品牌
+     */
+    private String termBrand;
+
+    /**
+     * 管理账号
+     */
+    private String adminName;
+
+    /**
+     * 管理密码
+     */
+    private String adminPwd;
+
+    /**
+     * 服务器IP
+     */
+    private String serverIp;
+
+    /**
+     * 服务器端口
+     */
+    private Integer serverPort;
+
+    /**
+     * 教室Id
+     */
+    private Long roomId;
+
+    /**
+     * 教室名称(冗余存储)
+     */
+    private String roomName;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+}

+ 96 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/bo/AttendForm.java

@@ -0,0 +1,96 @@
+package org.dromara.ecs.domain.bo;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 手工考勤表单对象(接收前端批量考勤提交)
+ *
+ * @author ruoyi
+ * @date 2026-04-21
+ */
+@Data
+public class AttendForm implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 班级Id
+     */
+    @NotNull(message = "请选择班级", groups = {AddGroup.class})
+    private Long classId;
+
+    /**
+     * 班级名称(选择班级后回填)
+     */
+    private String className;
+
+    /**
+     * 教室Id
+     */
+    @NotNull(message = "请选择教室", groups = {AddGroup.class})
+    private Long roomId;
+
+    /**
+     * 教室名称(选择教室后回填)
+     */
+    private String roomName;
+
+    /**
+     * 考勤时间
+     */
+    @NotNull(message = "请填写考勤时间", groups = {AddGroup.class})
+    private Date checkTime;
+
+    /**
+     * 考勤方式:0-刷卡,1-人脸 2-手工
+     */
+    private @NotNull(message = "请选择考勤方式", groups = {AddGroup.class}) String checkType;
+
+    /**
+     * 考勤学员列表
+     */
+    @NotEmpty(message = "请至少选择一名学员", groups = {AddGroup.class})
+    @Valid
+    private List<AttendUserItem> attendUserList;
+
+    /**
+     * 手工考勤 - 学员信息条目
+     */
+    @Data
+    public static class AttendUserItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 学员Id
+         */
+        @NotNull(message = "学员Id不能为空")
+        private Long userId;
+
+        /**
+         * 学员姓名
+         */
+        private String realName;
+
+        /**
+         * 学号
+         */
+        private String userNumb;
+
+        /**
+         * 手机号
+         */
+        private String phone;
+    }
+}

+ 99 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/bo/EcsAttendBo.java

@@ -0,0 +1,99 @@
+package org.dromara.ecs.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.tenant.core.TenantEntity;
+import org.dromara.ecs.domain.EcsAttend;
+
+import java.util.Date;
+
+/**
+ * 考勤管理业务对象 t_ecs_attend
+ *
+ * @author ruoyi
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = EcsAttend.class, reverseConvertGenerate = false)
+public class EcsAttendBo extends TenantEntity {
+
+    /**
+     * 考勤Id
+     */
+    @NotNull(message = "考勤Id不能为空", groups = { AddGroup.class })
+    private Long attendId;
+
+    /**
+     * 学员Id
+     */
+    @NotNull(message = "请选择学员", groups = { AddGroup.class })
+    private Long userId;
+
+    /**
+     * 学号
+     */
+    private String userNumb;
+
+    /**
+     * 学员姓名
+     */
+    private String realName;
+
+    /**
+     * 班级Id
+     */
+    @NotNull(message = "请选择班级", groups = { AddGroup.class })
+    private Long classId;
+
+    /**
+     * 班级名称
+     */
+    private String className;
+
+    /**
+     * 教室Id
+     */
+    @NotNull(message = "请选择教室", groups = { AddGroup.class })
+    private Long roomId;
+
+    /**
+     * 教室名称
+     */
+    private String roomName;
+
+    /**
+     * 考勤时间
+     */
+    @NotNull(message = "请填写考勤时间", groups = { AddGroup.class })
+    private Date checkTime;
+
+    /**
+     * 考勤方式:0-刷卡,1-人脸
+     */
+    private @NotNull(message = "请选择考勤方式", groups = {AddGroup.class}) String checkType;
+
+    /**
+     * 上传时间
+     */
+    private Date uploadTime;
+
+    /**
+     * 推送状态:0-未推送,1-推送成功,2-推送失败
+     */
+    private Integer pushStatus;
+
+    /**
+     * 推送时间
+     */
+    private Date pushTime;
+
+    /**
+     * 推送尝试次数
+     */
+    private Integer pushRetry;
+
+}

+ 63 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/domain/bo/EcsAttendRuleBo.java

@@ -0,0 +1,63 @@
+package org.dromara.ecs.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.tenant.core.TenantEntity;
+import org.dromara.ecs.domain.EcsAttendRule;
+
+/**
+ * 考勤规则业务对象 t_ecs_atten_rule
+ *
+ * @author ruoyi
+ * @date 2026-04-22
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = EcsAttendRule.class, reverseConvertGenerate = false)
+public class EcsAttendRuleBo extends TenantEntity {
+
+    /**
+     * 考勤规则Id
+     */
+    @NotNull(message = "考勤规则Id不能为空", groups = { EditGroup.class })
+    private Long attendRuleId;
+
+    /**
+     * 规则名称
+     */
+    @NotBlank(message = "规则名称不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String ruleName;
+
+    /**
+     * 规则编码
+     */
+    @NotBlank(message = "规则编码不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String ruleNumb;
+
+    /**
+     * 提前打卡分钟数
+     */
+    private Integer advanceStart;
+
+    /**
+     * 迟到延后分钟数
+     */
+    private Integer lateAfter;
+
+    /**
+     * 旷课延后分钟数
+     */
+    private Integer absentAfter;
+
+    /**
+     * 状态:0-正常,1-停用
+     */
+    @NotBlank(message = "状态不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String status;
+
+}

Неке датотеке нису приказане због велике количине промена