Explorar o código

feat: 新增同步服务和相关DTO

- 添加 `BatchSyncRequest` 通用请求封装类
- 引入 `TraineeRecordRequest`, `TrainClassRecordRequest`, `CourseRecordRequest`, `ClassroomRecordRequest` 和 `SectionRecordRequest` 数据传输对象
- 实现 `TrainBatchSyncService` 和 `SectionBatchSyncService` 批量同步服务
- 更新 `SyncReceiveController` 以支持新的同步接口
- 增加 `SysDept` 和 `SysDeptBo` 中的同步字段
- 配置 `RemoteSectionSyncServiceImpl` 远程同步实现类
- 添加相关的依赖和配置文件更新
yubo hai 1 mes
pai
achega
ed24805c91
Modificáronse 33 ficheiros con 1012 adicións e 1 borrados
  1. 28 0
      .claude/settings.json
  2. 12 0
      .claude/settings.local.json
  3. 27 0
      .claude/skills/debug-issue.md
  4. 28 0
      .claude/skills/explore-codebase.md
  5. 28 0
      .claude/skills/refactor-safely.md
  6. 29 0
      .claude/skills/review-changes.md
  7. 11 0
      .cursor/mcp.json
  8. 2 0
      .gitignore
  9. 11 0
      .mcp.json
  10. 12 0
      .opencode.json
  11. 14 0
      ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/RemoteSectionSyncService.java
  12. 17 0
      ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteBatchSyncResultDto.java
  13. 35 0
      ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteSectionSyncDto.java
  14. 3 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/self/TraineeBusiness.java
  15. 72 0
      ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/dubbo/RemoteSectionSyncServiceImpl.java
  16. 4 0
      ruoyi-server/ruoyi-server-sync/pom.xml
  17. 78 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/SectionBatchSyncService.java
  18. 83 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/TrainBatchSyncService.java
  19. 77 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/TraineeBatchSyncService.java
  20. 35 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/SectionRecordMapper.java
  21. 48 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/TrainClassRecordMapper.java
  22. 61 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/TraineeRecordMapper.java
  23. 68 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/controller/SyncReceiveController.java
  24. 2 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/controller/SyncTest.java
  25. 18 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/SysDept.java
  26. 20 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/bo/SysDeptBo.java
  27. 13 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/ClassroomRecordRequest.java
  28. 13 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/CourseRecordRequest.java
  29. 35 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/SectionRecordRequest.java
  30. 68 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/TrainClassRecordRequest.java
  31. 41 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/TraineeRecordRequest.java
  32. 16 0
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/wrapper/BatchSyncRequest.java
  33. 3 1
      ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/strategy/dept/impl/TrainClassStrategyImpl.java

+ 28 - 0
.claude/settings.json

@@ -0,0 +1,28 @@
+{
+  "hooks": {
+    "PostToolUse": [
+      {
+        "matcher": "Edit|Write|Bash",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "code-review-graph update --skip-flows",
+            "timeout": 30
+          }
+        ]
+      }
+    ],
+    "SessionStart": [
+      {
+        "matcher": "",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "code-review-graph status",
+            "timeout": 10
+          }
+        ]
+      }
+    ]
+  }
+}

+ 12 - 0
.claude/settings.local.json

@@ -0,0 +1,12 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(claude plugin *)",
+      "Bash(setx CLAUDE_CODE_GIT_BASH_PATH *)",
+      "Bash(CLAUDE_CODE_GIT_BASH_PATH='C:\\\\MyProgram\\\\Git\\\\bin\\\\bash.exe' claude plugin *)",
+      "Bash(pip install *)",
+      "Bash(code-review-graph install *)",
+      "Bash(code-review-graph build *)"
+    ]
+  }
+}

+ 27 - 0
.claude/skills/debug-issue.md

@@ -0,0 +1,27 @@
+---
+name: Debug Issue
+description: Systematically debug issues using graph-powered code navigation
+---
+
+## Debug Issue
+
+Use the knowledge graph to systematically trace and debug issues.
+
+### Steps
+
+1. Use `semantic_search_nodes` to find code related to the issue.
+2. Use `query_graph` with `callers_of` and `callees_of` to trace call chains.
+3. Use `get_flow` to see full execution paths through suspected areas.
+4. Run `detect_changes` to check if recent changes caused the issue.
+5. Use `get_impact_radius` on suspected files to see what else is affected.
+
+### Tips
+
+- Check both callers and callees to understand the full context.
+- Look at affected flows to find the entry point that triggers the bug.
+- Recent changes are the most common source of new issues.
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.

+ 28 - 0
.claude/skills/explore-codebase.md

@@ -0,0 +1,28 @@
+---
+name: Explore Codebase
+description: Navigate and understand codebase structure using the knowledge graph
+---
+
+## Explore Codebase
+
+Use the code-review-graph MCP tools to explore and understand the codebase.
+
+### Steps
+
+1. Run `list_graph_stats` to see overall codebase metrics.
+2. Run `get_architecture_overview` for high-level community structure.
+3. Use `list_communities` to find major modules, then `get_community` for details.
+4. Use `semantic_search_nodes` to find specific functions or classes.
+5. Use `query_graph` with patterns like `callers_of`, `callees_of`, `imports_of` to trace relationships.
+6. Use `list_flows` and `get_flow` to understand execution paths.
+
+### Tips
+
+- Start broad (stats, architecture) then narrow down to specific areas.
+- Use `children_of` on a file to see all its functions and classes.
+- Use `find_large_functions` to identify complex code.
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.

+ 28 - 0
.claude/skills/refactor-safely.md

@@ -0,0 +1,28 @@
+---
+name: Refactor Safely
+description: Plan and execute safe refactoring using dependency analysis
+---
+
+## Refactor Safely
+
+Use the knowledge graph to plan and execute refactoring with confidence.
+
+### Steps
+
+1. Use `refactor_tool` with mode="suggest" for community-driven refactoring suggestions.
+2. Use `refactor_tool` with mode="dead_code" to find unreferenced code.
+3. For renames, use `refactor_tool` with mode="rename" to preview all affected locations.
+4. Use `apply_refactor_tool` with the refactor_id to apply renames.
+5. After changes, run `detect_changes` to verify the refactoring impact.
+
+### Safety Checks
+
+- Always preview before applying (rename mode gives you an edit list).
+- Check `get_impact_radius` before major refactors.
+- Use `get_affected_flows` to ensure no critical paths are broken.
+- Run `find_large_functions` to identify decomposition targets.
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.

+ 29 - 0
.claude/skills/review-changes.md

@@ -0,0 +1,29 @@
+---
+name: Review Changes
+description: Perform a structured code review using change detection and impact
+---
+
+## Review Changes
+
+Perform a thorough, risk-aware code review using the knowledge graph.
+
+### Steps
+
+1. Run `detect_changes` to get risk-scored change analysis.
+2. Run `get_affected_flows` to find impacted execution paths.
+3. For each high-risk function, run `query_graph` with pattern="tests_for" to check test coverage.
+4. Run `get_impact_radius` to understand the blast radius.
+5. For any untested changes, suggest specific test cases.
+
+### Output Format
+
+Provide findings grouped by risk level (high/medium/low) with:
+- What changed and why it matters
+- Test coverage status
+- Suggested improvements
+- Overall merge recommendation
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.

+ 11 - 0
.cursor/mcp.json

@@ -0,0 +1,11 @@
+{
+  "mcpServers": {
+    "code-review-graph": {
+      "command": "code-review-graph",
+      "args": [
+        "serve"
+      ],
+      "type": "stdio"
+    }
+  }
+}

+ 2 - 0
.gitignore

@@ -48,3 +48,5 @@ nbdist/
 /jrebel-classpath-27688.jar
 /ruoyi-server/ruoyi-server-consume/Dockerfile
 /qodana.yaml
+# Added by code-review-graph
+.code-review-graph/

+ 11 - 0
.mcp.json

@@ -0,0 +1,11 @@
+{
+  "mcpServers": {
+    "code-review-graph": {
+      "command": "code-review-graph",
+      "args": [
+        "serve"
+      ],
+      "type": "stdio"
+    }
+  }
+}

+ 12 - 0
.opencode.json

@@ -0,0 +1,12 @@
+{
+  "mcpServers": {
+    "code-review-graph": {
+      "command": "code-review-graph",
+      "args": [
+        "serve"
+      ],
+      "type": "stdio",
+      "env": []
+    }
+  }
+}

+ 14 - 0
ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/RemoteSectionSyncService.java

@@ -0,0 +1,14 @@
+package org.dromara.ecs.api;
+
+import org.dromara.common.core.domain.R;
+import org.dromara.ecs.api.domain.dto.RemoteBatchSyncResultDto;
+import org.dromara.ecs.api.domain.dto.RemoteSectionSyncDto;
+
+import java.util.List;
+
+public interface RemoteSectionSyncService {
+
+    R<RemoteBatchSyncResultDto> syncSections(List<RemoteSectionSyncDto> records);
+
+    R<Void> syncSection(RemoteSectionSyncDto record);
+}

+ 17 - 0
ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteBatchSyncResultDto.java

@@ -0,0 +1,17 @@
+package org.dromara.ecs.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class RemoteBatchSyncResultDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private Integer total;
+    private Integer success;
+    private Integer failed;
+}

+ 35 - 0
ruoyi-api/ruoyi-api-ecs/src/main/java/org/dromara/ecs/api/domain/dto/RemoteSectionSyncDto.java

@@ -0,0 +1,35 @@
+package org.dromara.ecs.api.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class RemoteSectionSyncDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String sectionId;
+    private String courseId;
+    private String classId;
+    private String roomId;
+    private String courseName;
+    private String className;
+    private String roomName;
+    private String courseDate;
+    private Integer weekDay;
+    private String academicYear;
+    private String semester;
+    private Integer timeSlot;
+    private Integer sectionIndex;
+    private Integer startSection;
+    private Integer sectionCount;
+    private String sectionLList;
+    private String startTime;
+    private String endTime;
+    private Integer attendEnable;
+    private Integer delFlag;
+    private String tenantId;
+}

+ 3 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/self/TraineeBusiness.java

@@ -200,9 +200,11 @@ public class TraineeBusiness {
             String cardData = getRoomCardData(kfOrderVo);
             ycTraineeVo.setRoomCardData(cardData);
             // 发送报到短信(有房间)
+            // TODO [2026/4/28][luoyibo][P0-当日内完成]: 因调试需要被注释,需要在正式环境中打开
             selfBusiness.sendSmsHasRoom(ycTraineeVo.getMobilePhone(), ycTraineeVo.getUserXm(), remoteDeptVo.getDeptName(), roomCode);
         } else {
             // 发送短信,暂无房间
+            // TODO [2026/4/28][luoyibo][P0-当日内完成]: 因调试需要被注释,需要在正式环境中打开
             selfBusiness.sendSmsNoRoom(ycTraineeVo.getMobilePhone(), ycTraineeVo.getUserXm(), remoteDeptVo.getDeptName());
         }
         setCheckInfoStatus(bo, ycTraineeVo);
@@ -214,6 +216,7 @@ public class TraineeBusiness {
         BeanUtil.copyProperties(ycTraineeVo, traineeVo);
 
         // 发送报到的kafka消息
+        // TODO [2026/4/28][luoyibo][P0-当日内完成]: 因调试需要被注释,需要在正式环境中打开
         sendCheckInMessageToKafka(bo);
         //写发卡记录表
         //insertCardData(ycTraineeVo);

+ 72 - 0
ruoyi-modules/ruoyi-ecs/src/main/java/org/dromara/ecs/dubbo/RemoteSectionSyncServiceImpl.java

@@ -0,0 +1,72 @@
+package org.dromara.ecs.dubbo;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.dromara.common.core.domain.R;
+import org.dromara.ecs.api.RemoteSectionSyncService;
+import org.dromara.ecs.api.domain.dto.RemoteBatchSyncResultDto;
+import org.dromara.ecs.api.domain.dto.RemoteSectionSyncDto;
+import org.dromara.ecs.domain.dto.sync.SyncSectionDTO;
+import org.dromara.ecs.service.sync.ISyncSectionService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@DubboService
+@RequiredArgsConstructor
+public class RemoteSectionSyncServiceImpl implements RemoteSectionSyncService {
+
+    private final ISyncSectionService syncSectionService;
+
+    @Override
+    public R<RemoteBatchSyncResultDto> syncSections(List<RemoteSectionSyncDto> records) {
+        if (records == null || records.isEmpty()) {
+            return R.fail("同步数据不能为空");
+        }
+
+        List<SyncSectionDTO> syncDtos = records.stream().map(this::toSyncSectionDto).toList();
+        Integer successCount = syncSectionService.receiveSections(syncDtos);
+
+        RemoteBatchSyncResultDto result = new RemoteBatchSyncResultDto();
+        result.setTotal(records.size());
+        result.setSuccess(successCount);
+        result.setFailed(records.size() - successCount);
+        return R.ok(result);
+    }
+
+    @Override
+    public R<Void> syncSection(RemoteSectionSyncDto record) {
+        if (record == null) {
+            return R.fail("同步数据不能为空");
+        }
+        Boolean success = syncSectionService.receiveSection(toSyncSectionDto(record));
+        return Boolean.TRUE.equals(success) ? R.ok() : R.fail();
+    }
+
+    private SyncSectionDTO toSyncSectionDto(RemoteSectionSyncDto dto) {
+        SyncSectionDTO target = new SyncSectionDTO();
+        target.setSectionId(dto.getSectionId());
+        target.setCourseId(dto.getCourseId());
+        target.setClassId(dto.getClassId());
+        target.setRoomId(dto.getRoomId());
+        target.setCourseName(dto.getCourseName());
+        target.setClassName(dto.getClassName());
+        target.setRoomName(dto.getRoomName());
+        target.setCourseDate(dto.getCourseDate());
+        target.setWeekDay(dto.getWeekDay());
+        target.setAcademicYear(dto.getAcademicYear());
+        target.setSemester(dto.getSemester());
+        target.setTimeSlot(dto.getTimeSlot());
+        target.setSectionIndex(dto.getSectionIndex());
+        target.setStartSection(dto.getStartSection());
+        target.setSectionCount(dto.getSectionCount());
+        target.setSectionLList(dto.getSectionLList());
+        target.setStartTime(dto.getStartTime());
+        target.setEndTime(dto.getEndTime());
+        target.setAttendEnable(dto.getAttendEnable());
+        target.setDelFlag(dto.getDelFlag());
+        target.setTenantId(dto.getTenantId());
+        return target;
+    }
+}

+ 4 - 0
ruoyi-server/ruoyi-server-sync/pom.xml

@@ -103,6 +103,10 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-api-resource</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-ecs</artifactId>
+        </dependency>
         <dependency>
             <groupId>cn.com.kingbase</groupId>
             <artifactId>kingbase8</artifactId>

+ 78 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/SectionBatchSyncService.java

@@ -0,0 +1,78 @@
+package org.dromara.server.sync.business.batch;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.ecs.api.RemoteSectionSyncService;
+import org.dromara.ecs.api.domain.dto.RemoteBatchSyncResultDto;
+import org.dromara.ecs.api.domain.dto.RemoteSectionSyncDto;
+import org.dromara.server.sync.business.batch.mapper.SectionRecordMapper;
+import org.dromara.server.sync.domain.dto.request.SectionRecordRequest;
+import org.dromara.server.sync.domain.dto.wrapper.BatchSyncRequest;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @ClassName SectionBatchSyncService
+ * @Description 课表节次批量同步服务
+ * @Author luoyibo
+ * @Date 2026-04-28 09:00
+ * @Version 1.0
+ * @since jdk17
+ */
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class SectionBatchSyncService {
+
+    private final SectionRecordMapper sectionRecordMapper;
+
+    @DubboReference
+    private RemoteSectionSyncService remoteSectionSyncService;
+
+    public void syncSections(BatchSyncRequest<SectionRecordRequest> request) {
+        if (request == null || CollUtil.isEmpty(request.getRecords())) {
+            throw new ServiceException("records 不能为空");
+        }
+
+        request.getRecords().forEach(this::validateRecord);
+        String batchId = UUID.randomUUID().toString().replace("-", "");
+        List<RemoteSectionSyncDto> records = request.getRecords().stream().map(sectionRecordMapper::toRemoteDto).toList();
+
+        String tenantId = records.stream().map(RemoteSectionSyncDto::getTenantId).filter(StringUtils::isNotBlank).findFirst().orElse("default");
+        String sectionIds = records.stream().map(RemoteSectionSyncDto::getSectionId).filter(StringUtils::isNotBlank).limit(10).reduce((a, b) -> a + "," + b).orElse("");
+
+        log.info("[batch-sync-sections-start]-[batchId:{}]-[tenant:{}]-[total:{}]-[sampleSectionIds:{}]",
+            batchId, tenantId, records.size(), sectionIds);
+
+        R<RemoteBatchSyncResultDto> result = remoteSectionSyncService.syncSections(records);
+        if (R.isError(result)) {
+            log.error("[batch-sync-sections-fail]-[batchId:{}]-[msg:{}]", batchId, result.getMsg());
+            throw new ServiceException(result.getMsg());
+        }
+
+        RemoteBatchSyncResultDto data = result.getData();
+        if (data != null) {
+            log.info("[batch-sync-sections-done]-[batchId:{}]-[total:{}]-[成功:{}]-[失败:{}]",
+                batchId, data.getTotal(), data.getSuccess(), data.getFailed());
+        } else {
+            log.info("[batch-sync-sections-done]-[batchId:{}]-[total:{}]-[远端未返回统计]", batchId, request.getRecords().size());
+        }
+    }
+
+    private void validateRecord(SectionRecordRequest record) {
+        if (record == null) {
+            throw new ServiceException("record 不能为空");
+        }
+        if (StringUtils.isBlank(record.getSectionId())) {
+            throw new ServiceException("sectionId 不能为空");
+        }
+    }
+}

+ 83 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/TrainBatchSyncService.java

@@ -0,0 +1,83 @@
+package org.dromara.server.sync.business.batch;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.server.common.constant.SyncResourceConstants;
+import org.dromara.server.common.domain.bo.ResourceDept;
+import org.dromara.server.sync.business.batch.mapper.TrainClassRecordMapper;
+import org.dromara.server.sync.domain.dto.request.TrainClassRecordRequest;
+import org.dromara.server.sync.domain.dto.wrapper.BatchSyncRequest;
+import org.dromara.server.sync.strategy.dept.ISyncDeptStrategy;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class TrainBatchSyncService {
+
+    private final TrainClassRecordMapper trainClassRecordMapper;
+
+    public void syncTrainClass(BatchSyncRequest<TrainClassRecordRequest> request) {
+        if (request == null || CollUtil.isEmpty(request.getRecords())) {
+            throw new ServiceException("records 不能为空");
+        }
+
+        List<ResourceDept> addOrUpdateList = new ArrayList<>();
+        List<ResourceDept> deleteList = new ArrayList<>();
+
+        for (TrainClassRecordRequest record : request.getRecords()) {
+            validateRecord(record);
+            ResourceDept dept = trainClassRecordMapper.toResourceDept(record);
+            if ("1".equals(record.getDelFlag())) {
+                deleteList.add(dept);
+            } else {
+                addOrUpdateList.add(dept);
+            }
+        }
+
+        ISyncDeptStrategy syncDeptStrategy = SpringUtils.getBean(SyncResourceConstants.TRAIN_CLASS, ISyncDeptStrategy.class);
+        if (CollUtil.isNotEmpty(deleteList)) {
+            syncDeptStrategy.syncDelDept(deleteList);
+        }
+        if (CollUtil.isNotEmpty(addOrUpdateList)) {
+            syncDeptStrategy.syncDept(addOrUpdateList);
+        }
+
+        log.info("[batch-sync-train-class]-[总数:{}]-[新增更新:{}]-[删除:{}]",
+            request.getRecords().size(), addOrUpdateList.size(), deleteList.size());
+    }
+
+    private void validateRecord(TrainClassRecordRequest record) {
+        if (record == null) {
+            throw new ServiceException("record 不能为空");
+        }
+        if (StringUtils.isBlank(record.getClassId())) {
+            throw new ServiceException("classId 不能为空");
+        }
+        if (StringUtils.isBlank(record.getClassName())) {
+            throw new ServiceException("className 不能为空");
+        }
+        if (StringUtils.isBlank(record.getYear())) {
+            throw new ServiceException("year 不能为空");
+        }
+        if (StringUtils.isBlank(record.getSemester())) {
+            throw new ServiceException("semester 不能为空");
+        }
+        if (StringUtils.isBlank(record.getCheckinTime())) {
+            throw new ServiceException("checkinTime 不能为空");
+        }
+        if (StringUtils.isBlank(record.getStartTime())) {
+            throw new ServiceException("startTime 不能为空");
+        }
+        if (StringUtils.isBlank(record.getEndTime())) {
+            throw new ServiceException("endTime 不能为空");
+        }
+    }
+}

+ 77 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/TraineeBatchSyncService.java

@@ -0,0 +1,77 @@
+package org.dromara.server.sync.business.batch;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.server.common.constant.SyncResourceConstants;
+import org.dromara.server.common.domain.bo.ResourcePerson;
+import org.dromara.server.sync.business.batch.mapper.TraineeRecordMapper;
+import org.dromara.server.sync.domain.dto.request.TraineeRecordRequest;
+import org.dromara.server.sync.domain.dto.wrapper.BatchSyncRequest;
+import org.dromara.server.sync.strategy.user.ISyncUserStrategy;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class TraineeBatchSyncService {
+
+    private final TraineeRecordMapper traineeRecordMapper;
+
+    public void syncTrainee(BatchSyncRequest<TraineeRecordRequest> request) {
+        if (request == null || CollUtil.isEmpty(request.getRecords())) {
+            throw new ServiceException("records 不能为空");
+        }
+
+        List<ResourcePerson> addOrUpdateList = new ArrayList<>();
+        List<ResourcePerson> deleteList = new ArrayList<>();
+
+        for (TraineeRecordRequest record : request.getRecords()) {
+            validateRecord(record);
+            ResourcePerson person = traineeRecordMapper.toResourcePerson(record);
+            if ("1".equals(record.getStudent().getDelFlag())) {
+                deleteList.add(person);
+            } else {
+                addOrUpdateList.add(person);
+            }
+        }
+
+        ISyncUserStrategy syncUserStrategy = SpringUtils.getBean(SyncResourceConstants.TRAINEE, ISyncUserStrategy.class);
+        if (CollUtil.isNotEmpty(deleteList)) {
+            syncUserStrategy.syncDelUser(deleteList);
+        }
+        if (CollUtil.isNotEmpty(addOrUpdateList)) {
+            syncUserStrategy.syncUser(addOrUpdateList);
+        }
+
+        log.info("[batch-sync-trainee]-[总数:{}]-[新增更新:{}]-[删除:{}]",
+            request.getRecords().size(), addOrUpdateList.size(), deleteList.size());
+    }
+
+    private void validateRecord(TraineeRecordRequest record) {
+        if (record == null) {
+            throw new ServiceException("record 不能为空");
+        }
+        if (record.getStudent() == null) {
+            throw new ServiceException("student 不能为空");
+        }
+        if (record.getClassStudent() == null) {
+            throw new ServiceException("classStudent 不能为空");
+        }
+        if (StringUtils.isBlank(record.getStudent().getStudentId())) {
+            throw new ServiceException("student.studentId 不能为空");
+        }
+        if (StringUtils.isBlank(record.getStudent().getStudentName())) {
+            throw new ServiceException("student.studentName 不能为空");
+        }
+        if (StringUtils.isBlank(record.getStudent().getClassId()) && StringUtils.isBlank(record.getClassStudent().getClassId())) {
+            throw new ServiceException("classId 不能为空");
+        }
+    }
+}

+ 35 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/SectionRecordMapper.java

@@ -0,0 +1,35 @@
+package org.dromara.server.sync.business.batch.mapper;
+
+import org.dromara.ecs.api.domain.dto.RemoteSectionSyncDto;
+import org.dromara.server.sync.domain.dto.request.SectionRecordRequest;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SectionRecordMapper {
+
+    public RemoteSectionSyncDto toRemoteDto(SectionRecordRequest record) {
+        RemoteSectionSyncDto dto = new RemoteSectionSyncDto();
+        dto.setSectionId(record.getSectionId());
+        dto.setCourseId(record.getCourseId());
+        dto.setClassId(record.getClassId());
+        dto.setRoomId(record.getRoomId());
+        dto.setCourseName(record.getCourseName());
+        dto.setClassName(record.getClassName());
+        dto.setRoomName(record.getRoomName());
+        dto.setCourseDate(record.getCourseDate());
+        dto.setWeekDay(record.getWeekDay());
+        dto.setAcademicYear(record.getAcademicYear());
+        dto.setSemester(record.getSemester());
+        dto.setTimeSlot(record.getTimeSlot());
+        dto.setSectionIndex(record.getSectionIndex());
+        dto.setStartSection(record.getStartSection());
+        dto.setSectionCount(record.getSectionCount());
+        dto.setSectionLList(record.getSectionLList());
+        dto.setStartTime(record.getStartTime());
+        dto.setEndTime(record.getEndTime());
+        dto.setAttendEnable(record.getAttendEnable());
+        dto.setDelFlag(record.getDelFlag());
+        dto.setTenantId(record.getTenantId());
+        return dto;
+    }
+}

+ 48 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/TrainClassRecordMapper.java

@@ -0,0 +1,48 @@
+package org.dromara.server.sync.business.batch.mapper;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.config.DefaultConfig;
+import org.dromara.common.core.constant.DefaultConstants;
+import org.dromara.server.common.domain.bo.ResourceDept;
+import org.dromara.server.sync.domain.dto.request.TrainClassRecordRequest;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class TrainClassRecordMapper {
+
+    private final DefaultConfig defaultConfig;
+
+    public ResourceDept toResourceDept(TrainClassRecordRequest record) {
+        ResourceDept dept = new ResourceDept();
+        dept.setDept_id(record.getClassId());
+        dept.setDept_name(record.getClassName());
+        dept.setYear(Integer.valueOf(record.getYear()));
+        dept.setSemester(resolveSemester(record.getSemester()));
+        dept.setPayBegin(DateUtil.parse(record.getCheckinTime(), DefaultConstants.DATE_TIME_FORMAT));
+        dept.setCheckDate(DateUtil.parse(record.getCheckinTime(), DefaultConstants.DATE_TIME_FORMAT));
+        dept.setBeginDate(DateUtil.parse(record.getStartTime(), DefaultConstants.DATE_TIME_FORMAT));
+        dept.setEndDate(DateUtil.parse(record.getEndTime(), DefaultConstants.DATE_TIME_FORMAT));
+        dept.setPayEnd(DateUtil.parse(record.getEndTime(), DefaultConstants.DATE_TIME_FORMAT));
+        dept.setChooseRoom("0");
+        dept.setCanEat("1");
+        dept.setPayCheck("0");
+        dept.setPlanCount(ObjectUtil.defaultIfNull(record.getStudentNum(), 100));
+        dept.setTenantId(ObjectUtil.defaultIfEmpty(record.getTenantId(), defaultConfig.getTenantId()));
+        dept.setOperatorId(DefaultConstants.FULL_SYNC_ADMIN);
+        dept.setDelFlag(record.getDelFlag());
+        return dept;
+    }
+
+    private String resolveSemester(String semester) {
+        if ("1".equals(semester)) {
+            return "上学期";
+        }
+        if ("2".equals(semester)) {
+            return "下学期";
+        }
+        throw new IllegalArgumentException("semester 仅支持 1 或 2");
+    }
+}

+ 61 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/business/batch/mapper/TraineeRecordMapper.java

@@ -0,0 +1,61 @@
+package org.dromara.server.sync.business.batch.mapper;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.config.DefaultConfig;
+import org.dromara.common.core.constant.DefaultConstants;
+import org.dromara.server.common.domain.bo.ResourcePerson;
+import org.dromara.server.common.domain.bo.ResourcePersonDept;
+import org.dromara.server.sync.domain.dto.request.TraineeRecordRequest;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class TraineeRecordMapper {
+
+    private final DefaultConfig defaultConfig;
+
+    public ResourcePerson toResourcePerson(TraineeRecordRequest record) {
+        ResourcePerson person = new ResourcePerson();
+        TraineeRecordRequest.Student student = record.getStudent();
+        TraineeRecordRequest.ClassStudent classStudent = record.getClassStudent();
+
+        person.setUserId(student.getStudentId());
+        person.setRealName(student.getStudentName());
+        person.setSex(resolveSex(student.getSex()));
+        person.setPhone(student.getMobile());
+        person.setIdNumber(student.getIdNumber());
+        person.setDeptId(student.getClassId());
+        person.setDelFlag(student.getDelFlag());
+        person.setCategory(DefaultConstants.CATEGORY_TRAINEE);
+        person.setPostCode(DefaultConstants.TRAINEE_CODE);
+        person.setOperatorId(DefaultConstants.FULL_SYNC_ADMIN);
+        person.setTenantId(ObjectUtil.defaultIfEmpty(record.getTenantId(), defaultConfig.getTenantId()));
+
+        ResourcePersonDept personDept = new ResourcePersonDept();
+        personDept.setUserId(ObjectUtil.defaultIfEmpty(classStudent.getStudentId(), student.getStudentId()));
+        personDept.setDeptId(ObjectUtil.defaultIfEmpty(classStudent.getClassId(), student.getClassId()));
+        personDept.setPostCode(DefaultConstants.TRAINEE_CODE);
+        personDept.setMainDept("Y");
+        personDept.setDelFlag(resolveDeptDelFlag(classStudent.getDelFlag()));
+
+        person.setUserDeptList(List.of(personDept));
+        return person;
+    }
+
+    private String resolveSex(String sex) {
+        if ("1".equals(sex)) {
+            return "0";
+        }
+        if ("2".equals(sex)) {
+            return "1";
+        }
+        return "2";
+    }
+
+    private String resolveDeptDelFlag(String delFlag) {
+        return "1".equals(delFlag) ? "2" : "0";
+    }
+}

+ 68 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/controller/SyncReceiveController.java

@@ -0,0 +1,68 @@
+package org.dromara.server.sync.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.server.sync.business.batch.SectionBatchSyncService;
+import org.dromara.server.sync.business.batch.TrainBatchSyncService;
+import org.dromara.server.sync.business.batch.TraineeBatchSyncService;
+import org.dromara.server.sync.domain.dto.request.*;
+import org.dromara.server.sync.domain.dto.wrapper.BatchSyncRequest;
+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;
+
+/**
+ * @ClassName SyncReceiveController
+ * @Description 同步接收推送数据控制器
+ * @Author luoyibo
+ * @Date 2026-04-28 09:00
+ * @Version 1.0
+ * @since jdk17
+ */
+@Validated
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/sync/receive/")
+@SaIgnore
+public class SyncReceiveController {
+
+    private final TrainBatchSyncService trainBatchSyncService;
+    private final TraineeBatchSyncService traineeBatchSyncService;
+    private final SectionBatchSyncService sectionBatchSyncService;
+
+    @PostMapping("/classes")
+    public R<Void> syncTrainClass(@RequestBody BatchSyncRequest<TrainClassRecordRequest> request) {
+        trainBatchSyncService.syncTrainClass(request);
+        return R.ok();
+    }
+
+    @PostMapping("/trainee")
+    public R<Void> syncTrainee(@RequestBody BatchSyncRequest<TraineeRecordRequest> request) {
+        traineeBatchSyncService.syncTrainee(request);
+        return R.ok();
+    }
+
+    /**
+     * 同步课表节次信息
+     * @param request 课表节次请求信息
+     * @return 同步结果
+     */
+    @PostMapping("/sections")
+    public R<Void> syncSections(@RequestBody BatchSyncRequest<SectionRecordRequest> request) {
+        sectionBatchSyncService.syncSections(request);
+        return R.ok();
+    }
+
+    @PostMapping("/train/course")
+    public R<Void> syncCourse(@RequestBody BatchSyncRequest<CourseRecordRequest> request) {
+        return R.ok();
+    }
+
+    @PostMapping("/train/classroom")
+    public R<Void> syncClassroom(@RequestBody BatchSyncRequest<ClassroomRecordRequest> request) {
+        return R.ok();
+    }
+}

+ 2 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/controller/SyncTest.java

@@ -1,5 +1,6 @@
 package org.dromara.server.sync.controller;
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import cn.hutool.json.JSONUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -24,6 +25,7 @@ import java.util.Map;
 @Slf4j
 @RequiredArgsConstructor
 @RequestMapping("/test")
+@SaIgnore
 public class SyncTest {
     private final SyncKafkaService syncKafkaService;
 

+ 18 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/SysDept.java

@@ -131,5 +131,23 @@ public class SysDept extends TenantEntity {
      */
     private Date payEnd;
 
+    //以下同步新加字段
+    /**
+     * 计划人数
+     */
+    private Integer planCount;
+
+    /**
+     * 数据源 0-本地 1-同步
+     */
+    private String dataSource;
 
+    /**
+     * 房间id
+     */
+    private Long roomId;
+    /**
+     * 房间名称
+     */
+    private String roomName;
 }

+ 20 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/bo/SysDeptBo.java

@@ -124,4 +124,24 @@ public class SysDeptBo extends BaseEntity {
      * 部门状态
      */
     private String delFlag;
+
+    //以下同步新加字段
+    /**
+     * 计划人数
+     */
+    private Integer planCount;
+
+    /**
+     * 数据源 0-本地 1-同步
+     */
+    private String dataSource;
+
+    /**
+     * 房间id
+     */
+    private Long roomId;
+    /**
+     * 房间名称
+     */
+    private String roomName;
 }

+ 13 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/ClassroomRecordRequest.java

@@ -0,0 +1,13 @@
+package org.dromara.server.sync.domain.dto.request;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class ClassroomRecordRequest implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+}

+ 13 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/CourseRecordRequest.java

@@ -0,0 +1,13 @@
+package org.dromara.server.sync.domain.dto.request;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class CourseRecordRequest implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+}

+ 35 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/SectionRecordRequest.java

@@ -0,0 +1,35 @@
+package org.dromara.server.sync.domain.dto.request;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class SectionRecordRequest implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String sectionId;
+    private String courseId;
+    private String classId;
+    private String roomId;
+    private String courseName;
+    private String className;
+    private String roomName;
+    private String courseDate;
+    private Integer weekDay;
+    private String academicYear;
+    private String semester;
+    private Integer timeSlot;
+    private Integer sectionIndex;
+    private Integer startSection;
+    private Integer sectionCount;
+    private String sectionLList;
+    private String startTime;
+    private String endTime;
+    private Integer attendEnable;
+    private Integer delFlag;
+    private String tenantId;
+}

+ 68 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/TrainClassRecordRequest.java

@@ -0,0 +1,68 @@
+package org.dromara.server.sync.domain.dto.request;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class TrainClassRecordRequest implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 班级ID
+     */
+    private String classId;
+
+    /**
+     * 班级名称
+     */
+    private String className;
+
+    /**
+     * 学年
+     */
+    private String year;
+
+    /**
+     * 学期(1-上学期, 2-下学期)
+     */
+    private String semester;
+
+    /**
+     * 报到时间
+     */
+    private String checkinTime;
+
+    /**
+     * 开班时间(开课时间)
+     */
+    private String startTime;
+
+    /**
+     * 毕业时间(结业时间)
+     */
+    private String endTime;
+
+    /**
+     * 学生人数
+     */
+    private Integer studentNum;
+
+    /**
+     * 班主任姓名
+     */
+    private String headTeacherName;
+
+    /**
+     * 删除标志, 0-未删除 1-已删除
+     */
+    private String delFlag;
+
+    /**
+     * 租户ID
+     */
+    private String tenantId;
+}

+ 41 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/request/TraineeRecordRequest.java

@@ -0,0 +1,41 @@
+package org.dromara.server.sync.domain.dto.request;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class TraineeRecordRequest implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private Student student;
+    private ClassStudent classStudent;
+    private String tenantId;
+
+    @Data
+    public static class Student implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        private String studentId;
+        private String studentName;
+        private String sex;
+        private String mobile;
+        private String idNumber;
+        private String classId;
+        private String delFlag;
+    }
+
+    @Data
+    public static class ClassStudent implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        private String studentId;
+        private String classId;
+        private String delFlag;
+    }
+}

+ 16 - 0
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/domain/dto/wrapper/BatchSyncRequest.java

@@ -0,0 +1,16 @@
+package org.dromara.server.sync.domain.dto.wrapper;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class BatchSyncRequest<T extends Serializable> implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private List<T> records;
+}

+ 3 - 1
ruoyi-server/ruoyi-server-sync/src/main/java/org/dromara/server/sync/strategy/dept/impl/TrainClassStrategyImpl.java

@@ -188,7 +188,9 @@ public class TrainClassStrategyImpl implements ISyncDeptStrategy {
         }
 
         deptBo.setTenantId(tenantId);
-
+        deptBo.setPlanCount(resourceDept.getPlanCount().intValue());
+        // 固定为第三方同步
+        deptBo.setDataSource("1");
         return R.ok(deptBo);
     }