✨ 组织与人员支持OA增量同步,并完善用户启停与性别空值

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-10 13:56:29 +08:00
parent e20afc0d55
commit 4e7193893f
22 changed files with 423 additions and 100 deletions
@@ -216,4 +216,12 @@ public class Dept extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String oaSupSubComId;
/**
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用
*/
@JsonIgnore
@Schema(description = "OA上次更新时间", hidden = true)
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String oaModified;
}
@@ -35,8 +35,12 @@ public enum UserSex {
if (name == null || name.isEmpty()) {
return null;
}
String sexName = name.trim();
if ("-1".equals(sexName) || "0".equals(sexName) || "未知".equals(sexName)) {
return null;
}
return Stream.of(values())
.filter(e -> e.getName().equals(name))
.filter(e -> e.getName().equals(sexName) || String.valueOf(e.getCode()).equals(sexName))
.map(UserSex::getCode)
.findFirst().orElse(null);
}
@@ -66,4 +66,7 @@ public class OaOrgSyncPageVO implements Serializable {
@Schema(description = "是否已到最后一页")
private Boolean finished;
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
private String modifiedSince;
}
@@ -63,4 +63,7 @@ public class OaPersonSyncPageVO implements Serializable {
@Schema(description = "是否已到最后一页")
private Boolean finished;
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
private String modifiedSince;
}
@@ -25,6 +25,7 @@
*/
package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -131,6 +132,11 @@ public class User extends TenantEntity implements IPhone {
* 是否是oa的账号
*/
private Integer isOa;
/**
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用
*/
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String oaModified;
/**
* 是否推送mk
*/
@@ -93,7 +93,8 @@ public class DeptController extends BladeController {
@PreAuth(menu = "dept")
@GetMapping("/list")
@Parameters({
@Parameter(name = "deptName", description = "部门名称", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "deptName", description = "组织名称", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "shortName", description = "组织简称", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "fullName", description = "部门全称", in = ParameterIn.QUERY, schema = @Schema(type = "string"))
})
@ApiOperationSupport(order = 2)
@@ -108,7 +109,8 @@ public class DeptController extends BladeController {
@PreAuth(menu = "dept")
@GetMapping("/lazy-list")
@Parameters({
@Parameter(name = "deptName", description = "部门名称", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "deptName", description = "组织名称", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "shortName", description = "组织简称", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
@Parameter(name = "fullName", description = "部门全称", in = ParameterIn.QUERY, schema = @Schema(type = "string"))
})
@ApiOperationSupport(order = 3)
@@ -195,8 +197,10 @@ public class DeptController extends BladeController {
@Operation(summary = "同步OA公司")
public R<OaOrgSyncPageVO> syncOaCompany(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
return R.data(oaSyncService.syncCompanyPage(current, size));
@RequestParam(defaultValue = "20") Integer size,
@RequestParam(defaultValue = "false") Boolean incremental,
@RequestParam(required = false) String modifiedSince) {
return R.data(oaSyncService.syncCompanyPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince));
}
/**
@@ -208,8 +212,10 @@ public class DeptController extends BladeController {
@Operation(summary = "同步OA部门")
public R<OaOrgSyncPageVO> syncOaDepartment(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
return R.data(oaSyncService.syncDepartmentPage(current, size));
@RequestParam(defaultValue = "20") Integer size,
@RequestParam(defaultValue = "false") Boolean incremental,
@RequestParam(required = false) String modifiedSince) {
return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince));
}
/**
@@ -171,8 +171,10 @@ public class UserController {
@Operation(summary = "同步OA人员")
public R<OaPersonSyncPageVO> syncIamAccounts(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
return R.data(oaSyncService.syncPersonFromUserList(current, size));
@RequestParam(defaultValue = "20") Integer size,
@RequestParam(defaultValue = "false") Boolean incremental,
@RequestParam(required = false) String modifiedSince) {
return R.data(oaSyncService.syncPersonFromUserList(current, size, Boolean.TRUE.equals(incremental), modifiedSince));
}
/**
@@ -186,6 +188,18 @@ public class UserController {
return R.status(userService.updateUser(user));
}
/**
* 启用或停用
*/
@IsAdmin
@PostMapping("/change-status")
@ApiOperationSupport(order = 6)
@Operation(summary = "启用停用", description = "传入id和status")
public R changeStatus(@Parameter(description = "用户主键", required = true) @RequestParam Long id,
@Parameter(description = "状态:1启用,0停用", required = true) @RequestParam Integer status) {
return R.status(userService.changeUserStatus(id, status));
}
/**
* 删除
*/
@@ -47,7 +47,8 @@ public interface DeptConvert {
// 父公司id转为父部门编码,要拼接前缀
@Mapping(source = "supsubcomid", target = "parentCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "modified", target = "updateTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
// OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OACompanyResponse company);
@Mapping(source = "departmentname", target = "deptName")
@@ -57,7 +58,8 @@ public interface DeptConvert {
// 所属子公司id转为所属公司编码,要拼接前缀
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "modified", target = "updateTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
// OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OADepartmentResponse department);
/**
@@ -70,6 +70,7 @@ public interface UserConvert {
@Mapping(source = "lastname", target = "realName")
@Mapping(source = "mobile", target = "phone")
@Mapping(source = "email", target = "email")
@Mapping(source = "modified", target = "oaModified")
User baseConvert(OAPersonResponse person);
@Mapping(source = "subcompanyname", target = "companyName")
@@ -80,6 +80,9 @@
<if test="param3.deptName!=null and param3.deptName!=''">
and dept.dept_name like concat(concat('%', #{param3.deptName}),'%')
</if>
<if test="param3.shortName!=null and param3.shortName!=''">
and dept.short_name like concat(concat('%', #{param3.shortName}),'%')
</if>
<if test="param3.fullName!=null and param3.fullName!=''">
and dept.full_name like concat(concat('%', #{param3.fullName}),'%')
</if>
@@ -32,27 +32,33 @@ public interface IOASyncService {
/**
* 按页从 OA 人员接口同步组织与人员
*
* @param current 当前页,从 1 开始
* @param size 每页条数
* @param current 当前页,从 1 开始
* @param size 每页条数
* @param incremental 是否增量同步(按本地最大 oaModified 过滤 OA)
* @param modifiedSince 增量起始时间;首屏可空,后续分页回传首屏返回值
* @return 本页同步结果
*/
OaPersonSyncPageVO syncPersonFromUserList(int current, int size);
OaPersonSyncPageVO syncPersonFromUserList(int current, int size, boolean incremental, String modifiedSince);
/**
* 按页从 OA 公司接口同步公司
*
* @param current 当前页,从 1 开始
* @param size 每页条数
* @param current 当前页,从 1 开始
* @param size 每页条数
* @param incremental 是否增量同步(按本地最大 oaModified 过滤 OA)
* @param modifiedSince 增量起始时间;首屏可空,后续分页回传首屏返回值
* @return 本页同步结果
*/
OaOrgSyncPageVO syncCompanyPage(int current, int size);
OaOrgSyncPageVO syncCompanyPage(int current, int size, boolean incremental, String modifiedSince);
/**
* 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表
*
* @param current 当前页,从 1 开始
* @param size 每页条数
* @param current 当前页,从 1 开始
* @param size 每页条数
* @param incremental 是否增量同步(按本地最大 oaModified 过滤 OA)
* @param modifiedSince 增量起始时间;首屏可空,后续分页回传首屏返回值
* @return 本页同步结果
*/
OaOrgSyncPageVO syncDepartmentPage(int current, int size);
OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince);
}
@@ -93,6 +93,15 @@ public interface IUserService extends BaseService<User> {
*/
boolean updateUser(User user);
/**
* 启用或停用用户(租户守卫校验用户归属,仅更新状态)
*
* @param userId 用户主键
* @param status 状态:1 启用,0 停用
* @return 是否成功
*/
boolean changeUserStatus(Long userId, Integer status);
/**
* 修改用户基本信息(仅限当前登录用户自身,忽略角色 / 部门 / 岗位 / 账号等敏感字段)
*
@@ -434,6 +434,10 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (dept.getIsPlatformCompany() == null) {
dept.setIsPlatformCompany(0);
}
// 排序未填时默认 1
if (dept.getSort() == null) {
dept.setSort(1);
}
if (Func.isEmpty(dept.getTenantId())) {
throw new ServiceException("租户ID不能为空");
}
@@ -60,6 +60,7 @@ public class OASyncServiceImpl implements IOASyncService {
private final IOAClient oaClient;
private final DeptConvert deptConvert;
private final IDeptService deptService;
private final IUserService userService;
private final IRoleService roleService;
private final IMKPushService mkPushService;
private final IDataSyncRecordService dataSyncRecordService;
@@ -124,10 +125,10 @@ public class OASyncServiceImpl implements IOASyncService {
@Transactional(rollbackFor = Exception.class)
@Override
public OaPersonSyncPageVO syncPersonFromUserList(int current, int size) {
public OaPersonSyncPageVO syncPersonFromUserList(int current, int size, boolean incremental, String modifiedSince) {
try {
ComposeLogUtil.addLog(log);
return this.syncPersonFromOaPage(current, size);
return this.syncPersonFromOaPage(current, size, incremental, modifiedSince);
} finally {
ComposeLogUtil.removeLastLog();
}
@@ -135,10 +136,10 @@ public class OASyncServiceImpl implements IOASyncService {
@Transactional(rollbackFor = Exception.class)
@Override
public OaOrgSyncPageVO syncCompanyPage(int current, int size) {
public OaOrgSyncPageVO syncCompanyPage(int current, int size, boolean incremental, String modifiedSince) {
try {
ComposeLogUtil.addLog(log);
return this.syncCompanyFromOaPage(current, size);
return this.syncCompanyFromOaPage(current, size, incremental, modifiedSince);
} finally {
ComposeLogUtil.removeLastLog();
}
@@ -146,10 +147,10 @@ public class OASyncServiceImpl implements IOASyncService {
@Transactional(rollbackFor = Exception.class)
@Override
public OaOrgSyncPageVO syncDepartmentPage(int current, int size) {
public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince) {
try {
ComposeLogUtil.addLog(log);
return this.syncDepartmentFromOaPage(current, size);
return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince);
} finally {
ComposeLogUtil.removeLastLog();
}
@@ -280,14 +281,17 @@ public class OASyncServiceImpl implements IOASyncService {
/**
* 按页从 OA 人员列表同步组织与人员
*
* @param current 当前页
* @param size 每页条数
* @param current 当前页
* @param size 每页条数
* @param incremental 是否增量同步
* @param modifiedSince 增量起始时间(后续分页回传)
* @return 本页同步结果
*/
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size) {
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
OAPersonSearch personSearch = buildPersonSearch(null);
IncrementalStart incrementalStart = resolvePersonIncrementalStart(incremental, modifiedSince);
OAPersonSearch personSearch = buildPersonSearch(incrementalStart.getStartTime());
personSearch.setCurPage(pageNo);
personSearch.setPageSize(pageSize);
OAResponse<OAPersonResponse> oaResponse = oaClient.queryPersonPage(new OASearch<>(personSearch));
@@ -311,26 +315,31 @@ public class OASyncServiceImpl implements IOASyncService {
pageVO.setFetchedCount(oaPersons.size());
pageVO.setSyncedCount(personSyncCount.getSyncedCount());
pageVO.setSkippedCount(personSyncCount.getSkippedCount());
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
boolean finished = oaPersons.isEmpty()
|| oaPersons.size() < pageSize
|| (long) pageNo * pageSize >= totalSize;
pageVO.setFinished(finished);
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 {}/{},成功{},跳过{}",
pageNo, totalSize, personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{}",
incremental ? "incremental" : "full", pageNo, totalSize,
personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
return pageVO;
}
/**
* 按页从 OA 公司接口同步公司
*
* @param current 当前页
* @param size 每页条数
* @param current 当前页
* @param size 每页条数
* @param incremental 是否增量同步
* @param modifiedSince 增量起始时间(后续分页回传)
* @return 本页同步结果
*/
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size) {
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
OACompanySearch companySearch = buildCompanySearch(null);
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.COMPANY);
OACompanySearch companySearch = buildCompanySearch(incrementalStart.getStartTime());
companySearch.setCurPage(pageNo);
companySearch.setPageSize(pageSize);
OAResponse<OACompanyResponse> oaResponse = oaClient.queryCompanyPage(new OASearch<>(companySearch));
@@ -347,6 +356,7 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
}
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount);
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
if (Boolean.TRUE.equals(pageVO.getFinished())) {
// 公司全部分批入库后:重建层级,并删除上级不存在的数据
deptService.rebuildOaHierarchy();
@@ -354,19 +364,22 @@ public class OASyncServiceImpl implements IOASyncService {
}
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
ComposeLogUtil.getLastLog().info("OA公司分页同步完成 {}/{},成功{},跳过{}",
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
ComposeLogUtil.getLastLog().info("OA公司分页同步完成 mode={} {}/{},成功{},跳过{}",
incremental ? "incremental" : "full", pageNo, totalSize,
syncCount.getSyncedCount(), syncCount.getSkippedCount());
return pageVO;
}
/**
* 按页从 OA 部门接口同步部门
*
* @param current 当前页
* @param size 每页条数
* @param current 当前页
* @param size 每页条数
* @param incremental 是否增量同步
* @param modifiedSince 增量起始时间(后续分页回传)
* @return 本页同步结果
*/
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size) {
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
String subCompanyIds = getOaSubCompanyIds(true);
@@ -376,7 +389,8 @@ public class OASyncServiceImpl implements IOASyncService {
emptyPageVO.setFinished(true);
return emptyPageVO;
}
OADepartmentSearch departmentSearch = buildDepartmentSearch(null, subCompanyIds);
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.DEPT);
OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), subCompanyIds);
departmentSearch.setCurPage(pageNo);
departmentSearch.setPageSize(pageSize);
OAResponse<OADepartmentResponse> oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch));
@@ -393,6 +407,7 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
}
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
if (Boolean.TRUE.equals(pageVO.getFinished())) {
// 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
deptService.rebuildOaHierarchy();
@@ -401,11 +416,112 @@ public class OASyncServiceImpl implements IOASyncService {
}
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 {}/{},成功{},跳过{}",
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} {}/{},成功{},跳过{}",
incremental ? "incremental" : "full", pageNo, totalSize,
syncCount.getSyncedCount(), syncCount.getSkippedCount());
return pageVO;
}
/**
* 解析增量同步起始时间:优先使用前端回传值,保证同阶段分页起点不变
*/
private IncrementalStart resolveIncrementalStart(boolean incremental, String modifiedSince, DeptCategory deptCategory) {
if (!incremental) {
return IncrementalStart.empty();
}
if (StringUtils.isNotBlank(modifiedSince)) {
try {
Date startTime = DateUtil.parse(modifiedSince.trim(), DateUtil.PATTERN_DATETIME);
return new IncrementalStart(startTime, modifiedSince.trim());
} catch (Exception exception) {
ComposeLogUtil.getLastLog().warn("解析回传 modifiedSince 失败:{},重新计算", modifiedSince);
}
}
Dept latest = deptService.getOne(Wrappers.<Dept>lambdaQuery()
.select(Dept::getOaModified)
.eq(Dept::getIsOa, YES)
.eq(Dept::getDeptCategory, deptCategory.getCode())
.isNotNull(Dept::getOaModified)
.ne(Dept::getOaModified, "")
.orderByDesc(Dept::getOaModified)
.last("limit 1"), false);
if (latest == null || StringUtils.isBlank(latest.getOaModified())) {
ComposeLogUtil.getLastLog().info("本地无 {} oaModified 记录,增量同步按全量拉取后按字段比对跳过",
deptCategory.getName());
return IncrementalStart.empty();
}
try {
Date startTime = DateUtil.parse(latest.getOaModified(), DateUtil.PATTERN_DATETIME);
ComposeLogUtil.getLastLog().info("增量同步{} OA modified 起始时间:{}",
deptCategory.getName(), latest.getOaModified());
return new IncrementalStart(startTime, latest.getOaModified());
} catch (Exception exception) {
ComposeLogUtil.getLastLog().warn("解析本地最大 oaModified 失败:{},回退为全量拉取", latest.getOaModified());
return IncrementalStart.empty();
}
}
/**
* 人员增量同步起始时间:优先回传值,否则取本地 OA 用户最大 oaModified
*/
private IncrementalStart resolvePersonIncrementalStart(boolean incremental, String modifiedSince) {
if (!incremental) {
return IncrementalStart.empty();
}
if (StringUtils.isNotBlank(modifiedSince)) {
try {
Date startTime = DateUtil.parse(modifiedSince.trim(), DateUtil.PATTERN_DATETIME);
return new IncrementalStart(startTime, modifiedSince.trim());
} catch (Exception exception) {
ComposeLogUtil.getLastLog().warn("解析人员回传 modifiedSince 失败:{},重新计算", modifiedSince);
}
}
User latest = userService.getOne(Wrappers.<User>lambdaQuery()
.select(User::getOaModified)
.eq(User::getIsOa, YES)
.isNotNull(User::getOaModified)
.ne(User::getOaModified, "")
.orderByDesc(User::getOaModified)
.last("limit 1"), false);
if (latest == null || StringUtils.isBlank(latest.getOaModified())) {
ComposeLogUtil.getLastLog().info("本地无人员 oaModified 记录,增量同步按全量拉取后按字段比对跳过");
return IncrementalStart.empty();
}
try {
Date startTime = DateUtil.parse(latest.getOaModified(), DateUtil.PATTERN_DATETIME);
ComposeLogUtil.getLastLog().info("增量同步人员 OA modified 起始时间:{}", latest.getOaModified());
return new IncrementalStart(startTime, latest.getOaModified());
} catch (Exception exception) {
ComposeLogUtil.getLastLog().warn("解析本地人员最大 oaModified 失败:{},回退为全量拉取", latest.getOaModified());
return IncrementalStart.empty();
}
}
/**
* 增量同步起点
*/
private static class IncrementalStart {
private final Date startTime;
private final String modifiedSince;
private IncrementalStart(Date startTime, String modifiedSince) {
this.startTime = startTime;
this.modifiedSince = modifiedSince;
}
private static IncrementalStart empty() {
return new IncrementalStart(null, null);
}
private Date getStartTime() {
return startTime;
}
private String getModifiedSince() {
return modifiedSince;
}
}
/**
* 组装组织分页同步结果
*/
@@ -507,14 +623,10 @@ public class OASyncServiceImpl implements IOASyncService {
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
// 查询数据库的部门,转换成map
Map<String, Long> deptMap = getAllCompanyDeptMap();
// 处理部门
List<Dept> notHandledList = handleDept(allParam, deptMap, DeptCategory.COMPANY);
int syncedCount = allParam.size() - notHandledList.size();
int skippedCount = filteredSkipCount + notHandledList.size();
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
// 查询已存在公司(含 oaModified),按编码索引
Map<String, Dept> existingDeptMap = loadExistingDeptMap(DeptCategory.COMPANY, null);
Map<String, Long> deptIdByCode = toDeptIdMap(existingDeptMap);
return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
}
/**
@@ -558,39 +670,62 @@ public class OASyncServiceImpl implements IOASyncService {
deptCodes.add(dept.getParentCode());
}
});
// 查询数据库的部门,转换成map。包含本页及上级编码,便于命中已入库的上级
Map<String, Long> deptMap = deptCodes.isEmpty() ? new HashMap<>() : deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
.in(Dept::getDeptCode, deptCodes)
).stream()
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
.collect(Collectors.toMap(Dept::getDeptCode, Dept::getId, (a, b) -> b));
// 查询数据库已有部门(含 oaModified)
Map<String, Dept> existingDeptMap = loadExistingDeptMap(DeptCategory.DEPT, deptCodes);
Map<String, Long> deptIdByCode = toDeptIdMap(existingDeptMap);
// 公司编码也放入 map,根部门挂到公司时才能解析父级
companyDeptMap.forEach(deptMap::putIfAbsent);
// 处理部门
List<Dept> notHandledList = this.handleDept(allParam, deptMap, DeptCategory.DEPT);
int syncedCount = allParam.size() - notHandledList.size();
int skippedCount = filteredSkipCount + notHandledList.size();
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
companyDeptMap.forEach(deptIdByCode::putIfAbsent);
return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
}
/**
* 处理部门
* @param allParam 待处理的部门
* @param deptMap 数据库所有部门编码和id map
* @return 未处理的数据
* 加载已存在组织,按部门编码索引
*
* @param deptCategory 组织类型
* @param deptCodes 限定编码;为空则按类型全量加载
*/
private List<Dept> handleDept(List<Dept> allParam, Map<String, Long> deptMap, DeptCategory deptCategory) {
// 数据库存在的所有部门id
Set<Long> existsDeptIds = new HashSet<>(deptMap.values());
private Map<String, Dept> loadExistingDeptMap(DeptCategory deptCategory, Set<String> deptCodes) {
if (deptCodes != null && deptCodes.isEmpty()) {
return new HashMap<>();
}
List<Dept> deptList = deptService.list(Wrappers.<Dept>lambdaQuery()
.eq(Dept::getDeptCategory, deptCategory.getCode())
.in(deptCodes != null, Dept::getDeptCode, deptCodes)
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId));
return deptList.stream()
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
.collect(Collectors.toMap(Dept::getDeptCode, dept -> dept, (a, b) -> b));
}
private Map<String, Long> toDeptIdMap(Map<String, Dept> existingDeptMap) {
Map<String, Long> deptIdByCode = new HashMap<>();
existingDeptMap.forEach((code, dept) -> deptIdByCode.put(code, dept.getId()));
return deptIdByCode;
}
/**
* 持久化同步数据:不存在则新增;存在且 oaModified 不一致才修改
*
* @param allParam 待处理数据
* @param existingDeptMap 已存在组织(同类型)
* @param deptIdByCode 编码→id(含上级公司等)
* @param deptCategory 组织类型
* @param filteredSkipCount 上游过滤跳过数
* @return 同步统计
*/
private OrgSyncCount persistSyncedDepts(List<Dept> allParam, Map<String, Dept> existingDeptMap,
Map<String, Long> deptIdByCode, DeptCategory deptCategory,
int filteredSkipCount) {
Set<Long> existsDeptIds = existingDeptMap.values().stream()
.map(Dept::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
allParam.forEach(dept -> {
if (deptMap.containsKey(dept.getDeptCode())) {
// 根据部门编码获取对应的部门id
dept.setId(deptMap.get(dept.getDeptCode()));
if (deptIdByCode.containsKey(dept.getDeptCode())) {
dept.setId(deptIdByCode.get(dept.getDeptCode()));
} else {
// 没有就生成一个id
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
deptMap.put(dept.getDeptCode(), dept.getId());
deptIdByCode.put(dept.getDeptCode(), dept.getId());
}
});
@@ -600,7 +735,7 @@ public class OASyncServiceImpl implements IOASyncService {
if (dept.getParentId() != null) {
continue;
}
Long parentId = deptMap.get(dept.getParentCode());
Long parentId = deptIdByCode.get(dept.getParentCode());
if (parentId != null) {
dept.setParentId(parentId);
continue;
@@ -615,11 +750,10 @@ public class OASyncServiceImpl implements IOASyncService {
deptCategory.getName(), unresolvedParentCount);
}
// 父id不为空才新增;已存在的记录即使父id未解析也更新(空父id不会覆盖原值)
// 父id不为空才新增;已存在的记录即使父id未解析也允许更新(空父id不会覆盖原值)
List<Dept> list = allParam.stream()
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
.toList();
// 不存在的新增
List<Dept> addList = list.stream()
.filter(dept -> !existsDeptIds.contains(dept.getId()))
.toList();
@@ -627,18 +761,43 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
deptService.saveBatch(addList);
}
// 存在的修改
// 已存在且 oaModified 不一致(或不存在可比对值)才修改
List<Dept> updateList = list.stream()
.filter(dept -> existsDeptIds.contains(dept.getId()))
.filter(dept -> !shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
.toList();
if (CollectionUtil.isNotEmpty(updateList)) {
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
deptService.updateBatchById(updateList);
}
// 返回仍未保存的数据
return allParam.stream()
int unchangedCount = (int) list.stream()
.filter(dept -> existsDeptIds.contains(dept.getId()))
.filter(dept -> shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
.count();
List<Dept> notHandledList = allParam.stream()
.filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
.toList();
int syncedCount = addList.size() + updateList.size();
int skippedCount = filteredSkipCount + unchangedCount + notHandledList.size();
if (unchangedCount > 0) {
ComposeLogUtil.getLastLog().info("跳过未变更{}:{}", deptCategory.getName(), unchangedCount);
}
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
}
/**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
*/
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
if (existing == null) {
return false;
}
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
if (localModified.isEmpty() || remoteModified.isEmpty()) {
return false;
}
return localModified.equals(remoteModified);
}
/**
@@ -249,6 +249,7 @@ public class OaUserListSyncHelper {
});
Map<String, Long> userMap = new HashMap<>();
Set<Long> existsUserIds = new HashSet<>();
Set<Long> unchangedUserIds = new HashSet<>();
users.forEach(user -> {
User existingUser = existingByAccount.get(user.getAccount());
if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) {
@@ -258,6 +259,9 @@ public class OaUserListSyncHelper {
user.setId(existingUser.getId());
user.setPassword(null);
existsUserIds.add(existingUser.getId());
if (shouldSkipOaUpdate(existingUser, user)) {
unchangedUserIds.add(existingUser.getId());
}
} else {
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
user.setPostId("-1");
@@ -279,12 +283,18 @@ public class OaUserListSyncHelper {
}
List<User> updateUsers = users.stream()
.filter(user -> existsUserIds.contains(user.getId()))
.filter(user -> !unchangedUserIds.contains(user.getId()))
.toList();
if (CollectionUtil.isNotEmpty(updateUsers)) {
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
userService.updateBatchById(updateUsers);
}
if (!unchangedUserIds.isEmpty()) {
ComposeLogUtil.getLastLog().info("跳过未变更用户:{}", unchangedUserIds.size());
}
ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount);
int syncedCount = addUsers.size() + updateUsers.size();
skippedCount += unchangedUserIds.size();
if (userMap.isEmpty()) {
return new PersonSyncCount(0, skippedCount);
}
@@ -295,7 +305,13 @@ public class OaUserListSyncHelper {
.collect(Collectors.toMap(this::getUserDeptKey, UserDept::getId, (first, second) -> second));
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
List<UserDept> userDeptList = oaPersons.stream()
.filter(oaPerson -> userMap.containsKey(userConvert.resolveAccount(oaPerson)))
.filter(oaPerson -> {
String account = userConvert.resolveAccount(oaPerson);
if (!userMap.containsKey(account)) {
return false;
}
return !unchangedUserIds.contains(userMap.get(account));
})
.map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex))
.filter(Objects::nonNull)
.toList();
@@ -333,6 +349,7 @@ public class OaUserListSyncHelper {
.collect(Collectors.toSet());
String defaultRoleId = getDefaultRoleId();
List<User> updateUserParams = list.stream()
.filter(userDeptIds -> !unchangedUserIds.contains(userDeptIds.getUserId()))
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
.map(userDeptIds -> {
User user = new User();
@@ -347,7 +364,22 @@ public class OaUserListSyncHelper {
if (CollectionUtil.isNotEmpty(updateUserParams)) {
userService.updateBatchById(updateUserParams);
}
return new PersonSyncCount(users.size(), skippedCount);
return new PersonSyncCount(syncedCount, skippedCount);
}
/**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
*/
private boolean shouldSkipOaUpdate(User existing, User incoming) {
if (existing == null) {
return false;
}
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
if (localModified.isEmpty() || remoteModified.isEmpty()) {
return false;
}
return localModified.equals(remoteModified);
}
private Dept getOrCreateRootCompany() {
@@ -339,6 +339,23 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
return submitUserInfo(user) && submitUserDept(user) && submitUserScopes(user);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean changeUserStatus(Long userId, Integer status) {
if (Func.isEmpty(userId)) {
throw new ServiceException("用户主键不能为空");
}
if (!Objects.equals(status, DataStatusEnum.ENABLE.getCode()) && !Objects.equals(status, 0)
&& !Objects.equals(status, DataStatusEnum.DISABLE.getCode())) {
throw new ServiceException("启停状态不正确");
}
TenantGuard.verify(this, userId, USER);
Integer targetStatus = Objects.equals(status, DataStatusEnum.ENABLE.getCode())
? DataStatusEnum.ENABLE.getCode() : 0;
CacheUtil.clear(USER_CACHE);
return changeStatus(Collections.singletonList(userId), targetStatus);
}
@Override
public boolean updateUserInfo(User user) {
// 用户修改自身信息强制指定当前请求账号的ID
@@ -364,7 +381,14 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
throw new ServiceException(StringUtil.format("当前手机 [{}] 已存在!", user.getPhone()));
}
user.setPassword(null);
return updateById(user);
Integer originalSex = user.getSex();
Integer normalizedSex = normalizeUserSex(originalSex);
user.setSex(normalizedSex);
boolean updated = updateById(user);
if (user.getId() != null && originalSex != null && normalizedSex == null) {
this.update(Wrappers.<User>lambdaUpdate().set(User::getSex, null).eq(User::getId, user.getId()));
}
return updated;
}
private boolean submitUserDept(User user) {
@@ -1086,6 +1110,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
throw new ServiceException(StringUtil.format("当前手机 [{}] 已存在!", user.getPhone()));
}
CacheUtil.clear(USER_CACHE);
user.setSex(normalizeUserSex(user.getSex()));
return save(user) && submitUserDept(user) && submitUserScopes(user);
}
@@ -1096,6 +1121,16 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
if (user.getIncludeNewCustomer() == null) user.setIncludeNewCustomer(0);
}
/**
* 性别仅保留男 / 女 / 未知;空值或历史占位 -1 不回填,按空处理。
*/
private Integer normalizeUserSex(Integer sex) {
if (Objects.equals(sex, 1) || Objects.equals(sex, 2) || Objects.equals(sex, 3)) {
return sex;
}
return null;
}
private boolean submitUserScopes(User user) {
userDataScopeMapper.delete(Wrappers.<UserDataScope>lambdaQuery().eq(UserDataScope::getUserId, user.getId()));
userCustomerScopeMapper.delete(Wrappers.<UserCustomerScope>lambdaQuery().eq(UserCustomerScope::getUserId, user.getId()));
@@ -38,6 +38,7 @@ import org.springblade.system.pojo.enums.DictEnum;
import org.springblade.system.pojo.vo.UserVO;
import java.util.List;
import java.util.Objects;
/**
* 包装类,返回视图层所需的字段
@@ -81,7 +82,11 @@ public class UserWrapper extends BaseEntityWrapper<User, UserVO> {
userVO.setPostId(StringPool.EMPTY);
userVO.setPostName("暂未分配");
}
userVO.setSexName(DictCache.getValue(DictEnum.SEX, user.getSex()));
if (user.getSex() == null || Objects.equals(user.getSex(), -1)) {
userVO.setSexName("");
} else {
userVO.setSexName(DictCache.getValue(DictEnum.SEX, user.getSex()));
}
userVO.setUserTypeName(DictCache.getValue(DictEnum.USER_TYPE, user.getUserType()));
return userVO;
}
@@ -38,6 +38,7 @@ import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.transport.mapper.CreditRatingStandardMapper;
@@ -118,6 +119,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
private static final String APPROVAL_REVIEWING = "reviewing";
private static final String APPROVAL_APPROVED = "approved";
private static final String APPROVAL_REJECTED = "rejected";
/** 客商所属组织固定为顶层组织 */
private static final String FIXED_OWNER_DEPT_NAME = "桂物物流集团";
private static final String CUSTOMER_CODE_PREFIX = "KS";
/**
* 变更快照忽略字段:明细每次保存会重建主键/审计字段,不能作为“是否有修改”的依据
@@ -479,15 +482,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setRegisteredAddress(buildRegisteredAddress(customer));
customer.setLegalPerson(trimToEmpty(customer.getLegalPerson()));
customer.setContactPhone(trimToEmpty(customer.getContactPhone()));
customer.setDeptIds(trimToNull(customer.getDeptIds()));
if (Func.isNotEmpty(customer.getDeptIds())) {
List<Long> deptIdList = Func.toLongList(customer.getDeptIds());
if (Func.isEmpty(deptIdList)) {
throw new ServiceException("所属组织不正确");
}
customer.setDeptId(deptIdList.get(0));
}
customer.setDeptName(trimToEmpty(customer.getDeptName()));
applyFixedOwnerDept(customer);
customer.setAccessType(Func.isEmpty(customer.getAccessType()) ? ACCESS_TEMPORARY : customer.getAccessType());
customer.setApprovalStatus(Func.isEmpty(customer.getApprovalStatus()) ? APPROVAL_DRAFT : customer.getApprovalStatus());
if (Func.isEmpty(customer.getStatus())) {
@@ -1628,6 +1623,25 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
return trimToNull(fallbackAddress);
}
/**
* 客商所属组织固定为顶层「桂物物流集团」,忽略前端传入的其他组织。
*/
private void applyFixedOwnerDept(CustomerArchiveVO customer) {
customer.setDeptName(FIXED_OWNER_DEPT_NAME);
String tenantId = AuthUtil.getTenantId();
String deptIds = SysCache.getDeptIds(tenantId, FIXED_OWNER_DEPT_NAME);
if (Func.isEmpty(deptIds)) {
throw new ServiceException("未找到所属组织「" + FIXED_OWNER_DEPT_NAME + "」,请先维护组织数据");
}
List<Long> deptIdList = Func.toLongList(deptIds);
if (Func.isEmpty(deptIdList) || deptIdList.get(0) == null) {
throw new ServiceException("所属组织「" + FIXED_OWNER_DEPT_NAME + "」不正确");
}
Long deptId = deptIdList.get(0);
customer.setDeptId(deptId);
customer.setDeptIds(String.valueOf(deptId));
}
private String trimToEmpty(String value) {
return value == null ? "" : value.trim();
}
@@ -739,9 +739,6 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
TransportBusinessSupport.validateRequired(projectApply.getCarrierNames(), "请选择下游承运商");
validateAmount(projectApply.getFundLimit(), "项目资金使用额度", false);
validateAmount(projectApply.getReceivableLimit(), "项目应收账款额度", false);
if (projectApply.getReceivableLimit() != null && projectApply.getFundLimit() != null && projectApply.getReceivableLimit().compareTo(projectApply.getFundLimit()) > 0) {
throw new ServiceException("项目应收账款额度不能超过项目资金使用额度");
}
validateDays(projectApply.getReceivableDays(), "应收账款回款期限");
validateDays(projectApply.getPaymentDays(), "回款账期");
validateAmount(projectApply.getProjectScale(), "项目规模", true);
@@ -1926,7 +1926,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
if (isRoadTransport(waybill.getTransportType())) {
if ("承运商".equals(waybill.getCarrierType())) {
TransportBusinessSupport.validateRequired(waybill.getCarrierName(), "承运商不能为空");
} else {
} else if (!isBatchImport(waybill)) {
TransportBusinessSupport.validateRequired(waybill.getDriverName(), "司机不能为空");
}
}
@@ -2079,6 +2079,10 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
return value.contains("公路") || value.contains("道路") || value.contains("road") || "gl".equals(value);
}
private boolean isBatchImport(Waybill waybill) {
return waybill != null && TransportBusinessSupport.DATA_SOURCE_BATCH_IMPORT.equals(waybill.getDataSource());
}
private void fillJsonFromFlatFields(Waybill waybill) {
if (Func.isEmpty(waybill.getTaskEntryMode())) {
waybill.setTaskEntryMode("simple");
@@ -0,0 +1,4 @@
-- 机构表:记录 OA 的 modified(上次更新时间),用于增量同步比对
ALTER TABLE `blade_dept`
ADD COLUMN `oa_modified` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA上次更新时间(modified)' AFTER `oa_sup_sub_com_id`;
@@ -0,0 +1,4 @@
-- 用户表:记录 OA 的 modified(上次更新时间),用于增量同步比对
ALTER TABLE `blade_user`
ADD COLUMN `oa_modified` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA上次更新时间(modified)' AFTER `is_oa`;