diff --git a/script/crypto/resp_enc_private.pem b/script/crypto/resp_enc_private.pem new file mode 100644 index 0000000..aa56920 --- /dev/null +++ b/script/crypto/resp_enc_private.pem @@ -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----- diff --git a/script/crypto/resp_enc_private_b64.txt b/script/crypto/resp_enc_private_b64.txt new file mode 100644 index 0000000..5969090 --- /dev/null +++ b/script/crypto/resp_enc_private_b64.txt @@ -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 \ No newline at end of file diff --git a/script/crypto/resp_enc_public.pem b/script/crypto/resp_enc_public.pem new file mode 100644 index 0000000..60f725c --- /dev/null +++ b/script/crypto/resp_enc_public.pem @@ -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----- diff --git a/script/crypto/resp_enc_public_b64.txt b/script/crypto/resp_enc_public_b64.txt new file mode 100644 index 0000000..45e0106 --- /dev/null +++ b/script/crypto/resp_enc_public_b64.txt @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJa1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7SdeQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Zr6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMpBWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVWceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26kDwIDAQAB \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java index 237c9da..11a9322 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java @@ -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; } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java index 19cd16e..8bc3733 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java @@ -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查看二级分销员是否已经分销过 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java index bf3b23c..a55ae55 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java @@ -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))); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptConfiguration.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptConfiguration.java new file mode 100644 index 0000000..a4de7ef --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptConfiguration.java @@ -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 { +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptProperties.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptProperties.java new file mode 100644 index 0000000..733f5b9 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptProperties.java @@ -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; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptResponseAdvice.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptResponseAdvice.java new file mode 100644 index 0000000..d3dbf0d --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptResponseAdvice.java @@ -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 { + + 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 result = (CommonResult) body; + if (!result.isSuccess() || result.getData() == null) { + return body; + } + + String path = request.getURI().getPath(); + if (!RespBodyEncryptSupport.matchPath(path)) { + return body; + } + + List 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 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; + } +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptSupport.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptSupport.java new file mode 100644 index 0000000..6820989 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/crypto/RespBodyEncryptSupport.java @@ -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 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 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 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(); + } +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/opentype/OpenTypeServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/opentype/OpenTypeServiceImpl.java index 462d25f..4a24adb 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/opentype/OpenTypeServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/opentype/OpenTypeServiceImpl.java @@ -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 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() - .eq(OpenTypeUserDO::getId, openTypeUserDO.getOpenTypeId()) + .eq(OpenTypeUserDO::getId, id) .eq(OpenTypeUserDO::getRefundStatus, WAITING.getStatus()) ); } diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 7738807..db27c8a 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -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: diff --git a/yudao-server/src/main/resources/application-prod.yaml b/yudao-server/src/main/resources/application-prod.yaml index a573d7e..30de500 100644 --- a/yudao-server/src/main/resources/application-prod.yaml +++ b/yudao-server/src/main/resources/application-prod.yaml @@ -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: diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index d9ea2a2..c160025 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -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 的地址