This commit is contained in:
dzc committed 2026-08-04 10:26:59 +08:00
commit 6b1b4a03c4
12 files changed
+264 -131

No files matched your search

@@ -4,28 +4,29 @@ server:
basedir: logs
error:
include-message: always
#db:
# num: 1
# user: ${MYSQL_USER:root}
# password: ${MYSQL_PWD:root}
# url:
# 0: jdbc:mysql://${MYSQL_HOST:pjcloud-mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:pjcloudx_config}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
# pool:
# config:
# connectionTimeout: 30000
# validationTimeout: 10000
db:
num: 1
user: root
password: root
user: ${MYSQL_USER:root}
password: ${MYSQL_PWD:root}
url:
0: jdbc:mysql://127.0.0.1:3306/pjcloudx_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
0: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DB:pjcloudx_configzax}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
pool:
config:
connectionTimeout: 30000
validationTimeout: 10000
#db:
# num: 1
# user: root
# password: root
# url:
# 0: jdbc:mysql://127.0.0.1:3306/pjcloudx_config_zax?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
## 0: jdbc:mysql://127.0.0.1:3306/pjcloudx_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
# pool:
# config:
# connectionTimeout: 30000
# validationTimeout: 10000
nacos:
core:
auth:
@@ -29,12 +29,7 @@ public class OAuth2ClientConfig {
private String zzdTokenUrl;
/**
* 诗画江南的token接口
*/
private String shjnTokenUrl;
/**
* 浙政钉跟诗画的获取用户信息的url
* 浙政钉跟诗画的获取用户信息的url(诗画已直连,保留兼容)
*/
private String zzdShUserInfoUrl;
@@ -10,6 +10,8 @@ import com.bsd.pjcloud.thirdparty.entity.ThirdPartyDeptEntity;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyLogEntity;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity;
import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import com.bsd.pjcloud.thirdparty.shpj.ShpjOpenapiUser;
import com.bsd.pjcloud.thirdparty.shpj.ShpjUserClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -19,7 +21,6 @@ import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
@@ -27,7 +28,6 @@ import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -55,6 +55,8 @@ public class ThirdPartyOAuth2Service {
private final ThirdPartyLogService thirdPartyLogService;
private final ShpjUserClient shpjUserClient;
public TokenDTO getAccessToken() {
TokenDTO cachedToken = tokenCache.get("access_token");
@@ -447,113 +449,29 @@ public class ThirdPartyOAuth2Service {
/**
* 诗画江南的请求token
* @param accountId
* @param sessionId
* @return
*/
public String getSHJNToken(String accountId,String sessionId){
// 构建请求URL
String url = baseUrl + config.getShjnTokenUrl()+"?accountId=" + accountId + "&sessionId=" + sessionId;
ResponseEntity<ApiResponse<String>> response = restTemplate.exchange(
url, HttpMethod.POST, null, new ParameterizedTypeReference<ApiResponse<String>>() {}
);
return response.getBody().getData();
}
public String getUserDetails(String accountId, String sessionId, String connectNo, String appkey, String appsecret) {
String timestamp = System.currentTimeMillis() + "";
String noncestr = timestamp;
// 构造请求参数
Map<String, String> params = new HashMap<>();
params.put("connectNo", connectNo);
params.put("sessionId", sessionId);
// 签名生成
String signature = generateSignature(timestamp, appkey, appsecret, noncestr, params);
params.put("signature", signature);
params.put("timestamp", timestamp);
params.put("appkey", appkey);
params.put("noncestr", noncestr);
// 构造 URL
String url = String.format("https://vcms.tmuyun.com/openapi/v2/%s/detail?%s", accountId, mapToUrl(params));
// 构造请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 构造请求实体
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发起请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
// 返回响应体
return response.getBody();
}
/**
* 生成签名
*/
private String generateSignature(String timestamp, String appkey, String appsecret, String noncestr, Map<String, String> params) {
// 移除不需要参与签名的字段
params.remove("appkey");
params.remove("timestamp");
params.remove("noncestr");
params.remove("signature");
// 参数名按照ASCII码表升序排序
String[] keys = params.keySet().toArray(new String[0]);
Arrays.sort(keys);
// 按照排序拼接参数值
StringBuilder paramBuffer = new StringBuilder();
for (String key : keys) {
String value = params.get(key);
if (StringUtils.hasLength(value) && !"0".equals(value)) {
paramBuffer.append("&&").append(value);
}
}
// 拼接最终签名串
String rawSignature = String.format("%s&&%s&&%s&&%s", timestamp, appkey, appsecret, noncestr) + paramBuffer.toString();
return DigestUtils.md5DigestAsHex(rawSignature.getBytes(StandardCharsets.UTF_8));
}
/**
* 将Map转换为URL参数字符串
*/
private String mapToUrl(Map<String, String> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : map.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
/**
* 诗画
* @param accountId
* @param sessionId
* @return
* 诗画浦江单点:直连诗画开放接口获取用户,不再经对方 /login/shpj 中转
*/
public ZZDUserDTO shjnsso(String accountId, String sessionId) {
String shjnToken = getSHJNToken(accountId, sessionId);
if (StringUtils.isEmpty(shjnToken)){
throw new RuntimeException("获取诗画江南token失败");
ShpjOpenapiUser openapiUser = shpjUserClient.getUserInfo(accountId, sessionId);
if (openapiUser == null || openapiUser.getIsLogin() != 1) {
throw new ThirdPartyException("请先登录诗画客户端", "SHPJ_NOT_LOGIN");
}
ZZDUserDTO zzdUserDTO = getZZDUserInfo(shjnToken,UserTypeEnum.SH.getType());
return zzdUserDTO;
if (!StringUtils.hasText(openapiUser.getMobile())) {
throw new ThirdPartyException("诗画用户手机号为空", "SHPJ_MOBILE_EMPTY");
}
return mapShpjOpenapiUserToZzdDto(openapiUser);
}
private static ZZDUserDTO mapShpjOpenapiUserToZzdDto(ShpjOpenapiUser user) {
ZZDUserDTO dto = new ZZDUserDTO();
dto.setUsername(user.getNickName());
dto.setNickName(user.getNickName());
dto.setMobile(user.getMobile());
ZZDUserDTO.Ext ext = dto.new Ext();
ext.setNickNameCn(user.getNickName());
ext.setMobile(user.getMobile());
ext.setHeadImgUrl(user.getHeadImgUrl());
dto.setExt(ext);
return dto;
}
}
@@ -0,0 +1,17 @@
package com.bsd.pjcloud.thirdparty.shpj;
import lombok.Data;
@Data
public class ShpjApiWrapper<T> {
private int code;
private T data;
private String message;
public boolean isSuccessful() {
return code == 0;
}
}
@@ -0,0 +1,42 @@
package com.bsd.pjcloud.thirdparty.shpj;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* 诗画浦江开放接口用户信息(对齐对方 OpenapiUser)
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ShpjOpenapiUser {
private int is_faceVerify;
private String city;
private String nickName;
private boolean forbidden;
private String mobile;
private String deviceId;
private boolean freezed;
private String accountId;
private int isLogin;
private String createdAt;
private String headImgUrl;
private String clientCode;
private String recentActiveTime;
private int grade;
private String refCode;
}
@@ -0,0 +1,24 @@
package com.bsd.pjcloud.thirdparty.shpj;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 诗画浦江开放接口配置(直连 vcms,不经对方中转服务)
*/
@Data
@Component
@ConfigurationProperties(prefix = "pjcloud.shpj")
public class ShpjProperties {
private String appKey;
private String appSecret;
private String connectNo;
private String baseUrl = "https://vcms.tmuyun.com";
private int httpTimeoutSeconds = 5;
}
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.thirdparty.shpj;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 诗画浦江签名:timestamp&&appkey&&appsecret&&noncestr&&paramValues
*/
public final class ShpjSignHelper {
private ShpjSignHelper() {
}
public static String sign(Map<String, String> businessParams, String timestamp, String appKey, String appSecret, String nonce) {
String raw = String.format("%s&&%s&&%s&&%s", timestamp, appKey, appSecret, nonce) + extraSign(businessParams);
return DigestUtils.md5DigestAsHex(raw.getBytes(StandardCharsets.UTF_8));
}
private static String extraSign(Map<String, String> paramsMap) {
SortedMap<String, String> resMap = new TreeMap<>(paramsMap);
String[] keys = resMap.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuilder paramBuffer = new StringBuilder();
for (String key : keys) {
String value = resMap.get(key);
if (StringUtils.hasLength(value) && !"0".equals(value)) {
paramBuffer.append("&&").append(value);
}
}
return paramBuffer.toString();
}
}
@@ -0,0 +1,85 @@
package com.bsd.pjcloud.thirdparty.shpj;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 诗画浦江用户开放接口客户端(直连)
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ShpjUserClient {
private final ShpjProperties properties;
private final ObjectMapper objectMapper;
/**
* 按 accountId + sessionId 查询诗画用户详情
*/
public ShpjOpenapiUser getUserInfo(String accountId, String sessionId) {
if (!StringUtils.hasText(accountId) || "-1".equals(accountId)) {
throw new ThirdPartyException("请先登录诗画客户端", "SHPJ_NOT_LOGIN");
}
if (!StringUtils.hasText(sessionId)) {
throw new ThirdPartyException("sessionId不能为空", "SHPJ_SESSION_EMPTY");
}
if (!StringUtils.hasText(properties.getAppKey()) || !StringUtils.hasText(properties.getAppSecret())
|| !StringUtils.hasText(properties.getConnectNo())) {
throw new ThirdPartyException("诗画浦江开放接口未配置", "SHPJ_CFG");
}
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = timestamp + "shpj";
Map<String, String> businessParams = new HashMap<>(2);
businessParams.put("sessionId", sessionId);
businessParams.put("connectNo", properties.getConnectNo());
Map<String, Object> form = new HashMap<>();
form.put("appkey", properties.getAppKey());
form.put("noncestr", nonce);
form.put("timestamp", timestamp);
form.put("signature", ShpjSignHelper.sign(businessParams, timestamp, properties.getAppKey(), properties.getAppSecret(), nonce));
form.putAll(businessParams);
String apiUrl = "/openapi/v2/" + accountId + "/detail";
String fullUrl = properties.getBaseUrl() + apiUrl;
log.info("调用诗画浦江接口[{}]开始, accountId={}, sessionId={}", apiUrl, accountId, sessionId);
try (HttpResponse httpResponse = HttpRequest.get(fullUrl)
.timeout(properties.getHttpTimeoutSeconds() * 1000)
.form(form)
.execute()) {
String body = httpResponse.body();
if (!httpResponse.isOk()) {
log.error("诗画浦江接口[{}]HTTP错误:{} - {}", apiUrl, httpResponse.getStatus(), body);
throw new ThirdPartyException("诗画浦江接口调用失败", "SHPJ_HTTP");
}
log.info("诗画浦江接口[{}]完成, 返回:{}", apiUrl, body);
ShpjApiWrapper<ShpjOpenapiUser> resp = objectMapper.readValue(body,
objectMapper.getTypeFactory().constructParametricType(ShpjApiWrapper.class, ShpjOpenapiUser.class));
if (!resp.isSuccessful()) {
log.error("诗画浦江接口[{}]业务失败: [{}-{}]", apiUrl, resp.getCode(), resp.getMessage());
throw new ThirdPartyException("诗画浦江接口调用失败:" + resp.getMessage(), "SHPJ_BIZ_" + resp.getCode());
}
return resp.getData();
} catch (ThirdPartyException e) {
throw e;
} catch (Exception e) {
log.error("诗画浦江接口[{}]异常", apiUrl, e);
throw new ThirdPartyException("诗画浦江服务暂时不可用", "SHPJ_ERROR");
}
}
}
@@ -36,11 +36,18 @@ oauth2:
user_detail-url: /prod-api/oauth2/apis/orguser/v1/user_detail
# 数字政务单点接口
zzd_token_url: /prod-api/login/szzw/
# 诗画的获取token的接口
shjn_token_url: /prod-api/login/shpj
# 浙证钉和诗画公用的获取用户信息接口
# 浙证钉和诗画公用的获取用户信息接口(诗画已改为本系统直连 vcms)
zzd_sh_user_info_url: /prod-api/oauth2/apis/sso/v1/fetch_user_by_uabase_token
# 诗画浦江开放接口(直连 https://vcms.tmuyun.com,不经对方 /login/shpj)
pjcloud:
shpj:
app-key: ${SHPJ_APP_KEY:rQZzIvB7beaeZBf2IbQnemYv}
app-secret: ${SHPJ_APP_SECRET:4d248cd64bc5653fd2c1d6bb425b2817}
connect-no: ${SHPJ_CONNECT_NO:afb7e8fbf5f4eb9f}
base-url: ${SHPJ_BASE_URL:https://vcms.tmuyun.com}
http-timeout-seconds: 5
# 正元智慧
# https://gwyc.zj.gov.cn/API/%E7%9C%81%E5%B9%B3%E5%8F%B0API%E5%AF%B9%E6%8E%A5%E6%8E%A5%E5%8F%A3.html
zy:
@@ -228,7 +228,7 @@ public class SysUserController {
* @return 用户信息
*/
@GetMapping("/details/{id}")
// @ApiCryptoAes
@ApiCryptoAes
public R user(@PathVariable Long id) {
return R.ok(userService.selectUserVoById(id));
}
@@ -267,6 +267,7 @@ public class SysUserController {
* @return success/false
*/
@SysLog("添加用户")
@ApiCryptoAes
@PostMapping
@HasPermission("sys_user_add")
public R user(@RequestBody UserDTO userDto) {
@@ -287,12 +288,14 @@ public class SysUserController {
/**
* 分页查询用户
*/
@ApiCryptoAes
@PostMapping("/page")
public R getUserPage(@ParameterObject Page page, @RequestBody UserDTO userDTO) {
userDTO.setIsFindCheckUser(true);
return R.ok(userService.getUsersWithRolePage(page, userDTO, DataScope.of()));
}
@ApiCryptoAes
@PostMapping("/select")
public R userPage(@ParameterObject Page page, @RequestBody UserDTO userDTO) {
userDTO.setIsFindCheckUser(true);
@@ -322,6 +325,7 @@ public class SysUserController {
* @return R
*/
@SysLog("更新用户信息")
@ApiCryptoAes
@PutMapping
@HasPermission("sys_user_edit")
public R updateUser(@Valid @RequestBody UserDTO userDto) {
@@ -260,6 +260,7 @@ public class WalletManageController {
}
@Operation(summary = "分页查询", description = "分页查询")
@ApiCryptoAes
@PostMapping("/page-join")
@PreAuthorize("@pms.hasPermission('wallet_walletManage_view')")
public R getWalletManagePageJoin(@ParameterObject Page page, @RequestBody WalletManageDto dto) {
@@ -106,6 +106,7 @@ public class WalletTransactionController {
}
/* ************************************* end app end ************************************* */
@ApiCryptoAes
@PutMapping("/update-balance")
@Operation(summary = "更新会员用户余额")
@PreAuthorize("@pms.hasPermission('wallet_walletTransaction_update_balance')")