浙政钉

This commit is contained in:
zjf committed 2026-05-08 19:54:26 +08:00
1 parent fe3cdd8e55
commit 957641393c
19 files changed
+473 -283

No files matched your search

@@ -38,10 +38,9 @@ import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdPartyAuthService;
import com.bsd.pjcloud.thirdparty.dto.SHJNSSODTO;
import com.bsd.pjcloud.thirdparty.dto.UrlTokenDTO;
import com.bsd.pjcloud.thirdparty.dto.ZZDUserDTO;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -90,111 +89,18 @@ public class AppUserAutoLoginController {
private final RemoteOnlineCanteenSpuService remoteOnlineCanteenSpuService;
private final RemoteDailyMenuService remoteDailyMenuService;
/*
*
* @param token 浙政钉前端获取的token
* @return
/**
* 路径参数历史命名为 token,实为浙政钉 H5 {@code authCode},转发 thirdParty 统一单点(含短信核验等逻辑)。
*/
@GetMapping("/zzdsso/{token}")
@Operation(summary = "浙政钉单点登录")
@Inner(value = false)
@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
//todo 引入seata分布式事务
public R zzdsso(@PathVariable(name = "token") String token) {
//获取数字政务token通过浙政钉前端获取的token,调试阶段先写死
String json = "{" +
"\"username\": \"李宗相\"," +
"\"organizationId\": 2," +
"\"userId\": 1340," +
"\"nickName\": \"李宗相\"," +
"\"mobile\": \"18576496089\"," +
"\"regionCode\": null," +
"\"ext\": {" +
"\"employeeCode\": \"GE_ae12bdf21598498eb48bf2cf2c45a3e1\"," +
"\"organizationCode\": \"GO_63e77077d5e041789bbbe1d0d20bcf41\"," +
"\"nickNameCn\": \"李宗相\"," +
"\"openid\": \"1b51f0e11fc73b8adddcbaf7982daab6\"," +
"\"realmId\": \"196729\"," +
"\"mobile\": null" +
"}" +
"}";
ObjectMapper objectMapper = new ObjectMapper();
ZZDUserDTO zzdUserDTO;
try {
zzdUserDTO = objectMapper.readValue(json, ZZDUserDTO.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
//暂时先注释,模拟一下就行
// R zzdsso = remoterThirdPartyAuthService.zzdsso(token);
// ZZDUserDTO zzdUserDTO = (ZZDUserDTO) zzdsso.getData();
ZZDUserDTO.Ext ext = zzdUserDTO.getExt();
String employeeCode = ext.getEmployeeCode();
String mobile = zzdUserDTO.getMobile();
//两种方式,手机号+员工钉钉号,查到相同就视为同一个人,然后登录
//先系统表里面查看是否有,在第三方数据库表里面看下是否有
//1.根据手机号或者钉钉号先查询是否有这个人
R<List<SysUser>> userByMobileOrEmployeeCode = remoteUserService.getUserByMobileOrEmployeeCode(employeeCode, mobile);
List<SysUser> sysUserList = userByMobileOrEmployeeCode.getData();
if(!CollectionUtils.isEmpty(sysUserList)){
//登录成功生成token
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(sysUserList.get(0));
remoteTokenService.generateToken(userTokenDTO);
}
//2.根据浙政钉的员工钉钉号查询是否有这个人
R<List<ThirdPartyUserEntity>> thirdPartyUserEntityR=remoterThirdPartyAuthService.getUserByEmployeeCode(employeeCode);
List<ThirdPartyUserEntity> thirdPartyUserEntityList = thirdPartyUserEntityR.getData();
UserDTO userDTO = new UserDTO();
//写死,新加入到部门表的全部归类到其他部门里面,不按照浙政钉的部门
userDTO.setDeptId(1865990541445263361L);
userDTO.setUsername(zzdUserDTO.getUsername());
userDTO.setName(zzdUserDTO.getUsername());
userDTO.setNickname(zzdUserDTO.getUsername());
//浙里办用户
userDTO.setSystemType("1");
//取上面的手机号
userDTO.setPhone(mobile);
userDTO.setDingtalkEmployeeCode(employeeCode);
//随便写死给个密码
userDTO.setPassword("pj"+mobile);
/* if(!CollectionUtils.isEmpty(thirdPartyUserEntityList)){
//todo 这一步现在是没用的,这个最终要考虑是否通过浙政钉里面的人员来限制移动端的使用用户来决定
//第三方数据库里面有
//这里只在第三方库里查到,所以要对系统用户表进行新增当前用户,插入一条信息进去
R<Boolean> booleanR = remoteUserService.registerUser(userDTO);
//通过唯一手机号查询到刚注册的用户信息,生成token
R<List<SysUser>> userByMobile=remoteUserService.getUserByMobile(mobile);
SysUser sysUser = userByMobile.getData().get(0);
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(sysUser);
//登录成功,根据新注册的用户信息返回token
remoteTokenService.generateToken(userTokenDTO);
}
else {
//两个表都没查到人员不能够使用移动端的系统
}*/
// log.info("用户不存在,尝试创建用户");
R<Boolean> booleanR = remoteUserService.registerUser(userDTO);
//通过唯一手机号查询到刚注册的用户信息,生成token
R<List<SysUser>> userByMobile=remoteUserService.getUserByMobile(mobile);
SysUser sysUser = userByMobile.getData().get(0);
int i=8/0;
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(sysUser);
//登录成功,根据新注册的用户信息返回token
return remoteTokenService.generateToken(userTokenDTO);
UrlTokenDTO body = new UrlTokenDTO();
body.setAuthCode(token);
return remoterThirdPartyAuthService.zzdsso(body);
}
@@ -171,6 +171,46 @@ public class NoticeController {
return R.ok(notice);
}
/**
* IRS / 纯静态托管 H5:与 {@link #getAppNoticePage} 数据一致,凭 X-App-Id、X-App-Secret({@code pjcloud.external-app})访问,不依赖用户 OAuth2 Token。
*/
@Operation(summary = "开放-公告分页", description = "凭应用凭证访问,供无法携带登录 Token 的 H5")
@GetMapping("/open/page")
@Inner(value = false)
public R getOpenNoticePage(
@RequestHeader(value = "X-App-Id", required = false) String appId,
@RequestHeader(value = "X-App-Secret", required = false) String appSecret,
@ParameterObject Page page,
@RequestParam(required = false) String title) {
if (!isValidAppCredentials(appId, appSecret)) {
return R.failed("无效的应用ID或密钥");
}
LambdaQueryWrapper<NoticeEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(NoticeEntity::getStatus, "0");
wrapper.like(StrUtil.isNotBlank(title), NoticeEntity::getTitle, title);
wrapper.and(w -> w.isNull(NoticeEntity::getExpireTime)
.or()
.gt(NoticeEntity::getExpireTime, LocalDateTime.now()));
wrapper.orderByDesc(NoticeEntity::getIsTop, NoticeEntity::getCreateTime);
return R.ok(noticeService.page(page, wrapper));
}
/**
* IRS / 纯静态托管 H5:与 {@link #getAppNoticeDetail} 一致,凭应用凭证访问。
*/
@Operation(summary = "开放-公告详情", description = "凭应用凭证访问")
@GetMapping("/open/{id}")
@Inner(value = false)
public R getOpenNoticeDetail(
@RequestHeader(value = "X-App-Id", required = false) String appId,
@RequestHeader(value = "X-App-Secret", required = false) String appSecret,
@PathVariable("id") Long id) {
if (!isValidAppCredentials(appId, appSecret)) {
return R.failed("无效的应用ID或密钥");
}
return getAppNoticeDetail(id);
}
/**
* App公告浏览量+1
* @param id 公告ID
@@ -136,15 +136,43 @@ public class SysBannerController {
return R.failed("无效的应用ID或密钥");
}
return R.ok(buildAppBannerVoList());
}
/**
* H5 / 浙政钉工作台(zzdhome)轮播:与 {@link #appList} 数据一致,无需 X-App-Id/Secret,便于前端直接拉取。
* 若需对外收口鉴权,可在网关按路径单独配置。
*/
@Operation(summary = "H5/App轮播图列表", description = "上架且在有效期内,字段同外部 app 接口;图片 URL 规则与 /app/list 一致")
@GetMapping("/app/slides")
@Inner(value = false)
public R<List<SysBannerAppVo>> appSlides() {
return R.ok(buildAppBannerVoList());
}
/**
* IRS / 无法携带用户 Token 的 H5:与 {@link #appList} 相同鉴权与数据,路径便于与公告 {@code /notice/open/*} 一并加入网关放行。
*/
@Operation(summary = "开放-轮播图列表", description = "凭 X-App-Id、X-App-Secret 访问,同 /app/list")
@GetMapping("/open/slides")
@Inner(value = false)
public R<List<SysBannerAppVo>> openSlides(
@RequestHeader(value = "X-App-Id", required = false) String appId,
@RequestHeader(value = "X-App-Secret", required = false) String appSecret) {
if (!isValidAppCredentials(appId, appSecret)) {
return R.failed("无效的应用ID或密钥");
}
return R.ok(buildAppBannerVoList());
}
private List<SysBannerAppVo> buildAppBannerVoList() {
String imageBaseUrl = bannerAppConfig.getImageBaseUrl();
boolean needPrefix = StrUtil.isNotBlank(imageBaseUrl);
String baseUrl = needPrefix ? imageBaseUrl.trim() : "";
List<SysBannerEntity> list = sysBannerService.listAppBanners();
List<SysBannerAppVo> voList = list.stream()
return list.stream()
.map(e -> {
SysBannerAppVo vo = BeanUtils.toBean(e, SysBannerAppVo.class);
// imageUrl 前拼接统一基础路径
if (needPrefix && StrUtil.isNotBlank(e.getImageUrl())) {
String path = e.getImageUrl().trim();
if (!path.startsWith("http://") && !path.startsWith("https://")) {
@@ -159,7 +187,6 @@ public class SysBannerController {
return vo;
})
.collect(Collectors.toList());
return R.ok(voList);
}
/**
@@ -21,6 +21,7 @@ package com.bsd.pjcloud.thirdparty.api.feign;
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.feign.annotation.NoToken;
import com.bsd.pjcloud.thirdparty.dto.UrlTokenDTO;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@@ -35,13 +36,11 @@ import java.util.List;
public interface RemoterThirdPartyAuthService {
/**
* zzd单点
* @param token
* @return
* 浙政钉单点(与 thirdPartyOAth2/zzdsso 一致,body.authCode 为 H5 免登码)
*/
@NoToken
@GetMapping("/thirdPartyOAth2/zzd/sso")
R zzdsso(@RequestParam("token") String token);
@PostMapping("/thirdPartyOAth2/zzdsso")
R zzdsso(@RequestBody UrlTokenDTO body);
@NoToken
@@ -6,7 +6,7 @@ import lombok.Data;
@Data
public class UrlTokenDTO {
/**
* 旧数字政务/第三方网关下发的 uabaseToken(逐步淘汰)
* 已废弃:浙政钉单点仅使用 authCode,不再传网关 uabaseToken
*/
private String token;
/**
@@ -2,6 +2,11 @@ package com.bsd.pjcloud.thirdparty.dto;
import lombok.Data;
/**
* 第三方 SSO 在本服务内的统一用户视图(非开放平台原始报文体)。
* 浙政钉:由 {@code dingtalk_app_user} 等适配层填入;诗画:由网关用户信息接口填入。
* 后续业务(发 token、短信绑定快照等)只依赖本结构,与走网关还是走开放平台无关。
*/
@Data
public class ZZDUserDTO {
private String username;
@@ -0,0 +1,16 @@
package com.bsd.pjcloud.thirdparty.dto;
import lombok.Data;
@Data
public class ZzdPhoneConfirmDTO {
/**
* /zzdsso 返回的 bindSessionId
*/
private String bindSessionId;
private String mobilePhone;
private String smsCode;
}
@@ -0,0 +1,47 @@
package com.bsd.pjcloud.thirdparty.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 浙政钉会话快照(可 JSON 序列化入 Redis),避免 ZZDUserDTO 内部类反序列化问题。
*/
@Data
public class ZzdUserSnapshot implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String nickName;
/** 浙政钉接口若返回的顶层手机号 */
private String zzdMobile;
private String employeeCode;
private String openid;
private String organizationCode;
private String realmId;
public static ZzdUserSnapshot from(ZZDUserDTO dto) {
ZzdUserSnapshot s = new ZzdUserSnapshot();
if (dto == null) {
return s;
}
s.setUsername(dto.getUsername());
s.setNickName(dto.getNickName());
s.setZzdMobile(dto.getMobile());
ZZDUserDTO.Ext ext = dto.getExt();
if (ext != null) {
s.setEmployeeCode(ext.getEmployeeCode());
s.setOpenid(ext.getOpenid());
s.setOrganizationCode(ext.getOrganizationCode());
s.setRealmId(ext.getRealmId());
}
return s;
}
}
@@ -3,8 +3,10 @@ package com.bsd.pjcloud.thirdparty;
import com.bsd.pjcloud.common.feign.annotation.EnablePjcloudFeignClients;
import com.bsd.pjcloud.common.security.annotation.EnablePjcloudResourceServer;
import com.bsd.pjcloud.common.swagger.annotation.EnableOpenApi;
import com.bsd.pjcloud.thirdparty.config.ThirdPartyZzdSmsBindProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
@@ -16,6 +18,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnablePjcloudFeignClients
@EnableDiscoveryClient
@EnablePjcloudResourceServer
@EnableConfigurationProperties(ThirdPartyZzdSmsBindProperties.class)
@SpringBootApplication
public class ThirdPartyApp {
public static void main(String[] args) {
@@ -23,25 +23,15 @@ public class OAuth2ClientConfig {
private String userDetailUrl;
/**
* 浙政钉获取token的接口
*/
private String zzdTokenUrl;
/**
* 诗画江南的token接口
*/
private String shjnTokenUrl;
/**
* 浙政钉跟诗画的获取用户信息的url
* 诗画江南 uabaseToken 换用户信息(网关 GET ?token=)
*/
private String zzdShUserInfoUrl;
/**
* 浙政钉官方 H5 authCode 换取用户信息(GET ?authCode=),响应体需与 {@code ApiResponse<ZZDUserDTO>} 一致
*/
private String zzdAuthCodeUserInfoUrl;
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.thirdparty.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 浙政钉无手机号时:首次短信核验 + 与本系统用户手机号比对后再发 token。
*/
@Data
@ConfigurationProperties(prefix = "pjcloud.thirdparty.zzd-sms-bind")
public class ThirdPartyZzdSmsBindProperties {
/**
* 是否开启(关闭时 /zzdsso 在拿到浙政钉用户后直接按手机号+员工编码查用户并发 token)
*/
private boolean enabled = false;
/**
* 绑定会话在 Redis 中的有效时间(分钟)
*/
private int bindSessionTtlMinutes = 15;
}
@@ -15,7 +15,7 @@ import org.springframework.util.StringUtils;
public class ZzdOpenPlatformProperties {
/**
* 为 false 时不走开放平台直连,仍使用 oauth2.client.zzd-auth-code-user-info-url 等旧网关配置
* 为 false 时禁用开放平台 SDK;浙政钉 authCode 单点不可用(需为 true 且配置 appKey/appSecret)
*/
private boolean enabled = true;
@@ -21,13 +21,17 @@ import com.bsd.pjcloud.thirdparty.exception.ThirdPartyException;
import com.bsd.pjcloud.thirdparty.vo.ZzdJsapiConfigVO;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyOAuth2Service;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyUserService;
import com.bsd.pjcloud.thirdparty.service.ThirdPartyZzdSmsBindService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.bsd.pjcloud.thirdparty.utils.SmsCodeUtil;
import com.alibaba.fastjson.JSON;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
@@ -43,6 +47,7 @@ import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/thirdPartyOAth2")
@@ -70,12 +75,7 @@ public class ThirdPartyOAth2Cotroller {
private final RemoteUserTrustedDeviceService remoteUserTrustedDeviceService;
// @Operation(summary = "浙政钉token" )
// @GetMapping("/zzd/sso" )
// public R getThirdPartyzzdsso(@RequestParam("token")String token) {
// return R.ok(thirdPartyOAuth2Service.getZZDUserInfo(token));
//
// }
private final ThirdPartyZzdSmsBindService thirdPartyZzdSmsBindService;
@Operation(summary = "诗画token")
@GetMapping("/sh/sso")
@@ -84,11 +84,6 @@ public class ThirdPartyOAth2Cotroller {
}
/*
*
* @param token 浙政钉前端获取的token
* @return
*/
@GetMapping("/zzdJsapiConfig")
@Operation(summary = "政务钉钉H5 JSAPI鉴权(dd.authConfig 所需 ticket)")
@Inner(value = false)
@@ -107,111 +102,113 @@ public class ThirdPartyOAth2Cotroller {
@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public R zzdsso(@RequestBody UrlTokenDTO urlTokenDTO) {
String authCode = urlTokenDTO.getAuthCode();
String token = urlTokenDTO.getToken();
if (StringUtils.isBlank(authCode) && StringUtils.isBlank(token)) {
return R.failed("token与authCode不能均为空");
String authCode = urlTokenDTO != null ? urlTokenDTO.getAuthCode() : null;
if (StringUtils.isBlank(authCode)) {
return R.failed("请传入浙政钉 authCode(H5 免登临时码)");
}
//获取数字政务token通过浙政钉前端获取的token,调试阶段先写死
// String json = "{" +
// "\"username\": \"李宗相\"," +
// "\"organizationId\": 2," +
// "\"userId\": 1340," +
// "\"nickName\": \"李宗相\"," +
// "\"mobile\": \"18576496089\"," +
// "\"regionCode\": null," +
// "\"ext\": {" +
// "\"employeeCode\": \"GE_ae12bdf21598498eb48bf2cf2c45a3e1\"," +
// "\"organizationCode\": \"GO_63e77077d5e041789bbbe1d0d20bcf41\"," +
// "\"nickNameCn\": \"李宗相\"," +
// "\"openid\": \"1b51f0e11fc73b8adddcbaf7982daab6\"," +
// "\"realmId\": \"196729\"," +
// "\"mobile\": null" +
// "}" +
// "}";
//
//
// ObjectMapper objectMapper = new ObjectMapper();
// ZZDUserDTO zzdUserDTO;
// try {
// zzdUserDTO = objectMapper.readValue(json, ZZDUserDTO.class);
// } catch (JsonProcessingException e) {
// throw new RuntimeException(e);
// }
//暂时先注释,模拟一下就行
ZZDUserDTO zzdUserDTO = StringUtils.isNotBlank(authCode)
? thirdPartyOAuth2Service.zzdssoByAuthCode(authCode)
: thirdPartyOAuth2Service.zzdsso(token);
ZZDUserDTO zzdUserDTO = thirdPartyOAuth2Service.zzdssoByAuthCode(authCode.trim());
log.warn("[zzdsso][ZZDUserDTO_JSON] 浙政钉开放平台映射后的对象,可整段复制做本地写死调试(含姓名手机号等敏感信息,调试完建议关日志或删此行) body={}", JSON.toJSONString(zzdUserDTO));
ZZDUserDTO.Ext ext = zzdUserDTO.getExt();
String employeeCode = ext.getEmployeeCode();
String employeeCode = ext != null ? ext.getEmployeeCode() : null;
String mobile = zzdUserDTO.getMobile();
//两种方式,手机号+员工钉钉号,查到相同就视为同一个人,然后登录
//先系统表里面查看是否有,在第三方数据库表里面看下是否有
R<Map> r;
//1.根据手机号或者钉钉号先查询是否有这个人
R<List<SysUser>> userByMobileOrEmployeeCode = remoteUserService.getUserByMobileOrEmployeeCode(employeeCode, mobile);
List<SysUser> sysUserList = userByMobileOrEmployeeCode.getData();
if(!CollectionUtils.isEmpty(sysUserList)){
//登录成功生成token
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(sysUserList.get(0));
r = remoteTokenService.generateToken(userTokenDTO);
}else {
//2.根据浙政钉的员工钉钉号查询是否有这个人
// List<ThirdPartyUserEntity> thirdPartyUserEntityList = thirdPartyUserService.getUserByEmployeeCode(employeeCode);
// UserDTO userDTO = new UserDTO();
// //写死,新加入到部门表的全部归类到其他部门里面,不按照浙政钉的部门
// userDTO.setDeptId(1865990541445263361L);
// userDTO.setUsername(zzdUserDTO.getUsername());
// userDTO.setName(zzdUserDTO.getUsername());
// userDTO.setNickname(zzdUserDTO.getUsername());
// //浙里办用户
// userDTO.setSystemType("1");
// //取上面的手机号
// userDTO.setPhone(mobile);
// userDTO.setDingtalkEmployeeCode(employeeCode);
// //随便写死给个密码
// userDTO.setPassword("pj"+mobile);
// if(!CollectionUtils.isEmpty(thirdPartyUserEntityList)){
// //todo 这一步现在是没用的,这个最终要考虑是否通过浙政钉里面的人员来限制移动端的使用用户来决定
// //第三方数据库里面有
// //这里只在第三方库里查到,所以要对系统用户表进行新增当前用户,插入一条信息进去
// R<Boolean> booleanR = remoteUserService.registerUser(userDTO);
// //通过唯一手机号查询到刚注册的用户信息,生成token
// R<List<SysUser>> userByMobile=remoteUserService.getUserByMobile(mobile);
// SysUser sysUser = userByMobile.getData().get(0);
// UserTokenDTO userTokenDTO = new UserTokenDTO();
// userTokenDTO.setSysUser(sysUser);
// //登录成功,根据新注册的用户信息返回token
// r=remoteTokenService.generateToken(userTokenDTO);
// }else {
// return R.failed("没有找到当前用户信息,无法访问!");
// }
return R.failed("系统中没有当前用户信息,请联系管理员进行申请!");
if (thirdPartyZzdSmsBindService.isEnabled()) {
if (StringUtils.isBlank(employeeCode)) {
return R.failed("浙政钉未返回员工编码,无法登录");
}
var boundOpt = thirdPartyZzdSmsBindService.findByEmployeeCode(employeeCode);
if (boundOpt.isPresent()) {
return buildZzdSsoTokenResult(employeeCode, boundOpt.get().getVerifiedPhone());
}
try {
ZzdUserSnapshot snap = ThirdPartyZzdSmsBindService.snapshotOf(zzdUserDTO);
if (StringUtils.isBlank(snap.getEmployeeCode())) {
return R.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("创建绑定会话失败");
}
}
//两个表都没查到人员不能够使用移动端的系统
return buildZzdSsoTokenResult(employeeCode, mobile);
}
// log.info("用户不存在,尝试创建用户");
/**
* 浙政钉短信核验:校验验证码、写入绑定表、与本系统用户手机号+员工编码比对后返回 token。
*/
@PostMapping("/zzdsso/confirmPhone")
@Operation(summary = "浙政钉单点-短信核验手机号")
@SysLog("浙政钉单点短信核验")
@Inner(value = false)
@Transactional(rollbackFor = Exception.class)
public R confirmZzdPhoneBind(@RequestBody ZzdPhoneConfirmDTO dto) {
if (dto == null || StringUtils.isAnyBlank(dto.getBindSessionId(), dto.getMobilePhone(), dto.getSmsCode())) {
return R.failed("参数不完整");
}
try {
ZzdUserSnapshot snap = thirdPartyZzdSmsBindService.getBindSession(dto.getBindSessionId());
if (snap == null) {
return R.failed("绑定会话已过期,请重新登录");
}
if (StringUtils.isBlank(snap.getEmployeeCode())) {
return R.failed("会话数据异常");
}
String redisKey = CacheConstants.SMS_CODE_KEY + dto.getMobilePhone();
String storedCode = (String) redisTemplate.opsForValue().get(redisKey);
if (!dto.getSmsCode().equals(storedCode)) {
return R.failed("验证码错误");
}
redisTemplate.delete(redisKey);
// R<Boolean> booleanR = remoteUserService.registerUser(userDTO);
// //通过唯一手机号查询到刚注册的用户信息,生成token
//
// R<List<SysUser>> userByMobile=remoteUserService.getUserByMobile(mobile);
// SysUser sysUser = userByMobile.getData().get(0);
//// int i=8/0;
// UserTokenDTO userTokenDTO = new UserTokenDTO();
// userTokenDTO.setSysUser(sysUser);
// //登录成功,根据新注册的用户信息返回token
// R<Map> r = remoteTokenService.generateToken(userTokenDTO);
R<List<SysUser>> byPhone = remoteUserService.getUserByMobile(dto.getMobilePhone());
List<SysUser> users = byPhone.getData();
if (CollectionUtils.isEmpty(users)) {
return R.failed("本系统不存在该手机号用户");
}
SysUser sysUser = users.get(0);
if (StringUtils.isNotBlank(sysUser.getDingtalkEmployeeCode())
&& !sysUser.getDingtalkEmployeeCode().equals(snap.getEmployeeCode())) {
return R.failed("浙政钉账号与本系统用户不一致");
}
thirdPartyZzdSmsBindService.recordVerifiedBind(snap, dto.getMobilePhone());
thirdPartyZzdSmsBindService.removeBindSession(dto.getBindSessionId());
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(sysUser);
R<Map> r = remoteTokenService.generateToken(userTokenDTO);
HashMap<String, Object> out = new HashMap<>();
out.put("refreshToken", r.getData().get("refreshToken"));
out.put("accessToken", r.getData().get("accessToken"));
return R.ok(out);
}
catch (JsonProcessingException e) {
return R.failed("保存绑定信息失败");
}
}
private R buildZzdSsoTokenResult(String employeeCode, String mobileForLookup) {
R<List<SysUser>> userByMobileOrEmployeeCode = remoteUserService.getUserByMobileOrEmployeeCode(
StringUtils.defaultString(employeeCode),
StringUtils.defaultString(mobileForLookup));
List<SysUser> sysUserList = userByMobileOrEmployeeCode.getData();
if (CollectionUtils.isEmpty(sysUserList)) {
return R.failed("系统中没有当前用户信息,请联系管理员进行申请!");
}
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(sysUserList.get(0));
R<Map> r = remoteTokenService.generateToken(userTokenDTO);
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
Object accessToken = r.getData().get("accessToken");
Object refreshToken = r.getData().get("refreshToken");
objectObjectHashMap.put("refreshToken",refreshToken);
objectObjectHashMap.put("accessToken",accessToken);
objectObjectHashMap.put("refreshToken", r.getData().get("refreshToken"));
objectObjectHashMap.put("accessToken", r.getData().get("accessToken"));
return R.ok(objectObjectHashMap);
}
@@ -0,0 +1,63 @@
package com.bsd.pjcloud.thirdparty.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 浙政钉用户短信核验手机号绑定
*/
@Data
@TenantTable
@TableName("third_party_zzd_phone_bind")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "浙政钉短信核验手机号绑定")
public class ThirdPartyZzdPhoneBindEntity extends Model<ThirdPartyZzdPhoneBindEntity> {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "租户ID")
private Long tenantId;
@Schema(description = "浙政钉员工编码")
private String employeeCode;
@Schema(description = "openid")
private String openId;
@Schema(description = "核验手机号")
private String verifiedPhone;
@Schema(description = "浙政钉信息快照JSON")
private String zzdProfileJson;
@Schema(description = "删除标记")
private String delFlag;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新人")
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.thirdparty.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ThirdPartyZzdPhoneBindMapper extends PjcloudBaseMapper<ThirdPartyZzdPhoneBindEntity> {
}
@@ -42,7 +42,6 @@ import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -388,27 +387,13 @@ public class ThirdPartyOAuth2Service {
/**
* 获取浙政钉的token
* @param token
* @return
* 浙政钉 H5:仅通过开放平台 authCode + dingtalk_app_user 换用户信息(已移除旧网关 uabaseToken / 网关 authCode 接口)。
*/
public ZZDUserDTO zzdsso(String token) {
// String zzdToken = getZZDToken(token);
//todo 根据是诗画还是浙政钉分别去获取不同的token
//使用token获取当前登录人的信息(浙政钉和诗画的在一起)
// ZZDUserDTO zzdUser = getZZDUserInfo(zzdToken);
ZZDUserDTO zzdUser = getZZDUserInfo(token, UserTypeEnum.ZZD.getType(), false);
return zzdUser;
}
public ZZDUserDTO zzdssoByAuthCode(String authCode) {
if (zzdOpenPlatform.isEnabled() && zzdOpenPlatform.isAppCredentialConfigured()) {
return getZzdUserByOpenPlatformAuthCode(authCode);
}
return getZZDUserInfo(authCode, UserTypeEnum.ZZD.getType(), true);
throw new ThirdPartyException("浙政钉单点需配置 zzd.open.enabled 与 app-key/app-secret(开放平台企业内应用)", "ZZD_OPEN_CONFIG");
}
/**
@@ -577,42 +562,15 @@ public class ThirdPartyOAuth2Service {
}
public String getZZDToken(String token) {
String url = baseUrl+config.getZzdTokenUrl()+token;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> requestEntity = new HttpEntity<>(null, headers);
ResponseEntity<ZZDResponse> response = restTemplate.exchange(
url, HttpMethod.POST, requestEntity, ZZDResponse.class
);
String zzdToken = response.getBody().getData();
return zzdToken;
}
/**
* 浙证钉和诗画工用
* @param zzdToken
* @return
* 诗画江南 token 换用户信息(与浙政钉共用网关 {@code oauth2.client.zzd-sh-user-info-url})。
*/
public ZZDUserDTO getZZDUserInfo(String zzdToken,String type) {
return getZZDUserInfo(zzdToken, type, false);
}
/**
* @param zzdAuthCodeFlow true 时走 oauth2.client.zzd-auth-code-user-info-url,query 为 authCode
*/
public ZZDUserDTO getZZDUserInfo(String credential, String type, boolean zzdAuthCodeFlow) {
public ZZDUserDTO getZZDUserInfo(String credential, String type) {
// 构建请求URL
try {
String requestPath = zzdAuthCodeFlow ? config.getZzdAuthCodeUserInfoUrl() : config.getZzdShUserInfoUrl();
if (zzdAuthCodeFlow && !StringUtils.hasText(requestPath)) {
throw new ThirdPartyException("未配置浙政钉 authCode 换取用户:请设置 zzd.open.app-key/app-secret(开放平台)或 oauth2.client.zzd-auth-code-user-info-url(网关)", "ZZD_AUTH_CODE_CONFIG");
}
String param = zzdAuthCodeFlow ? "authCode" : "token";
String url = zzdAuthCodeFlow
? baseUrl + requestPath + "?" + param + "=" + URLEncoder.encode(credential, StandardCharsets.UTF_8)
: baseUrl + requestPath + "?" + param + "=" + credential;
String requestPath = config.getZzdShUserInfoUrl();
String url = baseUrl + requestPath + "?token=" + credential;
// 获取 Bearer Token
String accessToken = getAccessToken().getAccess_token();
@@ -828,7 +786,7 @@ public class ThirdPartyOAuth2Service {
if (StringUtils.isEmpty(shjnToken)){
throw new RuntimeException("获取诗画江南token失败");
}
ZZDUserDTO zzdUserDTO = getZZDUserInfo(shjnToken,UserTypeEnum.SH.getType());
ZZDUserDTO zzdUserDTO = getZZDUserInfo(shjnToken, UserTypeEnum.SH.getType());
return zzdUserDTO;
}
}
@@ -0,0 +1,104 @@
package com.bsd.pjcloud.thirdparty.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.thirdparty.config.ThirdPartyZzdSmsBindProperties;
import com.bsd.pjcloud.thirdparty.dto.ZZDUserDTO;
import com.bsd.pjcloud.thirdparty.dto.ZzdUserSnapshot;
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
import com.bsd.pjcloud.thirdparty.mapper.ThirdPartyZzdPhoneBindMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class ThirdPartyZzdSmsBindService {
public static final String REDIS_BIND_SESSION_PREFIX = "thirdparty:zzd:bind:session:";
private final ThirdPartyZzdSmsBindProperties properties;
private final StringRedisTemplate stringRedisTemplate;
private final ThirdPartyZzdPhoneBindMapper thirdPartyZzdPhoneBindMapper;
private final ObjectMapper objectMapper;
public boolean isEnabled() {
return properties.isEnabled();
}
public Optional<ThirdPartyZzdPhoneBindEntity> findByEmployeeCode(String employeeCode) {
if (StringUtils.isBlank(employeeCode)) {
return Optional.empty();
}
ThirdPartyZzdPhoneBindEntity one = thirdPartyZzdPhoneBindMapper.selectOne(Wrappers.<ThirdPartyZzdPhoneBindEntity>lambdaQuery()
.eq(ThirdPartyZzdPhoneBindEntity::getEmployeeCode, employeeCode)
.eq(ThirdPartyZzdPhoneBindEntity::getDelFlag, "0")
.last("limit 1"));
return Optional.ofNullable(one);
}
public String createBindSession(ZzdUserSnapshot snapshot) throws JsonProcessingException {
String id = UUID.randomUUID().toString().replace("-", "");
String key = REDIS_BIND_SESSION_PREFIX + id;
stringRedisTemplate.opsForValue()
.set(key, objectMapper.writeValueAsString(snapshot), properties.getBindSessionTtlMinutes(), TimeUnit.MINUTES);
return id;
}
public ZzdUserSnapshot getBindSession(String bindSessionId) throws JsonProcessingException {
if (StringUtils.isBlank(bindSessionId)) {
return null;
}
String json = stringRedisTemplate.opsForValue().get(REDIS_BIND_SESSION_PREFIX + bindSessionId);
if (StringUtils.isBlank(json)) {
return null;
}
return objectMapper.readValue(json, ZzdUserSnapshot.class);
}
public void removeBindSession(String bindSessionId) {
if (StringUtils.isBlank(bindSessionId)) {
return;
}
stringRedisTemplate.delete(REDIS_BIND_SESSION_PREFIX + bindSessionId);
}
public void recordVerifiedBind(ZzdUserSnapshot snapshot, String verifiedPhone) throws JsonProcessingException {
upsertBindRecord(snapshot, verifiedPhone, objectMapper.writeValueAsString(snapshot));
}
public void upsertBindRecord(ZzdUserSnapshot snapshot, String verifiedPhone, String profileJson) {
ThirdPartyZzdPhoneBindEntity exist = thirdPartyZzdPhoneBindMapper.selectOne(Wrappers.<ThirdPartyZzdPhoneBindEntity>lambdaQuery()
.eq(ThirdPartyZzdPhoneBindEntity::getEmployeeCode, snapshot.getEmployeeCode())
.eq(ThirdPartyZzdPhoneBindEntity::getDelFlag, "0")
.last("limit 1"));
if (exist == null) {
ThirdPartyZzdPhoneBindEntity e = new ThirdPartyZzdPhoneBindEntity();
e.setEmployeeCode(snapshot.getEmployeeCode());
e.setOpenId(snapshot.getOpenid());
e.setVerifiedPhone(verifiedPhone);
e.setZzdProfileJson(profileJson);
e.setDelFlag("0");
thirdPartyZzdPhoneBindMapper.insert(e);
}
else {
exist.setVerifiedPhone(verifiedPhone);
exist.setOpenId(snapshot.getOpenid());
exist.setZzdProfileJson(profileJson);
thirdPartyZzdPhoneBindMapper.updateById(exist);
}
}
public static ZzdUserSnapshot snapshotOf(ZZDUserDTO dto) {
return ZzdUserSnapshot.from(dto);
}
}
@@ -34,16 +34,12 @@ oauth2:
dept-url: /prod-api/oauth2/apis/orguser/v1/list_sub_depts
dept-users-url: /prod-api/oauth2/apis/orguser/v1/list_dept_users
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
# 浙证钉和诗画公用的获取用户信息接口
# 诗画江南 uabaseToken 换用户信息(浙政钉单点已改走下方 zzd.open 开放平台,不再使用本 URL)
zzd_sh_user_info_url: /prod-api/oauth2/apis/sso/v1/fetch_user_by_uabase_token
# 浙政钉官方免登:前端 authCode 换用户信息(按实际网关/开放平台对接后填写,留空则 authCode 登录会报错)
# zzd_auth_code_user_info_url: /prod-api/...
# 浙政钉开放平台直连(配置 app-key/app-secret 后 authCode 走 gettoken + dingtalk_app_user,无需 zzd_auth_code_user_info_url)
# 浙政钉:H5 authCode → gettoken + dingtalk_app_user(需 app-key / app-secret)
zzd:
open:
enabled: true
@@ -203,3 +199,10 @@ hikvision:
# base-url: https://api.mch.weixin.qq.com
# ali-pay-config:
# app-id:
# 浙政钉无手机号时:首次短信核验 + 绑定表 third_party_zzd_phone_bind,与本系统用户手机号比对后再发 token(关闭则直接按手机号/员工编码查用户并发 token)
pjcloud:
thirdparty:
zzd-sms-bind:
enabled: false
bind-session-ttl-minutes: 15
@@ -267,4 +267,5 @@ public class SysUser extends Model<SysUser> implements Serializable {
@Schema(description = "性别")
private Integer gender;
}