diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/constant/UserTypeEnum.java b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/constant/UserTypeEnum.java index 5c5a0d6a1..4164754ef 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/constant/UserTypeEnum.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/constant/UserTypeEnum.java @@ -15,6 +15,8 @@ public enum UserTypeEnum { ZZD_OPEN_GETTOKEN("10", "浙政钉开放平台-gettoken"), ZZD_OPEN_APP_USER("11", "浙政钉开放平台-dingtalk_app_user"), ZZD_SSO("12", "浙政钉单点-/zzdsso"), + SH_OPEN_USER("20", "诗画浦江-openapi用户详情"), + SH_SSO("21", "诗画单点-/shjnsso"), ; private final String type; private final String description; diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java index 179fa8dc9..59ebe2c50 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java @@ -183,9 +183,9 @@ public class ThirdPartyOAth2Cotroller { } /** - * 记录 /zzdsso 业务结果(成功发 token / 需短信 / 业务失败),与开放平台 SDK 调用日志分开查。 + * 记录单点业务结果(浙政钉 / 诗画),与开放平台/诗画 openapi 调用日志分开查。 */ - private void recordZzdSsoControllerLog(String requestUrl, String reqParam, R result, long startMs, String stage) { + private void recordSsoControllerLog(String apiNamePrefix, String requestUrl, String reqParam, R result, long startMs, String stage) { boolean ok = result != null && result.getCode() == 0; HashMap body = new HashMap<>(); body.put("stage", stage); @@ -196,7 +196,7 @@ public class ThirdPartyOAth2Cotroller { body.put("data", result.getData()); } thirdPartyOAuth2Service.recordSsoLog( - UserTypeEnum.ZZD_SSO.getDescription() + (ok ? "-成功" : "-失败"), + apiNamePrefix + (ok ? "-成功" : "-失败"), requestUrl, reqParam, JSON.toJSONString(body), @@ -205,6 +205,10 @@ public class ThirdPartyOAth2Cotroller { ok ? null : (result != null ? result.getMsg() : "unknown")); } + private void recordZzdSsoControllerLog(String requestUrl, String reqParam, R result, long startMs, String stage) { + recordSsoControllerLog(UserTypeEnum.ZZD_SSO.getDescription(), requestUrl, reqParam, result, startMs, stage); + } + /** * 浙政钉短信核验:校验验证码、写入绑定表、与本系统用户手机号+员工编码比对后返回 token。 */ @@ -320,50 +324,61 @@ public class ThirdPartyOAth2Cotroller { @Transactional @GlobalTransactional public R shjnsso(@RequestBody @Valid SHJNSSODTO shjnsso, HttpServletRequest httpServletRequest) { - //检查设备指纹 - ZZDUserDTO zzdUserDTO = thirdPartyOAuth2Service.shjnsso(shjnsso.getAccountId(), shjnsso.getSessionId()); - if (null == zzdUserDTO) { - throw new RuntimeException(""); - } - ZZDUserDTO.Ext ext = zzdUserDTO.getExt(); - String mobile = zzdUserDTO.getMobile(); - R> userByMobileOrEmployeeCode = remoteUserService.getUserByMobileOrEmployeeCode(null, mobile); - List sysUserList = userByMobileOrEmployeeCode.getData(); - if (!CollectionUtils.isEmpty(sysUserList)) { - SysUser sysUser = sysUserList.get(0); - //判断当前用户设备是否信任 - String md5 = DeviceFingerprintUtil.generate(httpServletRequest); - R userTrustedDeviceR = remoteUserTrustedDeviceService.getUserTrustedDeviceByIdAndMD5(sysUser.getPhone(), md5, SH_DEVICE_NAME); - UserTrustedDeviceEntity userTrustedDevice = userTrustedDeviceR.getData(); - if (userTrustedDevice == null || userTrustedDevice.getExpireTime().isBefore(LocalDateTime.now())) { - return R.failed(-1, "请先信任设备"); + long startMs = System.currentTimeMillis(); + String accountId = shjnsso != null ? shjnsso.getAccountId() : null; + String sessionId = shjnsso != null ? shjnsso.getSessionId() : null; + String reqParam = "accountId=" + StringUtils.defaultString(accountId) + + ",sessionId=" + StringUtils.defaultString(sessionId) + + ",startMs=" + startMs; + try { + ZZDUserDTO zzdUserDTO = thirdPartyOAuth2Service.shjnsso(accountId, sessionId); + if (null == zzdUserDTO) { + R failed = R.failed("诗画用户信息为空"); + recordSsoControllerLog(UserTypeEnum.SH_SSO.getDescription(), "/thirdPartyOAth2/shjnsso", + reqParam, failed, startMs, "emptyUser"); + return failed; } - //登录成功生成token - UserTokenDTO userTokenDTO = new UserTokenDTO(); - userTokenDTO.setSysUser(sysUser); - - return remoteTokenService.generateToken(userTokenDTO); - } else { - //自动注册 -// UserDTO userDTO = new UserDTO(); -//// userDTO.setDeptId(1865990541445263361L); -// userDTO.setName(zzdUserDTO.getUsername()); -// userDTO.setNickname(zzdUserDTO.getUsername()); -// userDTO.setSystemType("2"); -// userDTO.setPhone(mobile); -// userDTO.setCreateBy("诗画自动创建"); -// //赋予随机密码 -// userDTO.setPassword("pj*"+ UUID.randomUUID()); -// R booleanR = remoteUserService.registerUser(userDTO); -// R> userByMobile=remoteUserService.getUserByMobile(mobile); -// SysUser sysUser = userByMobile.getData().get(0); -// UserTokenDTO userTokenDTO = new UserTokenDTO(); -// userTokenDTO.setSysUser(sysUser); -//// //登录成功,根据新注册的用户信息返回token -// r=remoteTokenService.generateToken(userTokenDTO); - return R.failed("系统中没有当前用户信息,请联系管理员进行申请!"); + String mobile = zzdUserDTO.getMobile(); + reqParam = reqParam + ",mobile=" + StringUtils.defaultString(mobile) + + ",nickName=" + StringUtils.defaultString(zzdUserDTO.getNickName()); + R> userByMobileOrEmployeeCode = remoteUserService.getUserByMobileOrEmployeeCode(null, mobile); + List sysUserList = userByMobileOrEmployeeCode.getData(); + if (!CollectionUtils.isEmpty(sysUserList)) { + SysUser sysUser = sysUserList.get(0); + String md5 = DeviceFingerprintUtil.generate(httpServletRequest); + R userTrustedDeviceR = remoteUserTrustedDeviceService.getUserTrustedDeviceByIdAndMD5(sysUser.getPhone(), md5, SH_DEVICE_NAME); + UserTrustedDeviceEntity userTrustedDevice = userTrustedDeviceR.getData(); + if (userTrustedDevice == null || userTrustedDevice.getExpireTime().isBefore(LocalDateTime.now())) { + R failed = R.failed(-1, "请先信任设备"); + recordSsoControllerLog(UserTypeEnum.SH_SSO.getDescription(), "/thirdPartyOAth2/shjnsso", + reqParam + ",deviceMd5=" + md5, failed, startMs, "needTrustDevice"); + return failed; + } + UserTokenDTO userTokenDTO = new UserTokenDTO(); + userTokenDTO.setSysUser(sysUser); + R result = remoteTokenService.generateToken(userTokenDTO); + recordSsoControllerLog(UserTypeEnum.SH_SSO.getDescription(), "/thirdPartyOAth2/shjnsso", + reqParam + ",sysUserId=" + sysUser.getUserId(), result, startMs, "token"); + return result; + } + R failed = R.failed("系统中没有当前用户信息,请联系管理员进行申请!"); + recordSsoControllerLog(UserTypeEnum.SH_SSO.getDescription(), "/thirdPartyOAth2/shjnsso", + reqParam, failed, startMs, "sysUserNotFound"); + return failed; + } catch (ThirdPartyException e) { + thirdPartyOAuth2Service.recordSsoLog(UserTypeEnum.SH_SSO.getDescription() + "-失败", + "/thirdPartyOAth2/shjnsso", reqParam, + "costMs=" + (System.currentTimeMillis() - startMs), + "FAIL", e.getCode(), e.getMessage()); + return R.failed(e.getMessage()); + } catch (Exception e) { + log.error("[shjnsso] 未预期异常", e); + thirdPartyOAuth2Service.recordSsoLog(UserTypeEnum.SH_SSO.getDescription() + "-失败", + "/thirdPartyOAth2/shjnsso", reqParam, + "costMs=" + (System.currentTimeMillis() - startMs), + "FAIL", "SH_SSO_UNEXPECTED", e.getMessage()); + return R.failed("诗画单点登录失败"); } - } @Inner(value = false) diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyLogService.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyLogService.java index abeb1b631..d5862564d 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyLogService.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyLogService.java @@ -5,8 +5,11 @@ import com.bsd.pjcloud.thirdparty.entity.ThirdPartyLogEntity; public interface ThirdPartyLogService extends IService { - - - void saveLog(ThirdPartyLogEntity thirdPartyLogEntity); + + /** + * 浙政钉 / 诗画单点日志统一入口,写入 third_party_log。 + */ + void recordSsoLog(String apiName, String requestUrl, String requestParameter, + String responseBody, String httpStatus, String errorCode, String errorMsg); } \ No newline at end of file diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java index a5419267b..3ae28a111 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyOAuth2Service.java @@ -406,34 +406,11 @@ public class ThirdPartyOAuth2Service { } /** - * 写入单点日志表 third_party_log(异步),成功/失败都记;后台分析用,不做脱敏。 + * 写入单点日志表 third_party_log,成功/失败都记;后台「单点日志」菜单查看,不做脱敏。 */ public void recordSsoLog(String apiName, String requestUrl, String requestParameter, String responseBody, String httpStatus, String errorCode, String errorMsg) { - try { - ThirdPartyLogEntity entity = new ThirdPartyLogEntity(); - entity.setApiName(apiName); - entity.setRequestUrl(requestUrl); - entity.setRequestParameter(truncate(requestParameter, 8000)); - entity.setResponseBody(truncate(responseBody, 16000)); - entity.setHttpStatus(httpStatus); - entity.setErrorCode(errorCode); - entity.setErrorMsg(truncate(errorMsg, 2000)); - entity.setCreateBy("zzd-openapi"); - thirdPartyLogService.saveLog(entity); - } catch (Exception e) { - log.warn("写入单点日志失败 apiName={}", apiName, e); - } - } - - private static String truncate(String s, int max) { - if (s == null) { - return null; - } - if (s.length() <= max) { - return s; - } - return s.substring(0, max) + "...(truncated)"; + thirdPartyLogService.recordSsoLog(apiName, requestUrl, requestParameter, responseBody, httpStatus, errorCode, errorMsg); } /** @@ -868,17 +845,31 @@ public class ThirdPartyOAuth2Service { } /** - * 诗画浦江单点:直连诗画开放接口获取用户,不再经对方 /login/shpj 中转 + * 诗画浦江单点:直连诗画开放接口获取用户,不再经对方 /login/shpj 中转。 + * HTTP 原始请求/响应由 {@link com.bsd.pjcloud.thirdparty.shpj.ShpjUserClient} 写入单点日志。 */ public ZZDUserDTO shjnsso(String accountId, String sessionId) { + String reqLog = "accountId=" + accountId + ",sessionId=" + sessionId + ",time=" + System.currentTimeMillis(); ShpjOpenapiUser openapiUser = shpjUserClient.getUserInfo(accountId, sessionId); if (openapiUser == null || openapiUser.getIsLogin() != 1) { + recordSsoLog(UserTypeEnum.SH_OPEN_USER.getDescription() + "-失败", + "/openapi/v2/{accountId}/detail", reqLog, JSON.toJSONString(openapiUser), + "FAIL", "SHPJ_NOT_LOGIN", "请先登录诗画客户端"); throw new ThirdPartyException("请先登录诗画客户端", "SHPJ_NOT_LOGIN"); } if (!StringUtils.hasText(openapiUser.getMobile())) { + recordSsoLog(UserTypeEnum.SH_OPEN_USER.getDescription() + "-失败", + "/openapi/v2/{accountId}/detail", reqLog, JSON.toJSONString(openapiUser), + "FAIL", "SHPJ_MOBILE_EMPTY", "诗画用户手机号为空"); throw new ThirdPartyException("诗画用户手机号为空", "SHPJ_MOBILE_EMPTY"); } - return mapShpjOpenapiUserToZzdDto(openapiUser); + ZZDUserDTO dto = mapShpjOpenapiUserToZzdDto(openapiUser); + com.alibaba.fastjson.JSONObject okBody = new com.alibaba.fastjson.JSONObject(); + okBody.put("mappedUser", JSON.toJSON(dto)); + recordSsoLog(UserTypeEnum.SH_SSO.getDescription() + "-换用户成功", + "/thirdPartyOAth2/shjnsso", reqLog, okBody.toJSONString(), + "OK", "0", null); + return dto; } private static ZZDUserDTO mapShpjOpenapiUserToZzdDto(ShpjOpenapiUser user) { diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyLogServiceImpl.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyLogServiceImpl.java index 9a11fdabb..384faf275 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyLogServiceImpl.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyLogServiceImpl.java @@ -1,26 +1,88 @@ package com.bsd.pjcloud.thirdparty.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bsd.pjcloud.common.core.constant.CommonConstants; +import com.bsd.pjcloud.common.data.tenant.TenantContextHolder; import com.bsd.pjcloud.thirdparty.entity.ThirdPartyLogEntity; import com.bsd.pjcloud.thirdparty.mapper.ThirdPartyLogMapper; import com.bsd.pjcloud.thirdparty.service.ThirdPartyLogService; -import org.springframework.scheduling.annotation.Async; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + /** * 单点日志记录 * * @author zjf * @date 2025-03-31 13:48:37 */ +@Slf4j @Service public class ThirdPartyLogServiceImpl extends ServiceImpl implements ThirdPartyLogService { + /** + * 同步写入(REQUIRES_NEW)。不再 @Async:异步线程常丢租户上下文,导致 tenant_id 为空, + * 后台按租户查询时看不到日志。 + */ @Override - @Async @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLog(ThirdPartyLogEntity thirdPartyLogEntity) { - baseMapper.insert(thirdPartyLogEntity); + Long prevTenant = TenantContextHolder.getTenantId(); + try { + if (thirdPartyLogEntity.getTenantId() == null) { + thirdPartyLogEntity.setTenantId(prevTenant != null ? prevTenant : CommonConstants.TENANT_ID_1); + } + if (thirdPartyLogEntity.getCreateTime() == null) { + thirdPartyLogEntity.setCreateTime(LocalDateTime.now()); + } + if (thirdPartyLogEntity.getDeleted() == null) { + thirdPartyLogEntity.setDeleted(Boolean.FALSE); + } + TenantContextHolder.setTenantId(thirdPartyLogEntity.getTenantId()); + baseMapper.insert(thirdPartyLogEntity); + } catch (Exception e) { + log.error("写入 third_party_log 失败 apiName={}", thirdPartyLogEntity.getApiName(), e); + throw e; + } finally { + if (prevTenant != null) { + TenantContextHolder.setTenantId(prevTenant); + } else { + TenantContextHolder.clear(); + } + } + } + + @Override + public void recordSsoLog(String apiName, String requestUrl, String requestParameter, + String responseBody, String httpStatus, String errorCode, String errorMsg) { + try { + ThirdPartyLogEntity entity = new ThirdPartyLogEntity(); + entity.setApiName(apiName); + entity.setRequestUrl(requestUrl); + entity.setRequestParameter(truncate(requestParameter, 8000)); + entity.setResponseBody(truncate(responseBody, 16000)); + entity.setHttpStatus(httpStatus); + entity.setErrorCode(errorCode); + entity.setErrorMsg(truncate(errorMsg, 2000)); + entity.setCreateBy("sso-log"); + entity.setCreateTime(LocalDateTime.now()); + entity.setDeleted(Boolean.FALSE); + entity.setTenantId(CommonConstants.TENANT_ID_1); + saveLog(entity); + } catch (Exception e) { + log.warn("写入单点日志失败 apiName={}", apiName, e); + } + } + + private static String truncate(String s, int max) { + if (s == null) { + return null; + } + if (s.length() <= max) { + return s; + } + return s.substring(0, max) + "...(truncated)"; } } \ No newline at end of file diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/shpj/ShpjUserClient.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/shpj/ShpjUserClient.java index fcf7aa955..10e5a7d13 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/shpj/ShpjUserClient.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/shpj/ShpjUserClient.java @@ -2,7 +2,9 @@ package com.bsd.pjcloud.thirdparty.shpj; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; +import com.bsd.pjcloud.thirdparty.constant.UserTypeEnum; import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException; +import com.bsd.pjcloud.thirdparty.service.ThirdPartyLogService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,6 +26,8 @@ public class ShpjUserClient { private final ObjectMapper objectMapper; + private final ThirdPartyLogService thirdPartyLogService; + /** * 按 accountId + sessionId 查询诗画用户详情 */ @@ -55,6 +59,10 @@ public class ShpjUserClient { String apiUrl = "/openapi/v2/" + accountId + "/detail"; String fullUrl = properties.getBaseUrl() + apiUrl; + String reqLog = "accountId=" + accountId + ",sessionId=" + sessionId + + ",connectNo=" + properties.getConnectNo() + + ",appkey=" + properties.getAppKey() + + ",url=" + fullUrl; log.info("调用诗画浦江接口[{}]开始, accountId={}, sessionId={}", apiUrl, accountId, sessionId); try (HttpResponse httpResponse = HttpRequest.get(fullUrl) @@ -64,6 +72,9 @@ public class ShpjUserClient { String body = httpResponse.body(); if (!httpResponse.isOk()) { log.error("诗画浦江接口[{}]HTTP错误:{} - {}", apiUrl, httpResponse.getStatus(), body); + thirdPartyLogService.recordSsoLog(UserTypeEnum.SH_OPEN_USER.getDescription() + "-失败", + apiUrl, reqLog, body, String.valueOf(httpResponse.getStatus()), + "SHPJ_HTTP", "诗画浦江接口HTTP错误"); throw new ThirdPartyException("诗画浦江接口调用失败", "SHPJ_HTTP"); } log.info("诗画浦江接口[{}]完成, 返回:{}", apiUrl, body); @@ -72,13 +83,20 @@ public class ShpjUserClient { objectMapper.getTypeFactory().constructParametricType(ShpjApiWrapper.class, ShpjOpenapiUser.class)); if (!resp.isSuccessful()) { log.error("诗画浦江接口[{}]业务失败: [{}-{}]", apiUrl, resp.getCode(), resp.getMessage()); + thirdPartyLogService.recordSsoLog(UserTypeEnum.SH_OPEN_USER.getDescription() + "-失败", + apiUrl, reqLog, body, "OK", + "SHPJ_BIZ_" + resp.getCode(), resp.getMessage()); throw new ThirdPartyException("诗画浦江接口调用失败:" + resp.getMessage(), "SHPJ_BIZ_" + resp.getCode()); } + thirdPartyLogService.recordSsoLog(UserTypeEnum.SH_OPEN_USER.getDescription() + "-成功", + apiUrl, reqLog, body, "OK", "0", null); return resp.getData(); } catch (ThirdPartyException e) { throw e; } catch (Exception e) { log.error("诗画浦江接口[{}]异常", apiUrl, e); + thirdPartyLogService.recordSsoLog(UserTypeEnum.SH_OPEN_USER.getDescription() + "-失败", + apiUrl, reqLog, null, "FAIL", "SHPJ_ERROR", e.getMessage()); throw new ThirdPartyException("诗画浦江服务暂时不可用", "SHPJ_ERROR"); } }