接口加密,分销

This commit is contained in:
周鹏 committed 2026-08-11 20:16:48 +08:00
1 parent 30176e7268
commit c52bbc1a2c
15 files changed
+347 -19

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eE
Ud2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1y
wsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/
Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6o
mstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZx
AMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3J
p09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6M
kqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebt
pV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtB
UKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdP
uHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgL
DtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX
6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJS
OanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8o
ZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+W
ijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwv
O+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD
1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPY
w5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0V
FF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2
SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMx
yoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRsw
HDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585
E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD
1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4
pdry0NF+Wfjny8ATyFDKY08w
-----END PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJ
a1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7Sd
eQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Z
r6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMp
BWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVW
ceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26k
DwIDAQAB
-----END PUBLIC KEY-----
+1
View File
@@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJa1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7SdeQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Zr6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMpBWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVWceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26kDwIDAQAB
@@ -38,8 +38,8 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_C
@Validated
public class BrokerageCodeServiceImpl implements BrokerageCodeService {
/** v3:海报图印 trade_brokerage_code.code(不再用 member_user.user_code) */
private static final String POSTER_VERSION_MARKER = "#poster-v3-code";
/** v4:邀请链接改为 /invite.html?code=&userId=,二维码同步刷新 */
private static final String POSTER_VERSION_MARKER = "#poster-v4-invite-html";
private static final String BROKERAGE_CODE_GENERATE_LOCK_KEY = "trade:brokerage-code:generate:";
@@ -152,8 +152,7 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
if (ObjectUtil.isNotEmpty(user)) {
userAvatar = user.getAvatar();
}
String posterUrl = ObjectUtil.isNotEmpty(currentCode.getUrl())
? currentCode.getUrl() : generatePosterUrl(currentCode.getCode(), userId);
String posterUrl = generatePosterUrl(currentCode.getCode(), userId);
String posterPic = generatePosterPic(userAvatar, posterCode, posterUrl);
if (ObjectUtil.isEmpty(posterPic)) {
return currentCode;
@@ -232,7 +231,12 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
}
private String generatePosterUrl(String code, Long userId) {
return brokerageConfiguration.getDownloadUrl() + "?code=" + code + "&userId=" + userId;
String base = brokerageConfiguration.getDownloadUrl();
if (base != null && base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
// 格式:http://download.htysapp.com/invite.html?code=569876&userId=90
return base + "/invite.html?code=" + code + "&userId=" + userId;
}
}
@@ -84,9 +84,9 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
// 1.1 获得一级推广人
// 1.1 获得一级推广人(人人分销:只要绑定了上级即可分佣,不校验上级推广员开关)
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
if (firstUser == null) {
return;
}
// 1.2 计算一级分佣
@@ -98,7 +98,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId());
if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
if (secondUser == null) {
return;
}
// 2.2 计算二级分佣
@@ -124,9 +124,9 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
// 1.1 获得一级推广人
// 1.1 获得一级推广人(人人分销:只要绑定了上级即可分佣,不校验上级推广员开关)
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
if (firstUser == null) {
return;
}
// 1.1.1查看一级分销员是否已经分销过
@@ -145,7 +145,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId());
if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
if (secondUser == null) {
return;
}
// 2.1.1查看二级分销员是否已经分销过
@@ -189,14 +189,9 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
}
// 2.3 校验能否绑定
validateCanBindUser(brokerageUser, bindUserId);
// 2.3 绑定用户
// 2.3 绑定用户(人人分销:新用户默认具备分销资格)
if (isNewBrokerageUser) {
Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition();
if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
} else {
brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now());
}
brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
} else {
brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId)));
@@ -0,0 +1,9 @@
package cn.iocoder.yudao.module.member.framework.crypto;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(RespBodyEncryptProperties.class)
public class RespBodyEncryptConfiguration {
}
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.member.framework.crypto;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* App 敏感接口响应体加密配置
*/
@ConfigurationProperties(prefix = "yudao.member.resp-encrypt")
@Data
public class RespBodyEncryptProperties {
/**
* 是否开启(总开关)
*/
private boolean enabled = true;
/**
* RSA 私钥(PKCS#8 Base64,无 PEM 头尾)
*/
private String privateKeyBase64;
}
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.member.framework.crypto;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* 对白名单接口的 CommonResult.data 做响应加密(需客户端 Header 声明)。
*/
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "yudao.member.resp-encrypt", name = "enabled", havingValue = "true", matchIfMissing = true)
public class RespBodyEncryptResponseAdvice implements ResponseBodyAdvice<Object> {
private final RespBodyEncryptProperties properties;
private final ObjectMapper objectMapper;
private RespBodyEncryptSupport support;
@PostConstruct
public void init() {
if (StrUtil.isBlank(properties.getPrivateKeyBase64())) {
log.warn("[resp-encrypt] 未配置 yudao.member.resp-encrypt.private-key-base64,响应加密不可用");
support = null;
return;
}
support = new RespBodyEncryptSupport(properties.getPrivateKeyBase64(), objectMapper);
log.info("[resp-encrypt] 响应加密已启用,白名单={}", RespBodyEncryptSupport.PATH_SUFFIXES);
}
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return support != null
&& returnType.getMethod() != null
&& returnType.getMethod().getReturnType() == CommonResult.class;
}
@Override
@SuppressWarnings("unchecked")
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (!(body instanceof CommonResult) || support == null) {
return body;
}
CommonResult<Object> result = (CommonResult<Object>) body;
if (!result.isSuccess() || result.getData() == null) {
return body;
}
String path = request.getURI().getPath();
if (!RespBodyEncryptSupport.matchPath(path)) {
return body;
}
List<String> encHeaders = request.getHeaders().get(RespBodyEncryptSupport.HEADER_RESP_ENC);
String enc = (encHeaders != null && !encHeaders.isEmpty()) ? encHeaders.get(0) : null;
if (!RespBodyEncryptSupport.wantEncrypt(enc)) {
// 旧客户端未声明加密:仍返回明文,便于过渡
return body;
}
List<String> ekHeaders = request.getHeaders().get(RespBodyEncryptSupport.HEADER_RESP_EK);
String ek = (ekHeaders != null && !ekHeaders.isEmpty()) ? ekHeaders.get(0) : null;
try {
result.setData(support.encryptData(result.getData(), ek));
} catch (Exception e) {
log.error("[resp-encrypt] 加密失败 path={} err={}", path, e.getMessage());
return CommonResult.error(1_004_000_101, "响应加密失败,请升级客户端后重试");
}
return result;
}
}
@@ -0,0 +1,118 @@
package cn.iocoder.yudao.module.member.framework.crypto;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 响应体加密:客户端用 RSA 公钥加密随机 AES Key(Header),服务端 AES-GCM 加密 data。
*/
@Slf4j
public class RespBodyEncryptSupport {
public static final String HEADER_RESP_ENC = "X-Resp-Enc";
public static final String HEADER_RESP_EK = "X-Resp-Ek";
public static final Set<String> PATH_SUFFIXES = new HashSet<>(Arrays.asList(
"/member/recent/quick-page",
"/member/recent/getById",
"/member/recent/look-wechat"
));
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_BITS = 128;
private static final int AES_KEY_BITS = 256;
private static final int IV_LEN = 12;
private final PrivateKey privateKey;
private final ObjectMapper objectMapper;
private final SecureRandom secureRandom = new SecureRandom();
public RespBodyEncryptSupport(String privateKeyBase64, ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
try {
byte[] keyBytes = Base64.decode(privateKeyBase64);
this.privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (Exception e) {
throw new IllegalStateException("初始化响应加密 RSA 私钥失败", e);
}
}
public static boolean matchPath(String path) {
if (StrUtil.isBlank(path)) {
return false;
}
for (String suffix : PATH_SUFFIXES) {
if (path.endsWith(suffix)) {
return true;
}
}
return false;
}
public static boolean wantEncrypt(String encHeader) {
return "1".equals(encHeader) || "true".equalsIgnoreCase(encHeader);
}
/**
* 用 Header 中的 RSA 密文解出 AES Key,再加密 data 为信封 Map。
*/
public Map<String, Object> encryptData(Object data, String ekBase64) throws Exception {
if (StrUtil.isBlank(ekBase64)) {
throw new IllegalArgumentException("缺少 X-Resp-Ek");
}
byte[] aesKey = decryptAesKey(ekBase64);
byte[] plain = objectMapper.writeValueAsBytes(data == null ? new HashMap<>() : data);
byte[] iv = new byte[IV_LEN];
secureRandom.nextBytes(iv);
Cipher aes = Cipher.getInstance(AES_TRANSFORMATION);
aes.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv));
byte[] ct = aes.doFinal(plain);
Map<String, Object> envelope = new HashMap<>(8);
envelope.put("enc", 1);
envelope.put("v", 1);
envelope.put("alg", "RSA-PKCS1-AES-256-GCM");
envelope.put("iv", Base64.encode(iv));
envelope.put("ct", Base64.encode(ct));
return envelope;
}
private byte[] decryptAesKey(String ekBase64) throws Exception {
Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
rsa.init(Cipher.DECRYPT_MODE, privateKey);
byte[] key = rsa.doFinal(Base64.decode(ekBase64));
if (key.length != 16 && key.length != 24 && key.length != 32) {
throw new IllegalArgumentException("AES Key 长度非法: " + key.length);
}
return key;
}
/** 仅测试/工具用 */
public static byte[] generateAesKeyBytes() throws Exception {
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(AES_KEY_BITS);
SecretKey key = kg.generateKey();
return key.getEncoded();
}
}
@@ -33,6 +33,12 @@ import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -60,6 +66,7 @@ import static cn.hutool.core.util.ObjectUtil.notEqual;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.fenToYuanStr;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
@@ -90,6 +97,8 @@ public class OpenTypeServiceImpl implements OpenTypeService {
private PayOrderApi payOrderApi;
@Resource
private PayRefundApi payRefundApi;
@Resource
private TradeBrokerageRecordApi tradeBrokerageRecordApi;
/**
* 获取最后一条会员开通记录
@@ -423,10 +432,36 @@ public class OpenTypeServiceImpl implements OpenTypeService {
// 添加会员tag
memberRecentTagService.createOrUpdateVipTag(openTypeUserDO.getMemberId());
// 添加会员开通分销佣金(每次开通都分,对应 OPEN_TYPE)
addOpenTypeBrokerage(openTypeUserDO, openType, payOrder);
String message = String.format("恭喜您购买成功,此次购买的产品:%s", openType.getTitle());
this.chatApi.messageUserSendText(openTypeUserDO.getMemberId(), message);
}
/**
* 会员开通支付成功后,给上级增加分销佣金
*/
private void addOpenTypeBrokerage(OpenTypeUserDO openTypeUserDO, OpenTypeDO openType, PayOrderRespDTO payOrder) {
Integer basePrice = payOrder.getPrice();
if (basePrice == null || basePrice <= 0) {
log.info("[会员开通分佣][开通单({}) 支付金额为0,跳过分佣]", openTypeUserDO.getId());
return;
}
MemberUserDO user = memberUserService.getUser(openTypeUserDO.getMemberId());
String nickname = user != null && user.getNickname() != null ? user.getNickname() : String.valueOf(openTypeUserDO.getMemberId());
String productTitle = openType != null && openType.getTitle() != null ? openType.getTitle() : "会员";
List<BrokerageAddDTO> brokerageList = new ArrayList<>();
brokerageList.add(new BrokerageAddDTO()
.setBizId(String.valueOf(openTypeUserDO.getId()))
.setBasePrice(basePrice)
.setFirstFixedPrice(0)
.setSecondFixedPrice(0)
.setSourceUserId(openTypeUserDO.getMemberId())
.setTitle(String.format("用户[%s]开通会员[%s],支付[%s]元", nickname, productTitle, fenToYuanStr(basePrice))));
tradeBrokerageRecordApi.addBrokerage(openTypeUserDO.getMemberId(), BrokerageRecordBizTypeEnum.OPEN_TYPE, brokerageList);
}
/**
* 退款的回调
*/
@@ -445,6 +480,8 @@ public class OpenTypeServiceImpl implements OpenTypeService {
updateObj.setRefundStatus(SUCCESS.getStatus())
.setRefundTime(payRefund.getSuccessTime())
.setRefundPayPrice(openTypeUserDO.getPayPrice());
// 退款成功后作废对应会员佣金
tradeBrokerageRecordApi.cancelBrokerage(BrokerageRecordBizTypeEnum.OPEN_TYPE, String.valueOf(id));
}
// 退款失败
if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
@@ -452,7 +489,7 @@ public class OpenTypeServiceImpl implements OpenTypeService {
}
// 更新会员开通的退款状态
openTypeUserMapper.update(updateObj, new LambdaQueryWrapperX<OpenTypeUserDO>()
.eq(OpenTypeUserDO::getId, openTypeUserDO.getOpenTypeId())
.eq(OpenTypeUserDO::getId, id)
.eq(OpenTypeUserDO::getRefundStatus, WAITING.getStatus())
);
}
@@ -193,6 +193,11 @@ wx:
yudao:
captcha:
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
member:
resp-encrypt:
enabled: true
# RSA PKCS#8 Base64(无 PEM 头尾),与 friend/assets/crypto/resp_enc_public.pem 配对
private-key-base64: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w
security:
mock-enable: true
pay:
@@ -189,6 +189,11 @@ wx:
yudao:
captcha:
enable: true # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
member:
resp-encrypt:
enabled: true
# 生产建议用环境变量 YUDAO_RESP_ENCRYPT_PRIVATE_KEY;此处可临时写死与 local 同钥
private-key-base64: ${YUDAO_RESP_ENCRYPT_PRIVATE_KEY:MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w}
security:
mock-enable: true
pay:
@@ -235,6 +235,11 @@ yudao:
info:
version: 1.0.0
base-package: cn.iocoder.yudao
member:
resp-encrypt:
# 默认关;local/prod 配置私钥后开启。客户端带 X-Resp-Enc/X-Resp-Ek 时加密 3 个敏感接口 data
enabled: true
private-key-base64: ${YUDAO_RESP_ENCRYPT_PRIVATE_KEY:}
web:
admin-ui:
url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址