会员功能

This commit is contained in:
磷叶 2025-01-21 16:10:25 +08:00
parent 92b327658a
commit b1ff08d49b
18 changed files with 422 additions and 159 deletions

View File

@ -63,15 +63,9 @@ public class IotServiceImpl implements IotService {
@Value(value = "${sm.daysToExpire}")
private Long daysToExpire;
@Value(value = "${debug}")
private Boolean debug;
@Autowired
private IotConverter iotConverter;
@Autowired
private ScheduledExecutorService scheduledExecutorService;
@Autowired
private ICommandLogService commandLogService;
@ -100,7 +94,7 @@ public class IotServiceImpl implements IotService {
if (StringUtils.isBlank(status)) {
// log.info("进入command");
// 发送命令
CommandResponse res = uploadData(deviceName, productId, "获取在线状态", false);
CommandResponse res = uploadData(deviceName, productId, "获取在线状态");
// 若是离线则直接返回离线
if (res != null && res.isNotOnline()) {
@ -280,19 +274,19 @@ public class IotServiceImpl implements IotService {
ServiceUtil.assertion(device == null || StringUtils.isBlank(device.getProductId()), "设备为空");
CommandResponse res = null;
if (StringUtils.hasText(device.iotMac2())) {
res = this.uploadData(device.iotMac2(), device.getProductId(), reason, recordLog);
res = this.uploadData(device.iotMac2(), device.getProductId(), reason);
}
if ((res == null || !res.isSuccess()) && StringUtils.hasText(device.iotMac1())) {
res = this.uploadData(device.iotMac1(), device.getProductId(), reason, recordLog);
res = this.uploadData(device.iotMac1(), device.getProductId(), reason);
}
return res;
}
private CommandResponse uploadData(String deviceName, String productId, String reason, boolean recordLog) {
private CommandResponse uploadData(String deviceName, String productId, String reason) {
if (StringUtils.isBlank(deviceName) || StringUtils.isBlank(productId)) {
return null;
}
return sendCommand(deviceName, IotConstants.COMMAND_UPLOAD_DATA, productId, reason, recordLog);
return sendCommand(deviceName, IotConstants.COMMAND_UPLOAD_DATA, productId, reason);
}
private CommandResponse setVxs(String deviceName, BigDecimal vxs, String productId, String reason) {
@ -397,7 +391,7 @@ public class IotServiceImpl implements IotService {
if (seconds < 0) {
throw new ServiceException("设置剩余时长参数错误读数不允许小于0");
}
return sendCommand(deviceName, IotConstants.COMMAND_RECHARGE + seconds + IotConstants.COMMAND_SEPARATOR, 5, productId, reason, true);
return sendCommand(deviceName, IotConstants.COMMAND_RECHARGE + seconds + IotConstants.COMMAND_SEPARATOR, 5, productId, reason);
}
@Override
@ -423,7 +417,7 @@ public class IotServiceImpl implements IotService {
// throw new ServiceException("充值电量错误充值电量不允许小于0");
return null;
}
return sendCommand(deviceName, IotConstants.COMMAND_ADD_ELE + ele.multiply(BigDecimal.valueOf(1000)) + IotConstants.COMMAND_SEPARATOR, 5, productId, reason, true);
return sendCommand(deviceName, IotConstants.COMMAND_ADD_ELE + ele.multiply(BigDecimal.valueOf(1000)) + IotConstants.COMMAND_SEPARATOR, 5, productId, reason);
}
@Override
@ -449,7 +443,7 @@ public class IotServiceImpl implements IotService {
// throw new ServiceException("设置电量错误:电量不允许为空");
return null;
}
return sendCommand(deviceName, IotConstants.COMMAND_SET_ELE + ele.multiply(BigDecimal.valueOf(1000)) + IotConstants.COMMAND_SEPARATOR, 5, productId, reason, true);
return sendCommand(deviceName, IotConstants.COMMAND_SET_ELE + ele.multiply(BigDecimal.valueOf(1000)) + IotConstants.COMMAND_SEPARATOR, 5, productId, reason);
}
@Override
@ -537,16 +531,11 @@ public class IotServiceImpl implements IotService {
}
private CommandResponse sendCommand(String deviceName, String command, String productId, String reason) {
return sendCommand(deviceName, command, null, productId, reason, true);
}
private CommandResponse sendCommand(String deviceName, String command, String productId, String reason, boolean recordLog) {
return sendCommand(deviceName, command, null, productId, reason, recordLog);
return sendCommand(deviceName, command, null, productId, reason);
}
// 发送MQTT命令
private CommandResponse sendCommand(String deviceName, String command, Integer timeout, String productId, String reason, boolean recordLog) {
private CommandResponse sendCommand(String deviceName, String command, Integer timeout, String productId, String reason) {
if (timeout == null) {
timeout = this.timeout;
}
@ -561,9 +550,7 @@ public class IotServiceImpl implements IotService {
CommandResponse res = JSON.parseObject(result, CommandResponse.class);
// 记录成功日志
if (recordLog) {
this.addCommandLog(deviceName, command, res.getMsg(), reason, res.getCode());
}
this.addCommandLog(deviceName, command, res.getMsg(), reason, res.getCode());
// 若返回数据为离线或者超时则判断设备是否在线
// if (IotHttpStatus.checkOnlineList().contains(res.getCode())) {

View File

@ -18,6 +18,7 @@ import com.ruoyi.ss.storeStaff.domain.StoreStaffVO;
import com.ruoyi.ss.storeStaff.service.StoreStaffService;
import com.ruoyi.ss.transactionBill.domain.vo.TransactionBillVO;
import com.ruoyi.ss.user.domain.SmUserVO;
import com.ruoyi.ss.user.service.UserAssembler;
import com.ruoyi.ss.user.service.UserService;
import com.ruoyi.ss.vipOrder.domain.VipOrderVO;
import com.ruoyi.system.service.ISysDeptService;
@ -55,6 +56,9 @@ public class BonusConverterImpl implements BonusConverter {
@Autowired
private UserService userService;
@Autowired
private UserAssembler userAssembler;
@Override
public List<Bonus> toPoList(SysDept platform, DeviceVO device, List<StoreStaffVO> staffList) {
@ -140,6 +144,7 @@ public class BonusConverterImpl implements BonusConverter {
return Collections.emptyList();
}
SmUserVO mch = userService.selectSmUserByUserId(order.getMchId());
userAssembler.assembleRealServiceRate(Collections.singletonList(mch));
SysDept platform = deptService.selectDeptById(Constants.ROOT_DEPT);
if (mch == null || platform == null) {
return Collections.emptyList();
@ -152,8 +157,9 @@ public class BonusConverterImpl implements BonusConverter {
// 是否平台收取
boolean isPlatform = ChannelType.PLATFORM.getType().equals(channelType);
// 平台收取
if (isPlatform && mch.getServiceRate() != null) {
this.addBonus(result, toPo(platform, mch.getRealServiceRate()));
if (isPlatform) {
BigDecimal serviceRate = mch.getRealServiceRate() == null ? BigDecimal.ZERO : mch.getRealServiceRate();
result.add(toPo(platform, serviceRate));
}
// 员工收取

View File

@ -22,6 +22,7 @@ import com.ruoyi.ss.bonus.domain.vo.BonusMonthAmountVO;
import com.ruoyi.ss.bonus.domain.vo.ProvideBonusVO;
import com.ruoyi.ss.bonus.mapper.BonusMapper;
import com.ruoyi.ss.bonus.service.BonusService;
import com.ruoyi.ss.bonus.utils.BonusUtil;
import com.ruoyi.ss.recordBalance.domain.enums.RecordBalanceBstType;
import com.ruoyi.ss.user.domain.SmUserVO;
import com.ruoyi.ss.user.service.UserAssembler;
@ -199,8 +200,42 @@ public class BonusServiceImpl implements BonusService
}
BigDecimal decimal100 = new BigDecimal(100);
BigDecimal dividedAmount = BigDecimal.ZERO; // 已分配金额
// 获取平台的分成
Bonus platform = bonusList.stream().filter(bonus -> bonus.getArrivalType().equals(BonusArrivalType.PLATFORM.getType())).findFirst().orElse(null);
// 平台存在分成的情况需要处理最低分成
if (platform != null) {
BigDecimal minService = sysConfigService.getBigDecimal(ConfigKey.RECHARGE_MIN_SERVICE); // 平台最低需要的分成金额
BigDecimal platformAmount = money.multiply(platform.getPoint()).divide(decimal100, 2, RoundingMode.HALF_UP); // 平台预计分成金额
// 若总金额 < 最低分成则平台全收
if (money.compareTo(minService) < 0) {
BonusUtil.partBonusAllPlatform(platform, money);
}
// 若平台分成 < 最低金额则收取最低金额其他金额给其他分成方分成
else if (platformAmount.compareTo(minService) < 0) {
BonusUtil.partBonusMinService(bonusList, money, platform, minService);
}
// 其余正常收取
else {
BonusUtil.partBonusNormal(bonusList, money);
}
}
// 其余情况正常收取费用
else {
BonusUtil.partBonusNormal(bonusList, money);
}
// 误差处理将误差值交给可以处理的分成方处理
BigDecimal dividedAmount = CollectionUtils.sumDecimal(bonusList, Bonus::getAmount);
if (dividedAmount.compareTo(money) != 0) {
BigDecimal diff = money.subtract(dividedAmount);
BonusUtil.handlePartDiff(bonusList, diff);
}
dividedAmount = CollectionUtils.sumDecimal(bonusList, Bonus::getAmount);
ServiceUtil.assertion(dividedAmount.compareTo(money) != 0, "分成金额分配出错");
// 设置预计分成时间等数据
LocalDateTime now = LocalDateTime.now();
// 获取用户列表
List<SmUserVO> userList = userService.selectByUserIds(bonusList.stream()
.filter(item -> BonusArrivalType.userList().contains(item.getArrivalType()))
@ -209,60 +244,6 @@ public class BonusServiceImpl implements BonusService
);
// 拼接用户实际到账延迟
userAssembler.assembleRealArrivalDelay(userList);
LocalDateTime now = LocalDateTime.now();
// 获取平台的分成
Bonus platform = bonusList.stream().filter(bonus -> bonus.getArrivalType().equals(BonusArrivalType.PLATFORM.getType())).findFirst().orElse(null);
ServiceUtil.assertion(platform == null, "平台不存在");
BigDecimal platformAmount = money.multiply(platform.getPoint()).divide(decimal100, 2, RoundingMode.HALF_UP); // 平台预计分成金额
// 处理最低分成
BigDecimal minService = sysConfigService.getBigDecimal(ConfigKey.RECHARGE_MIN_SERVICE);
// 若总金额 < 最低分成则平台全收
if (money.compareTo(minService) < 0) {
this.setAmount(platform, money);
dividedAmount = platform.getAmount();
}
// 若平台分成 < 最低金额则收取最低金额其他金额给其他分成方分成
else if (platformAmount.compareTo(minService) < 0) {
// 平台设置分成为最低金额
this.setAmount(platform, minService);
dividedAmount = platform.getAmount();
// 计算其他分成方分成金额
BigDecimal remain = money.subtract(platform.getAmount()); // 剩余可分成金额
BigDecimal remainPoint = decimal100.subtract(platform.getPoint()); // 剩余总百分比
for (Bonus bonus : bonusList) {
if (bonus.getArrivalType().equals(BonusArrivalType.PLATFORM.getType())) {
continue;
}
// 计算分成比例占剩余可分成金额的比例
BigDecimal bonusPoint = bonus.getPoint().multiply(decimal100).divide(remainPoint, 2, RoundingMode.HALF_UP);
BigDecimal amount = remain.multiply(bonusPoint).divide(decimal100, 2, RoundingMode.HALF_UP);
this.setAmount(bonus, amount);
// 增加已分成金额
dividedAmount = dividedAmount.add(amount);
}
}
// 其余情况正常收取费用
else {
// 循环遍历构造分成金额
for (Bonus bonus : bonusList) {
BigDecimal amount = money.multiply(bonus.getPoint()).divide(decimal100, 2, RoundingMode.HALF_UP);
this.setAmount(bonus, amount);
// 增加已分成金额
dividedAmount = dividedAmount.add(amount);
}
}
// 若存在误差则平台吃掉误差
if (dividedAmount.compareTo(money) != 0) {
BigDecimal subtract = money.subtract(dividedAmount); // 误差值
this.setAmount(platform, platform.getAmount().add(subtract));
}
// 设置预计分成时间等数据
for (Bonus bonus : bonusList) {
bonus.setStatus(BonusStatus.WAIT_DIVIDE.getStatus());
bonus.setWaitAmount(bonus.getAmount());
@ -279,10 +260,6 @@ public class BonusServiceImpl implements BonusService
}
}
private void setAmount(Bonus bonus, BigDecimal amount) {
ServiceUtil.assertion(amount.compareTo(BigDecimal.ZERO) < 0, "分成金额不允许小于0");
bonus.setAmount(amount);
}
@Override
public List<CommonCountVO<Long>> selectCountByArrival(BonusQuery query) {

View File

@ -0,0 +1,166 @@
package com.ruoyi.ss.bonus.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.ss.bonus.domain.Bonus;
import com.ruoyi.ss.bonus.domain.BonusVO;
import com.ruoyi.ss.bonus.domain.enums.BonusArrivalType;
/**
* @author wjh
* 2025/1/21
*/
public class BonusUtil {
/**
* 平台全收服务费分成处理
*/
public static void partBonusAllPlatform(Bonus platform, BigDecimal money) {
setAmount(platform, money);
}
/**
* 最低服务费分成处理
*/
public static void partBonusMinService(List<Bonus> bonusList, BigDecimal money, Bonus platform, BigDecimal minService) {
// 平台设置分成为最低金额
setAmount(platform, minService);
BigDecimal decimal100 = new BigDecimal("100");
// 计算其他分成方分成金额
BigDecimal remain = money.subtract(platform.getAmount()); // 剩余可分成金额
BigDecimal remainPoint = decimal100.subtract(platform.getPoint()); // 剩余总百分比
for (Bonus bonus : bonusList) {
if (bonus.getArrivalType().equals(BonusArrivalType.PLATFORM.getType())) {
continue;
}
// 计算分成比例占剩余可分成金额的比例
BigDecimal bonusPoint = bonus.getPoint().multiply(decimal100).divide(remainPoint, 2, RoundingMode.HALF_UP);
BigDecimal amount = remain.multiply(bonusPoint).divide(decimal100, 2, RoundingMode.HALF_UP);
setAmount(bonus, amount);
}
}
/**
* 基础处理分成
*/
public static void partBonusNormal(List<Bonus> bonusList, BigDecimal money) {
BigDecimal decimal100 = new BigDecimal("100");
// 循环遍历构造分成金额
for (Bonus bonus : bonusList) {
BigDecimal amount = money.multiply(bonus.getPoint()).divide(decimal100, 2, RoundingMode.HALF_UP);
setAmount(bonus, amount);
}
}
private static void setAmount(Bonus bonus, BigDecimal amount) {
ServiceUtil.assertion(amount.compareTo(BigDecimal.ZERO) < 0, "分成金额不允许小于0");
bonus.setAmount(amount);
}
/**
* 处理分成的误差
*/
public static void handlePartDiff(List<Bonus> bonusList, BigDecimal diff) {
// 若误差金额为正数或者0则交给第一个分成方处理
if (diff.compareTo(BigDecimal.ZERO) >= 0) {
Bonus bonus = bonusList.get(0);
setAmount(bonus, bonus.getAmount().add(diff));
}
// 否则若为负数则多个分成方处理
else {
// 多个分成方共同承担误差
BigDecimal remainingDiff = diff;
for (Bonus bonus : bonusList) {
if (bonus == null || bonus.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
// 计算当前分成方可以承担的误差金额确保不会导致金额为负
BigDecimal maxDeductible = bonus.getAmount(); // 最大可扣除金额
BigDecimal adjustAmount = maxDeductible.min(remainingDiff.abs());
// 直接调整金额
setAmount(bonus, bonus.getAmount().subtract(adjustAmount));
remainingDiff = remainingDiff.add(adjustAmount);
// 如果误差已经分配完毕则退出
if (remainingDiff.compareTo(BigDecimal.ZERO) == 0) {
return;
}
}
// 如果所有分成方都处理完了还有剩余误差抛出异常
if (remainingDiff.compareTo(BigDecimal.ZERO) != 0) {
throw new ServiceException("分成金额误差处理失败:无法完全分配误差金额,剩余误差:" + remainingDiff);
}
}
}
/**
* 处理退款金额分配的误差
* @param refundList 退款列表
* @param bonusList 原始分成列表
* @param dividedAmount 已分配金额
* @param refundAmount 需要退款总金额
*/
public static void handleRefundAmountDiff(List<Bonus> refundList, List<BonusVO> bonusList, BigDecimal dividedAmount, BigDecimal refundAmount) {
if (dividedAmount.compareTo(refundAmount) == 0) {
return;
}
// 多个分成方共同承担误差
BigDecimal remainingDiff = refundAmount.subtract(dividedAmount);
for (Bonus refundBonus : refundList) {
// 获取原始分成记录
BonusVO originalBonus = bonusList.stream()
.filter(b -> b.getId().equals(refundBonus.getId()))
.findFirst()
.orElse(null);
if (originalBonus == null) {
continue;
}
if (remainingDiff.compareTo(BigDecimal.ZERO) > 0) {
// 处理正数误差少退了需要增加退款金额
// 计算当前分成方的剩余可退金额
BigDecimal availableRefund = originalBonus.getAmount()
.subtract(originalBonus.getRefundAmount() == null ? BigDecimal.ZERO : originalBonus.getRefundAmount())
.subtract(refundBonus.getRefundAmount());
// 如果剩余可退金额足够承担误差
if (availableRefund.compareTo(BigDecimal.ZERO) > 0) {
// 取可用退款金额与剩余误差中的较小值
BigDecimal adjustAmount = availableRefund.min(remainingDiff);
refundBonus.setRefundAmount(refundBonus.getRefundAmount().add(adjustAmount));
remainingDiff = remainingDiff.subtract(adjustAmount);
}
} else {
// 处理负数误差多退了需要减少退款金额
// 计算当前退款金额
BigDecimal currentRefund = refundBonus.getRefundAmount();
if (currentRefund.compareTo(BigDecimal.ZERO) > 0) {
// 取当前退款金额与剩余需要减少金额的较小值
BigDecimal adjustAmount = currentRefund.min(remainingDiff.abs());
refundBonus.setRefundAmount(currentRefund.subtract(adjustAmount));
remainingDiff = remainingDiff.add(adjustAmount);
}
}
// 如果误差已经分配完毕则退出
if (remainingDiff.compareTo(BigDecimal.ZERO) == 0) {
return;
}
}
// 如果仍然无法完全分配误差,抛出异常
if (remainingDiff.compareTo(BigDecimal.ZERO) != 0) {
throw new ServiceException("退款金额分配出错:无法处理误差金额,剩余误差:" + remainingDiff);
}
}
}

View File

@ -338,21 +338,26 @@ public class TransactionBill extends BaseEntity
@Excel(name = "使用vip的ID")
@ApiModelProperty("使用vip的ID")
@JsonView(JsonViewProfile.App.class)
private Long vipId;
@Excel(name = "会员折扣")
@ApiModelProperty("会员折扣")
@JsonView(JsonViewProfile.App.class)
private BigDecimal vipDiscount;
@Excel(name = "会员名称")
@ApiModelProperty("会员名称")
@JsonView(JsonViewProfile.App.class)
private String vipName;
@Excel(name = "优惠金额")
@ApiModelProperty("优惠金额")
@JsonView(JsonViewProfile.App.class)
private BigDecimal discountAmount;
@Excel(name = "优惠金额退款")
@ApiModelProperty("优惠金额退款")
@JsonView(JsonViewProfile.App.class)
private BigDecimal discountRefundAmount;
}

View File

@ -2,8 +2,6 @@ package com.ruoyi.ss.transactionBill.service.impl;
import java.util.Collections;
import com.ruoyi.ss.app.service.AppService;
import com.ruoyi.ss.transactionBill.utils.RechargeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -14,6 +12,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.ss.account.domain.AccountQuery;
import com.ruoyi.ss.account.domain.AccountVO;
import com.ruoyi.ss.account.service.AccountService;
import com.ruoyi.ss.app.service.AppService;
import com.ruoyi.ss.channel.service.ChannelService;
import com.ruoyi.ss.channelWithdraw.domain.ChannelWithdrawVO;
import com.ruoyi.ss.channelWithdraw.service.ChannelWithdrawService;
@ -36,6 +35,7 @@ import com.ruoyi.ss.transactionBill.domain.dto.RechargePayBO;
import com.ruoyi.ss.transactionBill.domain.dto.WithdrawDTO;
import com.ruoyi.ss.transactionBill.domain.vo.TransactionBillVO;
import com.ruoyi.ss.transactionBill.service.TransactionBillConverter;
import com.ruoyi.ss.transactionBill.utils.RechargeUtils;
import com.ruoyi.ss.user.domain.SmUserVO;
import com.ruoyi.ss.user.service.UserService;
import com.ruoyi.ss.vip.service.VipService;

View File

@ -18,6 +18,7 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.ruoyi.ss.bonus.utils.BonusUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -111,6 +112,7 @@ import com.ruoyi.ss.user.domain.SmUserVO;
import com.ruoyi.ss.user.service.UserService;
import com.ruoyi.ss.vip.domain.VipVO;
import com.ruoyi.ss.vip.service.VipService;
import com.ruoyi.ss.vip.utils.VipUtil;
import com.ruoyi.system.domain.enums.config.ConfigKey;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.task.bill.BillDelayedManager;
@ -196,8 +198,6 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
@Autowired
private VipService vipService;
private VipVO vip;
/**
* 查询充值记录
*
@ -301,7 +301,12 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
int monthFee = receiveBillService.genBillByMonthAndPay(bo.getMch(), bo.getDevice());
ServiceUtil.assertion(monthFee != 1, "月费收取失败,请联系商户处理");
// TODO 扣减会员次数,记录会员使用记录
// 扣减会员次数
VipVO vip = bo.getVip();
if (vip != null) {
int use = vipService.useVip(vip);
ServiceUtil.assertion(use != 1, "ID为%s的会员使用失败", vip.getId());
}
// 新增订单
int insert = this.insertSmTransactionBill(order);
@ -1072,20 +1077,36 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
totalEle = this.calcTotalEle(device, totalEle);
}
BigDecimal result = BigDecimal.ZERO;
// 智能计费
if (SuitFeeMode.SMART.getMode().equals(bill.getSuitFeeMode())) {
if (SuitFeeType.timingList().contains(bill.getSuitFeeType())) {
// 分时段计费
return this.calcTimingAmount(bill, endTime, totalEle);
result = this.calcTimingAmount(bill, endTime, totalEle);
} else {
// 非分时段计费
return this.calcSmartAmount(bill, endTime, totalEle);
result = this.calcSmartAmount(bill, endTime, totalEle);
}
}
// 非智能计费
else {
return bill.getMoney();
result = bill.getMoney();
}
// 计算会员折扣
result = VipUtil.calcDiscountAmount(bill.getVipDiscount(), result);
// 若需要金额 > 订单金额则返回订单金额
if (result.compareTo(bill.getMoney()) > 0) {
result = bill.getMoney();
}
// 若需要的金额 < 0则返回0
if (result.compareTo(BigDecimal.ZERO) < 0) {
result = BigDecimal.ZERO;
}
return result;
}
@Override
@ -1252,7 +1273,7 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
// 查询数据
TransactionBill bill = transactionBillMapper.selectSmTransactionBillByBillNo(billNo);
ServiceUtil.assertion(bill == null, "订单不存在");
ServiceUtil.assertion(bill == null || bill.getBillId() == null, "订单不存在");
ServiceUtil.assertion(!TransactionBillType.RECHARGE.getType().equals(bill.getType()), "非充值订单,不允许取消");
// 允许取消的状态
@ -1263,11 +1284,20 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
ServiceUtil.assertion(!allowCancel.contains(bill.getStatus()), "当前订单状态不允许取消");
Integer result = transactionTemplate.execute(s -> {
// TODO 执行取消订单改为updateByQuery的形式
int cancel = transactionBillMapper.cancelRecharge(billNo, status.getStatus());
// 执行取消订单
TransactionBill data = new TransactionBill();
data.setStatus(status.getStatus());
TransactionBillQuery query = new TransactionBillQuery();
query.setBillId(bill.getBillId());
query.setStatusList(allowCancel);
query.setType(TransactionBillType.RECHARGE.getType());
int cancel = this.updateByQuery(data, query);
ServiceUtil.assertion(cancel != 1, "取消订单失败:状态已发生变化");
// TODO 恢复会员次数
// 恢复会员次数
if (bill.getVipId() != null) {
vipService.unUseVip(bill.getVipId());
}
// 取消支付订单
boolean cancelPay = false;
@ -1487,7 +1517,7 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
}
// 处理误差
handleRefundAmountDiff(refundList, bonusList, dividedAmount, refundAmount);
BonusUtil.handleRefundAmountDiff(refundList, bonusList, dividedAmount, refundAmount);
// 校验金额
for (Bonus bonus : refundList) {
@ -1500,58 +1530,6 @@ public class TransactionBillServiceImpl implements TransactionBillService, After
return executeRefund(refundList);
}
/**
* 处理退款金额分配的误差
* @param refundList 退款列表
* @param bonusList 原始分成列表
* @param dividedAmount 已分配金额
* @param refundAmount 需要退款总金额
*/
private void handleRefundAmountDiff(List<Bonus> refundList, List<BonusVO> bonusList, BigDecimal dividedAmount, BigDecimal refundAmount) {
if (dividedAmount.compareTo(refundAmount) == 0) {
return;
}
// 误差值
BigDecimal diff = refundAmount.subtract(dividedAmount);
// 多个分成方共同承担误差
BigDecimal remainingDiff = diff;
for (Bonus refundBonus : refundList) {
// 获取原始分成记录
BonusVO originalBonus = bonusList.stream()
.filter(b -> b.getId().equals(refundBonus.getId()))
.findFirst()
.orElse(null);
if (originalBonus == null) {
continue;
}
// 计算当前分成方的剩余可退金额
BigDecimal availableRefund = originalBonus.getAmount()
.subtract(originalBonus.getRefundAmount() == null ? BigDecimal.ZERO : originalBonus.getRefundAmount())
.subtract(refundBonus.getRefundAmount());
// 如果剩余可退金额足够承担误差
if (availableRefund.compareTo(BigDecimal.ZERO) > 0) {
// 取可用退款金额与剩余误差中的较小值
BigDecimal adjustAmount = availableRefund.min(remainingDiff);
refundBonus.setRefundAmount(refundBonus.getRefundAmount().add(adjustAmount));
remainingDiff = remainingDiff.subtract(adjustAmount);
// 如果误差已经分配完毕则退出
if (remainingDiff.compareTo(BigDecimal.ZERO) == 0) {
return;
}
}
}
// 如果仍然无法完全分配误差,抛出异常
if (remainingDiff.compareTo(BigDecimal.ZERO) > 0) {
throw new ServiceException("退款金额分配出错:无法处理误差金额");
}
}
// 执行分成退款
private int executeRefund(List<Bonus> refundList) {
Integer result = transactionTemplate.execute(status -> {

View File

@ -203,11 +203,20 @@ public class TransactionBillValidatorImpl extends BaseValidator implements Trans
if (!vipValidator.isStore(vip, store)) {
return error(String.format("ID为%s的会员不能用在当前店铺请重新选择会员", dto.getVipId()));
}
if (!vipValidator.isUser(vip, user)) {
return error(String.format("ID为%s的会员不是ID为%s的用户请重新选择会员", dto.getVipId(), user.getUserId()));
}
// 有效期
if (vipValidator.isInValid(vip)) {
return error(String.format("ID为%s的会员已过期请重新选择会员", dto.getVipId()));
}
if (!vipValidator.isUser(vip, user)) {
return error(String.format("ID为%s的会员不是ID为%s的用户请重新选择会员", dto.getVipId(), user.getUserId()));
// 剩余次数
if (!vipValidator.hasSurplusCount(vip)) {
return error(String.format("ID为%s的会员已达到最大使用次数请重新选择会员", dto.getVipId()));
}
// 周期剩余次数
if (!vipValidator.hasSurplusRoundCount(vip)) {
return error(String.format("ID为%s的会员已达到本周期最大使用次数请重新选择会员", dto.getVipId()));
}
}
// 价格检查

View File

@ -1,6 +1,7 @@
package com.ruoyi.ss.vip.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import io.swagger.annotations.ApiModelProperty;
@ -33,4 +34,10 @@ public class VipQuery extends VipVO{
@ApiModelProperty("剩余次数最大值")
private BigDecimal surplusCountEnd;
@ApiModelProperty("需要刷新的日期")
private LocalDate nextResetDate;
@ApiModelProperty("限制类型列表")
private List<String> limitTypes;
}

View File

@ -1,11 +1,12 @@
package com.ruoyi.ss.vip.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.ss.vip.domain.Vip;
import com.ruoyi.ss.vip.domain.VipQuery;
import com.ruoyi.ss.vip.domain.VipVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 会员Mapper接口
@ -77,4 +78,14 @@ public interface VipMapper
* 逻辑删除
*/
int logicDel(@Param("ids") List<Long> ids);
/**
* 使用会员
*/
int useVip(@Param("id") Long id);
/**
* 恢复会员次数
*/
int unUseVip(@Param("id") Long id);
}

View File

@ -71,6 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="query.discountEnd != null">and sv.discount &lt;= #{query.discountEnd}</if>
<if test="query.surplusCountStart != null">and sv.surplus_count &gt;= #{query.surplusCountStart}</if>
<if test="query.surplusCountEnd != null">and sv.surplus_count &lt;= #{query.surplusCountEnd}</if>
<if test="query.nextResetDate != null">and date(sv.next_reset_time) = #{query.nextResetDate}</if>
<if test="query.inValid != null">
and
<if test="!query.inValid">
@ -97,6 +98,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{item}
</foreach>
</if>
<if test="query.limitTypes != null and query.limitTypes.size() > 0">
and sv.limit_type in
<foreach collection="query.limitTypes" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</if>
</sql>
<select id="selectVipList" parameterType="VipQuery" resultMap="VipResult">
@ -206,4 +214,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
and deleted = false
</update>
<update id="useVip">
update ss_vip sv
set sv.round_count = sv.round_count + 1,
sv.surplus_count = sv.surplus_count - 1
where sv.id = #{id}
and sv.deleted = false
and sv.surplus_count > 0
and <include refid="inValid"/>
and not <include refid="inLimit"/>
</update>
<update id="unUseVip">
update ss_vip sv
set sv.round_count = if(sv.round_count > 0, sv.round_count - 1, 0),
sv.surplus_count = sv.surplus_count + 1
where sv.id = #{id}
and sv.deleted = false
</update>
</mapper>

View File

@ -118,4 +118,21 @@ public interface VipService
*/
List<VipVO> selectUserAvaliableStoreVip(Long userId, Long storeId);
/**
* 使用会员
* @param vip
*/
int useVip(VipVO vip);
/**
* 恢复会员次数
* @param vipId
*/
int unUseVip(Long vipId);
/**
* 重置VIP周期计数
* @param vip
*/
int resetVipRoundCount(VipVO vip);
}

View File

@ -47,4 +47,14 @@ public interface VipValidator {
* 是否在有效期内
*/
boolean isInValid(VipVO vip);
/**
* 是否还有余额次数
*/
boolean hasSurplusCount(VipVO vip);
/**
* 是否还有周期剩余次数
*/
boolean hasSurplusRoundCount(VipVO vip);
}

View File

@ -1,6 +1,7 @@
package com.ruoyi.ss.vip.service.impl;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
@ -17,6 +18,7 @@ import com.ruoyi.ss.vip.domain.VipVO;
import com.ruoyi.ss.vip.mapper.VipMapper;
import com.ruoyi.ss.vip.service.VipService;
import com.ruoyi.ss.vip.service.VipValidator;
import com.ruoyi.ss.vip.utils.VipUtil;
/**
* 会员Service业务层处理
@ -203,5 +205,35 @@ public class VipServiceImpl implements VipService
query.setStoreId(storeId);
return selectAvailableList(query);
}
@Override
public int useVip(VipVO vip) {
if (vip == null || vip.getId() == null) {
return 0;
}
return vipMapper.useVip(vip.getId());
}
@Override
public int unUseVip(Long vipId) {
if (vipId == null) {
return 0;
}
return vipMapper.unUseVip(vipId);
}
@Override
public int resetVipRoundCount(VipVO vip) {
if (vip == null || vip.getId() == null) {
return 0;
}
Vip data = new Vip();
data.setId(vip.getId());
data.setNextResetTime(VipUtil.getNextResetTime(LocalDateTime.now(), vip.getLimitType()));
data.setRoundCount(0);
return vipMapper.updateVip(data);
}
}

View File

@ -18,6 +18,7 @@ import com.ruoyi.ss.vip.domain.VipQuery;
import com.ruoyi.ss.vip.domain.VipVO;
import com.ruoyi.ss.vip.service.VipService;
import com.ruoyi.ss.vip.service.VipValidator;
import com.ruoyi.ss.vipLevel.domain.enums.VipLevelLimitType;
/**
* @author wjh
@ -116,4 +117,22 @@ public class VipValidatorImpl implements VipValidator {
&& vip.getEndTime().isBefore(now)
&& vip.getStartTime().isAfter(now);
}
@Override
public boolean hasSurplusCount(VipVO vip) {
return vip != null && vip.getSurplusCount() != null && vip.getSurplusCount() > 0;
}
@Override
public boolean hasSurplusRoundCount(VipVO vip) {
if (vip == null || vip.getLimitType() == null) {
return false;
}
// 不限制次数
if (VipLevelLimitType.NONE.getType().equals(vip.getLimitType())) {
return true;
}
// 限制次数
return vip.getLimitCount() > vip.getRoundCount();
}
}

View File

@ -1,5 +1,9 @@
package com.ruoyi.ss.vipLevel.domain.enums;
import java.util.List;
import com.ruoyi.common.utils.collection.CollectionUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -18,4 +22,11 @@ public enum VipLevelLimitType {
private final String type;
private final String msg;
/**
* 需要重置周期计数的VIP等级
*/
public static List<String> needReset() {
return CollectionUtils.map(VipLevelLimitType::getType, WEEK, DAYS_30);
}
}

View File

@ -214,6 +214,7 @@ public class VipOrderServiceImpl implements VipOrderService, AfterPay, AfterRefu
data.setStatus(VipOrderStatus.SUCCESS.getStatus());
data.setPayId(pay.getPayId());
data.setPayNo(pay.getPayNo());
data.setPayTime(pay.getPayTime());
VipOrderQuery query = new VipOrderQuery();
query.setId(order.getId());
query.setStatus(VipOrderStatus.WAIT_PAY.getStatus());

View File

@ -7,7 +7,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.ruoyi.ss.transactionBill.domain.bo.RechargeBO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
@ -38,6 +37,7 @@ import com.ruoyi.ss.device.service.DeviceValidator;
import com.ruoyi.ss.suit.domain.enums.SuitFeeType;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.bo.EndUseBO;
import com.ruoyi.ss.transactionBill.domain.bo.RechargeBO;
import com.ruoyi.ss.transactionBill.domain.dto.BillPayDTO;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
import com.ruoyi.ss.transactionBill.domain.dto.EndUseDTO;
@ -235,10 +235,10 @@ public class AppTransactionBillController extends BaseController
@PutMapping("/recharge/cancel/{billNo}")
public AjaxResult cancelRecharge(@PathVariable @ApiParam("订单编号") String billNo) {
TransactionBillVO bill = transactionBillService.selectSmTransactionBillByBillNo(billNo);
if (!transactionBillValidator.isMch(bill, getUserId())) {
return error("不是该订单的商户,无法取消");
if (!transactionBillValidator.isMch(bill, getUserId()) && !transactionBillValidator.isUser(bill, getUserId())) {
return error("无权取消该订单");
}
return AjaxResult.success(transactionBillService.cancelRecharge(billNo, TransactionBillStatus.CANCELED));
return toAjax(transactionBillService.cancelRecharge(billNo, TransactionBillStatus.CANCELED));
}
@Log(title = "提现申请", businessType = BusinessType.INSERT, operatorType = OperatorType.MOBILE)