Merge remote-tracking branch 'ztcloud/main' into main-ztcloud

This commit is contained in:
hewencai
2025-12-16 16:36:56 +08:00
126 changed files with 2957 additions and 571 deletions

View File

@@ -1,7 +1,10 @@
package com.zt.plat.module.system.api.sms;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.system.api.sms.dto.log.SmsLogRespDTO;
import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
import com.zt.plat.module.system.service.sms.SmsLogService;
import com.zt.plat.module.system.service.sms.SmsSendService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@@ -16,6 +19,8 @@ public class SmsSendApiImpl implements SmsSendApi {
@Resource
private SmsSendService smsSendService;
@Resource
private SmsLogService smsLogService;
@Override
public CommonResult<Long> sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) {
@@ -29,4 +34,9 @@ public class SmsSendApiImpl implements SmsSendApi {
reqDTO.getTemplateCode(), reqDTO.getTemplateParams()));
}
@Override
public CommonResult<SmsLogRespDTO> getSmsLog(Long id) {
return success(BeanUtils.toBean(smsLogService.getSmsLog(id), SmsLogRespDTO.class));
}
}

View File

@@ -23,6 +23,15 @@ public class DeptSaveReqVO {
@Size(max = 50, message = "部门编码长度不能超过 50 个字符")
private String code;
@Schema(description = "外部系统标识,用于建立编码映射", example = "ERP")
private String externalSystemCode;
@Schema(description = "外部系统组织编码,用于建立映射", example = "ERP-001")
private String externalDeptCode;
@Schema(description = "外部系统组织名称", example = "ERP总部")
private String externalDeptName;
@Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZT")
@NotBlank(message = "部门名称不能为空")
@Size(max = 30, message = "部门名称长度不能超过 30 个字符")

View File

@@ -12,6 +12,8 @@ import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,6 +47,20 @@ public class IWorkIntegrationController {
return success(integrationService.acquireToken(reqVO));
}
@PostMapping("/oa/token")
@Operation(summary = "透传获取 OA Token")
public ResponseEntity<String> acquireOaToken(@Valid @RequestBody IWorkOaTokenReqVO reqVO) {
IWorkOaRawResponse resp = integrationService.getOaToken(reqVO);
return buildOaResponse(resp);
}
@PostMapping("/oa/check")
@Operation(summary = "透传校验 OA Token")
public ResponseEntity<String> checkOaToken(@Valid @RequestBody IWorkOaCheckTokenReqVO reqVO) {
IWorkOaRawResponse resp = integrationService.checkOaToken(reqVO);
return buildOaResponse(resp);
}
@PostMapping("/user/resolve")
@Operation(summary = "根据外部标识获取 iWork 用户编号")
public CommonResult<IWorkUserInfoRespVO> resolveUser(@Valid @RequestBody IWorkUserInfoReqVO reqVO) {
@@ -97,30 +113,6 @@ public class IWorkIntegrationController {
return success(orgRestService.listUsers(reqVO));
}
// @PostMapping("/hr/subcompany/sync")
// @Operation(summary = "同步分部信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncSubcompanies(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncSubcompanies(reqVO));
// }
//
// @PostMapping("/hr/department/sync")
// @Operation(summary = "同步部门信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncDepartments(reqVO));
// }
//
// @PostMapping("/hr/job-title/sync")
// @Operation(summary = "同步岗位信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncJobTitles(reqVO));
// }
//
// @PostMapping("/hr/user/sync")
// @Operation(summary = "同步人员信息至 iWork")
// public CommonResult<IWorkHrSyncRespVO> syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) {
// return success(orgRestService.syncUsers(reqVO));
// }
// ----------------- 同步到本地 -----------------
@PostMapping("/hr/departments/full-sync")
@@ -146,4 +138,20 @@ public class IWorkIntegrationController {
public CommonResult<IWorkFullSyncRespVO> fullSyncUsers(@Valid @RequestBody IWorkFullSyncReqVO reqVO) {
return success(syncService.fullSyncUsers(reqVO));
}
private ResponseEntity<String> buildOaResponse(IWorkOaRawResponse resp) {
if (resp == null) {
return ResponseEntity.internalServerError().body("OA 响应为空");
}
HttpHeaders headers = new HttpHeaders();
if (resp.getHeaders() != null) {
resp.getHeaders().forEach(headers::add);
}
if (resp.getContentType() != null) {
headers.setContentType(resp.getContentType());
}
return ResponseEntity.status(resp.getStatusCode())
.headers(headers)
.body(resp.getBody());
}
}

View File

@@ -18,4 +18,7 @@ public class IWorkFileCallbackReqVO {
@Schema(description = "文件名称,可选", example = "合同附件.pdf")
private String fileName;
@Schema(description = "OA 单点下载使用的 ssoToken可选", example = "6102A7C13F09DD6B1AF06CDA0E479AC8...")
private String ssoToken;
}

View File

@@ -1,74 +0,0 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* iWork 人力同步响应。
*/
@Data
@Schema(description = "iWork 人力同步响应")
public class IWorkHrSyncRespVO {
@Schema(description = "响应码")
private String code;
@Schema(description = "提示信息")
private String message;
@Schema(description = "是否成功")
private boolean success;
@Schema(description = "同步结果明细")
private List<SyncResult> result;
@Data
@Schema(description = "同步结果项")
public static class SyncResult {
@Schema(description = "操作动作 add/update/delete")
@JsonProperty("@action")
private String action;
@Schema(description = "外部编码")
@JsonProperty("code")
private String code;
@Schema(description = "执行结果 success/fail")
@JsonProperty("result")
private String result;
@Schema(description = "是否成功")
@JsonProperty("success")
private Boolean success;
@Schema(description = "失败描述")
@JsonProperty("message")
private String message;
@JsonIgnore
private Map<String, Object> attributes;
@JsonAnySetter
public void putAttribute(String key, Object value) {
if (attributes == null) {
attributes = new LinkedHashMap<>();
}
attributes.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> any() {
return attributes == null ? Collections.emptyMap() : attributes;
}
}
}

View File

@@ -0,0 +1,16 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 校验 OA Token 的请求参数。
*/
@Data
public class IWorkOaCheckTokenReqVO {
@Schema(description = "需要校验的 OA token")
@NotBlank(message = "token 不能为空")
private String token;
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.MediaType;
import java.util.Map;
/**
* 封装 OA 接口的原始返回,用于透传 HTTP 状态与 body。
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IWorkOaRawResponse {
private int statusCode;
private String body;
private MediaType contentType;
private Map<String, String> headers;
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 获取 OA Token 的请求参数。
*/
@Data
public class IWorkOaTokenReqVO {
@Schema(description = "OA 登录账号 loginid", example = "zixun004")
@NotBlank(message = "loginId 不能为空")
private String loginId;
@Schema(description = "应用 appid未填则使用配置默认值", example = "a17ca6ca-88b0-463e-bffa-7995086bf225")
private String appId;
}

View File

@@ -1,19 +0,0 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 同步 iWork 人力组织信息的请求。
*/
@Data
public class IWorkOrgSyncReqVO {
@Schema(description = "同步数据集合,将被序列化为 data 传给 iWork", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "同步数据不能为空")
private List<Map<String, Object>> data;
}

View File

@@ -62,4 +62,13 @@ public class SmsCallbackController {
return success(true);
}
@PostMapping("/hl95")
@PermitAll
@TenantIgnore
@Operation(summary = "鸿联九五短信的回调")
public CommonResult<Boolean> receiveHl95SmsStatus(@RequestBody String requestBody) throws Throwable {
smsSendService.receiveSmsStatus(SmsChannelEnum.HL95.getCode(), requestBody);
return success(true);
}
}

View File

@@ -71,6 +71,14 @@ public class SmsChannelController {
return success(BeanUtils.toBean(pageResult, SmsChannelRespVO.class));
}
@GetMapping("/balance")
@Operation(summary = "查询短信渠道余额")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
public CommonResult<Integer> getBalance(@RequestParam("id") Long id) {
return success(smsChannelService.queryBalance(id));
}
@GetMapping({"/list-all-simple", "/simple-list"})
@Operation(summary = "获得短信渠道精简列表", description = "包含被禁用的短信渠道")
public CommonResult<List<SmsChannelSimpleRespVO>> getSimpleSmsChannelList() {

View File

@@ -32,6 +32,9 @@ public class SmsChannelRespVO {
@NotNull(message = "短信 API 的账号不能为空")
private String apiKey;
@Schema(description = "企业编号epid", example = "123456")
private String epid;
@Schema(description = "短信 API 的密钥", example = "yuanma")
private String apiSecret;

View File

@@ -17,6 +17,9 @@ public class SmsChannelSaveReqVO {
@NotNull(message = "短信签名不能为空")
private String signature;
@Schema(description = "企业编号epid", example = "123456")
private String epid;
@Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN")
@NotNull(message = "渠道编码不能为空")
private String code;

View File

@@ -29,6 +29,9 @@ public class UserPageReqVO extends PageParam {
@Schema(description = "手机号码,模糊匹配", example = "zt")
private String mobile;
@Schema(description = "关键词(昵称/账号/手机号模糊匹配)", example = "张三")
private String keyword;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status;

View File

@@ -0,0 +1,100 @@
package com.zt.plat.module.system.controller.app.sms;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO;
import com.zt.plat.module.system.enums.ErrorCodeConstants;
import com.zt.plat.module.system.framework.sms.core.enums.SmsChannelEnum;
import com.zt.plat.module.system.service.sms.SmsChannelService;
import com.zt.plat.module.system.service.sms.SmsSendService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Comparator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
@Slf4j
@Validated
@RestController
@Tag(name = "鸿联九五 - 短信回调")
@RequestMapping("/system/sms/callback/hl95")
public class Hl95SmsCallbackController {
@Resource
private SmsSendService smsSendService;
@Resource
private SmsChannelService smsChannelService;
@RequestMapping(value = "/status", method = {RequestMethod.GET, RequestMethod.POST})
@Operation(summary = "状态报告回调")
@PermitAll
public String statusCallback(@RequestParam Map<String, String> params) {
try {
checkSign(params);
smsSendService.receiveSmsStatus(SmsChannelEnum.HL95.getCode(), JsonUtils.toJsonString(params));
return "OK";
} catch (Throwable e) {
log.warn("[statusCallback][鸿联九五回调处理失败 params={} error={}]", params, e.getMessage(), e);
return "FAIL";
}
}
@RequestMapping(value = "/mo", method = {RequestMethod.GET, RequestMethod.POST})
@Operation(summary = "上行短信回调")
@PermitAll
public String moCallback(@RequestParam Map<String, String> params) {
try {
checkSign(params);
log.info("[moCallback][收到鸿联九五上行:{}]", params);
return "OK";
} catch (Throwable e) {
log.warn("[moCallback][鸿联九五上行处理失败 params={} error={}]", params, e.getMessage(), e);
return "FAIL";
}
}
private void checkSign(Map<String, String> params) {
String sign = params.get("sign");
if (StrUtil.isBlank(sign)) {
throw exception(ErrorCodeConstants.SMS_CALLBACK_SIGN_INVALID);
}
SmsChannelDO channel = smsChannelService.getSmsChannelList().stream()
.filter(item -> StrUtil.equals(item.getCode(), SmsChannelEnum.HL95.getCode()))
.min(Comparator.comparing(SmsChannelDO::getId))
.orElse(null);
if (channel == null) {
throw exception(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS);
}
String expect = buildSign(params, channel.getApiSecret());
if (!StrUtil.equalsIgnoreCase(sign, expect)) {
throw exception(ErrorCodeConstants.SMS_CALLBACK_SIGN_INVALID);
}
}
private static String buildSign(Map<String, String> params, String secret) {
SortedMap<String, String> sorted = new TreeMap<>();
params.forEach((k, v) -> {
if (!"sign".equalsIgnoreCase(k)) {
sorted.put(k, StrUtil.nullToEmpty(v));
}
});
String base = sorted.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
return DigestUtil.md5Hex(base + "&key=" + secret);
}
}

View File

@@ -35,6 +35,10 @@ public class SmsChannelDO extends BaseDO {
* 短信签名
*/
private String signature;
/**
* 企业编号epid
*/
private String epid;
/**
* 渠道编码
*

View File

@@ -37,6 +37,10 @@ public interface DeptExternalCodeMapper extends BaseMapperX<DeptExternalCodeDO>
return selectList(DeptExternalCodeDO::getDeptId, deptId);
}
default int deleteByDeptId(Long deptId) {
return delete(DeptExternalCodeDO::getDeptId, deptId);
}
default List<DeptExternalCodeDO> selectListBySystemCode(String systemCode) {
return selectList(DeptExternalCodeDO::getSystemCode, systemCode);
}

View File

@@ -1,5 +1,6 @@
package com.zt.plat.module.system.dal.mysql.user;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.pojo.PageParam;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
@@ -36,18 +37,27 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
}
default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO, Collection<Long> deptIds, Collection<Long> userIds) {
MPJLambdaWrapperX<AdminUserDO> query = new MPJLambdaWrapperX<>();
query.leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId);
query.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername());
query.likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode());
query.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile());
query.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus());
query.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime());
query.inIfPresent(UserDeptDO::getDeptId, deptIds);
query.inIfPresent(AdminUserDO::getId, userIds);
query.distinct();
query.orderByDesc(AdminUserDO::getId);
return selectJoinPage(reqVO, AdminUserDO.class, new MPJLambdaWrapperX<AdminUserDO>()
.leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId)
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(UserDeptDO::getDeptId, deptIds)
.inIfPresent(AdminUserDO::getId, userIds)
.distinct()
.orderByDesc(AdminUserDO::getId));
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword().trim();
query.and(w -> w.like(AdminUserDO::getNickname, keyword)
.or().like(AdminUserDO::getUsername, keyword)
.or().like(AdminUserDO::getMobile, keyword)
.or().like(AdminUserDO::getWorkcode, keyword));
}
return selectJoinPage(reqVO, AdminUserDO.class, query);
}
default List<AdminUserDO> selectList(UserPageReqVO reqVO, Collection<Long> deptIds, Collection<Long> userIds) {

View File

@@ -17,6 +17,14 @@ public interface RedisKeyConstants {
*/
String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
/**
* 指定部门的外部组织编码映射列表缓存
* <p>
* KEY 格式dept_external_code_list:{deptId}
* VALUE 数据类型String 映射列表
*/
String DEPT_EXTERNAL_CODE_LIST = "dept_external_code_list";
/**
* 角色的缓存
* <p>

View File

@@ -1,12 +1,35 @@
package com.zt.plat.module.system.framework.integration.iwork.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 负责加载 {@link IWorkProperties} 的自动配置类。
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(IWorkProperties.class)
public class IWorkIntegrationConfiguration {
private final IWorkProperties properties;
@PostConstruct
void reportConfigurationStatus() {
if (!properties.hasAnyConfiguredValue()) {
log.info("[iWork] 未检测到集成配置,默认关闭 iWork 功能。需要时请在配置文件中补充 iwork.* 项。");
return;
}
List<String> issues = properties.collectCriticalIssues();
if (!issues.isEmpty()) {
log.warn("[iWork] 配置不完整:{}。系统会继续启动,但 iWork 能力将不可用。", String.join("", issues));
return;
}
log.info("[iWork] 已检测到完整配置iWork 集成能力已启用。");
}
}

View File

@@ -1,21 +1,18 @@
package com.zt.plat.module.system.framework.integration.iwork.config;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
/**
* iWork 集成所需的配置项。
*/
@Data
@Validated
@ConfigurationProperties(prefix = "iwork")
public class IWorkProperties {
@@ -39,44 +36,35 @@ public class IWorkProperties {
*/
private String userId;
@Valid
private final Paths paths = new Paths();
private final Headers headers = new Headers();
@Valid
private final Token token = new Token();
@Valid
private final Client client = new Client();
@Valid
private final OrgRest org = new OrgRest();
@Valid
private final Workflow workflow = new Workflow();
private final Oa oa = new Oa();
@Data
public static class Paths {
/**
* 负责交换公钥和密钥的注册接口路径。
*/
@NotBlank(message = "iWork 注册接口路径不能为空")
private String register;
/**
* 申请访问令牌的接口路径。
*/
@NotBlank(message = "iWork 申请 Token 接口路径不能为空")
private String applyToken;
/**
* 查询用户信息的接口路径。
*/
@NotBlank(message = "iWork 用户信息接口路径不能为空")
private String userInfo;
/**
* 发起流程的接口路径。
*/
@NotBlank(message = "iWork 发起流程接口路径不能为空")
private String createWorkflow;
/**
* 干预或作废流程的接口路径。
*/
@NotBlank(message = "iWork 作废流程接口路径不能为空")
private String voidWorkflow;
}
@@ -95,7 +83,6 @@ public class IWorkProperties {
/**
* 向 iWork 申请的 Token 有效期(单位秒)。
*/
@Min(value = 1, message = "iWork Token 有效期必须大于 0")
private long ttlSeconds;
}
@@ -104,12 +91,10 @@ public class IWorkProperties {
/**
* Reactor Netty 连接超时时间。
*/
@NotNull(message = "iWork 客户端连接超时时间不能为空")
private Duration connectTimeout;
/**
* Reactor Netty 响应超时时间。
*/
@NotNull(message = "iWork 客户端响应超时时间不能为空")
private Duration responseTimeout;
}
@@ -119,7 +104,6 @@ public class IWorkProperties {
* 认证所需的标识(与 iWork 约定)。
*/
private String tokenSeed;
@Valid
private final OrgPaths paths = new OrgPaths();
}
@@ -140,7 +124,119 @@ public class IWorkProperties {
/**
* 用印流程对应的 iWork 模板编号。
*/
@NotBlank(message = "iWork 用印流程模板编号不能为空")
private String sealWorkflowId;
}
@Data
public static class Oa {
/**
* OA 网关基础地址例如http://172.16.36.233:8080
*/
private String baseUrl;
/**
* 默认 appid调用方未传时使用。
*/
private String appId;
private final OaPaths paths = new OaPaths();
}
@Data
public static class OaPaths {
/**
* 获取 token 的接口路径,例如:/ssologin/getToken
*/
private String getToken;
/**
* 校验 token 的接口路径,例如:/ssologin/checkToken
*/
private String checkToken;
}
/**
* 是否显式配置了关键参数,用于判断是否需要提示用户。
*/
public boolean hasAnyConfiguredValue() {
return hasText(baseUrl)
|| hasText(appId)
|| hasText(clientPublicKey)
|| hasText(userId)
|| hasAnyPathConfigured()
|| token.getTtlSeconds() > 0
|| client.getConnectTimeout() != null
|| client.getResponseTimeout() != null
|| hasText(org.getTokenSeed())
|| hasText(workflow.getSealWorkflowId())
|| hasAnyOaConfigured();
}
/**
* 收集关键配置缺失信息,用于提示或日志告警。
*/
public List<String> collectCriticalIssues() {
List<String> issues = new ArrayList<>();
if (!hasText(baseUrl)) {
issues.add("iwork.base-url 未配置");
}
if (!hasText(appId)) {
issues.add("iwork.app-id 未配置");
}
if (!hasText(paths.getRegister())) {
issues.add("iwork.paths.register 未配置");
}
if (!hasText(paths.getApplyToken())) {
issues.add("iwork.paths.apply-token 未配置");
}
if (!hasText(paths.getUserInfo())) {
issues.add("iwork.paths.user-info 未配置");
}
if (!hasText(paths.getCreateWorkflow())) {
issues.add("iwork.paths.create-workflow 未配置");
}
if (!hasText(paths.getVoidWorkflow())) {
issues.add("iwork.paths.void-workflow 未配置");
}
if (token.getTtlSeconds() <= 0) {
issues.add("iwork.token.ttl-seconds 需要大于 0");
}
if (!hasText(workflow.getSealWorkflowId())) {
issues.add("iwork.workflow.seal-workflow-id 未配置");
}
if (oa != null) {
if (!hasText(oa.getBaseUrl())) {
issues.add("iwork.oa.base-url 未配置");
}
if (!hasText(oa.getAppId())) {
issues.add("iwork.oa.app-id 未配置");
}
if (oa.getPaths() == null || !hasText(oa.getPaths().getGetToken())) {
issues.add("iwork.oa.paths.get-token 未配置");
}
if (oa.getPaths() == null || !hasText(oa.getPaths().getCheckToken())) {
issues.add("iwork.oa.paths.check-token 未配置");
}
}
return issues;
}
private boolean hasAnyPathConfigured() {
return hasText(paths.getRegister())
|| hasText(paths.getApplyToken())
|| hasText(paths.getUserInfo())
|| hasText(paths.getCreateWorkflow())
|| hasText(paths.getVoidWorkflow());
}
private boolean hasAnyOaConfigured() {
return oa != null && (hasText(oa.getBaseUrl())
|| hasText(oa.getAppId())
|| (oa.getPaths() != null && (hasText(oa.getPaths().getGetToken())
|| hasText(oa.getPaths().getCheckToken()))));
}
private boolean hasText(String value) {
return StringUtils.hasText(value);
}
}

View File

@@ -31,7 +31,7 @@ public interface SmsClient {
* @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
* @return 短信发送结果
*/
SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId,
SmsSendRespDTO sendSms(Long logId, String mobile, String content, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable;
/**

View File

@@ -49,7 +49,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
}
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
Assert.notBlank(properties.getSignature(), "短信签名不能为空");
// 1. 执行请求

View File

@@ -36,16 +36,16 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
// 构建请求
String url = buildUrl("robot/send");
Map<String, Object> params = new HashMap<>();
params.put("msgtype", "text");
String content = String.format("【模拟短信】\n手机号%s\n短信日志编号%d\n模板参数%s",
mobile, sendLogId, MapUtils.convertMap(templateParams));
params.put("text", MapUtil.builder().put("content", content).build());
String sendContent = String.format("【模拟短信】\n手机号%s\n短信日志编号%d\n模板参数%s\n内容%s",
mobile, sendLogId, MapUtils.convertMap(templateParams), content);
params.put("text", MapUtil.builder().put("content", sendContent).build());
// 执行请求
String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
// 解析结果

View File

@@ -0,0 +1,161 @@
package com.zt.plat.module.system.framework.sms.core.client.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient;
import com.zt.plat.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class Hl95SmsClient extends AbstractSmsClient implements SmsBalanceClient {
private static final String SEND_URL = "https://api.sms.95ytx.com:9091/mxt/send";
private static final String BALANCE_URL = "https://api.sms.95ytx.com:9091/mxt/getfee";
private static final DateTimeFormatter STATUS_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
public Hl95SmsClient(SmsChannelProperties properties) {
super(properties);
Assert.notEmpty(properties.getApiKey(), "用户名(apiKey) 不能为空");
Assert.notEmpty(properties.getApiSecret(), "密码(apiSecret) 不能为空");
}
@Override
public SmsSendRespDTO sendSms(Long logId, String mobile, String content, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) {
Assert.notEmpty(properties.getEpid(), "鸿联九五需要配置 epid");
Assert.notEmpty(properties.getSignature(), "短信签名不能为空");
String finalContent = appendSignatureIfMissing(content, properties.getSignature());
String linkId = buildLinkId(logId);
Map<String, Object> form = new HashMap<>();
form.put("username", properties.getApiKey());
form.put("password", properties.getApiSecret());
form.put("epid", properties.getEpid());
form.put("phone", mobile);
form.put("message", finalContent);
form.put("linkid", linkId);
// subcode 可为空
String resp;
try (HttpResponse response = HttpRequest.post(SEND_URL)
.form(form)
.charset(StandardCharsets.UTF_8)
.execute()) {
resp = StrUtil.trim(response.body());
}
boolean success = StrUtil.equals(resp, "00");
return new SmsSendRespDTO()
.setSuccess(success)
.setApiCode(resp)
.setApiMsg(resp)
.setApiRequestId(linkId)
.setSerialNo(linkId);
}
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
JSONObject obj = JSONUtil.parseObj(text, false);
String reportCode = obj.getStr("FReportCode");
String linkId = obj.getStr("FLinkID");
LocalDateTime deliverTime = parseDeliverTime(obj.getStr("FDeliverTime"));
String mobile = obj.getStr("FDestAddr");
boolean success = StrUtil.equalsIgnoreCase(reportCode, "DELIVRD") || StrUtil.equals(reportCode, "0");
Long logId = parseLongSafely(linkId);
SmsReceiveRespDTO dto = new SmsReceiveRespDTO()
.setSuccess(success)
.setErrorCode(reportCode)
.setErrorMsg(reportCode)
.setMobile(mobile)
.setReceiveTime(deliverTime)
.setSerialNo(linkId)
.setLogId(logId);
return Collections.singletonList(dto);
}
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) {
// 鸿联九五无模板审核接口,直接返回可用
return new SmsTemplateRespDTO()
.setId(apiTemplateId)
.setContent(apiTemplateId)
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus());
}
@Override
public Integer queryBalance() {
Assert.notEmpty(properties.getEpid(), "鸿联九五需要配置 epid");
Map<String, Object> form = MapUtil.<String, Object>builder()
.put("username", properties.getApiKey())
.put("password", properties.getApiSecret())
.put("epid", properties.getEpid())
.build();
String resp;
try (HttpResponse response = HttpRequest.get(BALANCE_URL)
.form(form)
.charset(StandardCharsets.UTF_8)
.execute()) {
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
throw new IllegalStateException("余额查询失败HTTP 状态码:" + response.getStatus());
}
resp = StrUtil.trim(response.body());
}
if (!StrUtil.isNumeric(resp)) {
throw new IllegalStateException("余额查询失败,返回值:" + resp);
}
return Integer.valueOf(resp);
}
private static String appendSignatureIfMissing(String content, String signature) {
if (StrUtil.isBlank(signature)) {
return content;
}
String wrapped = StrUtil.startWithAny(signature, "", "[") ? signature : "" + signature + "";
return StrUtil.startWith(content, wrapped) ? content : wrapped + content;
}
private static String buildLinkId(Long logId) {
String raw = String.valueOf(logId);
return raw.length() > 20 ? raw.substring(raw.length() - 20) : raw;
}
private static LocalDateTime parseDeliverTime(String timeText) {
if (StrUtil.isBlank(timeText)) {
return null;
}
try {
return LocalDateTime.parse(timeText, STATUS_TIME_FORMATTER);
} catch (Exception ex) {
log.warn("[parseDeliverTime][无法解析时间:{}]", timeText, ex);
return null;
}
}
private static Long parseLongSafely(String text) {
try {
return Long.parseLong(text);
} catch (Exception ignore) {
return null;
}
}
}

View File

@@ -74,7 +74,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
}
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
StringBuilder requestBody = new StringBuilder();
appendToBody(requestBody, "from=", getSender());

View File

@@ -41,7 +41,7 @@ public class QiniuSmsClient extends AbstractSmsClient {
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
// 1. 执行请求
// 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages

View File

@@ -81,6 +81,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
case TENCENT: return new TencentSmsClient(properties);
case HUAWEI: return new HuaweiSmsClient(properties);
case QINIU: return new QiniuSmsClient(properties);
case HL95: return new Hl95SmsClient(properties);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);

View File

@@ -82,7 +82,7 @@ public class TencentSmsClient extends AbstractSmsClient {
}
@Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
// 1. 执行请求
// 参考链接 https://cloud.tencent.com/document/product/382/55981

View File

@@ -0,0 +1,15 @@
package com.zt.plat.module.system.framework.sms.core.client.impl.extra;
/**
* 支持查询余额的短信客户端扩展接口
*/
public interface SmsBalanceClient {
/**
* 查询当前渠道可用余额(条数)
*
* @return 余额条数
* @throws Throwable 查询失败时抛出异常
*/
Integer queryBalance() throws Throwable;
}

View File

@@ -19,6 +19,7 @@ public enum SmsChannelEnum {
TENCENT("TENCENT", "腾讯云"),
HUAWEI("HUAWEI", "华为云"),
QINIU("QINIU", "七牛云"),
HL95("HL95", "鸿联九五"),
;
/**

View File

@@ -26,6 +26,10 @@ public class SmsChannelProperties {
*/
@NotEmpty(message = "短信签名不能为空")
private String signature;
/**
* 企业编号epid。部分渠道需要例如鸿联九五
*/
private String epid;
/**
* 渠道编码
*

View File

@@ -25,6 +25,10 @@ public class SmsSendMessage {
*/
@NotNull(message = "手机号不能为空")
private String mobile;
/**
* 短信内容(已按模板格式化后的文本)
*/
private String content;
/**
* 短信渠道编号
*/

View File

@@ -32,9 +32,9 @@ public class SmsProducer {
* @param apiTemplateId 短信模板编号
* @param templateParams 短信模板参数
*/
public void sendSmsSendMessage(Long logId, String mobile,
public void sendSmsSendMessage(Long logId, String mobile, String content,
Long channelId, String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile);
SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile).setContent(content);
message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams);
rocketMQTemplate.syncSend(SmsSendMessage.TOPIC, message);
}

View File

@@ -49,6 +49,26 @@ public interface DeptExternalCodeService {
*/
List<DeptExternalCodeDO> getDeptExternalCodeListByDeptId(Long deptId);
/**
* 根据部门与外部系统保存/更新映射(存在则更新,不存在则创建)
*
* @param deptId 本系统部门 ID
* @param systemCode 外部系统标识
* @param externalDeptCode 外部系统组织编码
* @param externalDeptName 外部系统组织名称(可选)
* @param status 状态,默认启用
* @return 映射记录 ID
*/
Long saveOrUpdateDeptExternalCode(Long deptId, String systemCode, String externalDeptCode, String externalDeptName,
Integer status);
/**
* 根据部门删除全部外部编码映射
*
* @param deptId 部门编号
*/
void deleteDeptExternalCodesByDeptId(Long deptId);
/**
* 根据外部系统与外部组织编码查询映射
*/

View File

@@ -9,10 +9,17 @@ import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptEx
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Objects;
import java.util.List;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -28,11 +35,15 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
@Resource
private DeptExternalCodeMapper deptExternalCodeMapper;
@Resource
private DeptService deptService;
private DeptMapper deptMapper;
@Resource
private CacheManager cacheManager;
@Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST, key = "#createReqVO.deptId", beforeInvocation = false)
public Long createDeptExternalCode(DeptExternalCodeSaveReqVO createReqVO) {
normalizeRequest(createReqVO);
disableActiveMappingIfConflict(createReqVO.getDeptId(), createReqVO.getSystemCode(), createReqVO.getExternalDeptCode());
validateForCreateOrUpdate(null, createReqVO.getDeptId(), createReqVO.getSystemCode(),
createReqVO.getExternalDeptCode());
@@ -48,6 +59,7 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
public void updateDeptExternalCode(DeptExternalCodeSaveReqVO updateReqVO) {
normalizeRequest(updateReqVO);
DeptExternalCodeDO exists = validateExists(updateReqVO.getId());
disableActiveMappingIfConflict(updateReqVO.getDeptId(), updateReqVO.getSystemCode(), updateReqVO.getExternalDeptCode());
validateForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getDeptId(), updateReqVO.getSystemCode(),
updateReqVO.getExternalDeptCode());
@@ -57,12 +69,15 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
updateObj.setStatus(exists.getStatus() == null ? CommonStatusEnum.ENABLE.getStatus() : exists.getStatus());
}
deptExternalCodeMapper.updateById(updateObj);
evictCacheSafely(exists.getDeptId());
evictCacheSafely(updateObj.getDeptId());
}
@Override
public void deleteDeptExternalCode(Long id) {
validateExists(id);
DeptExternalCodeDO exists = validateExists(id);
deptExternalCodeMapper.deleteById(id);
evictCacheSafely(exists.getDeptId());
}
@Override
@@ -76,6 +91,7 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
}
@Override
@Cacheable(cacheNames = RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST, key = "#deptId")
public List<DeptExternalCodeDO> getDeptExternalCodeListByDeptId(Long deptId) {
return deptExternalCodeMapper.selectListByDeptId(deptId);
}
@@ -96,6 +112,50 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
return deptExternalCodeMapper.selectBySystemCodeAndDeptId(systemCode.trim(), deptId);
}
@Override
public Long saveOrUpdateDeptExternalCode(Long deptId, String systemCode, String externalDeptCode,
String externalDeptName, Integer status) {
if (StrUtil.hasEmpty(systemCode, externalDeptCode) || deptId == null) {
return null;
}
String normalizedSystemCode = systemCode.trim();
String normalizedExternalCode = externalDeptCode.trim();
String normalizedExternalName = StrUtil.blankToDefault(StrUtil.trimToNull(externalDeptName), null);
disableActiveMappingIfConflict(deptId, normalizedSystemCode, normalizedExternalCode);
// 如果存在则更新,否则创建
DeptExternalCodeDO exists = deptExternalCodeMapper.selectBySystemCodeAndDeptId(normalizedSystemCode, deptId);
if (exists != null) {
DeptExternalCodeSaveReqVO updateReqVO = new DeptExternalCodeSaveReqVO();
updateReqVO.setId(exists.getId());
updateReqVO.setDeptId(deptId);
updateReqVO.setSystemCode(normalizedSystemCode);
updateReqVO.setExternalDeptCode(normalizedExternalCode);
updateReqVO.setExternalDeptName(normalizedExternalName);
updateReqVO.setStatus(status == null ? exists.getStatus() : status);
updateDeptExternalCode(updateReqVO);
return exists.getId();
}
DeptExternalCodeSaveReqVO createReqVO = new DeptExternalCodeSaveReqVO();
createReqVO.setDeptId(deptId);
createReqVO.setSystemCode(normalizedSystemCode);
createReqVO.setExternalDeptCode(normalizedExternalCode);
createReqVO.setExternalDeptName(normalizedExternalName);
createReqVO.setStatus(status == null ? CommonStatusEnum.ENABLE.getStatus() : status);
return createDeptExternalCode(createReqVO);
}
@Override
public void deleteDeptExternalCodesByDeptId(Long deptId) {
if (deptId == null) {
return;
}
deptExternalCodeMapper.deleteByDeptId(deptId);
evictCacheSafely(deptId);
}
private DeptExternalCodeDO validateExists(Long id) {
if (id == null) {
throw exception(DEPT_EXTERNAL_RELATION_NOT_EXISTS);
@@ -109,7 +169,7 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
private void validateForCreateOrUpdate(Long id, Long deptId, String systemCode, String externalDeptCode) {
// 校验部门存在
DeptDO dept = deptService.getDept(deptId);
DeptDO dept = deptMapper.selectById(deptId);
if (dept == null) {
throw exception(DEPT_NOT_FOUND);
}
@@ -129,7 +189,11 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
DeptExternalCodeDO sameExternal = deptExternalCodeMapper
.selectBySystemCodeAndExternalCode(normalizedSystemCode, normalizedExternalCode);
if (sameExternal != null && (id == null || !sameExternal.getId().equals(id))) {
throw exception(DEPT_EXTERNAL_CODE_DUPLICATE, normalizedSystemCode, normalizedExternalCode);
boolean sameDept = Objects.equals(deptId, sameExternal.getDeptId());
boolean activeConflict = !sameDept && CommonStatusEnum.isEnable(sameExternal.getStatus());
if (activeConflict) {
throw exception(DEPT_EXTERNAL_CODE_DUPLICATE, normalizedSystemCode, normalizedExternalCode);
}
}
}
}
@@ -148,4 +212,39 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService {
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
}
}
private void disableActiveMappingIfConflict(Long targetDeptId, String systemCode, String externalDeptCode) {
String normalizedSystem = StrUtil.trimToNull(systemCode);
String normalizedExternal = StrUtil.trimToNull(externalDeptCode);
if (StrUtil.hasEmpty(normalizedSystem, normalizedExternal) || targetDeptId == null) {
return;
}
DeptExternalCodeDO existing = deptExternalCodeMapper.selectBySystemCodeAndExternalCode(normalizedSystem, normalizedExternal);
if (existing == null) {
return;
}
if (Objects.equals(existing.getDeptId(), targetDeptId)) {
return;
}
if (CommonStatusEnum.isEnable(existing.getStatus())) {
DeptExternalCodeDO update = new DeptExternalCodeDO();
update.setId(existing.getId());
update.setStatus(CommonStatusEnum.DISABLE.getStatus());
deptExternalCodeMapper.updateById(update);
evictCacheSafely(existing.getDeptId());
}
}
private void evictCacheSafely(Long deptId) {
if (deptId == null || cacheManager == null) {
return;
}
try {
if (cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST) != null) {
cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST).evict(deptId);
}
} catch (Exception ignore) {
// 缓存失效失败不影响主流程
}
}
}

View File

@@ -11,12 +11,18 @@ import com.zt.plat.framework.datapermission.core.annotation.DataPermission;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO;
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
import com.zt.plat.module.system.enums.DictTypeConstants;
import com.zt.plat.module.system.service.dict.DictDataService;
import com.zt.plat.module.system.service.dict.DictTypeService;
import org.apache.seata.spring.annotation.GlobalTransactional;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -52,6 +58,12 @@ public class DeptServiceImpl implements DeptService {
private UserDeptMapper userDeptMapper;
@Resource
private com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer databusChangeProducer;
@Resource
private DeptExternalCodeService deptExternalCodeService;
@Resource
private DictTypeService dictTypeService;
@Resource
private DictDataService dictDataService;
private static final String ROOT_CODE_PREFIX = "ZT";
private static final int CODE_SEGMENT_LENGTH = 3;
@@ -66,9 +78,8 @@ public class DeptServiceImpl implements DeptService {
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
public Long createDept(DeptSaveReqVO createReqVO) {
if (createReqVO.getParentId() == null) {
createReqVO.setParentId(DeptDO.PARENT_ID_ROOT);
}
// 允许上级组织为空,视为顶级组织
createReqVO.setParentId(normalizeParentId(createReqVO.getParentId()));
// 创建时默认有效
createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
// 校验父部门的有效性
@@ -76,23 +87,10 @@ public class DeptServiceImpl implements DeptService {
// 校验部门名的唯一性
validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
// 生成并校验部门编码
boolean isIWorkSource = Objects.equals(createReqVO.getDeptSource(), DeptSourceEnum.IWORK.getSource());
if (isIWorkSource) {
// iWork 来源直接使用提供的编码,不再生成
String providedCode = StrUtil.blankToDefault(createReqVO.getCode(), null);
createReqVO.setCode(providedCode);
} else {
Long effectiveParentId = normalizeParentId(createReqVO.getParentId());
boolean isTopLevel = Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT);
String resolvedCode;
if (isTopLevel) {
resolvedCode = resolveTopLevelCode(null, createReqVO.getCode());
} else {
resolvedCode = generateDeptCode(effectiveParentId);
validateDeptCodeUnique(null, resolvedCode);
}
createReqVO.setCode(resolvedCode);
}
Long effectiveParentId = normalizeParentId(createReqVO.getParentId());
String resolvedCode = generateDeptCode(effectiveParentId);
validateDeptCodeUnique(null, resolvedCode);
createReqVO.setCode(resolvedCode);
// 插入部门
DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class);
@@ -102,6 +100,9 @@ public class DeptServiceImpl implements DeptService {
}
deptMapper.insert(dept);
// 维护外部系统编码映射(若有传入)
upsertExternalCodeMapping(createReqVO, dept.getId());
// 发布部门创建事件
databusChangeProducer.sendDeptCreatedMessage(dept);
@@ -113,47 +114,25 @@ public class DeptServiceImpl implements DeptService {
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
@DataPermission(enable = false) // 禁用数据权限,避免确实上级部门导致无法保存
public void updateDept(DeptSaveReqVO updateReqVO) {
if (updateReqVO.getParentId() == null) {
updateReqVO.setParentId(DeptDO.PARENT_ID_ROOT);
}
// 允许上级组织为空,视为顶级组织
updateReqVO.setParentId(normalizeParentId(updateReqVO.getParentId()));
// 校验自己存在
DeptDO originalDept = getRequiredDept(updateReqVO.getId());
// 校验父部门的有效性
validateParentDept(updateReqVO.getId(), updateReqVO.getParentId());
// 校验部门名的唯一性
validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
// 如果上级发生变化,需要重新生成编码并同步子级
boolean isIWorkSource = Objects.equals(originalDept.getDeptSource(), DeptSourceEnum.IWORK.getSource());
Long newParentId = normalizeParentId(updateReqVO.getParentId());
Long oldParentId = normalizeParentId(originalDept.getParentId());
boolean parentChanged = !Objects.equals(newParentId, oldParentId);
if (isIWorkSource) {
// iWork 来源直接使用提供的编码,不再生成
String providedCode = StrUtil.blankToDefault(updateReqVO.getCode(), null);
updateReqVO.setCode(providedCode);
} else {
if (parentChanged) {
String newCode;
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
newCode = resolveTopLevelCode(updateReqVO.getId(), updateReqVO.getCode());
} else {
newCode = generateDeptCode(updateReqVO.getParentId());
validateDeptCodeUnique(updateReqVO.getId(), newCode);
}
updateReqVO.setCode(newCode);
} else {
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
String requestedCode = updateReqVO.getCode();
if (StrUtil.isNotBlank(requestedCode) && !StrUtil.equals(requestedCode.trim(), originalDept.getCode())) {
updateReqVO.setCode(resolveTopLevelCode(updateReqVO.getId(), requestedCode));
} else {
updateReqVO.setCode(originalDept.getCode());
}
} else {
updateReqVO.setCode(originalDept.getCode());
}
}
String existingCode = originalDept.getCode();
boolean needRegenerateCode = StrUtil.isBlank(existingCode);
String resolvedCode = existingCode;
if (needRegenerateCode) {
resolvedCode = generateDeptCode(newParentId);
validateDeptCodeUnique(updateReqVO.getId(), resolvedCode);
}
updateReqVO.setCode(resolvedCode);
// 更新部门
DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class);
@@ -165,9 +144,12 @@ public class DeptServiceImpl implements DeptService {
databusChangeProducer.sendDeptUpdatedMessage(updatedDept);
}
if (parentChanged) {
if (needRegenerateCode) {
refreshChildCodesRecursively(updateObj.getId(), updateReqVO.getCode());
}
// 维护外部系统编码映射(若有传入)
upsertExternalCodeMapping(updateReqVO, updateReqVO.getId());
}
@Override
@@ -185,6 +167,9 @@ public class DeptServiceImpl implements DeptService {
DeptDO dept = deptMapper.selectById(id);
Long tenantId = (dept != null) ? dept.getTenantId() : null;
// 级联删除外部编码映射并清理缓存
deptExternalCodeService.deleteDeptExternalCodesByDeptId(id);
// 删除部门
deptMapper.deleteById(id);
@@ -756,4 +741,65 @@ public class DeptServiceImpl implements DeptService {
return dept;
}
private void upsertExternalCodeMapping(DeptSaveReqVO reqVO, Long deptId) {
if (reqVO == null || deptId == null) {
return;
}
String systemCode = StrUtil.trimToNull(reqVO.getExternalSystemCode());
String externalCode = StrUtil.trimToNull(reqVO.getExternalDeptCode());
if (StrUtil.isBlank(systemCode) || StrUtil.isBlank(externalCode)) {
return;
}
// 缺失的外部系统字典类型或数据会自动补齐
ensureExternalSystemDict(systemCode);
deptExternalCodeService.saveOrUpdateDeptExternalCode(
deptId,
systemCode,
externalCode,
reqVO.getExternalDeptName(),
CommonStatusEnum.ENABLE.getStatus());
}
/**
* 确保外部系统字典存在(含字典类型与对应值),若缺失则自动创建
*/
private void ensureExternalSystemDict(String systemCode) {
String normalizedCode = StrUtil.trimToNull(systemCode);
if (normalizedCode == null) {
return;
}
try {
DictTypeDO dictType = dictTypeService.getDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM);
if (dictType == null) {
DictTypeSaveReqVO typeReq = new DictTypeSaveReqVO();
typeReq.setName("部门外部系统标识");
typeReq.setType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM);
typeReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
typeReq.setRemark("外部组织同步自动创建");
dictTypeService.createDictType(typeReq);
} else if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) {
DictTypeSaveReqVO updateReq = new DictTypeSaveReqVO();
updateReq.setId(dictType.getId());
updateReq.setName(dictType.getName());
updateReq.setType(dictType.getType());
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setRemark(dictType.getRemark());
dictTypeService.updateDictType(updateReq);
}
if (dictDataService.getDictData(DictTypeConstants.DEPT_EXTERNAL_SYSTEM, normalizedCode) == null) {
DictDataSaveReqVO dataReq = new DictDataSaveReqVO();
dataReq.setDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM);
dataReq.setLabel(normalizedCode);
dataReq.setValue(normalizedCode);
dataReq.setSort(0);
dataReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
dataReq.setRemark("外部组织同步自动创建");
dictDataService.createDictData(dataReq);
}
} catch (Exception ex) {
log.warn("[Dept] Ensure external system dict failed, systemCode={}", normalizedCode, ex);
}
}
}

View File

@@ -6,6 +6,9 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuth
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthTokenRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFileCallbackReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOperationRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaCheckTokenReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaRawResponse;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaTokenReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCreateReqVO;
@@ -48,4 +51,14 @@ public interface IWorkIntegrationService {
* @return 创建的业务附件关联记录 ID 或附件 ID视实现而定
*/
Long handleFileCallback(IWorkFileCallbackReqVO reqVO);
/**
* 透传调用 OA 获取 token。
*/
IWorkOaRawResponse getOaToken(IWorkOaTokenReqVO reqVO);
/**
* 透传调用 OA 校验 token。
*/
IWorkOaRawResponse checkOaToken(IWorkOaCheckTokenReqVO reqVO);
}

View File

@@ -4,10 +4,8 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepa
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO;
@@ -24,11 +22,4 @@ public interface IWorkOrgRestService {
IWorkHrUserPageRespVO listUsers(IWorkUserQueryReqVO reqVO);
IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO);
IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO);
IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO);
IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO);
}

View File

@@ -124,7 +124,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
Map<String, Object> payload = buildUserPayload(reqVO);
String responseBody = executeJsonRequest(properties.getPaths().getUserInfo(), reqVO.getQueryParams(), appId, session, payload);
String userInfoPath = requireConfiguredPath(properties.getPaths().getUserInfo(), "iwork.paths.user-info");
String responseBody = executeJsonRequest(userInfoPath, reqVO.getQueryParams(), appId, session, payload);
return buildUserInfoResponse(responseBody);
}
@@ -138,7 +139,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
Map<String, Object> payload = buildCreatePayload(reqVO);
String responseBody = executeFormRequest(properties.getPaths().getCreateWorkflow(), null, appId, session, payload);
String createWorkflowPath = requireConfiguredPath(properties.getPaths().getCreateWorkflow(), "iwork.paths.create-workflow");
String responseBody = executeFormRequest(createWorkflowPath, null, appId, session, payload);
return buildOperationResponse(responseBody);
}
@@ -154,7 +156,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
Map<String, Object> payload = buildVoidPayload(reqVO);
String responseBody = executeJsonRequest(properties.getPaths().getVoidWorkflow(), null, appId, session, payload);
String voidWorkflowPath = requireConfiguredPath(properties.getPaths().getVoidWorkflow(), "iwork.paths.void-workflow");
String responseBody = executeJsonRequest(voidWorkflowPath, null, appId, session, payload);
return buildOperationResponse(responseBody);
}
@@ -179,18 +182,83 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
AtomicReference<Long> attachmentIdRef = new AtomicReference<>();
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile)));
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile, reqVO.getSsoToken())));
return attachmentIdRef.get();
}
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) {
@Override
public IWorkOaRawResponse getOaToken(IWorkOaTokenReqVO reqVO) {
IWorkProperties.Oa oa = properties.getOa();
if (oa == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA 配置未初始化");
}
String loginId = Optional.ofNullable(reqVO)
.map(IWorkOaTokenReqVO::getLoginId)
.map(String::trim)
.orElse("");
if (!StringUtils.hasText(loginId)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "loginId 不能为空");
}
String appId = Optional.ofNullable(reqVO)
.map(IWorkOaTokenReqVO::getAppId)
.filter(StringUtils::hasText)
.map(StringUtils::trimWhitespace)
.orElseGet(() -> StringUtils.trimWhitespace(oa.getAppId()));
if (!StringUtils.hasText(appId)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA appid 未配置");
}
String path = requireOaPath(oa.getPaths().getGetToken(), "iwork.oa.paths.get-token");
FormBody formBody = new FormBody.Builder()
.add("loginid", loginId)
.add("appid", appId)
.build();
Request request = new Request.Builder()
.url(resolveOaUrl(path))
.post(formBody)
.build();
return executeOaRequest(request);
}
@Override
public IWorkOaRawResponse checkOaToken(IWorkOaCheckTokenReqVO reqVO) {
IWorkProperties.Oa oa = properties.getOa();
if (oa == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA 配置未初始化");
}
String token = Optional.ofNullable(reqVO)
.map(IWorkOaCheckTokenReqVO::getToken)
.map(String::trim)
.orElse("");
if (!StringUtils.hasText(token)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "token 不能为空");
}
String path = requireOaPath(oa.getPaths().getCheckToken(), "iwork.oa.paths.check-token");
FormBody formBody = new FormBody.Builder()
.add("token", token)
.build();
Request request = new Request.Builder()
.url(resolveOaUrl(path))
.post(formBody)
.build();
return executeOaRequest(request);
}
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile, String ssoToken) {
Long businessId = referenceBusinessFile.getBusinessId();
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl));
fileCreateReqDTO.setDirectory(null);
fileCreateReqDTO.setType(null);
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl));
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl, ssoToken));
CommonResult<FileRespDTO> fileResult = fileApi.createFileWithReturn(fileCreateReqDTO);
if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) {
@@ -229,9 +297,12 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return businessFile;
}
private byte[] downloadFileBytes(String fileUrl) {
private byte[] downloadFileBytes(String fileUrl, String ssoToken) {
// 如果回调已提供 ssoToken按需拼接后下载 OA 附件
String finalUrl = appendSsoTokenIfNeeded(fileUrl, ssoToken);
OkHttpClient client = okHttpClient();
Request request = new Request.Builder().url(fileUrl).get().build();
Request request = new Request.Builder().url(finalUrl).get().build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "下载文件失败HTTP 状态码: " + response.code());
@@ -246,6 +317,22 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
private String appendSsoTokenIfNeeded(String fileUrl, String ssoToken) {
// 未提供 token 或 URL 为空,直接返回原链接
if (!StringUtils.hasText(ssoToken) || !StringUtils.hasText(fileUrl)) {
return fileUrl;
}
// 已包含 ssoToken不区分大小写则不重复添加
String lower = fileUrl.toLowerCase();
if (lower.contains("ssotoken=")) {
return fileUrl;
}
// 简单拼接查询参数
return fileUrl.contains("?")
? fileUrl + "&ssoToken=" + ssoToken
: fileUrl + "?ssoToken=" + ssoToken;
}
private String resolveFileName(String overrideName, String fileUrl) {
if (StringUtils.hasText(overrideName)) {
return overrideName;
@@ -293,6 +380,17 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return StringUtils.trimWhitespace(value);
}
private String requireConfiguredPath(String value, String propertyPath) {
if (!StringUtils.hasText(value)) {
throw ServiceExceptionUtil.exception(IWORK_CONFIGURATION_INVALID, propertyPath + " 未配置");
}
return StringUtils.trimWhitespace(value);
}
private long resolveTokenTtlSeconds() {
return Math.max(1L, properties.getToken().getTtlSeconds());
}
private ClientKeyPair resolveClientKeyPair(String appId, boolean forceRefresh) {
String configured = properties.getClientPublicKey();
if (StringUtils.hasText(configured)) {
@@ -333,7 +431,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
String encryptedSecret = encryptWithPublicKey(registration.secret(), registration.spk());
String encryptedUserId = encryptWithPublicKey(operatorUserId, registration.spk());
String token = applyToken(appId, encryptedSecret);
Instant expiresAt = Instant.now().plusSeconds(Math.max(1L, properties.getToken().getTtlSeconds()));
long ttlSeconds = resolveTokenTtlSeconds();
Instant expiresAt = Instant.now().plusSeconds(ttlSeconds);
return new IWorkSession(token, encryptedUserId, expiresAt, registration.spk());
}
@@ -359,8 +458,9 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private RegistrationState register(String appId, ClientKeyPair clientKeyPair) {
String registerPath = requireConfiguredPath(properties.getPaths().getRegister(), "iwork.paths.register");
Request request = new Request.Builder()
.url(resolveUrl(properties.getPaths().getRegister()))
.url(resolveUrl(registerPath))
.header(properties.getHeaders().getAppId(), appId)
.header(properties.getHeaders().getClientPublicKey(), clientKeyPair.publicKey())
.post(RequestBody.create(null, new byte[0]))
@@ -376,11 +476,13 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private String applyToken(String appId, String encryptedSecret) {
String applyTokenPath = requireConfiguredPath(properties.getPaths().getApplyToken(), "iwork.paths.apply-token");
long ttlSeconds = resolveTokenTtlSeconds();
Request request = new Request.Builder()
.url(resolveUrl(properties.getPaths().getApplyToken()))
.url(resolveUrl(applyTokenPath))
.header(properties.getHeaders().getAppId(), appId)
.header(properties.getHeaders().getSecret(), encryptedSecret)
.header(properties.getHeaders().getTime(), String.valueOf(properties.getToken().getTtlSeconds()))
.header(properties.getHeaders().getTime(), String.valueOf(ttlSeconds))
.post(RequestBody.create(null, new byte[0]))
.build();
String responseBody = executeRequest(request, IWORK_APPLY_TOKEN_FAILED);
@@ -834,6 +936,30 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return baseUrl + path;
}
private String resolveOaUrl(String path) {
IWorkProperties.Oa oa = properties.getOa();
if (oa == null || !StringUtils.hasText(oa.getBaseUrl())) {
throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING, "iwork.oa.base-url 未配置");
}
String baseUrl = oa.getBaseUrl();
boolean baseEndsWithSlash = baseUrl.endsWith("/");
boolean pathStartsWithSlash = StringUtils.hasText(path) && path.startsWith("/");
if (baseEndsWithSlash && pathStartsWithSlash) {
return baseUrl + path.substring(1);
}
if (!baseEndsWithSlash && !pathStartsWithSlash) {
return baseUrl + "/" + path;
}
return baseUrl + path;
}
private String requireOaPath(String path, String propertyPath) {
if (!StringUtils.hasText(path)) {
throw ServiceExceptionUtil.exception(IWORK_CONFIGURATION_INVALID, propertyPath + " 未配置");
}
return StringUtils.trimWhitespace(path);
}
private String executeRequest(Request request, ErrorCode errorCode) {
logCurlCommand(request);
try (Response response = okHttpClient().newCall(request).execute()) {
@@ -849,6 +975,47 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
private IWorkOaRawResponse executeOaRequest(Request request) {
long start = System.currentTimeMillis();
try (Response response = okHttpClient().newCall(request).execute()) {
ResponseBody responseBody = response.body();
okhttp3.MediaType okhttpMediaType = responseBody != null ? responseBody.contentType() : null;
String bodyString = responseBody != null ? responseBody.string() : null;
MediaType contentType = null;
if (okhttpMediaType != null) {
try {
contentType = MediaType.parseMediaType(okhttpMediaType.toString());
} catch (Exception ignored) {
// ignore parse error
}
}
Map<String, String> headers = new LinkedHashMap<>();
Headers respHeaders = response.headers();
for (String name : respHeaders.names()) {
headers.put(name, respHeaders.get(name));
}
long duration = System.currentTimeMillis() - start;
String briefBody = abbreviate(bodyString, 200);
if (response.isSuccessful()) {
log.info("[OA] {} {} -> {} ({}ms)", request.method(), request.url(), response.code(), duration);
} else {
log.warn("[OA] {} {} -> {} ({}ms) body={}", request.method(), request.url(), response.code(), duration, briefBody);
}
return IWorkOaRawResponse.builder()
.statusCode(response.code())
.body(bodyString)
.contentType(contentType)
.headers(headers)
.build();
} catch (IOException ex) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "调用 OA 接口失败: " + ex.getMessage());
}
}
private ServiceException buildRemoteException(ErrorCode errorCode, int statusCode, String responseBody) {
String detail = buildRemoteErrorDetail(statusCode, responseBody);
if (!StringUtils.hasText(detail)) {

View File

@@ -9,11 +9,9 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepa
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgBaseQueryReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO;
import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
@@ -62,9 +60,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
private static final TypeReference<List<IWorkHrUserPageRespVO.User>> USER_LIST_TYPE =
new TypeReference<>() {
};
private static final TypeReference<List<IWorkHrSyncRespVO.SyncResult>> SYNC_RESULT_LIST_TYPE =
new TypeReference<>() {
};
private static final okhttp3.MediaType JSON_MEDIA_TYPE = okhttp3.MediaType.get("application/json; charset=UTF-8");
private final IWorkProperties properties;
@@ -178,34 +173,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
return params;
}
@Override
public IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncSubcompany();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
@Override
public IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncDepartment();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
@Override
public IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncJobTitle();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
@Override
public IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO) {
String path = orgPaths().getSyncUser();
JsonNode node = invokeDataEndpoint(path, reqVO.getData());
return buildSyncResp(node);
}
private JsonNode invokeParamsEndpoint(String path, Map<String, Object> params) {
Objects.requireNonNull(params, "查询参数不能为空");
Map<String, Object> payload = new HashMap<>();
@@ -213,13 +180,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
return executeJson(path, payload);
}
private JsonNode invokeDataEndpoint(String path, Object data) {
Objects.requireNonNull(data, "同步数据不能为空");
Map<String, Object> payload = new HashMap<>();
payload.put("data", data);
return executeJson(path, payload);
}
private JsonNode executeJson(String path, Map<String, Object> payload) {
// 统一封装请求体并发送 POST 调用
assertOrgConfigured(path);
@@ -369,17 +329,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService {
return respVO;
}
// 解析并封装同步结果
private IWorkHrSyncRespVO buildSyncResp(JsonNode node) {
ParsedEnvelope envelope = parseEnvelope(node);
IWorkHrSyncRespVO respVO = new IWorkHrSyncRespVO();
respVO.setCode(envelope.code());
respVO.setMessage(envelope.message());
respVO.setSuccess(envelope.success());
respVO.setResult(readList(envelope.root(), "result", SYNC_RESULT_LIST_TYPE));
return respVO;
}
private JsonNode parseJson(String body) {
try {
return objectMapper.readTree(body);

View File

@@ -18,6 +18,7 @@ import com.zt.plat.module.system.dal.mysql.dept.PostMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import com.zt.plat.module.system.enums.common.SexEnum;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
import com.zt.plat.module.system.enums.dept.ExternalPlatformEnum;
import com.zt.plat.module.system.enums.user.UserSourceEnum;
import com.zt.plat.module.system.service.dept.DeptService;
import com.zt.plat.module.system.service.dept.PostService;
@@ -399,14 +400,16 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
DeptSaveReqVO req = new DeptSaveReqVO();
req.setId(deptId);
req.setName(limitLength(StrUtil.blankToDefault(data.getSubcompanyname(), "未命名分部"), 30));
// req.setShortName(limitLength(data.getSubcompanyname(), 20));
req.setCode(trimToNull(data.getSubcompanycode()));
// req.setShortName(limitLength(data.getSubcompanyname(), 20));
req.setParentId(parentId == null ? DeptDO.PARENT_ID_ROOT : parentId);
req.setSort(defaultSort(data.getShoworder()));
req.setStatus(toStatus(canceled));
req.setIsCompany(Boolean.TRUE);
req.setIsGroup(Boolean.FALSE);
req.setDeptSource(DeptSourceEnum.IWORK.getSource());
req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode());
req.setExternalDeptCode(StrUtil.blankToDefault(trimToNull(data.getSubcompanycode()), String.valueOf(data.getId())));
req.setExternalDeptName(data.getSubcompanyname());
return req;
}
@@ -417,14 +420,16 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
DeptSaveReqVO req = new DeptSaveReqVO();
req.setId(deptId);
req.setName(limitLength(StrUtil.blankToDefault(data.getDepartmentname(), "未命名部门"), 30));
// req.setShortName(limitLength(StrUtil.blankToDefault(data.getDepartmentmark(), data.getDepartmentname()), 20));
req.setCode(trimToNull(data.getDepartmentcode()));
// req.setShortName(limitLength(StrUtil.blankToDefault(data.getDepartmentmark(), data.getDepartmentname()), 20));
req.setParentId(parentId == null ? DeptDO.PARENT_ID_ROOT : parentId);
req.setSort(defaultSort(data.getShoworder()));
req.setStatus(toStatus(canceled));
req.setIsCompany(Boolean.FALSE);
req.setIsGroup(Boolean.FALSE);
req.setDeptSource(DeptSourceEnum.IWORK.getSource());
req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode());
req.setExternalDeptCode(StrUtil.blankToDefault(trimToNull(data.getDepartmentcode()), String.valueOf(data.getId())));
req.setExternalDeptName(data.getDepartmentname());
return req;
}

View File

@@ -78,4 +78,11 @@ public interface SmsChannelService {
*/
SmsClient getSmsClient(String code);
/**
* 查询渠道余额(条数)
* @param id 渠道编号
* @return 余额条数
*/
Integer queryBalance(Long id);
}

View File

@@ -8,6 +8,7 @@ import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO;
import com.zt.plat.module.system.dal.mysql.sms.SmsChannelMapper;
import com.zt.plat.module.system.framework.sms.core.client.SmsClient;
import com.zt.plat.module.system.framework.sms.core.client.SmsClientFactory;
import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient;
import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -16,8 +17,7 @@ import org.springframework.stereotype.Service;
import java.util.List;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
/**
* 短信渠道 Service 实现类
@@ -100,4 +100,19 @@ public class SmsChannelServiceImpl implements SmsChannelService {
return smsClientFactory.getSmsClient(code);
}
@Override
public Integer queryBalance(Long id) {
SmsChannelDO channel = validateSmsChannelExists(id);
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
SmsClient client = smsClientFactory.createOrUpdateSmsClient(properties);
if (client instanceof SmsBalanceClient) {
try {
return ((SmsBalanceClient) client).queryBalance();
} catch (Throwable e) {
throw exception(SMS_TEMPLATE_API_ERROR, "查询余额失败:" + e.getMessage());
}
}
throw exception(SMS_CHANNEL_BALANCE_UNSUPPORTED);
}
}

View File

@@ -65,4 +65,12 @@ public interface SmsLogService {
*/
PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO);
/**
* 根据日志编号查询短信日志
*
* @param id 日志编号
* @return 短信日志
*/
SmsLogDO getSmsLog(Long id);
}

View File

@@ -76,4 +76,8 @@ public class SmsLogServiceImpl implements SmsLogService {
return smsLogMapper.selectPage(pageReqVO);
}
@Override
public SmsLogDO getSmsLog(Long id) {
return smsLogMapper.selectById(id);
}
}

View File

@@ -98,7 +98,7 @@ public class SmsSendServiceImpl implements SmsSendService {
// 发送 MQ 消息,异步执行发送短信
if (isSend) {
smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(),
smsProducer.sendSmsSendMessage(sendLogId, mobile, content, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}
return sendLogId;
@@ -161,8 +161,8 @@ public class SmsSendServiceImpl implements SmsSendService {
Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId());
// 发送短信
try {
SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(),
message.getApiTemplateId(), message.getTemplateParams());
SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(),
message.getContent(), message.getApiTemplateId(), message.getTemplateParams());
smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(),
sendResponse.getApiCode(), sendResponse.getApiMsg(),
sendResponse.getApiRequestId(), sendResponse.getSerialNo());

View File

@@ -196,9 +196,8 @@ public class AdminUserServiceImpl implements AdminUserService {
if (StrUtil.isNotBlank(updateReqVO.getNickname())) {
updateObj.setNickname(updateReqVO.getNickname());
}
if (updateReqVO.getWorkcode() != null) {
updateObj.setWorkcode(normalizeWorkcode(updateReqVO.getWorkcode()));
}
// 工号允许清空,因此直接归一化后写入
updateObj.setWorkcode(normalizeWorkcode(updateReqVO.getWorkcode()));
if (StrUtil.isNotBlank(updateReqVO.getMobile())) {
updateObj.setMobile(updateReqVO.getMobile());
}

View File

@@ -134,6 +134,12 @@ iwork:
sync-user: /api/hrm/resful/synHrmresource
workflow:
seal-workflow-id: "1753"
oa:
base-url: http://172.16.36.233:8080
app-id: a17ca6ca-88b0-463e-bffa-7995086bf225
paths:
get-token: /ssologin/getToken
check-token: /ssologin/checkToken
--- #################### RPC 远程调用相关配置 ####################

View File

@@ -0,0 +1,121 @@
package com.zt.plat.module.system.dal.mysql.user;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.module.system.controller.admin.user.vo.user.UserPageReqVO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import static com.zt.plat.framework.test.core.util.AssertUtils.assertPojoEquals;
import static com.zt.plat.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link AdminUserMapper} 单元测试
*/
public class AdminUserMapperTest extends BaseDbUnitTest {
@Resource
private AdminUserMapper adminUserMapper;
@Resource
private UserDeptMapper userDeptMapper;
@Test
public void testSelectPage_keywordMatch() {
// 准备数据:两个用户,只有第一个命中 keyword
AdminUserDO matchUser = randomUser("key-nick", "key-user", "13800000000", "WK001");
AdminUserDO otherUser = randomUser("other", "otherUser", "13900000000", "WK002");
adminUserMapper.insert(matchUser);
adminUserMapper.insert(otherUser);
// 调用
UserPageReqVO reqVO = new UserPageReqVO();
reqVO.setPageNo(1);
reqVO.setPageSize(10);
reqVO.setKeyword("key");
PageResult<AdminUserDO> page = adminUserMapper.selectPage(reqVO, null, (java.util.Collection<Long>) null);
// 断言:只返回命中的用户
assertEquals(1, page.getList().size());
assertPojoEquals(matchUser, page.getList().get(0));
}
@Test
public void testSelectPage_filterByDeptIds() {
// 用户 A 属于部门 1用户 B 属于部门 2
AdminUserDO dept1User = randomUser("dept1", "dept1User", "13000000001", "WK101");
AdminUserDO dept2User = randomUser("dept2", "dept2User", "13000000002", "WK102");
adminUserMapper.insert(dept1User);
adminUserMapper.insert(dept2User);
UserDeptDO ud1 = new UserDeptDO();
ud1.setUserId(dept1User.getId());
ud1.setDeptId(1L);
userDeptMapper.insert(ud1);
UserDeptDO ud2 = new UserDeptDO();
ud2.setUserId(dept2User.getId());
ud2.setDeptId(2L);
userDeptMapper.insert(ud2);
UserPageReqVO reqVO = new UserPageReqVO();
reqVO.setPageNo(1);
reqVO.setPageSize(10);
// 仅过滤部门 1
PageResult<AdminUserDO> page = adminUserMapper.selectPage(reqVO, Collections.singletonList(1L), null);
assertEquals(1, page.getList().size());
assertEquals(dept1User.getId(), page.getList().get(0).getId());
}
@Test
public void testSelectPage_filterByUserIds() {
AdminUserDO user1 = randomUser("u1", "u1", "15000000001", "WK201");
AdminUserDO user2 = randomUser("u2", "u2", "15000000002", "WK202");
adminUserMapper.insert(user1);
adminUserMapper.insert(user2);
UserPageReqVO reqVO = new UserPageReqVO();
reqVO.setPageNo(1);
reqVO.setPageSize(10);
PageResult<AdminUserDO> page = adminUserMapper.selectPage(reqVO, null, Collections.singletonList(user2.getId()));
assertEquals(1, page.getList().size());
assertEquals(user2.getId(), page.getList().get(0).getId());
}
private AdminUserDO randomUser(String nickname, String username, String mobile, String workcode) {
AdminUserDO user = randomPojo(AdminUserDO.class, o -> {
o.setId(null);
o.setNickname(nickname);
o.setUsername(username);
o.setMobile(mobile);
o.setWorkcode(workcode);
o.setSex(1);
o.setStatus(0);
o.setUserSource(1);
// 这些字段仅用于展示,不参与持久化,避免断言时与查询结果不一致
o.setDeptIds(null);
o.setDeptNames(null);
o.setCompanyIds(null);
o.setCompanyDeptInfos(null);
o.setCreateTime(LocalDateTime.now().withNano(0));
});
// 保证关键字段非空
if (!StringUtils.hasText(user.getPassword())) {
user.setPassword("pwd");
}
if (user.getTenantId() == null) {
user.setTenantId(1L);
}
return user;
}
}

View File

@@ -46,6 +46,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
// mock 方法
@@ -55,7 +56,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
.then((Answer<String>) invocationOnMock -> (String) invocationOnMock.getArguments()[0]);
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertTrue(result.getSuccess());
@@ -73,6 +74,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
// mock 方法
@@ -82,7 +84,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
.then((Answer<String>) invocationOnMock -> (String) invocationOnMock.getArguments()[0]);
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
assertEquals("B7700B8E-227E-5886-9564-26036172F01F", result.getApiRequestId());

View File

@@ -43,6 +43,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
@@ -51,7 +52,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
.thenReturn("{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertTrue(result.getSuccess());
@@ -67,6 +68,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
@@ -75,7 +77,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
.thenReturn("{\"result\":[{\"total\":1,\"originTo\":\"17321315478\",\"createTime\":\"2024-08-18T11:32:20Z\",\"from\":\"x8824060312575\",\"smsMsgId\":\"06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461\",\"countryId\":\"CN\",\"status\":\"E200033\"}],\"code\":\"E000510\",\"description\":\"The SMS fails to be sent. For details, see status.\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
@@ -91,6 +93,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
@@ -99,7 +102,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
.thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());

View File

@@ -45,13 +45,14 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.thenReturn("{\"message_id\":\"17245678901\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertTrue(result.getSuccess());
@@ -66,13 +67,14 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString() + " " + randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
.thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());

View File

@@ -29,6 +29,7 @@ public class SmsClientTests {
AliyunSmsClient client = new AliyunSmsClient(properties);
// 准备参数
String apiTemplateId = "SMS_207945135";
String content = "test";
// 调用
SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
// 打印结果
@@ -47,8 +48,9 @@ public class SmsClientTests {
Long sendLogId = System.currentTimeMillis();
String mobile = "15601691323";
String apiTemplateId = "SMS_207945135";
String content = "test";
// 调用
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, ListUtil.of(new KeyValue<String, Object>("code", "1024")));
// 打印结果
System.out.println(sendRespDTO);
}
@@ -68,8 +70,9 @@ public class SmsClientTests {
Long sendLogId = System.currentTimeMillis();
String mobile = "15601691323";
String apiTemplateId = "358212";
String content = "test";
// 调用
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, ListUtil.of(new KeyValue<String, Object>("code", "1024")));
// 打印结果
System.out.println(sendRespDTO);
}
@@ -106,9 +109,10 @@ public class SmsClientTests {
Long sendLogId = System.currentTimeMillis();
String mobile = "17321315478";
String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
String content = "test";
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<String, Object>("code", "1024"));
// 调用
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams);
// 打印结果
System.out.println(smsSendRespDTO);
}
@@ -126,9 +130,10 @@ public class SmsClientTests {
Long sendLogId = System.currentTimeMillis();
String mobile = "17321315478";
String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1122"));
String content = "test";
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<String, Object>("code", "1122"));
// 调用
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams);
// 打印结果
System.out.println(smsSendRespDTO);
}

View File

@@ -45,6 +45,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
// mock 方法
@@ -67,7 +68,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertTrue(result.getSuccess());
@@ -84,6 +85,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
@@ -107,7 +109,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
"}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());
@@ -124,6 +126,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
String content = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
@@ -132,7 +135,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
.thenReturn("{\"Response\":{\"Error\":{\"Code\":\"AuthFailure.SecretIdNotFound\",\"Message\":\"The SecretId is not found, please ensure that your SecretId is correct.\"},\"RequestId\":\"2a88f82a-261c-4ac6-9fa9-c7d01aaa486a\"}}");
// 调用
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content,
apiTemplateId, templateParams);
// 断言
assertFalse(result.getSuccess());

View File

@@ -0,0 +1,196 @@
package com.zt.plat.module.system.service.dept;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import java.util.List;
import java.util.Objects;
import static com.zt.plat.module.system.dal.redis.RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST;
import static org.junit.jupiter.api.Assertions.*;
@Import({DeptExternalCodeServiceImpl.class, DeptExternalCodeServiceImplTest.CacheConfig.class})
class DeptExternalCodeServiceImplTest extends BaseDbUnitTest {
@Resource
private DeptExternalCodeServiceImpl deptExternalCodeService;
@Resource
private DeptExternalCodeMapper deptExternalCodeMapper;
@Resource
private CacheManager cacheManager;
@Resource
private DeptMapper deptMapper;
@TestConfiguration
@EnableCaching
static class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST);
}
}
@Test
void testCacheEvictOnCreateAndUpdate() {
Long deptId = 100L;
DeptDO dept = new DeptDO();
dept.setId(deptId);
dept.setName("总部");
dept.setCode("ZT001");
dept.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(dept);
// 预热缓存(空结果)
List<?> firstCall = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId);
assertTrue(firstCall.isEmpty());
assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptId));
// 创建映射应触发缓存失效
DeptExternalCodeSaveReqVO createReq = new DeptExternalCodeSaveReqVO();
createReq.setDeptId(deptId);
createReq.setSystemCode("ERP");
createReq.setExternalDeptCode("ERP-001");
deptExternalCodeService.createDeptExternalCode(createReq);
List<?> refreshed = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId);
assertEquals(1, refreshed.size());
assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptId));
// 更新映射也会清理缓存
DeptExternalCodeSaveReqVO updateReq = new DeptExternalCodeSaveReqVO();
Long id = deptExternalCodeMapper.selectListByDeptId(deptId).get(0).getId();
updateReq.setId(id);
updateReq.setDeptId(deptId);
updateReq.setSystemCode("ERP");
updateReq.setExternalDeptCode("ERP-002");
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptExternalCodeService.updateDeptExternalCode(updateReq);
List<?> refreshedAfterUpdate = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId);
assertEquals(1, refreshedAfterUpdate.size());
assertEquals("ERP-002", deptExternalCodeMapper.selectById(id).getExternalDeptCode());
}
@Test
void testSaveOrUpdateDeptExternalCodeUpsert() {
Long deptId = 101L;
DeptDO dept = new DeptDO();
dept.setId(deptId);
dept.setName("事业部");
dept.setCode("ZT002");
dept.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(dept);
Long firstId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "OA", "OA-001", "OA-总部", null);
assertNotNull(firstId);
// upsert: 同一 system + dept 更新编码
Long secondId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "OA", "OA-002", "OA-新编码", CommonStatusEnum.ENABLE.getStatus());
assertEquals(firstId, secondId);
assertEquals("OA-002", deptExternalCodeMapper.selectById(firstId).getExternalDeptCode());
}
@Test
void testMultiSystemCoexistenceAndCrossSystemReuse() {
Long deptId = 200L;
DeptDO dept = new DeptDO();
dept.setId(deptId);
dept.setName("多系统部门");
dept.setCode("ZT200");
dept.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(dept);
deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "ERP", "CODE-001", "ERP编码", CommonStatusEnum.ENABLE.getStatus());
deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "OA", "CODE-001", "OA编码", CommonStatusEnum.ENABLE.getStatus());
List<DeptExternalCodeDO> list = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId);
assertEquals(2, list.size());
assertTrue(list.stream().anyMatch(it -> Objects.equals("ERP", it.getSystemCode()) && Objects.equals("CODE-001", it.getExternalDeptCode())));
assertTrue(list.stream().anyMatch(it -> Objects.equals("OA", it.getSystemCode()) && Objects.equals("CODE-001", it.getExternalDeptCode())));
assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptId));
}
@Test
void testConflictDisablesPreviousActiveMappingAndEvictsCache() {
Long deptAId = 201L;
Long deptBId = 202L;
DeptDO deptA = new DeptDO();
deptA.setId(deptAId);
deptA.setName("部门A");
deptA.setCode("ZTA");
deptA.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(deptA);
DeptDO deptB = new DeptDO();
deptB.setId(deptBId);
deptB.setName("部门B");
deptB.setCode("ZTB");
deptB.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(deptB);
// A 先建立启用映射并预热缓存
deptExternalCodeService.saveOrUpdateDeptExternalCode(deptAId, "ERP", "X-001", "ERP-A", CommonStatusEnum.ENABLE.getStatus());
deptExternalCodeService.getDeptExternalCodeListByDeptId(deptAId);
assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptAId));
// B 使用同 system+external占用后应禁用 A 的映射并驱逐 A 缓存
Long newId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptBId, "ERP", "X-001", "ERP-B", CommonStatusEnum.ENABLE.getStatus());
DeptExternalCodeDO oldRecord = deptExternalCodeMapper.selectBySystemCodeAndDeptId("ERP", deptAId);
DeptExternalCodeDO newRecord = deptExternalCodeMapper.selectById(newId);
assertNotNull(oldRecord);
assertNotNull(newRecord);
assertEquals(CommonStatusEnum.DISABLE.getStatus(), oldRecord.getStatus());
assertEquals(CommonStatusEnum.ENABLE.getStatus(), newRecord.getStatus());
assertNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptAId));
}
@Test
void testDisabledConflictDoesNotBlockNewMapping() {
Long deptAId = 203L;
Long deptBId = 204L;
DeptDO deptA = new DeptDO();
deptA.setId(deptAId);
deptA.setName("停用部门");
deptA.setCode("ZTDIS");
deptA.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(deptA);
DeptDO deptB = new DeptDO();
deptB.setId(deptBId);
deptB.setName("新部门");
deptB.setCode("ZTNEW");
deptB.setStatus(CommonStatusEnum.ENABLE.getStatus());
deptMapper.insert(deptB);
// 先插入停用态记录
deptExternalCodeService.saveOrUpdateDeptExternalCode(deptAId, "ERP", "Z-001", "停用编码", CommonStatusEnum.DISABLE.getStatus());
// 新部门占用相同 system+external 应直接成功,不触发异常
Long newId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptBId, "ERP", "Z-001", "启用编码", CommonStatusEnum.ENABLE.getStatus());
assertNotNull(newId);
DeptExternalCodeDO reused = deptExternalCodeMapper.selectById(newId);
assertEquals(deptBId, reused.getDeptId());
assertEquals(CommonStatusEnum.ENABLE.getStatus(), reused.getStatus());
DeptExternalCodeDO disabled = deptExternalCodeMapper.selectBySystemCodeAndDeptId("ERP", deptAId);
assertEquals(CommonStatusEnum.DISABLE.getStatus(), disabled.getStatus());
}
}

View File

@@ -5,11 +5,21 @@ import com.zt.plat.framework.common.util.object.ObjectUtils;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.service.dept.DeptExternalCodeServiceImpl;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.Arrays;
import java.util.List;
@@ -27,13 +37,31 @@ import static org.junit.jupiter.api.Assertions.*;
*
* @author niudehua
*/
@Import(DeptServiceImpl.class)
@Import({DeptServiceImpl.class, DeptExternalCodeServiceImpl.class, DeptServiceImplTest.CacheConfig.class})
public class DeptServiceImplTest extends BaseDbUnitTest {
@Resource
private DeptServiceImpl deptService;
@Resource
private DeptMapper deptMapper;
@Resource
private DeptExternalCodeServiceImpl deptExternalCodeService;
@Resource
private DeptExternalCodeMapper deptExternalCodeMapper;
@Resource
private CacheManager cacheManager;
@TestConfiguration
@EnableCaching
static class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(
RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST
);
}
}
private Long createDept(Long parentId, String name, int sort) {
DeptSaveReqVO reqVO = new DeptSaveReqVO();
@@ -108,7 +136,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testCreateDept_topLevelRespectCustomCode() {
public void testCreateDept_topLevelAutoCode_ignoreCustomInput() {
String customCode = "ROOT-001";
DeptSaveReqVO topLevelReq = new DeptSaveReqVO();
topLevelReq.setParentId(DeptDO.PARENT_ID_ROOT);
@@ -120,7 +148,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
Long deptId = deptService.createDept(topLevelReq);
DeptDO created = deptMapper.selectById(deptId);
assertEquals(customCode, created.getCode());
assertEquals("ZT001", created.getCode());
}
@Test
@@ -150,12 +178,15 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testUpdateDept_parentChangedRebuildsCodes() {
public void testUpdateDept_parentChangedKeepsCodes() {
Long parentAId = createDept(DeptDO.PARENT_ID_ROOT, "A公司", 1);
Long parentBId = createDept(DeptDO.PARENT_ID_ROOT, "B公司", 2);
Long childId = createDept(parentAId, "子部门", 1);
Long grandChildId = createDept(childId, "子部门-一组", 1);
DeptDO originalChild = deptMapper.selectById(childId);
DeptDO originalGrandChild = deptMapper.selectById(grandChildId);
DeptDO parentB = deptMapper.selectById(parentBId);
DeptSaveReqVO updateReq = new DeptSaveReqVO();
@@ -169,8 +200,119 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
DeptDO updatedChild = deptMapper.selectById(childId);
DeptDO updatedGrandChild = deptMapper.selectById(grandChildId);
assertEquals(parentB.getCode() + "001", updatedChild.getCode());
assertEquals(updatedChild.getCode() + "001", updatedGrandChild.getCode());
// 已有编码保持不变
assertEquals(originalChild.getCode(), updatedChild.getCode());
assertEquals(originalGrandChild.getCode(), updatedGrandChild.getCode());
// 父子关系按新父级变更
assertEquals(parentB.getId(), updatedChild.getParentId());
assertEquals(childId, updatedGrandChild.getParentId());
// 新父级自身编码不受影响
assertEquals(parentB.getCode(), deptMapper.selectById(parentBId).getCode());
}
@Test
public void testUpdateDept_addsMultiSystemMappingsWithoutChangingCode() {
Long parentId = createDept(DeptDO.PARENT_ID_ROOT, "父级", 1);
Long deptId = createDept(parentId, "多系统部门", 1);
DeptDO original = deptMapper.selectById(deptId);
// 第一次更新:写入 ERP 外部编码
DeptSaveReqVO updateReq1 = new DeptSaveReqVO();
updateReq1.setId(deptId);
updateReq1.setParentId(parentId);
updateReq1.setName("多系统部门");
updateReq1.setSort(1);
updateReq1.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq1.setDeptSource(1);
updateReq1.setExternalSystemCode("ERP");
updateReq1.setExternalDeptCode("ERP-100");
deptService.updateDept(updateReq1);
// 第二次更新:写入 OA 外部编码
DeptSaveReqVO updateReq2 = new DeptSaveReqVO();
updateReq2.setId(deptId);
updateReq2.setParentId(parentId);
updateReq2.setName("多系统部门");
updateReq2.setSort(1);
updateReq2.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq2.setDeptSource(1);
updateReq2.setExternalSystemCode("OA");
updateReq2.setExternalDeptCode("OA-100");
deptService.updateDept(updateReq2);
DeptDO updated = deptMapper.selectById(deptId);
assertEquals(original.getCode(), updated.getCode());
List<DeptExternalCodeDO> mappings = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId);
assertEquals(2, mappings.size());
assertTrue(mappings.stream().anyMatch(it -> "ERP".equals(it.getSystemCode()) && "ERP-100".equals(it.getExternalDeptCode())));
assertTrue(mappings.stream().anyMatch(it -> "OA".equals(it.getSystemCode()) && "OA-100".equals(it.getExternalDeptCode())));
}
@Test
public void testCreateDept_externalMappingConflictDisablesOld() {
// 首个部门创建时写入 IWORK 外部编码
DeptSaveReqVO createA = new DeptSaveReqVO();
createA.setParentId(DeptDO.PARENT_ID_ROOT);
createA.setName("iWork-A");
createA.setSort(1);
createA.setStatus(CommonStatusEnum.ENABLE.getStatus());
createA.setDeptSource(1);
createA.setExternalSystemCode("IWORK");
createA.setExternalDeptCode("IW-001");
Long deptAId = deptService.createDept(createA);
// 预热缓存
deptExternalCodeService.getDeptExternalCodeListByDeptId(deptAId);
assertNotNull(cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST).get(deptAId));
// 第二个部门使用相同 system+external应禁用旧映射
DeptSaveReqVO createB = new DeptSaveReqVO();
createB.setParentId(DeptDO.PARENT_ID_ROOT);
createB.setName("iWork-B");
createB.setSort(2);
createB.setStatus(CommonStatusEnum.ENABLE.getStatus());
createB.setDeptSource(1);
createB.setExternalSystemCode("IWORK");
createB.setExternalDeptCode("IW-001");
Long deptBId = deptService.createDept(createB);
DeptExternalCodeDO oldRecord = deptExternalCodeMapper.selectBySystemCodeAndDeptId("IWORK", deptAId);
DeptExternalCodeDO newRecord = deptExternalCodeMapper.selectBySystemCodeAndDeptId("IWORK", deptBId);
assertNotNull(oldRecord);
assertNotNull(newRecord);
assertEquals(CommonStatusEnum.DISABLE.getStatus(), oldRecord.getStatus());
assertEquals(CommonStatusEnum.ENABLE.getStatus(), newRecord.getStatus());
// A 的缓存应被清理
assertNull(cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST).get(deptAId));
}
@Test
public void testUpdateDept_parentChangeWithExternalMappingKeepsCode() {
Long parentAId = createDept(DeptDO.PARENT_ID_ROOT, "父A", 1);
Long parentBId = createDept(DeptDO.PARENT_ID_ROOT, "父B", 2);
Long deptId = createDept(parentAId, "", 1);
DeptDO original = deptMapper.selectById(deptId);
DeptSaveReqVO updateReq = new DeptSaveReqVO();
updateReq.setId(deptId);
updateReq.setParentId(parentBId);
updateReq.setName("子-更新");
updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(1);
updateReq.setExternalSystemCode("IWORK");
updateReq.setExternalDeptCode("IW-CHILD");
deptService.updateDept(updateReq);
DeptDO updated = deptMapper.selectById(deptId);
assertEquals(original.getCode(), updated.getCode());
assertEquals(parentBId, updated.getParentId());
DeptExternalCodeDO mapping = deptExternalCodeMapper.selectBySystemCodeAndDeptId("IWORK", deptId);
assertNotNull(mapping);
assertEquals("IW-CHILD", mapping.getExternalDeptCode());
}
@Test
@@ -187,6 +329,29 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
assertNull(deptMapper.selectById(id));
}
@Test
public void testDeleteDept_cascadeExternalCodesAndEvictCache() {
Long deptId = createDept(DeptDO.PARENT_ID_ROOT, "总部", 1);
// 创建映射并预热缓存
DeptExternalCodeSaveReqVO createReq = new DeptExternalCodeSaveReqVO();
createReq.setDeptId(deptId);
createReq.setSystemCode("ERP");
createReq.setExternalDeptCode("ERP-001");
deptExternalCodeService.createDeptExternalCode(createReq);
deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId);
assertNotNull(cacheManager.getCache(com.zt.plat.module.system.dal.redis.RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST)
.get(deptId));
// 删除部门
deptService.deleteDept(deptId);
// 校验映射被删除且缓存被清理
assertTrue(deptExternalCodeMapper.selectListByDeptId(deptId).isEmpty());
assertNull(cacheManager.getCache(com.zt.plat.module.system.dal.redis.RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST)
.get(deptId));
}
@Test
public void testDeleteDept_exitsChildren() {
// mock 数据

View File

@@ -85,7 +85,7 @@ public class DictDataServiceImplTest extends BaseDbUnitTest {
dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setValue("otherValue")));
// 准备参数
DictDataPageReqVO reqVO = new DictDataPageReqVO();
reqVO.setLabel("");
reqVO.setLabel("ZT");
reqVO.setDictType("yunai");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setValue("testValue");

View File

@@ -59,8 +59,8 @@ public class DictTypeServiceImplTest extends BaseDbUnitTest {
dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1))));
// 准备参数
DictTypePageReqVO reqVO = new DictTypePageReqVO();
reqVO.setName("nai");
reqVO.setType("");
reqVO.setName("yunai");
reqVO.setType("ZT");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setCreateTime(buildBetweenTime(2021, 1, 10, 2021, 1, 20));

View File

@@ -153,7 +153,7 @@ public class MenuServiceImplTest extends BaseDbUnitTest {
// 测试 name 不匹配
menuMapper.insert(cloneIgnoreId(menuDO, o -> o.setName("")));
// 准备参数
MenuListReqVO reqVO = new MenuListReqVO().setName("").setStatus(CommonStatusEnum.ENABLE.getStatus());
MenuListReqVO reqVO = new MenuListReqVO().setName("ZT").setStatus(CommonStatusEnum.ENABLE.getStatus());
// 调用
List<MenuDO> result = menuService.getMenuList(reqVO);

View File

@@ -0,0 +1,81 @@
package com.zt.plat.module.system.service.sms;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO;
import com.zt.plat.module.system.dal.mysql.sms.SmsChannelMapper;
import com.zt.plat.module.system.framework.sms.core.client.SmsClient;
import com.zt.plat.module.system.framework.sms.core.client.SmsClientFactory;
import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient;
import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static com.zt.plat.framework.test.core.util.RandomUtils.randomLongId;
import static com.zt.plat.framework.test.core.util.RandomUtils.randomPojo;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
public class SmsChannelServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private SmsChannelServiceImpl smsChannelService;
@Mock
private SmsClientFactory smsClientFactory;
@Mock
private SmsChannelMapper smsChannelMapper;
@Mock
private SmsTemplateService smsTemplateService;
@Test
public void testQueryBalance_success() throws Throwable {
Long channelId = randomLongId();
SmsChannelDO channel = randomPojo(SmsChannelDO.class, o -> o.setId(channelId));
when(smsChannelMapper.selectById(eq(channelId))).thenReturn(channel);
SmsClient client = mock(SmsClient.class, withSettings().extraInterfaces(SmsBalanceClient.class));
when(smsClientFactory.createOrUpdateSmsClient(any())).thenReturn(client);
when(((SmsBalanceClient) client).queryBalance()).thenReturn(88);
Integer balance = smsChannelService.queryBalance(channelId);
assertThat(balance).isEqualTo(88);
verify((SmsBalanceClient) client, times(1)).queryBalance();
}
@Test
public void testQueryBalance_unsupported() {
Long channelId = randomLongId();
when(smsChannelMapper.selectById(eq(channelId))).thenReturn(randomPojo(SmsChannelDO.class, o -> o.setId(channelId)));
SmsClient client = mock(SmsClient.class); // 未实现 SmsBalanceClient
when(smsClientFactory.createOrUpdateSmsClient(any())).thenReturn(client);
ServiceException ex = assertThrows(ServiceException.class, () -> smsChannelService.queryBalance(channelId));
assertThat(ex.getCode()).isEqualTo(SMS_CHANNEL_BALANCE_UNSUPPORTED.getCode());
}
@Test
public void testQueryBalance_apiError() throws Throwable {
Long channelId = randomLongId();
when(smsChannelMapper.selectById(eq(channelId))).thenReturn(randomPojo(SmsChannelDO.class, o -> o.setId(channelId)));
SmsClient client = mock(SmsClient.class, withSettings().extraInterfaces(SmsBalanceClient.class));
when(smsClientFactory.createOrUpdateSmsClient(any())).thenReturn(client);
when(((SmsBalanceClient) client).queryBalance()).thenThrow(new RuntimeException("boom"));
ServiceException ex = assertThrows(ServiceException.class, () -> smsChannelService.queryBalance(channelId));
assertThat(ex.getCode()).isEqualTo(SMS_TEMPLATE_API_ERROR.getCode());
}
@Test
public void testQueryBalance_channelNotFound() {
Long channelId = randomLongId();
when(smsChannelMapper.selectById(eq(channelId))).thenReturn(null);
ServiceException ex = assertThrows(ServiceException.class, () -> smsChannelService.queryBalance(channelId));
assertThat(ex.getCode()).isEqualTo(SMS_CHANNEL_NOT_EXISTS.getCode());
}
}

View File

@@ -85,7 +85,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()),
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(content),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@@ -124,7 +124,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile),
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(content),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@@ -164,7 +164,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile),
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(content),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@@ -204,7 +204,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(),
verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), anyString(),
anyLong(), any(), anyList());
}
@@ -260,13 +260,13 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
@SuppressWarnings("unchecked")
public void testDoSendSms() throws Throwable {
// 准备参数
SmsSendMessage message = randomPojo(SmsSendMessage.class);
SmsSendMessage message = randomPojo(SmsSendMessage.class, o -> o.setContent(randomString()));
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient);
// mock SmsClient 的方法
SmsSendRespDTO sendResult = randomPojo(SmsSendRespDTO.class);
when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()),
when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getContent()), eq(message.getApiTemplateId()),
eq(message.getTemplateParams()))).thenReturn(sendResult);
// 调用
@@ -287,12 +287,15 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
when(smsChannelService.getSmsClient(eq(channelCode))).thenReturn(smsClient);
// mock SmsClient 的方法
List<SmsReceiveRespDTO> receiveResults = randomPojoList(SmsReceiveRespDTO.class);
when(smsClient.parseSmsReceiveStatus(eq(text))).thenReturn(receiveResults);
// 调用
smsSendService.receiveSmsStatus(channelCode, text);
// 断言
receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()),
eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode())));
for (SmsReceiveRespDTO result : receiveResults) {
verify(smsLogService).updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()),
eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorMsg()));
}
}
}

View File

@@ -273,7 +273,7 @@ public class SocialUserServiceImplTest extends BaseDbUnitTest {
// 准备参数
SocialUserPageReqVO reqVO = new SocialUserPageReqVO();
reqVO.setType(SocialTypeEnum.GITEE.getType());
reqVO.setNickname("");
reqVO.setNickname("ZT");
reqVO.setOpenid("zt");
reqVO.setCreateTime(buildBetweenTime(2020, 1, 10, 2020, 1, 20));

View File

@@ -1,6 +1,8 @@
package com.zt.plat.module.system.service.sso.client;
import com.zt.plat.module.system.framework.sso.config.ExternalSsoProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
@@ -16,7 +18,6 @@ import static org.mockito.Mockito.mock;
* 使用 Mock 的 RedisTemplate避免外部依赖。
*/
@SpringBootTest(classes = { ExternalSsoClientConfiguration.class, ExternalSsoClientConfigurationLoadTest.TestBeans.class })
@Import(ExternalSsoProperties.class)
class ExternalSsoClientConfigurationLoadTest {
@TestConfiguration
@@ -33,6 +34,16 @@ class ExternalSsoClientConfigurationLoadTest {
props.getRemote().setBaseUrl("http://localhost");
return props;
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public RestTemplateBuilder restTemplateBuilder() {
return new RestTemplateBuilder();
}
}
@Test

View File

@@ -306,7 +306,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
// 准备参数
TenantPageReqVO reqVO = new TenantPageReqVO();
reqVO.setName("ZT");
reqVO.setContactName("");
reqVO.setContactName("ZT");
reqVO.setContactMobile("1560");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24));

View File

@@ -681,6 +681,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
o.setDeptNames("-");
o.setCompanyDeptInfos(null);// 保证 deptIds 的范围
o.setUserSource(null);
o.setWorkcode(null);
};
return randomPojo(AdminUserDO.class, ArrayUtils.append(consumer, consumers));
}

View File

@@ -1,4 +1,5 @@
DELETE FROM "system_dept";
DELETE FROM "system_dept_external_code";
DELETE FROM "system_dict_data";
DELETE FROM "system_role";
DELETE FROM "system_role_menu";

View File

@@ -34,6 +34,27 @@ CREATE TABLE IF NOT EXISTS "system_dept" (
PRIMARY KEY ("id")
) COMMENT '部门表';
CREATE TABLE IF NOT EXISTS "system_dept_external_code" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"dept_id" bigint NOT NULL,
"system_code" varchar(64) NOT NULL,
"external_dept_code" varchar(128) NOT NULL,
"external_dept_name" varchar(255),
"status" tinyint DEFAULT 0 NOT NULL,
"remark" varchar(512),
"tenant_id" bigint DEFAULT 0,
"creator" varchar(64),
"create_time" timestamp DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64),
"update_time" timestamp DEFAULT CURRENT_TIMESTAMP,
"deleted" tinyint DEFAULT 0 NOT NULL,
PRIMARY KEY ("id")
) COMMENT '部门外部组织编码映射';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_dept_external_code_ext" ON "system_dept_external_code" ("tenant_id", "system_code", "external_dept_code");
CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "system_code", "dept_id");
CREATE INDEX IF NOT EXISTS "idx_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "dept_id");
CREATE TABLE IF NOT EXISTS "system_dict_data" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"sort" int NOT NULL DEFAULT '0',
@@ -242,6 +263,7 @@ CREATE TABLE IF NOT EXISTS `system_operate_log` (
CREATE TABLE IF NOT EXISTS "system_users" (
"id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
"username" varchar(30) not null,
"workcode" varchar(100) default null,
"password" varchar(100) not null default '',
"nickname" varchar(30) not null,
"remark" varchar(500) default null,
@@ -267,6 +289,7 @@ CREATE TABLE IF NOT EXISTS "system_users" (
CREATE TABLE IF NOT EXISTS "system_sms_channel" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"signature" varchar(10) NOT NULL,
"epid" varchar(63) DEFAULT NULL,
"code" varchar(63) NOT NULL,
"status" tinyint NOT NULL,
"remark" varchar(255) DEFAULT NULL,