Commit 3215d3d8 by zhangxingmin

push

parent 7cc9acdd
......@@ -3,19 +3,32 @@ package com.yd.notice.api.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yd.common.result.Result;
import com.yd.common.result.JsonResult;
import com.yd.notice.api.service.ApiSubscribeRecordService;
import com.yd.notice.feign.dto.SubscribeRecordDTO;
import com.yd.notice.feign.request.SubscribeBatchRequest;
import com.yd.notice.feign.request.SubscribeRemainingRequest;
import com.yd.notice.feign.response.QuerySfpUserResponse;
import com.yd.notice.feign.response.SubscribeRemainingResponse;
import com.yd.notice.service.model.NotificationTemplate;
import com.yd.notice.service.model.SubscribeRecord;
import com.yd.notice.service.service.INotificationTemplateService;
import com.yd.notice.service.service.ISubscribeRecordService;
import com.yd.notice.service.utils.SfpTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
......@@ -36,6 +49,97 @@ public class ApiSubscribeRecordServiceImpl implements ApiSubscribeRecordService
@Autowired
private INotificationTemplateService templateService;
@Autowired
private RestTemplate restTemplate;
@Resource
private Environment env;
/**
* 根据当前环境获取 SFP API 基础地址
*/
private String getSfpApiBaseUrl() {
String activeProfile = env.getProperty("spring.profiles.active", "dev");
log.info("当前环境: {}, 获取 SFP API 地址", activeProfile);
if ("prod".equalsIgnoreCase(activeProfile)) {
return "https://center.supguard.cn/sfpApi"; // 生产环境
} else {
return "https://hoservice.ydhomeoffice.cn/hoserviceApi"; // 测试/开发环境
}
}
/**
* 从请求头中获取 Token,解析出 SFP 用户 ID,再调用 SFP 接口获取用户的 wxOpenId
*
* @return 用户的 openid
* @throws RuntimeException 如果获取失败
*/
private String getOpenidFromToken() {
// 从请求上下文中获取当前请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
log.error("无法获取当前请求上下文");
throw new RuntimeException("无法获取当前请求上下文");
}
HttpServletRequest request = attributes.getRequest();
String authorization = request.getHeader("X-Authorization");
if (!StringUtils.hasText(authorization)) {
log.error("请求头 X-Authorization 为空");
throw new RuntimeException("未提供认证 Token");
}
// 1. 解析 Token 获取用户 ID
String userIdStr = SfpTokenUtil.getUserIdFromToken(authorization);
if (!StringUtils.hasText(userIdStr)) {
log.error("Token 解析失败,未获取到用户 ID");
throw new RuntimeException("Token 无效");
}
Long sfpUserId;
try {
sfpUserId = Long.valueOf(userIdStr);
} catch (NumberFormatException e) {
log.error("用户 ID 格式错误: {}", userIdStr);
throw new RuntimeException("Token 中的用户 ID 格式错误");
}
// 2. 根据环境获取 SFP API 基础地址并调用接口
String sfpApiBaseUrl = getSfpApiBaseUrl();
String url = sfpApiBaseUrl + "/user/query/sfpUser";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String requestBody = String.format("{\"sfpUserId\":%d}", sfpUserId);
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
log.info("调用 SFP 查询用户信息, url={}, requestBody={}", url, requestBody);
try {
ResponseEntity<JsonResult> responseEntity = restTemplate.postForEntity(url, entity, JsonResult.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
JsonResult body = responseEntity.getBody();
if (body != null && body.isSuccess()) {
Object data = body.getData();
if (data != null) {
QuerySfpUserResponse userInfo = JSON.parseObject(JSON.toJSONString(data), QuerySfpUserResponse.class);
String wxOpenId = userInfo.getWxOpenId();
if (StringUtils.hasText(wxOpenId)) {
log.info("获取到用户 openid: {}", wxOpenId);
return wxOpenId;
} else {
log.error("用户信息中 wxOpenId 为空, sfpUserId={}", sfpUserId);
throw new RuntimeException("用户未绑定小程序 openid");
}
}
}
}
log.error("调用 SFP 接口失败, response={}", responseEntity);
throw new RuntimeException("获取用户信息失败");
} catch (Exception e) {
log.error("调用 SFP 查询用户接口异常", e);
throw new RuntimeException("获取用户信息异常: " + e.getMessage());
}
}
@Override
public Result<Void> batchSave(SubscribeBatchRequest request) {
log.info("批量保存订阅记录, request={}", JSON.toJSONString(request));
......@@ -45,9 +149,18 @@ public class ApiSubscribeRecordServiceImpl implements ApiSubscribeRecordService
return Result.fail("订阅记录列表为空");
}
// 从 Token 中获取当前用户的 openid(所有记录共用同一个用户)
String openid;
try {
openid = getOpenidFromToken();
} catch (Exception e) {
log.error("获取用户 openid 失败", e);
return Result.fail("获取用户信息失败:" + e.getMessage());
}
List<SubscribeRecord> records = new ArrayList<>();
for (SubscribeRecordDTO dto : dtoList) {
// 1. 根据 wxTemplateId 查询模板信息,获取 templateBizId 和 channelBizId
// 1. 根据 wxTemplateId 查询模板信息
NotificationTemplate template = templateService.getOne(
new LambdaQueryWrapper<NotificationTemplate>()
.eq(NotificationTemplate::getExtraTemplate, dto.getWxTemplateId())
......@@ -55,7 +168,6 @@ public class ApiSubscribeRecordServiceImpl implements ApiSubscribeRecordService
.orderByDesc(NotificationTemplate::getCreateTime)
.last("LIMIT 1")
);
if (template == null) {
log.error("模板不存在, wxTemplateId={}", dto.getWxTemplateId());
return Result.fail("模板不存在: " + dto.getWxTemplateId());
......@@ -65,13 +177,11 @@ public class ApiSubscribeRecordServiceImpl implements ApiSubscribeRecordService
SubscribeRecord record = new SubscribeRecord();
record.setProjectType(dto.getProjectType());
record.setWxTemplateId(dto.getWxTemplateId());
record.setOpenid(dto.getOpenid());
record.setOpenid(openid);
// 从模板表补齐的字段
record.setTemplateBizId(template.getTemplateBizId());
record.setChannelBizId(template.getChannelBizId());
// 系统自动填充字段(默认有效,7天后过期)
record.setStatus(0);
record.setSubscribeTime(LocalDateTime.now());
record.setExpireTime(LocalDateTime.now().plusDays(7));
......@@ -93,10 +203,16 @@ public class ApiSubscribeRecordServiceImpl implements ApiSubscribeRecordService
public Result<SubscribeRemainingResponse> getRemaining(SubscribeRemainingRequest request) {
log.info("查询剩余次数, request={}", JSON.toJSONString(request));
String openid = request.getOpenid();
String wxTemplateId = request.getWxTemplateId();
// 从 Token 中获取当前用户的 openid
String openid;
try {
openid = getOpenidFromToken();
} catch (Exception e) {
log.error("获取用户 openid 失败", e);
return Result.fail("获取用户信息失败:" + e.getMessage());
}
// 直接通过 wxTemplateId 统计剩余次数
String wxTemplateId = request.getWxTemplateId();
int count = subscribeRecordService.countRemainingByWxTemplateId(openid, wxTemplateId);
SubscribeRemainingResponse response = new SubscribeRemainingResponse();
......
......@@ -25,9 +25,9 @@ public class SubscribeRecordDTO {
@NotBlank(message = "wxTemplateId模板ID不能为空")
private String wxTemplateId;
/**
* 用户小程序openid(必填)
*/
@NotBlank(message = "用户小程序openid不能为空")
private String openid;
// /**
// * 用户小程序openid(必填)
// */
// @NotBlank(message = "用户小程序openid不能为空")
// private String openid;
}
\ No newline at end of file
......@@ -13,8 +13,8 @@ import javax.validation.constraints.NotBlank;
@Data
public class SubscribeRemainingRequest {
@NotBlank(message = "openid不能为空")
private String openid;
// @NotBlank(message = "openid不能为空")
// private String openid;
@NotBlank(message = "wxTemplateId模板ID不能为空")
private String wxTemplateId;
......
package com.yd.notice.feign.response;
import lombok.Data;
import java.util.Date;
@Data
public class QuerySfpUserResponse {
private Long id;//serial idPRIauto_increment
private String name;//userName
private String mobile;//手机号
private Integer gender;//性别,1,男,2,女
private String age;//年龄
private String manageMobile;//管理经纪人手机号(保单托管)
private Date loginDate;//登录时间(用于判断该用户是否登录过期 30天, 登录成功更新,退出或者切换账号更新)
private Integer loginState;//是否登录(0=No, 1=Yes)
private String customerSource;//客户来源
private String level;//用户等级
private String levelName;//用户等级名称
private Integer isVip;//是否vip
private Date vipExpireDate;//vip到期时间
private Date inactivateDate;//激活到期时间
private Integer remainingTimes;//剩余次数
private Integer isMax;//是否达到最大购买次数 0-否 1-是
private Integer userType;//用户类型
private Long roleId;//用户角色ID
private String systemType; //系统类型(tvm、sfp、policyManage)
private String tenantProjectRoleIds;//租户项目角色ID列表
private String flag;//标识
private String remark;//备注
private String wxNickname;//微信昵称
private String wxOpenId;//用户小程序登录的openid
private String wxOpenIdGzh;//用户公众号登录的openid
private Integer isBlacklist;//是否黑名单 0-否 1-是
private Integer isActive;//是否启用(0=No, 1=Yes)
private Date createdAt;//创建时间
private String createdBy;//创建人
private Date updatedAt;//修改时间
private String updatedBy;//修改人
/**
* 律家保-法税卡激活接口,请求⽅用户唯⼀识别号 (用于跳转到律家保页面的挂参)
*/
private String ljbRefId;
}
package com.yd.notice.service.request;
import com.yd.notice.service.model.SubscribeRecord;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data
public class SubscribeBatchRequest {
@NotEmpty(message = "订阅记录列表不能为空")
private List<SubscribeRecord> records;
}
\ No newline at end of file
package com.yd.notice.service.utils;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* SFP 项目专用 JWT Token 工具类
* 完全在代码内部获取环境标识(System.getProperty),硬编码不同环境的配置
* 支持生成 Token 和解析 Token(获取 userId、customerId 等)
*
* @author yourname
*/
@Slf4j
@Service
public class SfpTokenUtil {
// JWT 标准声明 Key
private static final String CLAIM_KEY_SUBJECT = "sub";
private static final String CLAIM_KEY_CREATED = "created";
// 自定义声明 Key(与 JwtTokenUtil 保持一致)
private static final String CLAIM_KEY_USERID = "UserId";
private static final String CLAIM_KEY_CUSTOMERID = "CustomerId";
// 静态配置字段
private static String seal;
private static String issuer;
private static Long expiration;
/**
* 初始化:通过 System.getProperty 获取环境,硬编码对应的配置
* 该方法在 Spring 容器启动完成后自动执行
*/
@PostConstruct
public void init() {
// 从系统属性中获取 spring.profiles.active,如果获取不到则默认为 "default"
String activeProfile = System.getProperty("spring.profiles.active", "default");
// 判断是否为生产环境(可自定义判断规则,如包含 "prod" 或 "production")
boolean isProd = "prod".equalsIgnoreCase(activeProfile)
|| "production".equalsIgnoreCase(activeProfile)
|| activeProfile.toLowerCase().contains("prod");
if (isProd) {
// 生产环境配置(请替换为实际的生产密钥)
seal = "SFP2023!@#";
issuer = "sfpfamilyfinancialplanning";
expiration = 2592000L; // 24小时
log.info("SfpTokenUtil 加载生产环境配置, issuer: {}, expiration: {} 秒", issuer, expiration);
} else {
// 测试/开发环境配置(请替换为实际的测试密钥)
seal = "zhb123!@#";
issuer = "zuihuibi";
expiration = 2592000L;
log.info("SfpTokenUtil 加载测试环境配置, issuer: {}, expiration: {} 秒", issuer, expiration);
}
}
// ======================== 生成 Token ========================
/**
* 生成 JWT Token(自动添加 issuer 前缀,如 "zuihuibi {token}")
*
* @param ticket 用户标识(通常为登录名或唯一标识)
* @param userId 用户 ID(字符串)
* @param customerId 客户 ID(可选)
* @return 完整 Token 字符串(含前缀)
*/
public static String generateToken(String ticket, String userId, String customerId) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_SUBJECT, ticket);
claims.put(CLAIM_KEY_USERID, userId);
if (customerId != null) {
claims.put(CLAIM_KEY_CUSTOMERID, customerId);
}
claims.put(CLAIM_KEY_CREATED, new Date());
String token = Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, seal)
.compact();
return issuer + " " + token;
}
// ======================== 解析 Token ========================
/**
* 从 Token 中解析 Claims(包含所有声明信息)
*
* @param token 原始 Token(可能包含 issuer 前缀)
* @return Map, key 包含 "resultCode"(SUCCESS/EXPIRED/INVALID)和 "claims"(解析成功时为 Claims 对象)
*/
public static Map<String, Object> getClaimsFromToken(String token) {
Map<String, Object> map = new HashMap<>();
String resultCode = "SUCCESS";
Claims claims = null;
try {
// 去除 issuer 前缀(如果存在)
String realToken = token;
if (token.startsWith(issuer + " ")) {
realToken = token.substring(issuer.length() + 1);
}
Jws<Claims> jwsClaims = Jwts.parser()
.setSigningKey(seal)
.parseClaimsJws(realToken);
claims = jwsClaims.getBody();
} catch (ExpiredJwtException e) {
resultCode = "EXPIRED";
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
resultCode = "INVALID";
} catch (Exception e) {
resultCode = "INVALID";
}
map.put("resultCode", resultCode);
map.put("claims", claims);
return map;
}
/**
* 从 Token 中获取用户 ID(String 形式)
*
* @param token 原始 Token
* @return 用户 ID 字符串,解析失败返回 null
*/
public static String getUserIdFromToken(String token) {
Map<String, Object> map = getClaimsFromToken(token);
if ("SUCCESS".equals(map.get("resultCode"))) {
Claims claims = (Claims) map.get("claims");
if (claims != null) {
Object userId = claims.get(CLAIM_KEY_USERID);
return userId != null ? userId.toString() : null;
}
}
return null;
}
/**
* 从 Token 中获取客户 ID
*
* @param token 原始 Token
* @return 客户 ID 字符串,解析失败返回 null
*/
public static String getCustomerIdFromToken(String token) {
Map<String, Object> map = getClaimsFromToken(token);
if ("SUCCESS".equals(map.get("resultCode"))) {
Claims claims = (Claims) map.get("claims");
if (claims != null) {
Object customerId = claims.get(CLAIM_KEY_CUSTOMERID);
return customerId != null ? customerId.toString() : null;
}
}
return null;
}
/**
* 验证 Token 是否有效(未过期、格式正确)
*
* @param token 原始 Token
* @return true 表示有效,false 表示无效或过期
*/
public static boolean validateToken(String token) {
Map<String, Object> map = getClaimsFromToken(token);
return "SUCCESS".equals(map.get("resultCode"));
}
/**
* 检查 Token 是否过期
*
* @param token 原始 Token
* @return true 表示已过期,false 表示未过期或解析失败
*/
public static boolean isTokenExpired(String token) {
Map<String, Object> map = getClaimsFromToken(token);
String resultCode = (String) map.get("resultCode");
if ("EXPIRED".equals(resultCode)) {
return true;
}
if ("SUCCESS".equals(resultCode)) {
Claims claims = (Claims) map.get("claims");
if (claims != null) {
Date expirationDate = claims.getExpiration();
return expirationDate != null && expirationDate.before(new Date());
}
}
return false;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment