feat(databus): 完成阶段一+二-数据契约层与数据提供者

阶段一:数据契约层(任务 1-16)
- 新增 DatabusDeptData, DatabusAdminUserData, DatabusPostData 数据对象
- 新增 CursorPageReqDTO, CursorPageResult 游标分页 DTO
- 新增 DatabusDeptProviderApi, DatabusUserProviderApi, DatabusPostProviderApi Feign 接口
- 修改 system-api pom.xml 添加 databus-api 依赖

阶段二:数据提供者实现(任务 17-38)
- 新增 DatabusDeptProviderApiImpl, DatabusUserProviderApiImpl, DatabusPostProviderApiImpl Feign 接口实现
- 实现游标分页查询(基于 cursorTime + cursorId 复合游标)
- 新增 DatabusDeptChangeMessage, DatabusUserChangeMessage, DatabusPostChangeMessage MQ 消息类
- 新增 DatabusChangeProducer 消息生产者(支持部门、用户、岗位三实体)
- 修改 DeptServiceImpl, AdminUserServiceImpl, PostServiceImpl 添加事件发布

技术要点:
- 游标分页:cursorTime + cursorId 复合游标解决雪花ID乱序问题
- 事件发布:create/update/delete 操作后异步发送 MQ 消息
- 数据聚合:用户数据包含部门和岗位简要信息

Ref: docs/databus/implementation-checklist.md 任务 1-38
This commit is contained in:
hewencai
2025-12-01 22:25:28 +08:00
parent 7fae3203bc
commit f5ba493f95
19 changed files with 1939 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
package com.zt.plat.module.databus.api.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户数据对象DataBus
*
* @author ZT
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户数据")
public class DatabusAdminUserData implements Serializable {
private static final long serialVersionUID = 1L;
// ========== 基本信息 ==========
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
private Long id;
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "zhangsan")
private String username;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
private String nickname;
@Schema(description = "用户手机号", example = "13800138000")
private String mobile;
@Schema(description = "用户邮箱", example = "zhangsan@example.com")
private String email;
@Schema(description = "用户性别0-未知 1-男 2-女", example = "1")
private Integer sex;
@Schema(description = "用户头像URL", example = "https://example.com/avatar.jpg")
private String avatar;
@Schema(description = "状态0-启用 1-禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "备注", example = "测试用户")
private String remark;
@Schema(description = "用户来源类型", example = "1")
private Integer userSource;
// ========== 组织信息 ==========
@Schema(description = "所属部门ID列表", example = "[100, 101]")
private List<Long> deptIds;
@Schema(description = "所属部门信息列表")
private List<DeptSimpleInfo> depts;
// ========== 岗位信息 ==========
@Schema(description = "岗位ID列表", example = "[1, 2]")
private List<Long> postIds;
@Schema(description = "岗位信息列表")
private List<PostSimpleInfo> posts;
// ========== 多租户 ==========
@Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long tenantId;
// ========== 时间信息 ==========
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
/**
* 部门简要信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "部门简要信息")
public static class DeptSimpleInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "部门ID", example = "100")
private Long deptId;
@Schema(description = "部门编码", example = "DEPT_001")
private String deptCode;
@Schema(description = "部门名称", example = "技术部")
private String deptName;
}
/**
* 岗位简要信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "岗位简要信息")
public static class PostSimpleInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "岗位ID", example = "1")
private Long postId;
@Schema(description = "岗位编码", example = "CEO")
private String postCode;
@Schema(description = "岗位名称", example = "首席执行官")
private String postName;
}
}

View File

@@ -0,0 +1,90 @@
package com.zt.plat.module.databus.api.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 部门数据对象DataBus
*
* @author ZT
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "部门数据")
public class DatabusDeptData implements Serializable {
private static final long serialVersionUID = 1L;
// ========== 基本信息 ==========
@Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Long id;
@Schema(description = "部门编码", example = "DEPT_001")
private String code;
@Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "技术部")
private String name;
@Schema(description = "部门简称", example = "技术")
private String shortName;
@Schema(description = "父部门ID顶级为0", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Long parentId;
@Schema(description = "排序号", example = "1")
private Integer sort;
@Schema(description = "状态0-启用 1-禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
// ========== 部门类型 ==========
@Schema(description = "部门类型28-公司 26-部门", example = "26")
private Integer deptType;
@Schema(description = "是否集团", example = "false")
private Boolean isGroup;
@Schema(description = "是否公司", example = "false")
private Boolean isCompany;
@Schema(description = "部门来源类型", example = "1")
private Integer deptSource;
// ========== 联系信息 ==========
@Schema(description = "负责人ID", example = "1001")
private Long leaderUserId;
@Schema(description = "负责人姓名", example = "张三")
private String leaderUserName;
@Schema(description = "联系电话", example = "010-12345678")
private String phone;
@Schema(description = "邮箱", example = "tech@example.com")
private String email;
// ========== 多租户 ==========
@Schema(description = "租户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long tenantId;
// ========== 时间信息 ==========
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,67 @@
package com.zt.plat.module.databus.api.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 岗位数据对象DataBus
*
* @author ZT
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DatabusPostData implements Serializable {
/**
* 岗位ID
*/
private Long id;
/**
* 岗位编码
*/
private String code;
/**
* 岗位名称
*/
private String name;
/**
* 排序
*/
private Integer sort;
/**
* 状态0正常 1停用
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 租户ID
*/
private Long tenantId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,48 @@
package com.zt.plat.module.databus.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 游标分页请求 DTO
* <p>
* 用于 Databus 全量同步时的断点续传
* 使用 create_time + id 复合游标解决雪花ID非连续问题
*
* @author ZT
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "游标分页请求")
public class CursorPageReqDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "游标时间(上一批最后一条数据的创建时间)", example = "2024-01-01T00:00:00")
private LocalDateTime cursorTime;
@Schema(description = "游标ID上一批最后一条数据的ID用于同一时间戳内去重", example = "1234567890")
private Long cursorId;
@Schema(description = "批量大小", example = "100")
private Integer batchSize;
@Schema(description = "租户ID可选用于多租户过滤", example = "1")
private Long tenantId;
/**
* 是否为首次查询(无游标)
*/
public boolean isFirstPage() {
return cursorTime == null && cursorId == null;
}
}

View File

@@ -0,0 +1,81 @@
package com.zt.plat.module.databus.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 游标分页结果 DTO
* <p>
* 用于 Databus 全量同步时的断点续传响应
*
* @author ZT
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "游标分页结果")
public class CursorPageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "数据列表")
private List<T> list;
@Schema(description = "下一页游标时间(最后一条数据的创建时间)")
private LocalDateTime nextCursorTime;
@Schema(description = "下一页游标ID最后一条数据的ID")
private Long nextCursorId;
@Schema(description = "本次返回的数据量")
private Integer count;
@Schema(description = "是否还有更多数据")
private Boolean hasMore;
@Schema(description = "总数据量(可选,首次查询时返回)")
private Long total;
/**
* 构建首页结果
*/
public static <T> CursorPageResult<T> of(List<T> list, LocalDateTime nextCursorTime,
Long nextCursorId, boolean hasMore, Long total) {
return CursorPageResult.<T>builder()
.list(list)
.nextCursorTime(nextCursorTime)
.nextCursorId(nextCursorId)
.count(list != null ? list.size() : 0)
.hasMore(hasMore)
.total(total)
.build();
}
/**
* 构建后续页结果
*/
public static <T> CursorPageResult<T> of(List<T> list, LocalDateTime nextCursorTime,
Long nextCursorId, boolean hasMore) {
return of(list, nextCursorTime, nextCursorId, hasMore, null);
}
/**
* 构建空结果
*/
public static <T> CursorPageResult<T> empty() {
return CursorPageResult.<T>builder()
.list(List.of())
.count(0)
.hasMore(false)
.build();
}
}

View File

@@ -0,0 +1,71 @@
package com.zt.plat.module.databus.api.provider;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusDeptData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Databus 部门数据提供者 API
* <p>
* 供 Databus 调用,获取部门数据用于全量/增量同步
*
* @author ZT
*/
@FeignClient(name = "${databus.provider.dept.service:system-server}")
@Tag(name = "RPC 服务 - Databus 部门数据提供者")
public interface DatabusDeptProviderApi {
String PREFIX = "/rpc/databus/dept";
/**
* 游标分页查询部门数据(用于全量同步)
*
* @param reqDTO 游标分页请求
* @return 部门数据分页结果
*/
@PostMapping(PREFIX + "/page-by-cursor")
@Operation(summary = "游标分页查询部门数据")
CommonResult<CursorPageResult<DatabusDeptData>> getPageByCursor(@RequestBody CursorPageReqDTO reqDTO);
/**
* 根据ID查询部门详情用于增量同步
*
* @param id 部门ID
* @return 部门数据
*/
@GetMapping(PREFIX + "/get")
@Operation(summary = "查询部门详情")
@Parameter(name = "id", description = "部门ID", required = true, example = "100")
CommonResult<DatabusDeptData> getById(@RequestParam("id") Long id);
/**
* 批量查询部门详情(用于增量同步批量获取)
*
* @param ids 部门ID列表
* @return 部门数据列表
*/
@GetMapping(PREFIX + "/list")
@Operation(summary = "批量查询部门详情")
@Parameter(name = "ids", description = "部门ID列表", required = true, example = "100,101,102")
CommonResult<List<DatabusDeptData>> getListByIds(@RequestParam("ids") List<Long> ids);
/**
* 统计部门总数(用于全量同步进度计算)
*
* @param tenantId 租户ID可选
* @return 部门总数
*/
@GetMapping(PREFIX + "/count")
@Operation(summary = "统计部门总数")
@Parameter(name = "tenantId", description = "租户ID", example = "1")
CommonResult<Long> count(@RequestParam(value = "tenantId", required = false) Long tenantId);
}

View File

@@ -0,0 +1,71 @@
package com.zt.plat.module.databus.api.provider;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusPostData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Databus 岗位数据提供者 API
* <p>
* 供 Databus 调用,获取岗位数据用于全量/增量同步
*
* @author ZT
*/
@FeignClient(name = "${databus.provider.post.service:system-server}")
@Tag(name = "RPC 服务 - Databus 岗位数据提供者")
public interface DatabusPostProviderApi {
String PREFIX = "/rpc/databus/post";
/**
* 游标分页查询岗位数据(用于全量同步)
*
* @param reqDTO 游标分页请求
* @return 岗位数据分页结果
*/
@PostMapping(PREFIX + "/page-by-cursor")
@Operation(summary = "游标分页查询岗位数据")
CommonResult<CursorPageResult<DatabusPostData>> getPageByCursor(@RequestBody CursorPageReqDTO reqDTO);
/**
* 根据ID查询岗位详情用于增量同步
*
* @param id 岗位ID
* @return 岗位数据
*/
@GetMapping(PREFIX + "/get")
@Operation(summary = "查询岗位详情")
@Parameter(name = "id", description = "岗位ID", required = true, example = "1")
CommonResult<DatabusPostData> getById(@RequestParam("id") Long id);
/**
* 批量查询岗位详情(用于增量同步批量获取)
*
* @param ids 岗位ID列表
* @return 岗位数据列表
*/
@GetMapping(PREFIX + "/list")
@Operation(summary = "批量查询岗位详情")
@Parameter(name = "ids", description = "岗位ID列表", required = true, example = "1,2,3")
CommonResult<List<DatabusPostData>> getListByIds(@RequestParam("ids") List<Long> ids);
/**
* 统计岗位总数(用于全量同步进度计算)
*
* @param tenantId 租户ID可选
* @return 岗位总数
*/
@GetMapping(PREFIX + "/count")
@Operation(summary = "统计岗位总数")
@Parameter(name = "tenantId", description = "租户ID", example = "1")
CommonResult<Long> count(@RequestParam(value = "tenantId", required = false) Long tenantId);
}

View File

@@ -0,0 +1,71 @@
package com.zt.plat.module.databus.api.provider;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.api.data.DatabusAdminUserData;
import com.zt.plat.module.databus.api.dto.CursorPageReqDTO;
import com.zt.plat.module.databus.api.dto.CursorPageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Databus 用户数据提供者 API
* <p>
* 供 Databus 调用,获取用户数据用于全量/增量同步
*
* @author ZT
*/
@FeignClient(name = "${databus.provider.user.service:system-server}")
@Tag(name = "RPC 服务 - Databus 用户数据提供者")
public interface DatabusUserProviderApi {
String PREFIX = "/rpc/databus/user";
/**
* 游标分页查询用户数据(用于全量同步)
*
* @param reqDTO 游标分页请求
* @return 用户数据分页结果
*/
@PostMapping(PREFIX + "/page-by-cursor")
@Operation(summary = "游标分页查询用户数据")
CommonResult<CursorPageResult<DatabusAdminUserData>> getPageByCursor(@RequestBody CursorPageReqDTO reqDTO);
/**
* 根据ID查询用户详情用于增量同步
*
* @param id 用户ID
* @return 用户数据
*/
@GetMapping(PREFIX + "/get")
@Operation(summary = "查询用户详情")
@Parameter(name = "id", description = "用户ID", required = true, example = "1001")
CommonResult<DatabusAdminUserData> getById(@RequestParam("id") Long id);
/**
* 批量查询用户详情(用于增量同步批量获取)
*
* @param ids 用户ID列表
* @return 用户数据列表
*/
@GetMapping(PREFIX + "/list")
@Operation(summary = "批量查询用户详情")
@Parameter(name = "ids", description = "用户ID列表", required = true, example = "1001,1002,1003")
CommonResult<List<DatabusAdminUserData>> getListByIds(@RequestParam("ids") List<Long> ids);
/**
* 统计用户总数(用于全量同步进度计算)
*
* @param tenantId 租户ID可选
* @return 用户总数
*/
@GetMapping(PREFIX + "/count")
@Operation(summary = "统计用户总数")
@Parameter(name = "tenantId", description = "租户ID", example = "1")
CommonResult<Long> count(@RequestParam(value = "tenantId", required = false) Long tenantId);
}