feat: 提现

pull/1121/head
梁宇奇 2025-04-22 22:35:46 +08:00
parent 7ee4abd483
commit a2cc166908
11 changed files with 330 additions and 18 deletions

View File

@ -47,6 +47,10 @@ public class ExternalAccount extends SimpleEntity {
*
*/
private Boolean accountAuthAccess;
/**
*
*/
private String password;
/**
*
*/

View File

@ -48,6 +48,10 @@ public class ExternalAccountDTO {
*
*/
private Boolean accountAuthAccess;
/**
*
*/
private String password;
/**
*
*/

View File

@ -0,0 +1,42 @@
package com.ruoyi.xkt.enums;
import com.ruoyi.common.exception.ServiceException;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author liangyq
* @date 2025-04-22 13:42
*/
@Getter
@AllArgsConstructor
public enum EAccountType {
ALI_PAY(1, "支付宝"),
;
private final Integer value;
private final String label;
public static EAccountType of(Integer value) {
for (EAccountType e : EAccountType.values()) {
if (e.getValue().equals(value)) {
return e;
}
}
return null;
}
/**
*
*
* @param payChannel
* @return
*/
public static EAccountType getByChannel(EPayChannel payChannel) {
if (EPayChannel.ALI_PAY == payChannel) {
return ALI_PAY;
}
throw new ServiceException("无法获取与支付通道匹配的账户类型");
}
}

View File

@ -5,6 +5,8 @@ import com.ruoyi.xkt.dto.order.StoreOrderRefund;
import com.ruoyi.xkt.enums.EPayChannel;
import com.ruoyi.xkt.enums.EPayPage;
import java.math.BigDecimal;
/**
* @author liangyq
* @date 2025-04-06 19:36
@ -42,4 +44,14 @@ public interface PaymentManager {
*/
boolean isStoreOrderPaid(String orderNo);
/**
*
*
* @param bizNo
* @param identity
* @param realName
* @param amount
*/
void transfer(String bizNo, String identity, String realName, BigDecimal amount);
}

View File

@ -1,19 +1,18 @@
package com.ruoyi.xkt.manager.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.NumberUtil;
import com.alibaba.fastjson2.JSON;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.diagnosis.DiagnosisUtils;
import com.alipay.api.domain.AlipayFundTransUniTransferModel;
import com.alipay.api.domain.AlipayTradeQueryModel;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.domain.Participant;
import com.alipay.api.request.*;
import com.alipay.api.response.AlipayFundTransUniTransferResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.ruoyi.common.exception.ServiceException;
@ -150,9 +149,9 @@ public class AliPaymentMangerImpl implements PaymentManager {
model.setTradeNo(orderRefund.getOriginOrder().getPayTradeNo());
// 设置退款金额
BigDecimal amount = BigDecimal.ZERO;
for (StoreOrderDetail orderDetail:orderRefund.getRefundOrderDetails()){
for (StoreOrderDetail orderDetail : orderRefund.getRefundOrderDetails()) {
//TODO 暂时商品金额+快递费一起退,需调整为实际退款金额
amount = NumberUtil.add(amount,orderDetail.getTotalAmount());
amount = NumberUtil.add(amount, orderDetail.getTotalAmount());
}
model.setRefundAmount(amount.toPlainString());
// 设置退款原因说明
@ -161,12 +160,12 @@ public class AliPaymentMangerImpl implements PaymentManager {
model.setOutRequestNo(orderRefund.getRefundOrder().getOrderNo());
try {
AlipayTradeRefundResponse response = alipayClient.execute(request);
log.info("支付宝退款:{}",response.getBody());
if (response.isSuccess()){
log.info("支付宝退款:{}", response.getBody());
if (response.isSuccess()) {
//TODO 沙箱环境接口不通
return;
}
}catch (Exception e){
} catch (Exception e) {
log.error("退款异常", e);
}
throw new ServiceException("退款失败");
@ -215,4 +214,51 @@ public class AliPaymentMangerImpl implements PaymentManager {
throw new ServiceException("查询订单支付结果失败");
}
@Override
public void transfer(String bizNo, String identity, String realName, BigDecimal amount) {
Assert.notEmpty(bizNo);
Assert.notEmpty(identity);
Assert.notEmpty(realName);
Assert.notNull(amount);
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, appId, privateKey, DEFAULT_FORMAT, charset,
alipayPublicKey, signType);
// 构造请求参数以调用接口
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
// 设置商家侧唯一订单号
model.setOutBizNo(bizNo);
// 设置订单总金额
model.setTransAmount(amount.toPlainString());
// 设置描述特定的业务场景
model.setBizScene("DIRECT_TRANSFER");
// 设置业务产品码
model.setProductCode("TRANS_ACCOUNT_NO_PWD");
// 设置收款方信息
Participant payeeInfo = new Participant();
payeeInfo.setIdentity(identity);
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
payeeInfo.setName(realName);
model.setPayeeInfo(payeeInfo);
// 设置业务备注
model.setRemark("档口提现");
// 设置转账业务请求的扩展参数
model.setBusinessParams("{\"payer_show_name_use_alias\":\"true\"}");
request.setBizModel(model);
AlipayFundTransUniTransferResponse response;
try {
response = alipayClient.certificateExecute(request);
if (response.isSuccess()) {
//TODO 接口未调通
return;
} else {
//获取诊断链接
String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
log.warn("支付宝转账异常: {}", diagnosisUrl);
}
} catch (Exception e) {
log.error("支付宝转账异常", e);
}
throw new ServiceException("支付宝转账失败");
}
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.xkt.service;
import com.ruoyi.xkt.dto.finance.FinanceBillExt;
import com.ruoyi.xkt.enums.EPayChannel;
import java.math.BigDecimal;
/**
* @author liangyq
* @date 2025-04-22 21:06
*/
public interface IAccountService {
/**
*
*
* @param storeId
* @param amount
* @param password
* @param payChannel
* @return
*/
FinanceBillExt prepareWithdraw(Long storeId, BigDecimal amount, String password, EPayChannel payChannel);
/**
*
*
* @param financeBillId
*/
void withdrawSuccess(Long financeBillId);
}

View File

@ -2,9 +2,7 @@ package com.ruoyi.xkt.service;
import com.ruoyi.xkt.domain.ExternalAccount;
import com.ruoyi.xkt.dto.finance.TransInfo;
import com.ruoyi.xkt.enums.EEntryStatus;
import com.ruoyi.xkt.enums.EFinBillType;
import com.ruoyi.xkt.enums.ELoanDirection;
import com.ruoyi.xkt.enums.*;
/**
* @author liangyq
@ -19,6 +17,16 @@ public interface IExternalAccountService {
*/
ExternalAccount getById(Long id);
/**
*
*
* @param ownerId
* @param ownerType
* @param accountType
* @return
*/
ExternalAccount getExternalAccount(Long ownerId, EAccountOwnerType ownerType, EAccountType accountType);
/**
*
*

View File

@ -4,6 +4,8 @@ import com.ruoyi.xkt.dto.finance.FinanceBillExt;
import com.ruoyi.xkt.dto.order.StoreOrderExt;
import com.ruoyi.xkt.enums.EPayChannel;
import java.math.BigDecimal;
/**
* @author liangyq
* @date 2025-04-08 21:14
@ -42,5 +44,22 @@ public interface IFinanceBillService {
*/
void entryRefundOrderPaymentBill(Long storeOrderId);
/**
*
*
* @param storeId
* @param amount
* @param payChannel
* @return
*/
FinanceBillExt createWithdrawPaymentBill(Long storeId, BigDecimal amount, EPayChannel payChannel);
/**
*
*
* @param financeBillId
*/
void entryWithdrawPaymentBill(Long financeBillId);
}

View File

@ -0,0 +1,70 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.xkt.domain.ExternalAccount;
import com.ruoyi.xkt.domain.InternalAccount;
import com.ruoyi.xkt.dto.finance.FinanceBillExt;
import com.ruoyi.xkt.enums.EAccountOwnerType;
import com.ruoyi.xkt.enums.EAccountStatus;
import com.ruoyi.xkt.enums.EAccountType;
import com.ruoyi.xkt.enums.EPayChannel;
import com.ruoyi.xkt.service.IAccountService;
import com.ruoyi.xkt.service.IExternalAccountService;
import com.ruoyi.xkt.service.IFinanceBillService;
import com.ruoyi.xkt.service.IInternalAccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* @author liangyq
* @date 2025-04-22 21:07
*/
@Slf4j
@Service
public class AccountServiceImpl implements IAccountService {
@Autowired
private IFinanceBillService financeBillService;
@Autowired
private IInternalAccountService internalAccountService;
@Autowired
private IExternalAccountService externalAccountService;
@Transactional(rollbackFor = Exception.class)
@Override
public FinanceBillExt prepareWithdraw(Long storeId, BigDecimal amount, String password, EPayChannel payChannel) {
Assert.notNull(storeId);
Assert.notEmpty(password);
Assert.isTrue(NumberUtil.isLessOrEqual(amount, BigDecimal.ZERO), "提现金额异常");
InternalAccount internalAccount = internalAccountService.getInternalAccount(storeId, EAccountOwnerType.STORE);
ExternalAccount externalAccount = externalAccountService.getExternalAccount(storeId, EAccountOwnerType.STORE,
EAccountType.getByChannel(payChannel));
if (!EAccountStatus.NORMAL.getValue().equals(internalAccount.getAccountStatus())
|| !EAccountStatus.NORMAL.getValue().equals(externalAccount.getAccountStatus())) {
throw new ServiceException("档口账户已冻结");
}
if (StrUtil.isEmpty(externalAccount.getPassword())) {
throw new ServiceException("请先设置支付密码");
}
if (!StrUtil.equals(password, externalAccount.getPassword())) {
throw new ServiceException("支付密码错误");
}
return financeBillService.createWithdrawPaymentBill(storeId, amount, payChannel);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void withdrawSuccess(Long financeBillId) {
financeBillService.entryWithdrawPaymentBill(financeBillId);
}
}

View File

@ -1,6 +1,7 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.NumberUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.constant.Constants;
@ -10,10 +11,7 @@ import com.ruoyi.common.utils.bean.BeanValidators;
import com.ruoyi.xkt.domain.ExternalAccount;
import com.ruoyi.xkt.domain.ExternalAccountTransDetail;
import com.ruoyi.xkt.dto.finance.TransInfo;
import com.ruoyi.xkt.enums.EAccountStatus;
import com.ruoyi.xkt.enums.EEntryStatus;
import com.ruoyi.xkt.enums.EFinBillType;
import com.ruoyi.xkt.enums.ELoanDirection;
import com.ruoyi.xkt.enums.*;
import com.ruoyi.xkt.mapper.ExternalAccountMapper;
import com.ruoyi.xkt.mapper.ExternalAccountTransDetailMapper;
import com.ruoyi.xkt.service.IExternalAccountService;
@ -47,6 +45,22 @@ public class ExternalAccountServiceImpl implements IExternalAccountService {
return externalAccountMapper.selectById(id);
}
@Override
public ExternalAccount getExternalAccount(Long ownerId, EAccountOwnerType ownerType, EAccountType accountType) {
Assert.notNull(ownerId);
Assert.notNull(ownerType);
Assert.notNull(accountType);
ExternalAccount account = externalAccountMapper.selectOne(Wrappers.lambdaQuery(ExternalAccount.class)
.eq(ExternalAccount::getOwnerId, ownerId)
.eq(ExternalAccount::getOwnerType, ownerType.getValue())
.eq(ExternalAccount::getAccountType, accountType.getValue()));
if (!BeanValidators.exists(account)) {
throw new ServiceException(CharSequenceUtil.format("{}[{}]未设置{}账户", ownerType.getLabel(),
ownerId, accountType.getLabel()));
}
return account;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int addTransDetail(Long externalAccountId, TransInfo transInfo, ELoanDirection loanDirection,

View File

@ -1,5 +1,7 @@
package com.ruoyi.xkt.service.impl;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.NumberUtil;
@ -19,7 +21,6 @@ import com.ruoyi.xkt.mapper.FinanceBillMapper;
import com.ruoyi.xkt.service.IExternalAccountService;
import com.ruoyi.xkt.service.IFinanceBillService;
import com.ruoyi.xkt.service.IInternalAccountService;
import io.jsonwebtoken.lang.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -223,6 +224,68 @@ public class FinanceBillServiceImpl implements IFinanceBillService {
internalAccountService.entryTransDetail(financeBill.getId(), EFinBillType.of(financeBill.getBillType()));
}
@Transactional(rollbackFor = Exception.class)
@Override
public FinanceBillExt createWithdrawPaymentBill(Long storeId, BigDecimal amount, EPayChannel payChannel) {
Assert.notNull(storeId);
Assert.isTrue(NumberUtil.isLessOrEqual(amount, BigDecimal.ZERO), "提现金额异常");
Assert.notNull(payChannel);
FinanceBill bill = new FinanceBill();
bill.setBillNo(generateBillNo(EFinBillType.PAYMENT));
bill.setBillType(EFinBillType.PAYMENT.getValue());
//执行中
bill.setBillStatus(EFinBillStatus.PROCESSING.getValue());
bill.setSrcType(EFinBillSrcType.WITHDRAW.getValue());
bill.setSrcId(storeId);
//业务唯一键
String businessUniqueKey = IdUtil.simpleUUID();
bill.setBusinessUniqueKey(businessUniqueKey);
Long outputInternalAccountId = internalAccountService.getInternalAccount(storeId, EAccountOwnerType.STORE)
.getId();
Long inputExternalAccountId = externalAccountService.getExternalAccount(storeId, EAccountOwnerType.STORE,
EAccountType.getByChannel(payChannel)).getId();
bill.setOutputInternalAccountId(outputInternalAccountId);
bill.setInputExternalAccountId(inputExternalAccountId);
bill.setOutputExternalAccountId(payChannel.getPlatformExternalAccountId());
bill.setBusinessAmount(amount);
bill.setTransAmount(amount);
bill.setVersion(0L);
bill.setDelFlag(Constants.UNDELETED);
financeBillMapper.insert(bill);
TransInfo transInfo = TransInfo.builder()
.srcBillId(bill.getId())
.srcBillType(bill.getBillType())
.transAmount(bill.getTransAmount())
.transTime(new Date())
.handlerId(null)
.remark("账户提现")
.build();
//内部账户
internalAccountService.addTransDetail(outputInternalAccountId, transInfo,
ELoanDirection.CREDIT, EEntryStatus.WAITING);
//外部账户
externalAccountService.addTransDetail(inputExternalAccountId, transInfo,
ELoanDirection.DEBIT, EEntryStatus.WAITING);
externalAccountService.addTransDetail(payChannel.getPlatformExternalAccountId(), transInfo,
ELoanDirection.CREDIT, EEntryStatus.WAITING);
return new FinanceBillExt(bill, ListUtil.empty());
}
@Transactional(rollbackFor = Exception.class)
@Override
public void entryWithdrawPaymentBill(Long financeBillId) {
Assert.notNull(financeBillId);
//业务唯一键
FinanceBill financeBill = financeBillMapper.selectById(financeBillId);
if (!BeanValidators.exists(financeBill)) {
throw new ServiceException(CharSequenceUtil.format("提现付款单[{}]不存在", financeBillId));
}
//内部账户入账
internalAccountService.entryTransDetail(financeBill.getId(), EFinBillType.of(financeBill.getBillType()));
//外部账户入账
externalAccountService.entryTransDetail(financeBill.getId(), EFinBillType.of(financeBill.getBillType()));
}
/**
*