Files
PayPro/src/main/java/com/wendao/service/impl/OrderServiceImpl.java
2026-08-12 15:59:57 +08:00

746 lines
29 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.wendao.service.impl;
import cn.hutool.http.HttpUtil;
import com.wendao.config.PayProConfig;
import com.wendao.entity.Order;
import com.wendao.exception.ApiException;
import com.wendao.mapper.OrderMapper;
import com.wendao.service.ProductService;
import com.wendao.enums.OrderStatesEnum;
import com.wendao.model.req.GetOrderListReq;
import com.wendao.model.req.OpenApiOrderReq;
import com.wendao.model.resp.AddOrderResp;
import com.wendao.model.resp.CountResp;
import com.wendao.model.resp.OpenApiOrderResp;
import com.wendao.service.OrderService;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Snowflake;
import com.wendao.entity.Product;
import com.wendao.model.ResponseVO;
import com.wendao.common.utils.*;
import com.wendao.model.req.OrderReq;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wendao.utils.OpenApiSignUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
*/
@Service
public class OrderServiceImpl implements OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private OrderMapper orderMapper;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
OrderService thisService;
@Autowired
PayProConfig payProConfig;
@Autowired
private EmailUtils emailUtils;
@Autowired
ProductService productService;
@Autowired
Snowflake snowflake;
@Autowired
OpenApiSignUtil openApiSignUtil;
@Autowired
private ResourceLoader resourceLoader;
@Override
public Order getOrderById(String id) {
Order byId = orderMapper.selectById(id);
byId.setTime(StringUtils.getTimeStamp(byId.getCreateTime()));
return byId;
}
@Override
public int addOrder(Order pay) {
pay.setId(UUID.randomUUID().toString().replace("-", ""));
pay.setCreateTime(new Date());
pay.setState(OrderStatesEnum.WAIT_PAY.getState());
// 如果未设置匹配模式,默认为备注模式
if (pay.getMatchMode() == null) {
pay.setMatchMode("REMARK");
}
if (pay.getActualAmount() == null) {
pay.setActualAmount(pay.getMoney());
}
orderMapper.insert(pay);
return 1;
}
@Override
public int updateOrder(Order pay) {
pay.setUpdateTime(new Date());
orderMapper.updateById(pay);
return 1;
}
@Override
public int changeOrderState(String id, Integer state) {
Order pay = getOrderById(id);
pay.setState(state);
pay.setUpdateTime(new Date());
orderMapper.updateById(pay);
return 1;
}
@Override
public int delOrder(String id) {
orderMapper.deleteById(id);
return 1;
}
@Override
public CountResp statistic(Integer type, String start, String end) {
CountResp count = new CountResp();
if (type == -1) {
// 总
count.setAmount(orderMapper.countAllMoney());
count.setWeixin(orderMapper.countAllMoneyByType("Wechat"));
count.setAlipay(orderMapper.countAllMoneyByType("Alipay"));
return count;
}
Date startDate = null, endDate = null;
if (type == 0) {
// 今天
startDate = DateUtils.getDayBegin();
endDate = DateUtils.getDayEnd();
}
if (type == 6) {
// 昨天
startDate = DateUtils.getBeginDayOfYesterday();
endDate = DateUtils.getEndDayOfYesterDay();
} else if (type == 1) {
// 本周
startDate = DateUtils.getBeginDayOfWeek();
endDate = DateUtils.getEndDayOfWeek();
} else if (type == 2) {
// 本月
startDate = DateUtils.getBeginDayOfMonth();
endDate = DateUtils.getEndDayOfMonth();
} else if (type == 3) {
// 本年
startDate = DateUtils.getBeginDayOfYear();
endDate = DateUtils.getEndDayOfYear();
} else if (type == 4) {
// 上周
startDate = DateUtils.getBeginDayOfLastWeek();
endDate = DateUtils.getEndDayOfLastWeek();
} else if (type == 5) {
// 上个月
startDate = DateUtils.getBeginDayOfLastMonth();
endDate = DateUtils.getEndDayOfLastMonth();
} else if (type == -2) {
// 自定义
startDate = DateUtils.parseStartDate(start);
endDate = DateUtils.parseEndDate(end);
}
count.setAmount(orderMapper.countMoney(startDate, endDate));
count.setWeixin(orderMapper.countMoneyByType("Wechat", startDate, endDate));
count.setAlipay(orderMapper.countMoneyByType("Alipay", startDate, endDate));
return count;
}
@Override
@Transactional
public ResponseVO<AddOrderResp> addOrder(OrderReq req, HttpServletRequest request) {
if(StringUtils.isBlank(String.valueOf(req.getMoney()))){
return ResponseVO.errorResponse("请填写完整信息和正确金额");
}
String ip = IpInfoUtils.getIpAddr(request);
Order entity = new Order();
BeanUtils.copyProperties(req, entity);
// --- 减额匹配逻辑 ---
PayProConfig.Decrement decrementConfig = payProConfig.getDecrement();
boolean isCustomAmount = req.getCustom() != null && req.getCustom();
boolean isLocalQr = payProConfig.getUseLocalQrCode(req.getPayType());
boolean useDecrement = decrementConfig.isEnabled()
&& !isCustomAmount
&& isLocalQr
&& !"wechat_zs".equals(req.getPayType());
String matchMode = "REMARK";
BigDecimal actualAmount = req.getMoney();
Integer decrementIndex = null;
boolean fallbackToRemark = false;
if (useDecrement) {
String lockKey = getDecrementLockKey(
req.getPayType(), String.format("%.2f", req.getMoney()));
Boolean locked = null;
int retries = 3;
while (retries > 0) {
locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
break;
}
try { Thread.sleep(100); } catch (InterruptedException e) { break; }
retries--;
}
try {
DecrementSlotResult slotResult = tryAllocateDecrementSlot(
req.getPayType(), req.getMoney(), decrementConfig);
if (!slotResult.isFallbackToRemark()) {
matchMode = "DECREMENT";
actualAmount = slotResult.getActualAmount();
decrementIndex = slotResult.getSlotIndex();
} else {
matchMode = "REMARK";
actualAmount = slotResult.getActualAmount(); // base + step
fallbackToRemark = true;
}
} finally {
if (Boolean.TRUE.equals(locked)) {
redisTemplate.delete(lockKey);
}
}
}
entity.setMatchMode(matchMode);
entity.setActualAmount(actualAmount);
entity.setDecrementIndex(decrementIndex);
// --- 减额匹配逻辑结束 ---
try {
if (!isCustomAmount) {
int i = new Random().nextInt(payProConfig.getQrCodeNum()) + 1;
entity.setPayQrNum(i);
} else {
entity.setPayQrNum(1);
}
// 减额模式(非回退)不需要备注, 其他模式需要生成payNum
if ("DECREMENT".equals(matchMode) && !fallbackToRemark) {
entity.setPayNum(null);
} else {
entity.setPayNum(StringUtils.getRandomNum());
}
thisService.addOrder(entity);
} catch (Exception e) {
log.error(e.toString());
return ResponseVO.errorResponse("添加捐赠支付订单失败");
}
//记录缓存
redisTemplate.opsForValue().set(ip, "added", payProConfig.getRateLimit().getIpExpire(), TimeUnit.MINUTES);
//给管理员发送审核邮件
String tokenAdmin = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(entity.getId(), tokenAdmin, payProConfig.getToken().getExpire(), TimeUnit.DAYS);
entity = getAdminUrl(entity, entity.getId(), tokenAdmin, payProConfig.getToken().getValue());
emailUtils.sendTemplateMail(payProConfig.getEmail().getSender(), payProConfig.getEmail().getReceiver(), "" + payProConfig.getTitle() + "】待审核处理", "payment-review", entity);
AddOrderResp addPayResp = new AddOrderResp();
addPayResp.setId(entity.getId());
addPayResp.setPayNum(entity.getPayNum());
addPayResp.setPayType(entity.getPayType());
addPayResp.setMoney(entity.getMoney());
addPayResp.setPayQrNum(entity.getPayQrNum());
addPayResp.setCustom(isCustomAmount || fallbackToRemark);
addPayResp.setMatchMode(matchMode);
addPayResp.setActualAmount(actualAmount);
addPayResp.setFallbackToRemark(fallbackToRemark);
return ResponseVO.successResponse(addPayResp);
}
/**
* 处理充值业务,直接调用游戏的接口。订单状态在游戏的地方修改
*/
@Override
@Transactional
public int pass(String id) {
//Pay pay = thisService.changePayState(id, 1);
Order pay = orderMapper.selectById(id);
if (pay.getOrderSource().equals("PRODUCT") && pay.getProductId() != null) {
Product product = productService.getProductById(pay.getProductId() != null ? pay.getProductId().intValue() : null);
if (product != null && "CODE".equals(product.getType())) {
emailUtils.sendTemplateMail(payProConfig.getEmail().getSender(), pay.getEmail(), "【Pay个人收款支付系统】支付成功通知附下载链接",
"order-success", pay);
pay.setState(OrderStatesEnum.SUCCESS_PAY.getState());
orderMapper.updateById(pay);
}
} else if (pay.getOrderSource().equals("OPENAPI")) {
callbackFaka(pay.getNotifyUrl(),pay.getId(),pay.getMoney(),pay.getPayNum());
pay.setState(OrderStatesEnum.SUCCESS_PAY.getState());
orderMapper.updateById(pay);
}
return 1;
}
@Override
public IPage<Order> list(GetOrderListReq req) {
LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery();
wrapper.in(Order::getState, req.getStates());
if (StringUtils.isNotBlank(req.getOrderBy())) {
if (req.getOrder().equals("createTime")) {
if (req.getOrder().equals("desc")) {
wrapper.orderByDesc(Order::getCreateTime);
} else {
wrapper.orderByAsc(Order::getCreateTime);
}
}
if (req.getOrder().equals("money")) {
if (req.getOrder().equals("desc")) {
wrapper.orderByDesc(Order::getMoney);
} else {
wrapper.orderByAsc(Order::getMoney);
}
}
}
if (StringUtils.isNotBlank(req.getKeyword())) {
//字符串类型的处理统一全部like查询
wrapper.like(Order::getEmail, req.getKeyword());
wrapper.or().like(Order::getNickName, req.getKeyword());
}
Page<Order> page = new Page<>(req.getPageIndex(), req.getPageSize());
Page<Order> payPage = orderMapper.selectPage(page, wrapper);
for (Order record : payPage.getRecords()) {
// 屏蔽隐私数据
record.setId("");
record.setEmail("");
record.setPayNum(null);
record.setMobile(null);
record.setCustom(null);
record.setDevice(null);
}
return payPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public OpenApiOrderResp createOpenApiOrder(OpenApiOrderReq req) {
if (!openApiSignUtil.verifyTimestamp(req.getTimestamp())) {
throw new ApiException(ApiException.ErrorCode.TIMESTAMP_ERROR, "请求时间戳无效或已过期");
}
if (!openApiSignUtil.verifySign(req)) {
throw new ApiException(ApiException.ErrorCode.SIGN_ERROR, "签名验证失败");
}
if (req.getAmount() == null || req.getAmount().compareTo(new BigDecimal("0")) <= 0) {
throw new ApiException(ApiException.ErrorCode.AMOUNT_ERROR, "金额必须大于0");
}
if (req.getAmount().compareTo(new BigDecimal("100000")) > 0) {
throw new ApiException(ApiException.ErrorCode.AMOUNT_ERROR, "金额超出限制单笔订单不能超过100000元");
}
Order existingOrder = orderMapper.selectById(req.getOrderNo());
if (existingOrder != null) {
throw new ApiException(ApiException.ErrorCode.DUPLICATE_ORDER, "订单号已存在");
}
Order order = new Order();
order.setId(req.getOrderNo());
order.setMoney(req.getAmount());
order.setPayType(req.getPayType());
order.setNickName(req.getNickName());
order.setEmail(req.getEmail());
order.setNotifyUrl(req.getNotifyUrl());
order.setUserId(req.getUserId());
order.setProductId(req.getProductId());
order.setOrderSource("OPENAPI");
order.setState(OrderStatesEnum.WAIT_PAY.getState());
order.setCreateTime(new Date());
// --- 减额匹配逻辑(OpenAPI) ---
PayProConfig.Decrement decrementConfig = payProConfig.getDecrement();
boolean isCustomAmount = req.getCustom() != null && req.getCustom();
boolean isLocalQr = payProConfig.getUseLocalQrCode(req.getPayType());
boolean useDecrement = decrementConfig.isEnabled()
&& !isCustomAmount
&& isLocalQr
&& !"wechat_zs".equals(req.getPayType());
String matchMode = "REMARK";
BigDecimal actualAmount = req.getAmount();
Integer decrementIndex = null;
boolean fallbackToRemark = false;
if (useDecrement) {
String lockKey = getDecrementLockKey(
req.getPayType(), String.format("%.2f", req.getAmount()));
Boolean locked = false;
int retries = 3;
while (retries > 0) {
locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) { break; }
try { Thread.sleep(100); } catch (InterruptedException e) { break; }
retries--;
}
try {
DecrementSlotResult slotResult = tryAllocateDecrementSlot(
req.getPayType(), req.getAmount(), decrementConfig);
if (!slotResult.isFallbackToRemark()) {
matchMode = "DECREMENT";
actualAmount = slotResult.getActualAmount();
decrementIndex = slotResult.getSlotIndex();
} else {
matchMode = "REMARK";
actualAmount = slotResult.getActualAmount();
fallbackToRemark = true;
}
} finally {
if (Boolean.TRUE.equals(locked)) {
redisTemplate.delete(lockKey);
}
}
}
order.setMatchMode(matchMode);
order.setActualAmount(actualAmount);
order.setDecrementIndex(decrementIndex);
// 减额模式(非回退)不需要备注
if ("DECREMENT".equals(matchMode) && !fallbackToRemark) {
order.setPayNum(null);
} else {
order.setPayNum(StringUtils.getRandomNum());
}
// --- 减额匹配逻辑结束 ---
String qrUrl = "";
int i = new Random().nextInt(payProConfig.getQrCodeNum()) + 1;
order.setPayQrNum(i);
/** 查看二维码是否存在 */
// 格式化金额为两位小数
String formattedAmount = String.format("%.2f", req.getAmount());
String actualFormattedAmount = String.format("%.2f", actualAmount);
/** 如果不存在 */
// 减额模式或回退模式: 放在基础金额目录下; 普通备注模式: 检查金额QR是否存在
if ("DECREMENT".equals(matchMode) && !fallbackToRemark) {
if (actualAmount.compareTo(req.getAmount()) == 0) {
qrUrl = payProConfig.getSite() + "/assets/qr/" + req.getPayType() + "/" +
formattedAmount + "/" + i + ".png";
} else {
qrUrl = payProConfig.getSite() + "/assets/qr/" + req.getPayType() + "/" +
formattedAmount + "/" + actualFormattedAmount + "/" + i + ".png";
}
req.setCustom(false);
} else if (fallbackToRemark) {
// 回退模式: 金额+0.01区分如5.01放在5.00目录下
qrUrl = payProConfig.getSite() + "/assets/qr/" + req.getPayType() + "/" +
formattedAmount + "/" + actualFormattedAmount + "/" + i + ".png";
req.setCustom(true);
} else {
boolean b = checkQrFileExists(req.getPayType(), req.getAmount(), i);
if (!b) {
qrUrl = payProConfig.getSite() + "/assets/qr/" + req.getPayType() + "/" +
formattedAmount + "/" + i + ".png";
req.setCustom(false);
} else {
qrUrl = payProConfig.getSite() + "/assets/qr/" + req.getPayType() + "/" + "custom.png";
req.setCustom(true);
}
}
// 获取支付类型配置
Boolean useLocalQrCodeConfig = payProConfig.getUseLocalQrCode(req.getPayType());
String returnUrl = payProConfig.getSite() + "/payment.html?" +
"orderId=" + req.getOrderNo() +
"&money=" + req.getAmount() +
"&payType=" + req.getPayType() +
"&payNum=" + (order.getPayNum() != null ? order.getPayNum() : "") +
"&customerQr=" + (req.getCustom() || fallbackToRemark) +
"&picName=" + formattedAmount +
"&qrCode=" + "undefined" +
"&payQrNum=" + i +
"&useLocalQrCode=" + useLocalQrCodeConfig +
"&matchMode=" + matchMode +
"&actualAmount=" + actualFormattedAmount +
"&fallbackToRemark=" + fallbackToRemark;
try {
orderMapper.insert(order);
} catch (Exception e) {
log.error("创建OpenApi订单失败: {}", e.getMessage(), e);
throw new ApiException(ApiException.ErrorCode.SYSTEM_ERROR, "创建订单失败");
}
String tokenAdmin = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(order.getId(), tokenAdmin, payProConfig.getToken().getExpire(), TimeUnit.DAYS);
order = getAdminUrl(order, order.getId(), tokenAdmin, payProConfig.getToken().getValue());
emailUtils.sendTemplateMail(payProConfig.getEmail().getSender(), payProConfig.getEmail().getReceiver(), "【OPENAPI】待审核处理", "payment-review", order);
return OpenApiOrderResp.builder()
.orderId(order.getId())
.orderNo(req.getOrderNo())
.amount(req.getAmount())
.payType(req.getPayType())
.payNum(order.getPayNum())
.state(order.getState())
.message("订单创建成功")
.qrCodeUrl(qrUrl)
.returnUrl(returnUrl)
.timestamp(System.currentTimeMillis())
.matchMode(matchMode)
.actualAmount(actualAmount)
.fallbackToRemark(fallbackToRemark)
.build();
}
/**
* 检查指定支付类型和金额的二维码文件是否存在
*
* @param payType 支付类型,如 "wechat"、"alipay",需与文件夹名称匹配
* @param amount 金额,将格式化为两位小数作为文件夹名
* @param qrNum 二维码编号,用于构建文件名
* @return 文件存在返回 true否则返回 false
*/
private boolean checkQrFileExists(String payType, BigDecimal amount, int qrNum) {
// 格式化金额为两位小数,与文件夹名称一致
String amountStr = String.format("%.2f", amount);
String relativePath = payType.toLowerCase() + "/" + amountStr + "/" + qrNum + ".png";
// 优先检查外部目录
String qrDir = payProConfig.getQrDir();
if (org.springframework.util.StringUtils.hasText(qrDir)) {
String externalPath = qrDir.endsWith("/") ? qrDir + relativePath : qrDir + "/" + relativePath;
try {
if (resourceLoader.getResource("file:" + externalPath).exists()) {
return true;
}
} catch (Exception e) {
// ignore, try classpath fallback
}
}
// classpath 回退(修复原 bugstatic/qr -> static/assets/qr
String classpathPath = "classpath:static/assets/qr/" + relativePath;
try {
Resource resource = resourceLoader.getResource(classpathPath);
return resource.exists();
} catch (Exception e) {
return false;
}
}
/**
* 拼接管理员链接
*/
public Order getAdminUrl(Order pay, String id, String token, String myToken) {
String pass = payProConfig.getSite() + "/order/pass?id=" + id + "&token=" + token + "&myToken=" + myToken;
pay.setPassUrl(pass);
String back = payProConfig.getSite() + "/order/back?id=" + id + "&token=" + token + "&myToken=" + myToken;
pay.setBackUrl(back);
String edit = payProConfig.getSite() + "/order-edit?id=" + id + "&token=" + token;
pay.setEditUrl(edit);
String del = payProConfig.getSite() + "/order-del?id=" + id + "&token=" + token;
pay.setDelUrl(del);
String close = payProConfig.getSite() + "/order-close?id=" + id + "&token=" + token;
pay.setCloseUrl(close);
String statistic = payProConfig.getSite() + "/statistic?myToken=" + myToken;
pay.setStatistic(statistic);
return pay;
}
public void callbackFaka(String notifyUrl, String orderNo, BigDecimal amount, String payNum) {
Map<String, Object> params = new TreeMap<>();
params.put("orderNo", orderNo);
params.put("amount", amount);
params.put("payNum", payNum);
// 生成签名
String sign = openApiSignUtil.generateSign(params);
params.put("sign", sign);
HttpUtil.post(notifyUrl, com.alibaba.fastjson2.JSONObject.toJSONString(params));
}
// ==================== 减额匹配相关方法 ====================
/**
* 减额匹配槽位分配结果
*/
@lombok.Data
private static class DecrementSlotResult {
private boolean success;
private boolean fallbackToRemark;
private int slotIndex;
private BigDecimal actualAmount;
}
/**
* 尝试分配减额槽位
* maxCount = 向下递减次数(不含基准价)
* 例如 maxCount=2: 槽位0=5.00, 槽位1=4.99, 槽位2=4.98
*
* @param payType 支付类型
* @param baseAmount 基础金额(原始订单金额)
* @param config 减额配置
* @return 槽位分配结果
*/
private DecrementSlotResult tryAllocateDecrementSlot(
String payType, BigDecimal baseAmount, PayProConfig.Decrement config) {
BigDecimal step = config.getStep();
int maxCount = config.getMaxCount();
// 收集已占用的槽位索引
Set<Integer> occupiedSlots = new HashSet<>();
for (int i = 0; i <= maxCount; i++) {
BigDecimal slotAmount = baseAmount.subtract(step.multiply(new BigDecimal(i)));
// 实际金额必须 > 0
if (slotAmount.compareTo(BigDecimal.ZERO) <= 0) {
occupiedSlots.add(i);
continue;
}
// 检查该金额的QR码文件是否存在(任意编号)
if (!checkAnyQrFileExists(payType, baseAmount, slotAmount)) {
log.warn("减额槽位{}金额{}的QR码文件不存在跳过该槽位", i, String.format("%.2f", slotAmount));
occupiedSlots.add(i);
continue;
}
// 查询该金额下是否有活跃订单
List<Order> activeOrders = orderMapper.selectList(
new LambdaQueryWrapper<Order>()
.eq(Order::getActualAmount, slotAmount)
.eq(Order::getMatchMode, "DECREMENT")
.in(Order::getState, 0, 4));
if (activeOrders != null && !activeOrders.isEmpty()) {
occupiedSlots.add(i);
}
}
// 找第一个可用槽位
for (int i = 0; i <= maxCount; i++) {
if (!occupiedSlots.contains(i)) {
BigDecimal actualAmount = baseAmount.subtract(step.multiply(new BigDecimal(i)));
DecrementSlotResult result = new DecrementSlotResult();
result.setSuccess(true);
result.setSlotIndex(i);
result.setActualAmount(actualAmount);
result.setFallbackToRemark(false);
return result;
}
}
// 所有槽位被占用 -- 回退到备注模式
DecrementSlotResult result = new DecrementSlotResult();
result.setSuccess(true);
result.setFallbackToRemark(true);
// 回退金额 = 基础金额 + step用不同金额区分
result.setActualAmount(baseAmount.add(step));
return result;
}
/**
* 检查指定支付类型和金额下是否有任意QR码文件存在(使用正确的assets/qr路径)
* 减额二维码放在基础金额目录下: {payType}/{baseAmount}/{amount}/{qrNum}.png
* 槽位0(金额等于基础金额)保持原有路径: {payType}/{baseAmount}/{qrNum}.png
*/
private boolean checkAnyQrFileExists(String payType, BigDecimal baseAmount, BigDecimal amount) {
String baseAmountStr = String.format("%.2f", baseAmount);
String amountStr = String.format("%.2f", amount);
for (int i = 1; i <= payProConfig.getQrCodeNum(); i++) {
String filePath;
if (amount.compareTo(baseAmount) == 0) {
// 槽位0: 基础金额目录下直接放二维码
filePath = "classpath:static/assets/qr/" + payType.toLowerCase()
+ "/" + baseAmountStr + "/" + i + ".png";
} else {
// 减额槽位: 放在基础金额目录下的子目录
filePath = "classpath:static/assets/qr/" + payType.toLowerCase()
+ "/" + baseAmountStr + "/" + amountStr + "/" + i + ".png";
}
try {
Resource resource = resourceLoader.getResource(filePath);
if (resource.exists()) {
return true;
}
} catch (Exception e) {
// ignore, try next
}
}
return false;
}
@Override
public Order getByActualAmount(BigDecimal actualAmount, Date startTime, Date endTime) {
return orderMapper.getDecrementOrderByAmount(actualAmount, startTime, endTime);
}
@Override
public Set<Integer> getOccupiedDecrementSlots(String payType, BigDecimal baseAmount) {
PayProConfig.Decrement config = payProConfig.getDecrement();
if (config == null || !config.isEnabled()) {
return Collections.emptySet();
}
Set<Integer> occupied = new HashSet<>();
BigDecimal step = config.getStep();
for (int i = 0; i <= config.getMaxCount(); i++) {
BigDecimal slotAmount = baseAmount.subtract(step.multiply(new BigDecimal(i)));
if (slotAmount.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
List<Order> orders = orderMapper.selectList(
new LambdaQueryWrapper<Order>()
.eq(Order::getActualAmount, slotAmount)
.eq(Order::getMatchMode, "DECREMENT")
.in(Order::getState, 0, 4));
if (orders != null && !orders.isEmpty()) {
occupied.add(i);
}
}
return occupied;
}
public static String getDecrementLockKey(String payType, String baseAmount) {
return "pay:decrement_lock:" + payType + ":" + baseAmount;
}
}