feat(用户同步): 添加手动同步AB98用户信息功能

新增手动同步AB98用户信息接口和定时任务服务
移除Ab98ApiUtil中的main方法测试代码
修正用户地址字段映射为idCardAddress
添加/manual/**路径到安全白名单
更新测试用例中的身份证号参数
This commit is contained in:
dzq 2025-12-09 09:00:16 +08:00
parent a5fc5201aa
commit 60f3595d0d
7 changed files with 239 additions and 10 deletions

View File

@ -0,0 +1,47 @@
package com.agileboot.admin.controller.job;
import com.agileboot.admin.customize.service.job.SyncAb98UserJob;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 手动执行任务控制器
* 用于手动触发定时任务方便调试和即时执行
* 该接口无需权限验证路径为 /manual/** 已在SecurityConfig中配置为permitAll
*/
@Tag(name = "手动执行任务API", description = "手动触发定时任务执行")
@RestController
@RequestMapping("/manual/job")
@RequiredArgsConstructor
@Slf4j
public class ManualJobController {
private final SyncAb98UserJob syncAb98UserJob;
/**
* 手动同步AB98用户信息
* 对应定时任务@Scheduled(cron = "0 0 2 * * ?")
* 从汇邦云平台拉取最新用户信息进行对比更新
*/
@Operation(summary = "手动同步AB98用户信息", description = "从汇邦云平台同步AB98用户信息到本地数据库")
@PostMapping("/sync-ab98-user")
public ResponseDTO<String> syncAb98UserInfo() {
try {
log.info("开始手动执行AB98用户信息同步任务...");
syncAb98UserJob.syncAb98UserInfo();
log.info("手动执行AB98用户信息同步任务完成");
return ResponseDTO.ok("AB98用户信息同步任务执行成功");
} catch (Exception e) {
log.error("手动执行AB98用户信息同步任务失败", e);
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "AB98用户信息同步失败" + e.getMessage()));
}
}
}

View File

@ -137,7 +137,7 @@ public class SecurityConfig {
.antMatchers("/login", "/register", "/captchaImage", "/api/**", "/file/**").anonymous()
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js",
"/profile/**").permitAll()
.antMatchers("/qywx/**", "/test/**", "/monitor/**", "/getQyUserinfo", "/getConfig").permitAll()
.antMatchers("/manual/**", "/qywx/**", "/test/**", "/monitor/**", "/getQyUserinfo", "/getConfig").permitAll()
// TODO this is danger.
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()

View File

@ -0,0 +1,187 @@
package com.agileboot.admin.customize.service.job;
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
import com.agileboot.domain.ab98.api.Ab98UserDto;
import com.agileboot.domain.ab98.user.Ab98UserApplicationService;
import com.agileboot.domain.ab98.user.command.UpdateAb98UserCommand;
import com.agileboot.domain.ab98.user.db.Ab98UserEntity;
import com.agileboot.domain.ab98.user.db.Ab98UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 同步ab98用户信息定时任务
* 定时从汇邦云平台拉取用户信息与本地数据库中的用户信息进行对比
* 如有差异则更新本地用户信息
*
* @author your-name
* @since 2025-01-01
*/
@RequiredArgsConstructor
@Component
@Slf4j
public class SyncAb98UserJob {
private final Ab98UserService ab98UserService;
private final Ab98UserApplicationService ab98UserApplicationService;
private static final String APP_NAME = "wxshop";
private static final String APP_SECRET = "34164e41f0c6694be6bbbba0dc50c14a";
/**
* 定时任务同步ab98用户信息
* 每天凌晨2点执行获取所有本地ab98用户信息
* 从汇邦云平台拉取最新用户信息进行对比更新
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void syncAb98UserInfo() {
log.info("开始执行ab98用户信息同步任务...");
try {
// 1. 获取所有本地ab98用户只查询有身份证号的用户
List<Ab98UserEntity> localUserList = ab98UserService.selectAll();
if (localUserList == null || localUserList.isEmpty()) {
log.info("本地数据库中未找到ab98用户跳过同步任务.");
return;
}
log.info("本地数据库共有 {} 个ab98用户需要同步.", localUserList.size());
int successCount = 0;
int skipCount = 0;
int errorCount = 0;
// 2. 遍历每个用户查询汇邦云平台的最新信息
for (Ab98UserEntity localUser : localUserList) {
try {
// 跳过没有身份证号的用户可能是测试数据或异常数据
if (StringUtils.isBlank(localUser.getIdnum())) {
log.debug("用户[{}]身份证号为空,跳过同步.", localUser.getAb98UserId());
skipCount++;
continue;
}
// 从汇邦云平台拉取用户信息
Ab98UserDto remoteUserDto = Ab98ApiUtil.pullUserInfoByIdnum(
APP_NAME,
APP_SECRET,
localUser.getIdnum()
);
// 如果平台没有此用户信息跳过
if (remoteUserDto == null) {
log.debug("汇邦云平台未找到身份证号[{}]对应的用户,跳过同步.", localUser.getIdnum());
skipCount++;
continue;
}
// 对比并更新用户信息
boolean isUpdated = compareAndUpdateUser(localUser, remoteUserDto);
if (isUpdated) {
successCount++;
log.info("用户[{}]信息已更新,身份证号: {}", localUser.getAb98UserId(), localUser.getIdnum());
} else {
skipCount++;
log.debug("用户[{}]信息无变化,跳过更新.", localUser.getAb98UserId());
}
} catch (Exception e) {
errorCount++;
log.error("同步用户[{}]信息时发生错误,身份证号: {}", localUser.getAb98UserId(), localUser.getIdnum(), e);
}
}
log.info("ab98用户信息同步任务执行完成. 总数: {}, 成功: {}, 跳过: {}, 错误: {}",
localUserList.size(), successCount, skipCount, errorCount);
} catch (Exception globalException) {
// 记录整个定时任务的全局错误
log.error("执行ab98用户信息同步任务时发生全局错误:", globalException);
}
}
/**
* 对比并更新用户信息
*
* @param localUser 本地用户实体
* @param remoteUserDto 汇邦云平台用户信息
* @return 是否进行了更新
*/
private boolean compareAndUpdateUser(Ab98UserEntity localUser, Ab98UserDto remoteUserDto) {
boolean isUpdated = false;
// 对比需要更新的字段
UpdateAb98UserCommand updateCommand = new UpdateAb98UserCommand();
updateCommand.setAb98UserId(localUser.getAb98UserId());
// 对比并更新姓名
if (StringUtils.isNotBlank(remoteUserDto.getRealName())
&& !StringUtils.equals(remoteUserDto.getRealName(), localUser.getName())) {
updateCommand.setName(remoteUserDto.getRealName());
isUpdated = true;
}
// 对比并更新手机号
if (StringUtils.isNotBlank(remoteUserDto.getPhone())
&& !StringUtils.equals(remoteUserDto.getPhone(), localUser.getTel())) {
updateCommand.setTel(remoteUserDto.getPhone());
isUpdated = true;
}
// 对比并更新性别
if (StringUtils.isNotBlank(remoteUserDto.getSex())
&& !StringUtils.equals(remoteUserDto.getSex(), localUser.getSex())) {
updateCommand.setSex(remoteUserDto.getSex());
isUpdated = true;
}
// 对比并更新身份证正面
if (StringUtils.isNotBlank(remoteUserDto.getIdCardFront())
&& !StringUtils.equals(remoteUserDto.getIdCardFront(), localUser.getIdcardFront())) {
updateCommand.setIdcardFront(remoteUserDto.getIdCardFront());
isUpdated = true;
}
// 对比并更新身份证背面
if (StringUtils.isNotBlank(remoteUserDto.getIdCardBack())
&& !StringUtils.equals(remoteUserDto.getIdCardBack(), localUser.getIdcardBack())) {
updateCommand.setIdcardBack(remoteUserDto.getIdCardBack());
isUpdated = true;
}
// 对比并更新人脸照片
if (StringUtils.isNotBlank(remoteUserDto.getFacePicture())
&& !StringUtils.equals(remoteUserDto.getFacePicture(), localUser.getFaceImg())) {
updateCommand.setFaceImg(remoteUserDto.getFacePicture());
isUpdated = true;
}
// 对比并更新地址
if (StringUtils.isNotBlank(remoteUserDto.getIdCardAddress())
&& !StringUtils.equals(remoteUserDto.getIdCardAddress(), localUser.getAddress())) {
updateCommand.setAddress(remoteUserDto.getIdCardAddress());
isUpdated = true;
}
// 如果有字段需要更新则执行更新
if (isUpdated) {
try {
ab98UserApplicationService.updateUser(updateCommand);
log.debug("用户[{}]信息更新成功", localUser.getAb98UserId());
} catch (Exception e) {
log.error("用户[{}]信息更新失败", localUser.getAb98UserId(), e);
throw e; // 抛出异常触发事务回滚
}
}
return isUpdated;
}
}

View File

@ -49,7 +49,7 @@ public class CabinetControllerTest {
@Test
public void testPullUserInfoByIdnum() {
String IdNum = "452526196206215829";
String IdNum = "";
Ab98UserDto ab98UserDto = Ab98ApiUtil.pullUserInfoByIdnum("wxshop", "34164e41f0c6694be6bbbba0dc50c14a", IdNum);
log.info("ab98UserDto:{}", JSONUtil.toJsonStr(ab98UserDto));
}

View File

@ -208,11 +208,6 @@ public class Ab98ApiUtil {
return null;
}
public static void main(String[] args) {
Ab98UserDto userDto = pullUserInfoByIdnum("wxshop", "34164e41f0c6694be6bbbba0dc50c14a", "450981199505186050");
log.info("拉取用户信息: {}", JSONUtil.toJsonStr(userDto));
}
// 基础响应对象
@Data
public static class BaseResponse {

View File

@ -259,7 +259,7 @@ public class Ab98UserApplicationService {
addAb98UserCommand.setIdcardFront(ab98UserDto.getIdCardFront());
addAb98UserCommand.setIdcardBack(ab98UserDto.getIdCardBack());
addAb98UserCommand.setFaceImg(ab98UserDto.getFacePicture());
addAb98UserCommand.setAddress(ab98UserDto.getAddress());
addAb98UserCommand.setAddress(ab98UserDto.getIdCardAddress());
addAb98UserCommand.setRegistered(true);
addAb98UserCommand.initBaseEntity();
return addAb98UserCommand;

View File

@ -327,7 +327,7 @@ public class WxUserApplicationService {
addCommand.setIdcardFront(ab98UserDto.getIdCardFront());
addCommand.setIdcardBack(ab98UserDto.getIdCardBack());
addCommand.setFaceImg(ab98UserDto.getFacePicture());
addCommand.setAddress(ab98UserDto.getAddress());
addCommand.setAddress(ab98UserDto.getIdCardAddress());
addCommand.setRegistered(true);
addCommand.initBaseEntity();
@ -345,7 +345,7 @@ public class WxUserApplicationService {
updateCommand.setIdcardFront(ab98UserDto.getIdCardFront());
updateCommand.setIdcardBack(ab98UserDto.getIdCardBack());
updateCommand.setFaceImg(ab98UserDto.getFacePicture());
updateCommand.setAddress(ab98UserDto.getAddress());
updateCommand.setAddress(ab98UserDto.getIdCardAddress());
updateCommand.setRegistered(true);
ab98UserApplicationService.updateUser(updateCommand);
ab98UserEntity = existingUser;