Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
hewencai
2025-12-04 14:07:00 +08:00
84 changed files with 1619 additions and 945 deletions

View File

@@ -1,7 +1,6 @@
## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性
ARG BASE_IMAGE=172.16.46.66:10043/base-service/skywalking-agent-jre:9.7.0
FROM ${BASE_IMAGE}
FROM 172.16.46.66:10043/base-service/eclipse-temurin:21-jre
## 创建目录,并使用它作为工作目录
RUN mkdir -p /zt-module-system-server
@@ -11,15 +10,10 @@ COPY ./target/zt-module-system-server.jar app.jar
## 设置 TZ 时区
## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖
ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xms512m -Xmx512m"
ENV SW_AGENT_HOME=/opt/skywalking/agent
ENV SW_AGENT_NAME=zt-module-system-server
ENV SW_AGENT_COLLECTOR_BACKEND_SERVICES=172.16.46.63:30201
ENV AGENT_JAVA_OPTS="-javaagent:${SW_AGENT_HOME}/skywalking-agent.jar -Dskywalking.agent.service_name=${SW_AGENT_NAME} -Dskywalking.collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES}"
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx1024m"
## 暴露后端项目的 48080 端口
EXPOSE 48081
## 启动后端项目
CMD java ${AGENT_JAVA_OPTS} ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar
CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar

View File

@@ -50,6 +50,9 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "zt")
private String username;
@Schema(description = "工号", example = "A00123")
private String workcode;
@Schema(description = "用户邮箱", example = "zt@iocoder.cn")
private String email;

View File

@@ -1,12 +1,14 @@
package com.zt.plat.module.system.controller.admin.integration.iwork;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService;
import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
@@ -14,7 +16,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.security.PermitAll;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@@ -57,6 +58,7 @@ public class IWorkIntegrationController {
}
@PermitAll
@TenantIgnore
@PostMapping("/callback/file")
@Operation(summary = "iWork 文件回调:根据文件 URL 保存为附件并创建业务附件关联")
public CommonResult<Long> callbackFile(@Valid @RequestBody IWorkFileCallbackReqVO reqVO) {

View File

@@ -1,23 +0,0 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* 传递给 iWork 的单条明细记录。
*/
@Data
public class IWorkDetailRecordVO {
@Schema(description = "记录序号,从 0 开始", example = "0")
private Integer recordOrder;
@Schema(description = "明细字段列表")
@NotEmpty(message = "明细字段不能为空")
@Valid
private List<IWorkFormFieldVO> fields;
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* iWork 流程请求中的明细表定义。
*/
@Data
public class IWorkDetailTableVO {
@Schema(description = "表名", example = "formtable_main_26_dt1")
@NotBlank(message = "明细表名不能为空")
private String tableDBName;
@Schema(description = "明细记录集合")
@NotEmpty(message = "明细记录不能为空")
@Valid
private List<IWorkDetailRecordVO> records;
}

View File

@@ -12,9 +12,9 @@ public class IWorkFileCallbackReqVO {
@NotBlank(message = "文件 URL 不能为空")
private String fileUrl;
@Schema(description = "业务 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotBlank(message = "业务 ID 不能为空")
private String businessId;
@Schema(description = "业务编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "DJ-2025-0001")
@NotBlank(message = "业务编码不能为空")
private String businessCode;
@Schema(description = "文件名称,可选", example = "合同附件.pdf")
private String fileName;

View File

@@ -1,20 +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.NotBlank;
import lombok.Data;
/**
* 提交给 iWork 的单个表单字段。
*/
@Data
public class IWorkFormFieldVO {
@Schema(description = "字段名", example = "sqr")
@NotBlank(message = "字段名不能为空")
private String fieldName;
@Schema(description = "字段值", example = "张三")
@NotBlank(message = "字段值不能为空")
private String fieldValue;
}

View File

@@ -33,6 +33,9 @@ public class IWorkFullSyncReqVO {
@Schema(description = "同步范围列表默认同步全部。可选subcompany、department、jobTitle、user")
private List<String> scopes;
@Schema(description = "指定同步记录的 iWork ID。传入后仅同步对应记录", example = "12345")
private String id;
@Schema(description = "是否包含已失效canceled=1的记录", example = "false")
private Boolean includeCanceled = Boolean.FALSE;

View File

@@ -1,5 +1,6 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -11,12 +12,49 @@ import java.util.Map;
@Data
public class IWorkOperationRespVO {
@Schema(description = "iWork 返回的原始数据")
private Map<String, Object> payload;
@Schema(description = "iWork 返回的原始数据结构")
private Payload payload;
@Schema(description = "是否判断为成功")
private boolean success;
@Schema(description = "返回提示信息")
private String message;
@Data
public static class Payload {
@Schema(description = "iWork 返回的业务状态码,例如 SUCCESS")
private String code;
@Schema(description = "iWork 返回的数据体")
private PayloadData data;
@Schema(description = "错误信息对象,通常为空对象")
private Map<String, Object> errMsg;
@Schema(description = "返回失败时的详细信息")
private ReqFailMsg reqFailMsg;
}
@Data
public static class PayloadData {
@Schema(description = "iWork 生成的请求编号 requestid")
@JsonProperty("requestid")
private Long requestId;
}
@Data
public static class ReqFailMsg {
@Schema(description = "失败时的关键参数集合")
private Map<String, Object> keyParameters;
@Schema(description = "失败消息对象")
private Map<String, Object> msgInfo;
@Schema(description = "其他附加参数,例如 doAutoApprove")
private Map<String, Object> otherParams;
}
}

View File

@@ -1,73 +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.Map;
/**
* 查询 iWork 人力组织信息所需的参数。
*/
@Data
public class IWorkOrgQueryReqVO {
@Schema(description = "当前页码", example = "1")
private Integer curpage;
@Schema(description = "每页条数", example = "10")
private Integer pagesize;
// ================= 分部查询 =================
@Schema(description = "分部编码")
private String subcompanyCode;
@Schema(description = "分部名称")
private String subcompanyName;
// ================= 部门查询 =================
@Schema(description = "部门编码")
private String departmentCode;
@Schema(description = "部门名称")
private String departmentName;
@Schema(description = "所属分部ID")
private String subcompanyId;
// ================= 岗位查询 =================
@Schema(description = "岗位编码")
private String jobTitleCode;
@Schema(description = "岗位名称")
private String jobTitleName;
// ================= 人员查询 =================
@Schema(description = "人员工号")
private String workCode;
@Schema(description = "人员姓名")
private String lastName;
@Schema(description = "所属部门ID")
private String departmentId;
@Schema(description = "所属岗位ID")
private String jobTitleId;
@Schema(description = "人员状态 (0:试用, 1:正式, 2:临时, 3:试用延期, 4:解聘, 5:离职, 6:退休, 7:无效)")
private String status;
@Schema(description = "手机号")
private String mobile;
@Schema(description = "邮箱")
private String email;
@Schema(description = "查询参数(扩展用),将被序列化为 params 传给 iWork")
private Map<String, Object> params;
}

View File

@@ -1,12 +0,0 @@
package com.zt.plat.module.system.controller.admin.integration.iwork.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 获取 iWork 会话令牌的请求载荷。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class IWorkTokenApplyReqVO extends IWorkBaseReqVO {
}

View File

@@ -22,6 +22,9 @@ public class UserProfileRespVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "zt")
private String username;
@Schema(description = "工号", example = "A00123")
private String workcode;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZT")
private String nickname;

View File

@@ -23,6 +23,9 @@ public class UserPageReqVO extends PageParam {
@Schema(description = "用户账号,模糊匹配", example = "zt")
private String username;
@Schema(description = "工号,模糊匹配", example = "A00123")
private String workcode;
@Schema(description = "手机号码,模糊匹配", example = "zt")
private String mobile;

View File

@@ -25,6 +25,10 @@ public class UserRespVO{
@ExcelProperty("用户名称")
private String username;
@Schema(description = "工号", example = "A00123")
@ExcelProperty("工号")
private String workcode;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZT")
@ExcelProperty("用户昵称")
private String nickname;

View File

@@ -33,6 +33,11 @@ public class UserSaveReqVO {
@DiffLogField(name = "用户账号")
private String username;
@Schema(description = "工号", example = "A00123")
@Length(max = 64, message = "工号长度不能超过64个字符")
@DiffLogField(name = "工号")
private String workcode;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZT")
@Size(max = 30, message = "用户昵称长度不能超过30个字符")
@DiffLogField(name = "用户昵称")

View File

@@ -38,6 +38,10 @@ public class AdminUserDO extends TenantBaseDO {
*/
@NotEmpty
private String username;
/**
* 工号
*/
private String workcode;
/**
* 加密后的密码
*

View File

@@ -23,6 +23,10 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
return selectOne(AdminUserDO::getUsername, username);
}
default AdminUserDO selectByWorkcode(String workcode) {
return selectOne(AdminUserDO::getWorkcode, workcode);
}
default AdminUserDO selectByEmail(String email) {
return selectOne(AdminUserDO::getEmail, email);
}
@@ -36,6 +40,7 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
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())

View File

@@ -97,11 +97,6 @@ public class IWorkProperties {
*/
@Min(value = 1, message = "iWork Token 有效期必须大于 0")
private long ttlSeconds;
/**
* Token 过期前提前刷新的秒数。
*/
@Min(value = 0, message = "iWork Token 提前刷新秒数不能为负数")
private long refreshAheadSeconds;
}
@Data

View File

@@ -10,13 +10,13 @@ public interface IWorkIntegrationErrorCodeConstants {
ErrorCode IWORK_BASE_URL_MISSING = new ErrorCode(1_010_200_001, "iWork 集成未配置网关地址");
ErrorCode IWORK_CONFIGURATION_INVALID = new ErrorCode(1_010_200_002,
"iWork 集成缺少必填配置appId/userId/workflowId或配置无效");
ErrorCode IWORK_REGISTER_FAILED = new ErrorCode(1_010_200_003, "iWork 注册授权失败");
ErrorCode IWORK_APPLY_TOKEN_FAILED = new ErrorCode(1_010_200_004, "iWork 令牌申请失败");
ErrorCode IWORK_REMOTE_REQUEST_FAILED = new ErrorCode(1_010_200_005, "iWork 接口请求失败");
ErrorCode IWORK_REGISTER_FAILED = new ErrorCode(1_010_200_003, "iWork 注册授权失败{}");
ErrorCode IWORK_APPLY_TOKEN_FAILED = new ErrorCode(1_010_200_004, "iWork 令牌申请失败{}");
ErrorCode IWORK_REMOTE_REQUEST_FAILED = new ErrorCode(1_010_200_005, "iWork 接口请求失败{}");
ErrorCode IWORK_USER_IDENTIFIER_MISSING = new ErrorCode(1_010_200_006, "缺少用户识别信息,无法调用 iWork 接口");
ErrorCode IWORK_OPERATOR_USER_MISSING = new ErrorCode(1_010_200_007, "缺少 iWork 操作人用户编号");
ErrorCode IWORK_WORKFLOW_ID_MISSING = new ErrorCode(1_010_200_008, "缺少 iWork 流程模板编号");
ErrorCode IWORK_ORG_IDENTIFIER_MISSING = new ErrorCode(1_010_200_009, "iWork 人力组织接口缺少认证标识");
ErrorCode IWORK_ORG_REMOTE_FAILED = new ErrorCode(1_010_200_010, "iWork 人力组织接口请求失败{}");
ErrorCode IWORK_ORG_REMOTE_FAILED = new ErrorCode(1_010_200_010, "iWork 人力组织接口请求失败{}");
ErrorCode IWORK_SEAL_REQUIRED_FIELD_MISSING = new ErrorCode(1_010_200_011, "缺少用印必填字段:{}");
}

View File

@@ -11,6 +11,7 @@ import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.infra.api.businessfile.BusinessFileApi;
import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileRespDTO;
import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileSaveReqDTO;
import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.infra.api.file.dto.FileCreateReqDTO;
@@ -18,9 +19,9 @@ import com.zt.plat.module.infra.api.file.dto.FileRespDTO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties;
import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService;
import com.zt.plat.framework.tenant.core.util.TenantUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okio.Buffer;
@@ -37,6 +38,7 @@ import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.*;
@@ -59,10 +61,6 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
private final FileApi fileApi;
private final BusinessFileApi businessFileApi;
private final Cache<SessionKey, IWorkSession> sessionCache = Caffeine.newBuilder()
.maximumSize(256)
.build();
private final Cache<String, RegistrationState> registrationCache = Caffeine.newBuilder()
.maximumSize(32)
.build();
@@ -104,7 +102,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
boolean force = forceRegistration || forceToken;
ClientKeyPair clientKeyPair = resolveClientKeyPair(appId, forceRegistration);
IWorkSession session = ensureSession(appId, clientKeyPair, operatorUserId, force);
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, force);
IWorkAuthTokenRespVO respVO = new IWorkAuthTokenRespVO();
respVO.setAppId(appId);
@@ -124,7 +122,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
String operatorUserId = resolveOperatorUserId(reqVO.getOperatorUserId());
ensureIdentifier(reqVO.getIdentifierKey(), reqVO.getIdentifierValue());
IWorkSession session = ensureSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
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);
@@ -137,7 +135,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
String appId = resolveAppId();
ClientKeyPair clientKeyPair = resolveClientKeyPair(appId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
String operatorUserId = resolveOperatorUserId(reqVO.getOperatorUserId());
IWorkSession session = ensureSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
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);
@@ -153,7 +151,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
if (!StringUtils.hasText(reqVO.getRequestId())) {
throw ServiceExceptionUtil.exception(IWORK_USER_IDENTIFIER_MISSING);
}
IWorkSession session = ensureSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken()));
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);
@@ -166,24 +164,30 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "回调请求不能为空");
}
String fileUrl = Optional.ofNullable(reqVO.getFileUrl()).map(String::trim).orElse("");
String businessIdStr = Optional.ofNullable(reqVO.getBusinessId()).map(String::trim).orElse("");
String businessCode = Optional.ofNullable(reqVO.getBusinessCode()).map(String::trim).orElse("");
if (!StringUtils.hasText(fileUrl)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "文件 URL 不能为空");
}
if (!StringUtils.hasText(businessIdStr)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务 ID 不能为空");
if (!StringUtils.hasText(businessCode)) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务编码不能为空");
}
Long businessId;
try {
businessId = Long.valueOf(businessIdStr);
} catch (NumberFormatException ex) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务 ID 必须是数字: " + businessIdStr);
BusinessFileRespDTO referenceBusinessFile = loadBusinessFileByBusinessCode(businessCode);
Long tenantId = referenceBusinessFile.getTenantId();
if (tenantId == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务附件缺少租户信息,无法创建回调附件: " + businessCode);
}
// 通过文件 API 创建文件
AtomicReference<Long> attachmentIdRef = new AtomicReference<>();
TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile)));
return attachmentIdRef.get();
}
private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) {
Long businessId = referenceBusinessFile.getBusinessId();
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
fileCreateReqDTO.setName(resolveFileName(reqVO.getFileName(), fileUrl));
fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl));
fileCreateReqDTO.setDirectory(null);
fileCreateReqDTO.setType(null);
fileCreateReqDTO.setContent(downloadFileBytes(fileUrl));
@@ -197,6 +201,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
BusinessFileSaveReqDTO businessReq = BusinessFileSaveReqDTO.builder()
.businessId(businessId)
.businessCode(referenceBusinessFile.getBusinessCode())
.fileId(fileId)
.fileName(fileResult.getData().getName())
.source("iwork")
@@ -209,6 +214,21 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return bizResult.getData();
}
private BusinessFileRespDTO loadBusinessFileByBusinessCode(String businessCode) {
CommonResult<BusinessFileRespDTO> businessResult = businessFileApi.getBusinessFileByBusinessCode(businessCode);
if (businessResult == null || !businessResult.isSuccess() || businessResult.getData() == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(),
"根据业务编码获取业务附件关联失败: " + Optional.ofNullable(businessResult)
.map(CommonResult::getMsg)
.orElse("未知错误"));
}
BusinessFileRespDTO businessFile = businessResult.getData();
if (businessFile.getBusinessId() == null) {
throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务编码未绑定业务 ID: " + businessCode);
}
return businessFile;
}
private byte[] downloadFileBytes(String fileUrl) {
OkHttpClient client = okHttpClient();
Request request = new Request.Builder().url(fileUrl).get().build();
@@ -308,26 +328,6 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
}
private IWorkSession ensureSession(String appId, ClientKeyPair clientKeyPair, String operatorUserId, boolean forceRefresh) {
SessionKey key = new SessionKey(appId, operatorUserId);
Instant now = Instant.now();
if (!forceRefresh) {
IWorkSession cached = sessionCache.getIfPresent(key);
if (cached != null && cached.isValid(now, properties.getToken().getRefreshAheadSeconds())) {
return cached;
}
}
synchronized (key.intern()) {
IWorkSession cached = sessionCache.getIfPresent(key);
if (!forceRefresh && cached != null && cached.isValid(now, properties.getToken().getRefreshAheadSeconds())) {
return cached;
}
IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, forceRefresh);
sessionCache.put(key, session);
return session;
}
}
private IWorkSession createSession(String appId, ClientKeyPair clientKeyPair, String operatorUserId, boolean forceRefreshRegistration) {
RegistrationState registration = obtainRegistration(appId, clientKeyPair, forceRefreshRegistration);
String encryptedSecret = encryptWithPublicKey(registration.secret(), registration.spk());
@@ -651,7 +651,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
return respVO;
}
JsonNode node = parseJson(responseBody, IWORK_REMOTE_REQUEST_FAILED);
respVO.setPayload(objectMapper.convertValue(node, MAP_TYPE));
respVO.setPayload(objectMapper.convertValue(node, IWorkOperationRespVO.Payload.class));
respVO.setSuccess(isSuccess(node));
respVO.setMessage(resolveMessage(node));
return respVO;
@@ -850,15 +850,26 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
}
private ServiceException buildRemoteException(ErrorCode errorCode, int statusCode, String responseBody) {
StringBuilder message = new StringBuilder(errorCode.getMsg());
String detail = buildRemoteErrorDetail(statusCode, responseBody);
if (!StringUtils.hasText(detail)) {
detail = "未知错误";
}
return ServiceExceptionUtil.exception(errorCode, detail);
}
private String buildRemoteErrorDetail(int statusCode, String responseBody) {
StringBuilder detail = new StringBuilder();
if (statusCode > 0) {
message.append("HTTP ").append(statusCode).append("");
detail.append("HTTP ").append(statusCode);
}
String reason = extractReadableReason(responseBody);
if (StringUtils.hasText(reason)) {
message.append("").append(reason);
if (detail.length() > 0) {
detail.append("");
}
detail.append(reason);
}
return ServiceExceptionUtil.exception0(errorCode.getCode(), message.toString());
return detail.toString();
}
private String extractReadableReason(String responseBody) {
@@ -981,46 +992,5 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService {
this.expiresAt = expiresAt;
this.spk = spk;
}
private boolean isValid(Instant now, long refreshAheadSeconds) {
Instant refreshThreshold = expiresAt.minusSeconds(Math.max(0L, refreshAheadSeconds));
return refreshThreshold.isAfter(now) && StringUtils.hasText(token) && StringUtils.hasText(encryptedUserId);
}
}
@ToString
private static final class SessionKey {
private final String appId;
private final String operatorUserId;
private SessionKey(String appId, String operatorUserId) {
this.appId = appId;
this.operatorUserId = operatorUserId;
}
private String cacheKey() {
return appId + "::" + operatorUserId;
}
private String intern() {
return cacheKey().intern();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SessionKey that)) {
return false;
}
return Objects.equals(appId, that.appId)
&& Objects.equals(operatorUserId, that.operatorUserId);
}
@Override
public int hashCode() {
return Objects.hash(appId, operatorUserId);
}
}
}

View File

@@ -435,6 +435,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
CommonStatusEnum status) {
UserSaveReqVO req = new UserSaveReqVO();
req.setUsername(username);
req.setWorkcode(trimToNull(source.getWorkcode()));
req.setNickname(limitLength(StrUtil.blankToDefault(source.getLastname(), username), 30));
req.setRemark(buildUserRemark(source));
if (deptId != null) {

View File

@@ -14,7 +14,9 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED;
@@ -86,6 +88,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query);
ensureIWorkSuccess("拉取分部", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrSubcompanyPageRespVO.Subcompany> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -103,6 +106,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query);
ensureIWorkSuccess("拉取部门", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrDepartmentPageRespVO.Department> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -120,6 +124,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkJobTitleQueryReqVO query = new IWorkJobTitleQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrJobTitlePageRespVO pageResp = orgRestService.listJobTitles(query);
ensureIWorkSuccess("拉取岗位", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrJobTitlePageRespVO.JobTitle> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -137,6 +142,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkUserQueryReqVO query = new IWorkUserQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query);
ensureIWorkSuccess("拉取人员", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrUserPageRespVO.User> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -181,6 +187,21 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
stat.incrementFailed(result.getFailed());
}
private void applyQueryConditions(IWorkOrgBaseQueryReqVO query, IWorkFullSyncReqVO reqVO) {
if (query == null || reqVO == null) {
return;
}
if (StrUtil.isBlank(reqVO.getId())) {
return;
}
Map<String, Object> params = query.getParams();
if (params == null) {
params = new HashMap<>();
query.setParams(params);
}
params.put("id", reqVO.getId());
}
private IWorkSyncProcessor.SyncOptions buildFullSyncOptions(IWorkFullSyncReqVO reqVO) {
boolean includeCanceled = Boolean.TRUE.equals(reqVO.getIncludeCanceled());
boolean allowUpdate = Boolean.TRUE.equals(reqVO.getAllowUpdate());

View File

@@ -72,16 +72,16 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
String username = StrUtil.trim(StrUtil.blankToDefault(userInfo.getLoginName(), userInfo.getUsername()));
if (StrUtil.isBlank(username)) {
log.error("E办OAuth2用户信息缺少 username 与 loginName无法匹配账号: {}", JSONUtil.toJsonStr(userInfo));
String workcode = StrUtil.trim(StrUtil.blankToDefault(userInfo.getLoginName(), userInfo.getUsername()));
if (StrUtil.isBlank(workcode)) {
log.error("E办OAuth2用户信息缺少工号(loginName),无法匹配账号: {}", JSONUtil.toJsonStr(userInfo));
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
AdminUserDO user = userService.getUserByUsername(username);
AdminUserDO user = userService.getUserByWorkcode(workcode);
if (user == null) {
createLoginLog(null, username, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS);
log.warn("E办OAuth2用户displayName未在系统中找到对应账号: {}", username);
createLoginLog(null, workcode, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS);
log.warn("E办OAuth2用户工号未在系统中找到对应账号: {}", workcode);
throw exception(AUTH_LOGIN_EBAN_USER_NOT_SYNC);
}
@@ -92,7 +92,7 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
EbanTokenInfo tokenInfo = userInfo.getTokenInfo();
if (tokenInfo == null || StrUtil.isBlank(tokenInfo.getAccessToken())) {
log.error("E办OAuth2回调缺少有效的token信息uid={}, username={}", userInfo.getUid(), username);
log.error("E办OAuth2回调缺少有效的token信息uid={}, username={}", userInfo.getUid(), userInfo.getUsername());
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
@@ -105,7 +105,7 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
tokenInfo.getExpiresIn(),
userInfo
);
log.info("成功保存E办tokenuserId={}, uid={}, username={}", user.getId(), userInfo.getUid(), username);
log.info("成功保存E办tokenuserId={}, uid={}, workcode={}", user.getId(), userInfo.getUid(), workcode);
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL, ebanAccessTokenDO);

View File

@@ -101,6 +101,14 @@ public interface AdminUserService {
*/
AdminUserDO getUserByUsername(String username);
/**
* 通过工号查询用户
*
* @param workcode 工号
* @return 用户对象信息
*/
AdminUserDO getUserByWorkcode(String workcode);
/**
* 通过手机号获取用户
*

View File

@@ -128,6 +128,7 @@ public class AdminUserServiceImpl implements AdminUserService {
if (user.getUserSource() == null) {
user.setUserSource(UserSourceEnum.EXTERNAL.getSource());
}
user.setWorkcode(normalizeWorkcode(createReqVO.getWorkcode()));
PasswordStrategyEnum passwordStrategy = determinePasswordStrategy(user.getUserSource());
user.setAvatar(normalizeAvatarValue(createReqVO.getAvatar()));
user.setPassword(encodePassword(createReqVO.getPassword(), passwordStrategy));
@@ -196,6 +197,9 @@ public class AdminUserServiceImpl implements AdminUserService {
if (StrUtil.isNotBlank(updateReqVO.getNickname())) {
updateObj.setNickname(updateReqVO.getNickname());
}
if (updateReqVO.getWorkcode() != null) {
updateObj.setWorkcode(normalizeWorkcode(updateReqVO.getWorkcode()));
}
if (StrUtil.isNotBlank(updateReqVO.getMobile())) {
updateObj.setMobile(updateReqVO.getMobile());
}
@@ -345,6 +349,19 @@ public class AdminUserServiceImpl implements AdminUserService {
return user;
}
@Override
public AdminUserDO getUserByWorkcode(String workcode) {
String normalized = normalizeWorkcode(workcode);
if (StrUtil.isBlank(normalized)) {
return null;
}
AdminUserDO user = userMapper.selectByWorkcode(normalized);
if (user != null) {
fillUserDeptInfo(Collections.singletonList(user));
}
return user;
}
@Override
public AdminUserDO getUserByMobile(String mobile) {
AdminUserDO user = userMapper.selectByMobile(mobile);
@@ -525,6 +542,13 @@ public class AdminUserServiceImpl implements AdminUserService {
return StrUtil.isBlank(avatarValue) ? null : avatarValue.trim();
}
private String normalizeWorkcode(String workcode) {
if (StrUtil.isBlank(workcode)) {
return null;
}
return workcode.trim();
}
private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
Set<Long> deptIds, Set<Long> postIds, boolean skipAssociationValidation,
boolean skipMobileValidation, boolean skipEmailValidation) {

View File

@@ -118,10 +118,9 @@ iwork:
void-workflow: /api/workflow/paService/doCancelRequest
token:
ttl-seconds: 3600
refresh-ahead-seconds: 60
client:
connect-timeout: 5s
response-timeout: 30s
connect-timeout: 60s
response-timeout: 60s
org:
token-seed: 456465
paths: