diff --git a/nxhs-service/src/main/java/cc/yunxi/config/props/WxPayV3Properties.java b/nxhs-service/src/main/java/cc/yunxi/config/props/WxPayV3Properties.java new file mode 100644 index 0000000..6bc40f9 --- /dev/null +++ b/nxhs-service/src/main/java/cc/yunxi/config/props/WxPayV3Properties.java @@ -0,0 +1,22 @@ +package cc.yunxi.config.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +@Component +@Data +@PropertySource("classpath:/wxpay_v3.properties") +@ConfigurationProperties(prefix = "v3") +public class WxPayV3Properties { + private String appId; + private String keyPath; + private String certPath; + +// private String platformCertPath; + private String mchId; + private String apiKey3; + private String domain; + +} \ No newline at end of file diff --git a/nxhs-service/src/main/java/cc/yunxi/controller/IndexController.java b/nxhs-service/src/main/java/cc/yunxi/controller/IndexController.java index 11ade86..3cfcb01 100644 --- a/nxhs-service/src/main/java/cc/yunxi/controller/IndexController.java +++ b/nxhs-service/src/main/java/cc/yunxi/controller/IndexController.java @@ -1,18 +1,28 @@ package cc.yunxi.controller; +import cc.yunxi.aspect.UserTypeAnnotation; import cc.yunxi.common.domain.CommonResult; import cc.yunxi.common.utils.BeanUtils; import cc.yunxi.domain.dto.UserDTO; -import cc.yunxi.domain.dto.WxLoginDTO; +import cc.yunxi.domain.po.Client; import cc.yunxi.domain.po.Product; import cc.yunxi.domain.vo.priceproduct.ProductSimpleVO; +import cc.yunxi.enums.UserTypeEnum; +import cc.yunxi.service.IClientService; import cc.yunxi.service.IPriceProductService; +import cc.yunxi.utils.UserContext; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; import java.util.Collections; import java.util.List; @@ -21,10 +31,13 @@ import java.util.List; @RequestMapping("/index") @RequiredArgsConstructor @Slf4j +@Validated public class IndexController { private final IPriceProductService priceProductService; + private final IClientService clientService; + @ApiOperation("废品类目列表") @GetMapping("/products") @@ -34,4 +47,17 @@ public class IndexController { return CommonResult.success(productSimpleVOList); } + + @ApiOperation("发起提现") + @GetMapping("/draw-cash") + @UserTypeAnnotation(UserTypeEnum.CLIENT) + public CommonResult drawCash( + @NotNull(message = "提取金额不能为空") + @Min(value = 1, message = "提取金额最小为1元") + @Max(value = 500, message = "提取金额最大为500元") Integer amount) throws Exception { + UserDTO userDTO = UserContext.getUser(); + clientService.cashBalance(userDTO.getOpenid(), amount); + return CommonResult.success(true); + } + } diff --git a/nxhs-service/src/main/java/cc/yunxi/service/IClientService.java b/nxhs-service/src/main/java/cc/yunxi/service/IClientService.java index 18ffde1..070be7d 100644 --- a/nxhs-service/src/main/java/cc/yunxi/service/IClientService.java +++ b/nxhs-service/src/main/java/cc/yunxi/service/IClientService.java @@ -7,10 +7,12 @@ import cc.yunxi.domain.query.ClientAccountQuery; import cc.yunxi.domain.query.ClientQuery; import cc.yunxi.domain.query.RecyclerQuery; import cc.yunxi.domain.vo.client.ClientUpdateVO; +import cc.yunxi.enums.BalanceChangeTypeEnum; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import javax.validation.Valid; +import java.math.BigDecimal; /** *

@@ -53,7 +55,6 @@ public interface IClientService extends IService { Client registerClient(String phoneNumber, String openId); - /** * 更新散户信息 * @param clientUpdateVO @@ -68,4 +69,20 @@ public interface IClientService extends IService { */ Page queryAccountStatementByPage(ClientAccountQuery clientAccountQuery); + + /** + * 通过openId更新散户余额 + * @param openId + * @param amount + * @param changeTypeEnum + */ + void changeBalanceByOpenId(String openId, BigDecimal amount, BalanceChangeTypeEnum changeTypeEnum); + + + /** + * 散户提现 + * @param openId + */ + void cashBalance(String openId, Integer amount) throws Exception; + } diff --git a/nxhs-service/src/main/java/cc/yunxi/service/impl/ClientServiceImpl.java b/nxhs-service/src/main/java/cc/yunxi/service/impl/ClientServiceImpl.java index a6d46e2..be31edd 100644 --- a/nxhs-service/src/main/java/cc/yunxi/service/impl/ClientServiceImpl.java +++ b/nxhs-service/src/main/java/cc/yunxi/service/impl/ClientServiceImpl.java @@ -4,6 +4,7 @@ import cc.yunxi.common.domain.LambdaQueryWrapperX; import cc.yunxi.common.exception.BizIllegalException; import cc.yunxi.common.exception.DbException; import cc.yunxi.common.utils.BeanUtils; +import cc.yunxi.config.props.WxPayV3Properties; import cc.yunxi.domain.dto.UserDTO; import cc.yunxi.domain.po.Client; import cc.yunxi.domain.po.ClientAccountDetail; @@ -13,21 +14,41 @@ import cc.yunxi.domain.query.ClientAccountQuery; import cc.yunxi.domain.query.ClientQuery; import cc.yunxi.domain.query.RecyclerQuery; import cc.yunxi.domain.vo.client.ClientUpdateVO; +import cc.yunxi.enums.BalanceChangeTypeEnum; import cc.yunxi.mapper.ClientAccountDetailMapper; import cc.yunxi.mapper.ClientMapper; import cc.yunxi.mapper.RecyclerMapper; import cc.yunxi.service.IClientService; import cc.yunxi.utils.UserContext; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ijpay.core.IJPayHttpResponse; +import com.ijpay.core.enums.RequestMethodEnum; +import com.ijpay.core.kit.PayKit; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.enums.WxDomainEnum; +import com.ijpay.wxpay.enums.v3.TransferApiEnum; +import com.ijpay.wxpay.model.v3.BatchTransferModel; +import com.ijpay.wxpay.model.v3.TransferDetailInput; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; +import java.math.BigDecimal; +import java.security.cert.X509Certificate; +import java.util.Collections; /** *

@@ -38,11 +59,19 @@ import javax.annotation.Resource; * @since 2024-02-28 06:08:09 */ @Service +@Slf4j public class ClientServiceImpl extends ServiceImpl implements IClientService { @Autowired private ClientAccountDetailMapper accountDetailMapper; + @Resource + private WxPayV3Properties wxPayV3Properties; + + private String serialNo; + + private final static int OK = 200; + @Override public Page queryClientByPage(ClientQuery clientQuery) { LambdaQueryWrapperX wrapperX = new LambdaQueryWrapperX<>(); @@ -94,6 +123,24 @@ public class ClientServiceImpl extends ServiceImpl impleme this.updateById(client); } + @Override // 金额交易! + public void changeBalanceByOpenId(String openId, BigDecimal amount, BalanceChangeTypeEnum changeTypeEnum) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Client::getWxOpenid, openId); + wrapper.last("for update"); + Client client = this.getOne(wrapper); + BigDecimal balance = client.getBanlance(); + if (changeTypeEnum.equals(BalanceChangeTypeEnum.INCOME)) { // 收入 + client.setBanlance(balance.add(amount)); + } else { // 支出/提现 + if (balance.compareTo(amount) < 0) { + throw new BizIllegalException("余额不足"); + } + client.setBanlance(balance.subtract(amount)); + } + this.updateById(client); + } + @Override public Page queryAccountStatementByPage(ClientAccountQuery clientAccountQuery) { @@ -106,6 +153,50 @@ public class ClientServiceImpl extends ServiceImpl impleme return accountDetailMapper.selectPage(pageDO, wrapperX); } + @Override + @Transactional(rollbackFor = Exception.class) + public void cashBalance(String openId, Integer amount) throws Exception { +// BatchTransferModel batchTransferModel = new BatchTransferModel() +// .setAppid(wxPayV3Properties.getAppId()) +// .setOut_batch_no(PayKit.generateStr()) +// .setBatch_name("测试商户转账到零钱") +// .setBatch_remark("测试商户转账到零钱") +// .setTotal_amount(amount) +// .setTotal_num(1) +// .setTransfer_detail_list(Collections.singletonList( +// new TransferDetailInput() +// .setOut_detail_no(PayKit.generateStr()) +// .setTransfer_amount(1) +// .setTransfer_remark("测试商户转账到零钱") +// .setOpenid(openId))); +// +// log.info("发起商家转账请求参数 {}", JSONUtil.toJsonStr(batchTransferModel)); +// // 删除 +// IJPayHttpResponse response = WxPayApi.v3( +// RequestMethodEnum.POST, +// WxDomainEnum.CHINA.toString(), +// TransferApiEnum.TRANSFER_BATCHES.toString(), +// wxPayV3Properties.getMchId(), +// getSerialNumber(), +// null, +// wxPayV3Properties.getKeyPath(), +// JSONUtil.toJsonStr(batchTransferModel) +// ); +// log.info("发起商家转账响应 {}", response); + // 根据证书序列号查询对应的证书来验证签名结果 +// boolean verifySignature = WxPayKit.verifySignature(response, wxPayV3Properties.getPlatformCertPath()); +// log.info("verifySignature: {}", verifySignature); +// if (response.getStatus() == OK && verifySignature) { +// return response.getBody(); +// } +// if (response.getStatus() != OK) { +// throw new BizIllegalException("提现失败"); +// } + // 模拟提现成功 + this.changeBalanceByOpenId(openId, new BigDecimal(amount), BalanceChangeTypeEnum.CASH_OUT); +// return response.getBody(); + } + // 校验散户是否存在 private void validateClientExists(String id) { @@ -113,4 +204,30 @@ public class ClientServiceImpl extends ServiceImpl impleme throw new BizIllegalException("散户不存在"); } } + + // 商户API证书序列号 + private String getSerialNumber() { + if (StrUtil.isEmpty(serialNo)) { + // 获取证书序列号 + X509Certificate certificate = PayKit.getCertificate(wxPayV3Properties.getCertPath()); + if (null != certificate) { + serialNo = certificate.getSerialNumber().toString(16).toUpperCase(); + // 提前两天检查证书是否有效 + boolean isValid = PayKit.checkCertificateIsValid(certificate, wxPayV3Properties.getMchId(), -2); + log.info("证书是否可用 {} 证书有效期为 {}", isValid, DateUtil.format(certificate.getNotAfter(), DatePattern.NORM_DATETIME_PATTERN)); + } +// System.out.println("输出证书信息:\n" + certificate.toString()); +// // 输出关键信息,截取部分并进行标记 +// System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16)); +// System.out.println("版本号:" + certificate.getVersion()); +// System.out.println("签发者:" + certificate.getIssuerDN()); +// System.out.println("有效起始日期:" + certificate.getNotBefore()); +// System.out.println("有效终止日期:" + certificate.getNotAfter()); +// System.out.println("主体名:" + certificate.getSubjectDN()); +// System.out.println("签名算法:" + certificate.getSigAlgName()); +// System.out.println("签名:" + certificate.getSignature().toString()); + } + System.out.println("serialNo:" + serialNo); + return serialNo; + } } diff --git a/nxhs-service/src/main/resources/apiclient_cert.pem b/nxhs-service/src/main/resources/apiclient_cert.pem new file mode 100644 index 0000000..0f6d1dc --- /dev/null +++ b/nxhs-service/src/main/resources/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCAxagAwIBAgIUL3BGpENxaOOyw7pbzx/5QVJq1N0wDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjQwMzExMDkwMjA4WhcNMjkwMzEwMDkwMjA4WjCBhzETMBEGA1UEAwwK +MTY2ODUyNzk1MzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTMwMQYDVQQL +DCrkuIrmtbfplb/msZ/kupHmga/mlbDlrZfnp5HmioDmnInpmZDlhazlj7gxCzAJ +BgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOKEZU4HFlimKzHLOgEV8lwJxkJrifWYSSo6l8SbNcv4lEgZ +mXha5y3a2XqzCAX3DIfXb5KiV1LP/QyWSAgsTV+g7ryyyP5i9JWvTiyWC8tHLGj5 +cp58V7ifq6UrXIWSeSt81r33ZHnngDIWYTtHPy3DfU+1+yLlKP3t+5UCWRp7w+DC +UZKen9ygAMW9ZXSIB/LasAu6d3h9CVZQd/cVG7pslvO/Iedf1jvVATNx5d08XLSY +bQGgenNpRrAIgnWzapa2fiBlHR7vmTI8GimmXEkHXLBy6GUbwrMOuLJ/qVWNtgGs +OZ5QKYyAufWfp8nf4ZibU0Mk/ZELr3iWAiJap1UCAwEAAaOBuTCBtjAJBgNVHRME +AjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDov +L2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUw +REJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJF +MTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IB +AQCt9QtqT/i2d6aJx4TNjuh3CF6WKjphFVAKqnse+3qr6PQR4D8NzgdbAECdBbJt +U0R20n4UGMZNijoIy1CaOqJCuLavuU+wZxvQnzFJihjCKWmn8NRs9vYtDaDpbns5 +X+9Dt/uw1tuvXZbRqg0X+bfPNBDUU/WBFqwQ2XpawVa2v5+BIvYovuOeI01VxuiF +MXqs6Lteg3FiMnOFgTYSPq5jcFZK/FvhPGVu8pmPkmDeRoc0jPPNDryvhVeR91cQ +kdEgYktuprSWJlXGP+Og4S7DLOBenLnwoB2MzN/E/jE2gHwxfff/ckyGVZ5nC2CJ +za+em1H7eL+LTMqdqHRckg0N +-----END CERTIFICATE----- diff --git a/nxhs-service/src/main/resources/apiclient_key.pem b/nxhs-service/src/main/resources/apiclient_key.pem new file mode 100644 index 0000000..cb1414f --- /dev/null +++ b/nxhs-service/src/main/resources/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDihGVOBxZYpisx +yzoBFfJcCcZCa4n1mEkqOpfEmzXL+JRIGZl4Wuct2tl6swgF9wyH12+SoldSz/0M +lkgILE1foO68ssj+YvSVr04slgvLRyxo+XKefFe4n6ulK1yFknkrfNa992R554Ay +FmE7Rz8tw31Ptfsi5Sj97fuVAlkae8PgwlGSnp/coADFvWV0iAfy2rALund4fQlW +UHf3FRu6bJbzvyHnX9Y71QEzceXdPFy0mG0BoHpzaUawCIJ1s2qWtn4gZR0e75ky +PBopplxJB1ywcuhlG8KzDriyf6lVjbYBrDmeUCmMgLn1n6fJ3+GYm1NDJP2RC694 +lgIiWqdVAgMBAAECggEAMY2nH8+7Gr+XWXhNAynK8EmPHr6p2plrABr7Up5lwW5q ++e7nvQWd51EmHIqH6c4m5pzBosFy+wHXKObDcU5HyvDI6WzvPX9mZEyKfISCpgC2 +/Wv8oCExi4kaorJ+thXZ/iJ8U+iICYcUa2Cr+qzWbGuEwWhdPUAGnK5wSCPQXFNn +oAuC86dD/IWsDgT5UreofNl0Sd4aepq3gNzAqpCKG16golkwMD0vybpaPc4uiwKv +CZt06GRfgwOzI4S5kqM/HYptgBQiO6+SKbiS2MKVmamadsN/NIpd/24xH6IYaiMX +sXYVd1gYxE8bQ79XdZ6xthLYoLGspGPfYWabym2CLQKBgQD1jITwJ+oVHyDscBan +TEJhOaVv7n1dMFka9JAouYYJrhTF2LAwSTmqf3Yp0YrY/ucv9WXRpzO6YBN2bWO+ +9PCgqgCEFirBEftfaC0CA2D2bpED5iBe6Y09ueJBIWSgzCFDiMlDhVKy0+ZraUZ3 +WkYNW9CEd3LnhQMnIiEEhsdMywKBgQDsKIIbog39E0hAY/02edH+tVEDjyJrdb2m +kqv65sH3NmFsWGWEeZLS0KJomgQdCuEOmdWWFqOIrfs6Ugmu/3g1hzC8O01xJEJr +XrI3Nnn+sghsdgHvorRYsb8pRZbPQD24p2MmbQcanPbO2HB8ydeD6G5WYju7Me6l +RvnjgHJ4XwKBgFEYE19tDzXijfEII7Mk0Fdjvmt1DBoWuZbZZjLM8qOHxnyAz89i +n5Tbe57cAUshBCEmnnXbDo5i5IqcHfEW29Fz06/L0lrpIWek9BhSHVfFtEbhXZd6 +8paMKVjxOlaQk/vF1RJjmahNqy+WGRuZyMDorbjR6jTkDOKDtvvTt0Z5AoGABPC4 +ABH8zu2HVmmBE5Gq1fQ/FJ767lqRNBnfZSlMp3pIwXZ78TCF5MkejKekLGNc3+xR +7ojctPBG0CqFL1cC0cPZPibTcOl8Rji966/FD5Hz4Sj602OI+E4HnLpq9Dz4zZMa +3OPtAR6Ff0BB4ipqysSjAkWd0EW0I1r/wUWfn2sCgYEA3OZ6OBEZ/sTJMGke9/CL +3k2mS9jKpqJTZfIUbNtwwhPah/vL13zVfT2srOzL0IPX7SIQGowHhg6NjxUTOayd +AGDQs65NfKkyt9+8bf4F93afo9fFQAxcRJ1rqXranscqDRfzdJFCubGIkYq9cN0J +6kUN10GoZtQZnCvSdMB8SSs= +-----END PRIVATE KEY----- diff --git a/nxhs-service/src/main/resources/wxpay_v3.properties b/nxhs-service/src/main/resources/wxpay_v3.properties new file mode 100644 index 0000000..9c3f139 --- /dev/null +++ b/nxhs-service/src/main/resources/wxpay_v3.properties @@ -0,0 +1,6 @@ +v3.appId=wx630bc4f43990c80c +v3.keyPath=apiclient_key.pem +v3.certPath=apiclient_cert.pem +v3.mchId=1668527953 +v3.apiKey3=529c750f4971d17db01ef595da8edfd3 +v3.domain=http://nxsh.cjyx.cc \ No newline at end of file