散户余额提现功能开发v1

wxpay
LI-CCONG\李聪聪 7 months ago
parent 2f593b9022
commit 8a8f5e0d84

@ -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;
}

@ -1,18 +1,28 @@
package cc.yunxi.controller; package cc.yunxi.controller;
import cc.yunxi.aspect.UserTypeAnnotation;
import cc.yunxi.common.domain.CommonResult; import cc.yunxi.common.domain.CommonResult;
import cc.yunxi.common.utils.BeanUtils; import cc.yunxi.common.utils.BeanUtils;
import cc.yunxi.domain.dto.UserDTO; 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.po.Product;
import cc.yunxi.domain.vo.priceproduct.ProductSimpleVO; 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.service.IPriceProductService;
import cc.yunxi.utils.UserContext;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; 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.Collections;
import java.util.List; import java.util.List;
@ -21,10 +31,13 @@ import java.util.List;
@RequestMapping("/index") @RequestMapping("/index")
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@Validated
public class IndexController { public class IndexController {
private final IPriceProductService priceProductService; private final IPriceProductService priceProductService;
private final IClientService clientService;
@ApiOperation("废品类目列表") @ApiOperation("废品类目列表")
@GetMapping("/products") @GetMapping("/products")
@ -34,4 +47,17 @@ public class IndexController {
return CommonResult.success(productSimpleVOList); return CommonResult.success(productSimpleVOList);
} }
@ApiOperation("发起提现")
@GetMapping("/draw-cash")
@UserTypeAnnotation(UserTypeEnum.CLIENT)
public CommonResult<Boolean> 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);
}
} }

@ -7,10 +7,12 @@ import cc.yunxi.domain.query.ClientAccountQuery;
import cc.yunxi.domain.query.ClientQuery; import cc.yunxi.domain.query.ClientQuery;
import cc.yunxi.domain.query.RecyclerQuery; import cc.yunxi.domain.query.RecyclerQuery;
import cc.yunxi.domain.vo.client.ClientUpdateVO; 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.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import javax.validation.Valid; import javax.validation.Valid;
import java.math.BigDecimal;
/** /**
* <p> * <p>
@ -53,7 +55,6 @@ public interface IClientService extends IService<Client> {
Client registerClient(String phoneNumber, String openId); Client registerClient(String phoneNumber, String openId);
/** /**
* *
* @param clientUpdateVO * @param clientUpdateVO
@ -68,4 +69,20 @@ public interface IClientService extends IService<Client> {
*/ */
Page<ClientAccountDetail> queryAccountStatementByPage(ClientAccountQuery clientAccountQuery); Page<ClientAccountDetail> 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;
} }

@ -4,6 +4,7 @@ import cc.yunxi.common.domain.LambdaQueryWrapperX;
import cc.yunxi.common.exception.BizIllegalException; import cc.yunxi.common.exception.BizIllegalException;
import cc.yunxi.common.exception.DbException; import cc.yunxi.common.exception.DbException;
import cc.yunxi.common.utils.BeanUtils; import cc.yunxi.common.utils.BeanUtils;
import cc.yunxi.config.props.WxPayV3Properties;
import cc.yunxi.domain.dto.UserDTO; import cc.yunxi.domain.dto.UserDTO;
import cc.yunxi.domain.po.Client; import cc.yunxi.domain.po.Client;
import cc.yunxi.domain.po.ClientAccountDetail; 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.ClientQuery;
import cc.yunxi.domain.query.RecyclerQuery; import cc.yunxi.domain.query.RecyclerQuery;
import cc.yunxi.domain.vo.client.ClientUpdateVO; import cc.yunxi.domain.vo.client.ClientUpdateVO;
import cc.yunxi.enums.BalanceChangeTypeEnum;
import cc.yunxi.mapper.ClientAccountDetailMapper; import cc.yunxi.mapper.ClientAccountDetailMapper;
import cc.yunxi.mapper.ClientMapper; import cc.yunxi.mapper.ClientMapper;
import cc.yunxi.mapper.RecyclerMapper; import cc.yunxi.mapper.RecyclerMapper;
import cc.yunxi.service.IClientService; import cc.yunxi.service.IClientService;
import cc.yunxi.utils.UserContext; 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.ObjectUtil;
import cn.hutool.core.util.StrUtil; 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.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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 javax.annotation.Resource;
import java.math.BigDecimal;
import java.security.cert.X509Certificate;
import java.util.Collections;
/** /**
* <p> * <p>
@ -38,11 +59,19 @@ import javax.annotation.Resource;
* @since 2024-02-28 06:08:09 * @since 2024-02-28 06:08:09
*/ */
@Service @Service
@Slf4j
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements IClientService { public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements IClientService {
@Autowired @Autowired
private ClientAccountDetailMapper accountDetailMapper; private ClientAccountDetailMapper accountDetailMapper;
@Resource
private WxPayV3Properties wxPayV3Properties;
private String serialNo;
private final static int OK = 200;
@Override @Override
public Page<Client> queryClientByPage(ClientQuery clientQuery) { public Page<Client> queryClientByPage(ClientQuery clientQuery) {
LambdaQueryWrapperX<Client> wrapperX = new LambdaQueryWrapperX<>(); LambdaQueryWrapperX<Client> wrapperX = new LambdaQueryWrapperX<>();
@ -94,6 +123,24 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
this.updateById(client); this.updateById(client);
} }
@Override // 金额交易!
public void changeBalanceByOpenId(String openId, BigDecimal amount, BalanceChangeTypeEnum changeTypeEnum) {
LambdaQueryWrapper<Client> 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 @Override
public Page<ClientAccountDetail> queryAccountStatementByPage(ClientAccountQuery clientAccountQuery) { public Page<ClientAccountDetail> queryAccountStatementByPage(ClientAccountQuery clientAccountQuery) {
@ -106,6 +153,50 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
return accountDetailMapper.selectPage(pageDO, wrapperX); 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) { private void validateClientExists(String id) {
@ -113,4 +204,30 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
throw new BizIllegalException("散户不存在"); 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;
}
} }

@ -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-----

@ -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-----

@ -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
Loading…
Cancel
Save