Gqingci před 1 týdnem
rodič
revize
04395cf132

+ 13 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/MainBackOrderController.java

@@ -1,7 +1,9 @@
 package org.dromara.main.controller;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.web.core.BaseController;
@@ -10,6 +12,8 @@ import org.dromara.main.domain.vo.MainBackOrderVo;
 import org.dromara.main.service.IMainBackOrderService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -29,4 +33,13 @@ public class MainBackOrderController extends BaseController {
     public TableDataInfo<MainBackOrderVo> list(MainBackOrderBo bo, PageQuery pageQuery) {
         return mainBackOrderService.queryPageList(bo, pageQuery);
     }
+
+    /**
+     * 完成背调订单
+     */
+    @SaCheckPermission("system:backOrder:complete")
+    @PutMapping("/{id}/complete")
+    public R<Void> complete(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return toAjax(mainBackOrderService.completeOrder(id));
+    }
 }

+ 10 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCheckController.java

@@ -161,6 +161,16 @@ public class PortalCheckController extends BaseController {
         return R.ok(mainBackOrderService.createPortalOrder(tenantId, bo));
     }
 
+    @PostMapping("/order/{orderId}/payByBalance")
+    public R<Void> payByBalance(@PathVariable Long orderId) {
+        String tenantId = LoginHelper.getTenantId();
+        if (tenantId == null || tenantId.isBlank()) {
+            return R.fail("未登录或token已失效");
+        }
+        mainBackOrderService.payOrderByBalance(orderId, tenantId);
+        return R.ok();
+    }
+
     private PortalCheckCategoryVo toPortalCategoryVo(MainBackCategoryVo category) {
         PortalCheckCategoryVo vo = new PortalCheckCategoryVo();
         vo.setId(category.getId());

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/CompanyAccountFlow.java

@@ -46,6 +46,8 @@ public class CompanyAccountFlow extends BaseEntity {
 
     private String remark;
 
+    private String tenantId;
+
     @TableLogic
     private String delFlag;
 }

+ 49 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/ICompanyAccountService.java

@@ -0,0 +1,49 @@
+package org.dromara.main.service;
+
+import java.math.BigDecimal;
+
+/**
+ * 企业账户服务接口
+ */
+public interface ICompanyAccountService {
+
+    /**
+     * 检查可用余额是否充足
+     */
+    Boolean checkAvailableBalance(Long companyId, BigDecimal amount);
+
+    /**
+     * 增加可用余额
+     */
+    Boolean addCommission(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark);
+
+    /**
+     * 余额支付:可用余额 -> 使用中余额
+     */
+    Boolean payByBalance(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark);
+
+    /**
+     * 订单完成:使用中余额 -> 累计消费
+     */
+    Boolean deductInUseBalance(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark);
+
+    /**
+     * 订单退款:使用中余额 -> 可用余额
+     */
+    Boolean refundToAvailable(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark);
+
+    /**
+     * 申请提现:可用余额 -> 提现中余额
+     */
+    Boolean applyWithdraw(Long companyId, BigDecimal amount, String withdrawNo, String remark);
+
+    /**
+     * 提现成功:扣减提现中余额
+     */
+    Boolean deductWithdrawingBalance(Long companyId, BigDecimal amount, String withdrawNo, String remark);
+
+    /**
+     * 提现失败:提现中余额 -> 可用余额
+     */
+    Boolean refundWithdrawToAvailable(Long companyId, BigDecimal amount, String withdrawNo, String remark);
+}

+ 10 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainBackOrderService.java

@@ -32,4 +32,14 @@ public interface IMainBackOrderService {
      * 门户创建背调订单
      */
     MainBackOrderVo createPortalOrder(String tenantId, PortalCheckOrderCreateBo bo);
+
+    /**
+     * 门户余额支付
+     */
+    Boolean payOrderByBalance(Long orderId, String tenantId);
+
+    /**
+     * 完成背调订单
+     */
+    Boolean completeOrder(Long id);
 }

+ 211 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/CompanyAccountServiceImpl.java

@@ -0,0 +1,211 @@
+package org.dromara.main.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.RandomUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.main.domain.CompanyAccountFlow;
+import org.dromara.main.mapper.CompanyAccountFlowMapper;
+import org.dromara.main.service.ICompanyAccountService;
+import org.dromara.system.domain.SysTenant;
+import org.dromara.system.mapper.SysTenantMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 企业账户服务实现
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class CompanyAccountServiceImpl implements ICompanyAccountService {
+
+    private final SysTenantMapper sysTenantMapper;
+    private final CompanyAccountFlowMapper companyAccountFlowMapper;
+
+    @Override
+    public Boolean checkAvailableBalance(Long companyId, BigDecimal amount) {
+        SysTenant tenant = getTenant(companyId);
+        return defaultAmount(tenant.getAvailableBalance()).compareTo(defaultAmount(amount)) >= 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean addCommission(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal availableBefore = defaultAmount(tenant.getAvailableBalance());
+        BigDecimal availableAfter = availableBefore.add(delta);
+
+        updateTenantBalance(tenant.getId(), availableAfter, null, null, null);
+        insertFlow(tenant, 1, delta, 1, availableBefore, availableAfter, 4, orderId, orderNo, remark);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean payByBalance(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal availableBefore = defaultAmount(tenant.getAvailableBalance());
+        BigDecimal inUseBefore = defaultAmount(tenant.getInUseBalance());
+        if (availableBefore.compareTo(delta) < 0) {
+            throw new ServiceException("可使用余额不足,当前可用余额:{}元", availableBefore);
+        }
+
+        BigDecimal availableAfter = availableBefore.subtract(delta);
+        BigDecimal inUseAfter = inUseBefore.add(delta);
+        updateTenantBalance(tenant.getId(), availableAfter, inUseAfter, null, null);
+
+        insertFlow(tenant, 2, delta, 1, availableBefore, availableAfter, 2, orderId, orderNo, remark);
+        insertFlow(tenant, 1, delta, 2, inUseBefore, inUseAfter, 2, orderId, orderNo, remark + "(冻结)");
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deductInUseBalance(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal inUseBefore = defaultAmount(tenant.getInUseBalance());
+        BigDecimal totalConsumeBefore = defaultAmount(tenant.getTotalConsume());
+        if (inUseBefore.compareTo(delta) < 0) {
+            throw new ServiceException("使用中余额不足");
+        }
+
+        BigDecimal inUseAfter = inUseBefore.subtract(delta);
+        BigDecimal totalConsumeAfter = totalConsumeBefore.add(delta);
+        updateTenantBalance(tenant.getId(), null, inUseAfter, null, totalConsumeAfter);
+
+        insertFlow(tenant, 2, delta, 2, inUseBefore, inUseAfter, 8, orderId, orderNo, remark);
+        insertFlow(tenant, 1, delta, 4, totalConsumeBefore, totalConsumeAfter, 8, orderId, orderNo, remark);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean refundToAvailable(Long companyId, BigDecimal amount, Long orderId, String orderNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal inUseBefore = defaultAmount(tenant.getInUseBalance());
+        BigDecimal availableBefore = defaultAmount(tenant.getAvailableBalance());
+        if (inUseBefore.compareTo(delta) < 0) {
+            throw new ServiceException("使用中余额不足");
+        }
+
+        BigDecimal inUseAfter = inUseBefore.subtract(delta);
+        BigDecimal availableAfter = availableBefore.add(delta);
+        updateTenantBalance(tenant.getId(), availableAfter, inUseAfter, null, null);
+
+        insertFlow(tenant, 2, delta, 2, inUseBefore, inUseAfter, 3, orderId, orderNo, remark + "(解冻)");
+        insertFlow(tenant, 1, delta, 1, availableBefore, availableAfter, 3, orderId, orderNo, remark);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean applyWithdraw(Long companyId, BigDecimal amount, String withdrawNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal availableBefore = defaultAmount(tenant.getAvailableBalance());
+        BigDecimal withdrawingBefore = defaultAmount(tenant.getWithdrawingBalance());
+        if (availableBefore.compareTo(delta) < 0) {
+            throw new ServiceException("可使用余额不足,当前可用余额:{}元", availableBefore);
+        }
+
+        BigDecimal availableAfter = availableBefore.subtract(delta);
+        BigDecimal withdrawingAfter = withdrawingBefore.add(delta);
+        updateTenantBalance(tenant.getId(), availableAfter, null, withdrawingAfter, null);
+
+        insertFlow(tenant, 2, delta, 1, availableBefore, availableAfter, 5, null, withdrawNo, remark);
+        insertFlow(tenant, 1, delta, 3, withdrawingBefore, withdrawingAfter, 5, null, withdrawNo, remark + "(冻结)");
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deductWithdrawingBalance(Long companyId, BigDecimal amount, String withdrawNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal withdrawingBefore = defaultAmount(tenant.getWithdrawingBalance());
+        if (withdrawingBefore.compareTo(delta) < 0) {
+            throw new ServiceException("提现中余额不足");
+        }
+
+        BigDecimal withdrawingAfter = withdrawingBefore.subtract(delta);
+        updateTenantBalance(tenant.getId(), null, null, withdrawingAfter, null);
+
+        insertFlow(tenant, 2, delta, 3, withdrawingBefore, withdrawingAfter, 6, null, withdrawNo, remark);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean refundWithdrawToAvailable(Long companyId, BigDecimal amount, String withdrawNo, String remark) {
+        SysTenant tenant = getTenant(companyId);
+        BigDecimal delta = defaultAmount(amount);
+        BigDecimal withdrawingBefore = defaultAmount(tenant.getWithdrawingBalance());
+        BigDecimal availableBefore = defaultAmount(tenant.getAvailableBalance());
+        if (withdrawingBefore.compareTo(delta) < 0) {
+            throw new ServiceException("提现中余额不足");
+        }
+
+        BigDecimal withdrawingAfter = withdrawingBefore.subtract(delta);
+        BigDecimal availableAfter = availableBefore.add(delta);
+        updateTenantBalance(tenant.getId(), availableAfter, null, withdrawingAfter, null);
+
+        insertFlow(tenant, 2, delta, 3, withdrawingBefore, withdrawingAfter, 7, null, withdrawNo, remark + "(解冻)");
+        insertFlow(tenant, 1, delta, 1, availableBefore, availableAfter, 7, null, withdrawNo, remark);
+        return true;
+    }
+
+    private SysTenant getTenant(Long companyId) {
+        SysTenant tenant = sysTenantMapper.selectById(companyId);
+        if (tenant == null) {
+            throw new ServiceException("企业不存在");
+        }
+        return tenant;
+    }
+
+    private void updateTenantBalance(Long tenantId, BigDecimal availableBalance, BigDecimal inUseBalance,
+                                     BigDecimal withdrawingBalance, BigDecimal totalConsume) {
+        SysTenant update = new SysTenant();
+        update.setId(tenantId);
+        update.setAvailableBalance(availableBalance);
+        update.setInUseBalance(inUseBalance);
+        update.setWithdrawingBalance(withdrawingBalance);
+        update.setTotalConsume(totalConsume);
+        sysTenantMapper.updateById(update);
+    }
+
+    private void insertFlow(SysTenant tenant, Integer flowType, BigDecimal amount, Integer balanceType,
+                            BigDecimal balanceBefore, BigDecimal balanceAfter, Integer businessType,
+                            Long businessId, String businessNo, String remark) {
+        CompanyAccountFlow flow = new CompanyAccountFlow();
+        flow.setCompanyId(tenant.getId());
+        flow.setTenantId(tenant.getTenantId());
+        flow.setFlowNo(generateFlowNo());
+        flow.setFlowType(flowType);
+        flow.setAmount(amount);
+        flow.setBalanceType(balanceType);
+        flow.setBalanceBefore(balanceBefore);
+        flow.setBalanceAfter(balanceAfter);
+        flow.setBusinessType(businessType);
+        flow.setBusinessId(businessId);
+        flow.setBusinessNo(businessNo);
+        flow.setRemark(remark);
+        companyAccountFlowMapper.insert(flow);
+    }
+
+    private BigDecimal defaultAmount(BigDecimal amount) {
+        return amount == null ? BigDecimal.ZERO : amount;
+    }
+
+    private String generateFlowNo() {
+        return "CF" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomNumbers(4);
+    }
+}

+ 148 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java

@@ -33,9 +33,12 @@ import org.dromara.main.mapper.MainBackRecordMapper;
 import org.dromara.main.mapper.MainOrderMapper;
 import org.dromara.main.mapper.MainStudentMapper;
 import org.dromara.main.mapper.PaymentMapper;
+import org.dromara.main.service.ICompanyAccountService;
 import org.dromara.main.service.IMainBackOrderService;
+import org.dromara.system.domain.SysTenant;
 import org.dromara.system.domain.bo.SysTenantBo;
 import org.dromara.system.domain.vo.SysTenantVo;
+import org.dromara.system.mapper.SysTenantMapper;
 import org.dromara.system.service.ISysTenantService;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -62,7 +65,9 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
     private final MainBackCandidateMapper mainBackCandidateMapper;
     private final MainStudentMapper mainStudentMapper;
     private final PaymentMapper paymentMapper;
+    private final SysTenantMapper sysTenantMapper;
     private final ISysTenantService tenantService;
+    private final ICompanyAccountService companyAccountService;
 
     @Override
     public TableDataInfo<MainBackOrderVo> queryPageList(MainBackOrderBo bo, PageQuery pageQuery) {
@@ -327,6 +332,136 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         return vo;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean payOrderByBalance(Long orderId, String tenantId) {
+        MainOrder mainOrder = mainOrderMapper.selectById(orderId);
+        if (mainOrder == null) {
+            throw new ServiceException("订单不存在");
+        }
+        if (!StringUtils.equals(mainOrder.getTenantId(), tenantId)) {
+            throw new ServiceException("无权操作该订单");
+        }
+        if (mainOrder.getOrderStatus() == null || mainOrder.getOrderStatus() != 0) {
+            throw new ServiceException("订单状态不正确,无法支付");
+        }
+        if (mainOrder.getPayStatus() != null && mainOrder.getPayStatus() != 0) {
+            throw new ServiceException("订单已支付或已关闭");
+        }
+        if (mainOrder.getBusinessId() == null) {
+            throw new ServiceException("背调订单不存在");
+        }
+
+        MainBackOrder backOrder = baseMapper.selectById(mainOrder.getBusinessId());
+        if (backOrder == null) {
+            throw new ServiceException("背调订单不存在");
+        }
+
+        SysTenant tenant = sysTenantMapper.selectOne(
+            Wrappers.<SysTenant>lambdaQuery()
+                .eq(SysTenant::getTenantId, tenantId)
+                .last("limit 1")
+        );
+        if (tenant == null) {
+            throw new ServiceException("企业信息不存在");
+        }
+
+        BigDecimal totalAmount = mainOrder.getTotalAmount() == null ? BigDecimal.ZERO : mainOrder.getTotalAmount();
+        if (defaultAmount(tenant.getAvailableBalance()).compareTo(totalAmount) < 0) {
+            throw new ServiceException("可用余额不足,请充值或选择支付宝支付");
+        }
+        companyAccountService.payByBalance(
+            tenant.getId(),
+            totalAmount,
+            mainOrder.getId(),
+            mainOrder.getOrderNo(),
+            "背调订单支付:" + mainOrder.getOrderNo()
+        );
+
+        Payment payment = new Payment();
+        payment.setPaymentNo("PAY" + cn.hutool.core.date.DateUtil.format(new Date(), "yyyyMMddHHmmss")
+            + cn.hutool.core.util.RandomUtil.randomNumbers(4));
+        payment.setOrderId(orderId);
+        payment.setOrderNo(mainOrder.getOrderNo());
+        payment.setPaymentType(3);
+        payment.setPaymentMethod(3);
+        payment.setPaymentAmount(totalAmount);
+        payment.setPaymentStatus(2);
+        payment.setPayTime(new Date());
+        payment.setRemark("余额支付");
+        paymentMapper.insert(payment);
+
+        mainOrder.setOrderStatus(1);
+        mainOrder.setPayStatus(2);
+        mainOrder.setPaidAmount(totalAmount);
+        mainOrder.setPayTime(new Date());
+        mainOrderMapper.updateById(mainOrder);
+
+        backOrder.setStatus("1");
+        baseMapper.updateById(backOrder);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean completeOrder(Long id) {
+        MainBackOrder backOrder = baseMapper.selectById(id);
+        if (backOrder == null) {
+            throw new ServiceException("背调订单不存在");
+        }
+        if ("2".equals(backOrder.getStatus())) {
+            throw new ServiceException("订单已完成,无需重复操作");
+        }
+        if ("3".equals(backOrder.getStatus())) {
+            throw new ServiceException("订单已退款,无法完成");
+        }
+
+        MainOrder mainOrder = mainOrderMapper.selectOne(
+            Wrappers.<MainOrder>lambdaQuery()
+                .eq(MainOrder::getBusinessId, id)
+                .eq(MainOrder::getOrderType, 3)
+                .last("limit 1")
+        );
+        if (mainOrder == null) {
+            throw new ServiceException("订单不存在");
+        }
+        if (mainOrder.getPayStatus() == null || mainOrder.getPayStatus() != 2) {
+            throw new ServiceException("订单未支付,无法完成");
+        }
+
+        List<MainBackRecord> records = recordMapper.selectList(
+            Wrappers.<MainBackRecord>lambdaQuery()
+                .eq(MainBackRecord::getOrderId, id)
+        );
+        if (records.isEmpty()) {
+            throw new ServiceException("订单下暂无候选人记录,无法完成");
+        }
+        boolean hasUnfinished = records.stream()
+            .anyMatch(record -> !"已完成".equals(record.getStatus()) && !"完成".equals(record.getStatus()));
+        if (hasUnfinished) {
+            throw new ServiceException("仍有候选人未完成背调,无法完成订单");
+        }
+
+        Payment latestPayment = queryLatestPayment(mainOrder.getId());
+        if (isBalancePayment(latestPayment)) {
+            companyAccountService.deductInUseBalance(
+                mainOrder.getBuyerId(),
+                defaultAmount(mainOrder.getTotalAmount()),
+                mainOrder.getId(),
+                mainOrder.getOrderNo(),
+                "订单完成扣款:" + mainOrder.getOrderNo()
+            );
+        }
+
+        backOrder.setStatus("2");
+        baseMapper.updateById(backOrder);
+
+        mainOrder.setOrderStatus(2);
+        mainOrder.setCompleteTime(new Date());
+        mainOrderMapper.updateById(mainOrder);
+        return true;
+    }
+
     private void fillPortalFields(MainBackOrderVo vo, MainOrder mainOrder) {
         Long count = recordMapper.selectCount(
             new LambdaQueryWrapper<MainBackRecord>()
@@ -390,6 +525,19 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         return amount == null ? BigDecimal.ZERO : amount;
     }
 
+    private Payment queryLatestPayment(Long orderId) {
+        return paymentMapper.selectOne(
+            Wrappers.<Payment>lambdaQuery()
+                .eq(Payment::getOrderId, orderId)
+                .orderByDesc(Payment::getCreateTime)
+                .last("limit 1")
+        );
+    }
+
+    private boolean isBalancePayment(Payment payment) {
+        return payment == null || Integer.valueOf(3).equals(payment.getPaymentMethod());
+    }
+
     private List<MainBackOrderCandidateVo> buildPortalCandidates(Long backOrderId) {
         List<MainBackRecord> records = recordMapper.selectList(
             Wrappers.<MainBackRecord>lambdaQuery()