单点日志,菜单

This commit is contained in:
zjf committed 2026-08-10 00:07:21 +08:00
1 parent dc80a20b71
commit 60446146f9
4 files changed
+202 -41

No files matched your search

@@ -57,11 +57,8 @@ public class AppMenuOpenController {
if (!isValidAppCredentials(appId, appSecret)) {
return R.failed("无效的应用ID或密钥");
}
//todo 由于浙政钉还要irs发布太麻烦,先写死吧,下次更新在由前端控制
R<List<AppMenuEntity>> listR = remoteAppMenuService.innerAppList(typeEnum, AppMenuPlatformEnum.ZZD);
// List<AppMenuEntity> list = listR.getData().stream().filter(appMenu -> appMenu.getZzdVisible() != null).toList();
// listR.setData(list);
return listR;
// 按前端 platformEnum 区分:WECHAT(10) / ZZD(30) / SHPJ(40),勿写死浙政钉
return remoteAppMenuService.innerAppList(typeEnum, platformEnum);
}
private boolean isValidAppCredentials(String appId, String appSecret) {
@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 出库入口
* 出库入口 / 单点日志 apiName
*/
@AllArgsConstructor
@Getter
@@ -12,6 +12,9 @@ public enum UserTypeEnum {
ZZD("1", "浙政钉用户登录失败"),
SH("2", "诗画用户登录失败"),
ZZD_OPEN_GETTOKEN("10", "浙政钉开放平台-gettoken"),
ZZD_OPEN_APP_USER("11", "浙政钉开放平台-dingtalk_app_user"),
ZZD_SSO("12", "浙政钉单点-/zzdsso"),
;
private final String type;
private final String description;
@@ -20,6 +20,7 @@ import com.bsd.pjcloud.thirdparty.service.SmsService;
import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import com.bsd.pjcloud.thirdparty.vo.ZzdJsapiConfigVO;
import com.bsd.pjcloud.thirdparty.constant.ThirdPartyZzdBizCodes;
import com.bsd.pjcloud.thirdparty.constant.UserTypeEnum;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyOAuth2Service;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyUserService;
@@ -104,44 +105,104 @@ public class ThirdPartyOAth2Cotroller {
@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public R zzdsso(@RequestBody UrlTokenDTO urlTokenDTO) {
long startMs = System.currentTimeMillis();
String authCode = urlTokenDTO != null ? urlTokenDTO.getAuthCode() : null;
String reqParam = "authCode=" + StringUtils.defaultString(authCode)
+ ",startMs=" + startMs;
if (StringUtils.isBlank(authCode)) {
thirdPartyOAuth2Service.recordSsoLog(UserTypeEnum.ZZD_SSO.getDescription() + "-失败",
"/thirdPartyOAth2/zzdsso", reqParam, null,
"FAIL", "PARAM", "请传入浙政钉 authCode(H5 免登临时码)");
return R.failed("请传入浙政钉 authCode(H5 免登临时码)");
}
ZZDUserDTO zzdUserDTO = thirdPartyOAuth2Service.zzdssoByAuthCode(authCode.trim());
log.warn("[zzdsso][ZZDUserDTO_JSON] 浙政钉开放平台映射后的对象,可整段复制做本地写死调试(含姓名手机号等敏感信息,调试完建议关日志或删此行) body={}", JSON.toJSONString(zzdUserDTO));
ZZDUserDTO.Ext ext = zzdUserDTO.getExt();
String employeeCode = ext != null ? ext.getEmployeeCode() : null;
String zzdOpenId = ext != null ? ext.getOpenid() : null;
String mobile = zzdUserDTO.getMobile();
try {
ZZDUserDTO zzdUserDTO = thirdPartyOAuth2Service.zzdssoByAuthCode(authCode.trim());
log.warn("[zzdsso][ZZDUserDTO_JSON] 浙政钉开放平台映射后的对象,可整段复制做本地写死调试(含姓名手机号等敏感信息,调试完建议关日志或删此行) body={}", JSON.toJSONString(zzdUserDTO));
ZZDUserDTO.Ext ext = zzdUserDTO.getExt();
String employeeCode = ext != null ? ext.getEmployeeCode() : null;
String zzdOpenId = ext != null ? ext.getOpenid() : null;
String mobile = zzdUserDTO.getMobile();
reqParam = reqParam + ",employeeCode=" + StringUtils.defaultString(employeeCode)
+ ",openid=" + StringUtils.defaultString(zzdOpenId)
+ ",mobile=" + StringUtils.defaultString(mobile)
+ ",nickName=" + StringUtils.defaultString(zzdUserDTO.getNickName());
if (thirdPartyZzdSmsBindService.isEnabled()) {
var boundOpt = thirdPartyZzdSmsBindService.findBound(employeeCode, zzdOpenId);
if (boundOpt.isPresent()) {
ThirdPartyZzdPhoneBindEntity bound = boundOpt.get();
return buildZzdSsoTokenResult(bound.getEmployeeCode(), bound.getVerifiedPhone());
}
try {
ZzdUserSnapshot snap = ThirdPartyZzdSmsBindService.snapshotOf(zzdUserDTO);
if (StringUtils.isBlank(snap.getEmployeeCode()) && StringUtils.isBlank(snap.getOpenid())) {
return R.failed("浙政钉未返回员工编码或 openid,无法绑定登录");
if (thirdPartyZzdSmsBindService.isEnabled()) {
var boundOpt = thirdPartyZzdSmsBindService.findBound(employeeCode, zzdOpenId);
if (boundOpt.isPresent()) {
ThirdPartyZzdPhoneBindEntity bound = boundOpt.get();
R result = buildZzdSsoTokenResult(bound.getEmployeeCode(), bound.getVerifiedPhone());
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso", reqParam, result, startMs, "boundUser");
return result;
}
if (StringUtils.isBlank(snap.getEmployeeCode())) {
return R.failed("浙政钉未返回员工编码,无法完成首次手机号绑定,请在开放平台核对应用权限或联系管理员。");
try {
ZzdUserSnapshot snap = ThirdPartyZzdSmsBindService.snapshotOf(zzdUserDTO);
if (StringUtils.isBlank(snap.getEmployeeCode()) && StringUtils.isBlank(snap.getOpenid())) {
R failed = R.failed("浙政钉未返回员工编码或 openid,无法绑定登录");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso", reqParam, failed, startMs, "needBind");
return failed;
}
if (StringUtils.isBlank(snap.getEmployeeCode())) {
R failed = R.failed("浙政钉未返回员工编码,无法完成首次手机号绑定,请在开放平台核对应用权限或联系管理员。");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso", reqParam, failed, startMs, "needBind");
return failed;
}
String bindSessionId = thirdPartyZzdSmsBindService.createBindSession(snap);
HashMap<String, Object> need = new HashMap<>();
need.put("needSmsVerify", Boolean.TRUE);
need.put("bindSessionId", bindSessionId);
need.put("displayName", StringUtils.defaultIfBlank(zzdUserDTO.getNickName(), zzdUserDTO.getUsername()));
R ok = R.ok(need);
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso", reqParam, ok, startMs, "needSmsVerify");
return ok;
} catch (JsonProcessingException e) {
R failed = R.failed("创建绑定会话失败");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso", reqParam, failed, startMs, "needBind");
return failed;
}
String bindSessionId = thirdPartyZzdSmsBindService.createBindSession(snap);
HashMap<String, Object> need = new HashMap<>();
need.put("needSmsVerify", Boolean.TRUE);
need.put("bindSessionId", bindSessionId);
need.put("displayName", StringUtils.defaultIfBlank(zzdUserDTO.getNickName(), zzdUserDTO.getUsername()));
return R.ok(need);
} catch (JsonProcessingException e) {
return R.failed("创建绑定会话失败");
}
R result = buildZzdSsoTokenResult(employeeCode, mobile);
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso", reqParam, result, startMs, "directToken");
return result;
} catch (ThirdPartyException e) {
thirdPartyOAuth2Service.recordSsoLog(UserTypeEnum.ZZD_SSO.getDescription() + "-失败",
"/thirdPartyOAth2/zzdsso", reqParam,
"costMs=" + (System.currentTimeMillis() - startMs),
"FAIL", e.getCode(), e.getMessage());
return R.failed(e.getMessage());
} catch (Exception e) {
log.error("[zzdsso] 未预期异常", e);
thirdPartyOAuth2Service.recordSsoLog(UserTypeEnum.ZZD_SSO.getDescription() + "-失败",
"/thirdPartyOAth2/zzdsso", reqParam,
"costMs=" + (System.currentTimeMillis() - startMs),
"FAIL", "ZZD_SSO_UNEXPECTED", e.getMessage());
return R.failed("浙政钉单点登录失败");
}
}
return buildZzdSsoTokenResult(employeeCode, mobile);
/**
* 记录 /zzdsso 业务结果(成功发 token / 需短信 / 业务失败),与开放平台 SDK 调用日志分开查。
*/
private void recordZzdSsoControllerLog(String requestUrl, String reqParam, R result, long startMs, String stage) {
boolean ok = result != null && result.getCode() == 0;
HashMap<String, Object> body = new HashMap<>();
body.put("stage", stage);
body.put("costMs", System.currentTimeMillis() - startMs);
body.put("code", result != null ? result.getCode() : null);
body.put("msg", result != null ? result.getMsg() : null);
if (result != null && result.getData() != null) {
body.put("data", result.getData());
}
thirdPartyOAuth2Service.recordSsoLog(
UserTypeEnum.ZZD_SSO.getDescription() + (ok ? "-成功" : "-失败"),
requestUrl,
reqParam,
JSON.toJSONString(body),
ok ? "OK" : "FAIL",
ok ? "0" : String.valueOf(result != null ? result.getCode() : -1),
ok ? null : (result != null ? result.getMsg() : "unknown"));
}
/**
@@ -153,35 +214,53 @@ public class ThirdPartyOAth2Cotroller {
@Inner(value = false)
@Transactional(rollbackFor = Exception.class)
public R confirmZzdPhoneBind(@RequestBody ZzdPhoneConfirmDTO dto) {
long startMs = System.currentTimeMillis();
String reqParam = "bindSessionId=" + (dto != null ? StringUtils.defaultString(dto.getBindSessionId()) : "")
+ ",mobilePhone=" + (dto != null ? StringUtils.defaultString(dto.getMobilePhone()) : "")
+ ",smsCode=" + (dto != null ? StringUtils.defaultString(dto.getSmsCode()) : "")
+ ",startMs=" + startMs;
if (dto == null || StringUtils.isAnyBlank(dto.getBindSessionId(), dto.getMobilePhone(), dto.getSmsCode())) {
return R.failed("参数不完整");
R failed = R.failed("参数不完整");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
}
try {
ZzdUserSnapshot snap = thirdPartyZzdSmsBindService.getBindSession(dto.getBindSessionId());
if (snap == null) {
return R.failed("绑定会话已过期,请重新登录");
R failed = R.failed("绑定会话已过期,请重新登录");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
}
if (StringUtils.isBlank(snap.getEmployeeCode())) {
return R.failed("会话数据异常");
R failed = R.failed("会话数据异常");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
}
reqParam = reqParam + ",employeeCode=" + snap.getEmployeeCode();
String redisKey = CacheConstants.SMS_CODE_KEY + dto.getMobilePhone();
String storedCode = (String) redisTemplate.opsForValue().get(redisKey);
if (!dto.getSmsCode().equals(storedCode)) {
return R.failed("验证码错误");
R failed = R.failed("验证码错误");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
}
redisTemplate.delete(redisKey);
R<List<SysUser>> byPhone = remoteUserService.getUserByMobile(dto.getMobilePhone());
List<SysUser> users = byPhone.getData();
if (CollectionUtils.isEmpty(users)) {
return R.failed(ThirdPartyZzdBizCodes.SYS_USER_NOT_FOUND_FOR_ZZD_BIND,
R failed = R.failed(ThirdPartyZzdBizCodes.SYS_USER_NOT_FOUND_FOR_ZZD_BIND,
"本系统暂无该手机号对应的用户,请联系管理员申请开通账号后再完成绑定。");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
}
SysUser sysUser = users.get(0);
if (StringUtils.isNotBlank(sysUser.getDingtalkEmployeeCode())
&& StringUtils.isNotBlank(snap.getEmployeeCode())
&& !sysUser.getDingtalkEmployeeCode().equals(snap.getEmployeeCode())) {
return R.failed("浙政钉账号与本系统用户不一致");
R failed = R.failed("浙政钉账号与本系统用户不一致");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
}
thirdPartyZzdSmsBindService.recordVerifiedBind(snap, dto.getMobilePhone());
@@ -193,9 +272,20 @@ public class ThirdPartyOAth2Cotroller {
HashMap<String, Object> out = new HashMap<>();
out.put("refreshToken", r.getData().get("refreshToken"));
out.put("accessToken", r.getData().get("accessToken"));
return R.ok(out);
R ok = R.ok(out);
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, ok, startMs, "confirmPhone");
return ok;
} catch (JsonProcessingException e) {
return R.failed("保存绑定信息失败");
R failed = R.failed("保存绑定信息失败");
recordZzdSsoControllerLog("/thirdPartyOAth2/zzdsso/confirmPhone", reqParam, failed, startMs, "confirmPhone");
return failed;
} catch (Exception e) {
log.error("[zzdsso/confirmPhone] 未预期异常", e);
thirdPartyOAuth2Service.recordSsoLog(UserTypeEnum.ZZD_SSO.getDescription() + "-失败",
"/thirdPartyOAth2/zzdsso/confirmPhone", reqParam,
"costMs=" + (System.currentTimeMillis() - startMs),
"FAIL", "ZZD_CONFIRM_UNEXPECTED", e.getMessage());
return R.failed("短信核验失败");
}
}
@@ -395,9 +395,47 @@ public class ThirdPartyOAuth2Service {
if (zzdOpenPlatform.isEnabled() && zzdOpenPlatform.isAppCredentialConfigured()) {
return getZzdUserByOpenPlatformAuthCode(authCode);
}
recordSsoLog(UserTypeEnum.ZZD_SSO.getDescription() + "-失败",
"/thirdPartyOAth2/zzdsso",
"authCode=" + (authCode != null ? authCode : ""),
null,
"CONFIG",
"ZZD_OPEN_CONFIG",
"浙政钉单点需配置 zzd.open.enabled 与 app-key/app-secret");
throw new ThirdPartyException("浙政钉单点需配置 zzd.open.enabled 与 app-key/app-secret(开放平台企业内应用)", "ZZD_OPEN_CONFIG");
}
/**
* 写入单点日志表 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)";
}
/**
* 开放平台:官方 SDK {@code IntelligentGetClient /gettoken.json},解析 content.data 中 accessToken
*/
@@ -411,6 +449,11 @@ public class ThirdPartyOAuth2Service {
if (zzdOpenCachedAppAccessToken != null && now < zzdOpenCachedAppAccessTokenExpireMs - 60_000L) {
return zzdOpenCachedAppAccessToken;
}
String reqLog = "appkey=" + zzdOpenPlatform.getAppKey()
+ ",appsecret=" + zzdOpenPlatform.getAppSecret()
+ ",domain=" + zzdOpenPlatform.getDomain()
+ ",time=" + System.currentTimeMillis();
String lastRespBody = null;
try {
ExecutableClient ec = zzdExecutableClientHolder.client();
IntelligentGetClient getClient = ec.newIntelligentGetClient("/gettoken.json");
@@ -420,6 +463,7 @@ public class ThirdPartyOAuth2Service {
OapiGettokenJsonResponse resp = getClient.get(req);
zzdAssertSdkTopSuccess(resp, "gettoken");
JSONObject data = zzdSdkSpResultData(resp.getContent(), "gettoken");
lastRespBody = data != null ? data.toJSONString() : null;
String accessToken = data.getString("accessToken");
if (!StringUtils.hasText(accessToken)) {
throw new ThirdPartyException("浙政钉 gettoken 未返回 accessToken", "ZZD_OPEN_TOKEN_BAD");
@@ -434,11 +478,24 @@ public class ThirdPartyOAuth2Service {
}
zzdOpenCachedAppAccessToken = accessToken;
zzdOpenCachedAppAccessTokenExpireMs = System.currentTimeMillis() + expiresIn * 1000L;
JSONObject okBody = new JSONObject();
okBody.put("accessToken", accessToken);
okBody.put("expiresIn", expiresIn);
okBody.put("cachedExpireAt", zzdOpenCachedAppAccessTokenExpireMs);
recordSsoLog(UserTypeEnum.ZZD_OPEN_GETTOKEN.getDescription() + "-成功",
"/gettoken.json", reqLog, okBody.toJSONString(),
"OK", "0", null);
return accessToken;
} catch (ThirdPartyException e) {
recordSsoLog(UserTypeEnum.ZZD_OPEN_GETTOKEN.getDescription() + "-失败",
"/gettoken.json", reqLog, lastRespBody,
"FAIL", e.getCode(), e.getMessage());
throw e;
} catch (Exception e) {
log.error("浙政钉 gettoken SDK 调用异常", e);
recordSsoLog(UserTypeEnum.ZZD_OPEN_GETTOKEN.getDescription() + "-失败",
"/gettoken.json", reqLog, lastRespBody,
"FAIL", "ZZD_OPEN_TOKEN_SDK", e.getMessage());
throw new ThirdPartyException("浙政钉获取应用 access_token 失败", "ZZD_OPEN_TOKEN_SDK");
}
}
@@ -448,8 +505,10 @@ public class ThirdPartyOAuth2Service {
* 官方 SDK {@code IntelligentPostClient /rpc/oauth2/dingtalk_app_user.json}
*/
private ZZDUserDTO getZzdUserByOpenPlatformAuthCode(String authCode) {
String reqLog = "auth_code=" + authCode + ",time=" + System.currentTimeMillis();
try {
String appToken = getZzdOpenAppAccessToken();
reqLog = reqLog + ",access_token=" + appToken;
ExecutableClient ec = zzdExecutableClientHolder.client();
IntelligentPostClient postClient = ec.newIntelligentPostClient("/rpc/oauth2/dingtalk_app_user.json");
OapiRpcOauth2DingtalkAppUserJsonRequest req = new OapiRpcOauth2DingtalkAppUserJsonRequest();
@@ -469,11 +528,23 @@ public class ThirdPartyOAuth2Service {
&& !zzdOpenPlatform.getTenantId().equals(dto.getExt().getRealmId())) {
log.warn("浙政钉用户 realmId={} 与配置 zzd.open.tenant-id={} 不一致", dto.getExt().getRealmId(), zzdOpenPlatform.getTenantId());
}
JSONObject successBody = new JSONObject();
successBody.put("rawContent", rawContent);
successBody.put("mappedUser", JSON.toJSON(dto));
recordSsoLog(UserTypeEnum.ZZD_OPEN_APP_USER.getDescription() + "-成功",
"/rpc/oauth2/dingtalk_app_user.json", reqLog, successBody.toJSONString(),
"OK", "0", null);
return dto;
} catch (ThirdPartyException e) {
recordSsoLog(UserTypeEnum.ZZD_OPEN_APP_USER.getDescription() + "-失败",
"/rpc/oauth2/dingtalk_app_user.json", reqLog, null,
"FAIL", e.getCode(), e.getMessage());
throw e;
} catch (Exception e) {
log.error("浙政钉 dingtalk_app_user SDK 调用异常", e);
recordSsoLog(UserTypeEnum.ZZD_OPEN_APP_USER.getDescription() + "-失败",
"/rpc/oauth2/dingtalk_app_user.json", reqLog, null,
"FAIL", "ZZD_OPEN_USER_SDK", e.getMessage());
throw new ThirdPartyException("浙政钉获取用户失败", "ZZD_OPEN_USER_SDK");
}
}