Gqingci vor 1 Woche
Ursprung
Commit
0d15c1bcf8
26 geänderte Dateien mit 1154 neuen und 21 gelöschten Zeilen
  1. 2 2
      ruoyi-admin/src/main/resources/application-dev.yml
  2. 13 4
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/CsMessageController.java
  3. 2 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/CsSessionController.java
  4. 4 8
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalAccountController.java
  5. 2 2
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCheckController.java
  6. 18 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalPaymentController.java
  7. 153 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalWithdrawController.java
  8. 72 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/WithdrawController.java
  9. 51 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/CompanyAccountFlow.java
  10. 52 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/Withdraw.java
  11. 47 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/WithdrawAccount.java
  12. 38 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/WithdrawBo.java
  13. 1 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/PaymentVo.java
  14. 62 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/WithdrawAccountVo.java
  15. 66 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/WithdrawVo.java
  16. 7 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/CompanyAccountFlowMapper.java
  17. 8 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/WithdrawAccountMapper.java
  18. 8 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/WithdrawMapper.java
  19. 1 1
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainBackOrderService.java
  20. 2 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IPaymentConfigService.java
  21. 26 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IWithdrawService.java
  22. 27 4
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java
  23. 12 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/PaymentConfigServiceImpl.java
  24. 434 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WithdrawServiceImpl.java
  25. 21 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysTenant.java
  26. 25 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysTenantVo.java

+ 2 - 2
ruoyi-admin/src/main/resources/application-dev.yml

@@ -96,8 +96,8 @@ spring:
 spring.data:
   redis:
     # 地址
-    host: localhost
-#    password: 123456
+    host: 192.168.194.130
+    password: 123456
     # 端口,默认为6379
     port: 6379
     # 数据库索引

+ 13 - 4
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/CsMessageController.java

@@ -1,5 +1,6 @@
 package org.dromara.main.controller;
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.log.annotation.Log;
@@ -28,6 +29,7 @@ public class CsMessageController extends BaseController {
     /**
      * 获取历史消息
      */
+    @SaIgnore
     @GetMapping("/history")
     public TableDataInfo<CsMessageVo> history(
         @RequestParam Long sessionId,
@@ -39,40 +41,46 @@ public class CsMessageController extends BaseController {
     /**
      * 发送文本消息
      */
+    @SaIgnore
     @Log(title = "发送文本消息", businessType = BusinessType.INSERT)
     @PostMapping("/send/text")
     public R<CsMessageVo> sendText(@Validated @RequestBody CsMessageBo bo) {
         Long currentUserId = LoginHelper.getUserId();
-        bo.setSenderId(currentUserId);
+        if (currentUserId != null) {
+            bo.setSenderId(currentUserId);
+        }
         return R.ok(messageService.sendTextMessage(bo));
     }
 
     /**
      * 发送图片消息
      */
+    @SaIgnore
     @Log(title = "发送图片消息", businessType = BusinessType.INSERT)
     @PostMapping("/send/image")
     public R<CsMessageVo> sendImage(
         @RequestParam Long sessionId,
         @RequestParam String msgNo,
-//        @RequestParam Long senderId,
+        @RequestParam(required = false) Long senderId,
         @RequestParam("file") MultipartFile file) {
         Long currentUserId = LoginHelper.getUserId();
-        return R.ok(messageService.sendImageMessage(sessionId, msgNo, currentUserId, file));
+        return R.ok(messageService.sendImageMessage(sessionId, msgNo, currentUserId != null ? currentUserId : senderId, file));
     }
 
     /**
      * 发送文件消息
      */
+    @SaIgnore
     @Log(title = "发送文件消息", businessType = BusinessType.INSERT)
     @PostMapping("/send/file")
     public R<CsMessageVo> sendFile(
         @RequestParam Long sessionId,
         @RequestParam String msgNo,
+        @RequestParam(required = false) Long senderId,
         @RequestParam("file") MultipartFile file) {
         Long currentUserId = LoginHelper.getUserId();
 
-        return R.ok(messageService.sendFileMessage(sessionId, msgNo, currentUserId, file));
+        return R.ok(messageService.sendFileMessage(sessionId, msgNo, currentUserId != null ? currentUserId : senderId, file));
     }
 
     /**
@@ -87,6 +95,7 @@ public class CsMessageController extends BaseController {
     /**
      * 标记消息已读
      */
+    @SaIgnore
     @PutMapping("/read")
     public R<Void> markAsRead(@RequestBody Map<String, Long> params) {
         return toAjax(messageService.markAsRead(

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/CsSessionController.java

@@ -1,5 +1,6 @@
 package org.dromara.main.controller;
 
+import cn.dev33.satoken.annotation.SaIgnore;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.log.annotation.Log;
@@ -26,6 +27,7 @@ public class CsSessionController extends BaseController {
     /**
      * 创建或获取会话
      */
+    @SaIgnore
     @PostMapping("/create")
     public R<CsSessionVo> createOrGetSession(@RequestBody Map<String, Object> params) {
         Integer sessionType = Integer.valueOf(params.get("sessionType").toString());

+ 4 - 8
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalAccountController.java

@@ -19,8 +19,7 @@ import java.util.Map;
 /**
  * 门户企业账户信息接口
  *
- * 当前新版项目的企业主体挂在 sys_tenant 上,但钱包余额相关字段/表尚未迁移,
- * 因此这里先返回当前登录企业基础信息,并对余额字段做零值兜底,避免前端 404。
+ * 当前新版项目的企业主体挂在 sys_tenant 上,钱包余额直接从租户表读取。
  */
 @Slf4j
 @Validated
@@ -50,12 +49,9 @@ public class PortalAccountController extends BaseController {
         result.put("companyId", company.getId());
         result.put("companyName", company.getCompanyName());
 
-        // TODO: 钱包体系迁移后,改为读取真实余额字段/账户表。
-        result.put("availableBalance", BigDecimal.ZERO);
-        result.put("inUseBalance", BigDecimal.ZERO);
-        result.put("withdrawingBalance", BigDecimal.ZERO);
-
-        log.warn("portal/account/balance 使用零值兜底返回,tenantId={},待接入真实钱包数据", tenantId);
+        result.put("availableBalance", company.getAvailableBalance() == null ? BigDecimal.ZERO : company.getAvailableBalance());
+        result.put("inUseBalance", company.getInUseBalance() == null ? BigDecimal.ZERO : company.getInUseBalance());
+        result.put("withdrawingBalance", company.getWithdrawingBalance() == null ? BigDecimal.ZERO : company.getWithdrawingBalance());
         return R.ok(result);
     }
 }

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

@@ -140,12 +140,12 @@ public class PortalCheckController extends BaseController {
     }
 
     @GetMapping("/order/detail")
-    public R<MainBackOrderVo> getOrderDetail(@RequestParam Long orderId) {
+    public R<MainBackOrderVo> getOrderDetail(@RequestParam String orderNo) {
         String tenantId = LoginHelper.getTenantId();
         if (tenantId == null || tenantId.isBlank()) {
             return R.fail("未登录或token已失效");
         }
-        MainBackOrderVo detail = mainBackOrderService.queryPortalDetail(orderId, tenantId);
+        MainBackOrderVo detail = mainBackOrderService.queryPortalDetail(orderNo, tenantId);
         if (detail == null) {
             return R.fail("订单不存在");
         }

+ 18 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalPaymentController.java

@@ -66,6 +66,9 @@ public class PortalPaymentController extends BaseController {
     @GetMapping("/status")
     public R<PaymentVo> queryPaymentStatus(@NotNull(message = "订单ID不能为空") @RequestParam Long orderId) {
         PaymentVo vo = paymentService.queryPaymentStatus(orderId);
+        if (vo != null) {
+            vo.setPaymentStatusText(toPaymentStatusText(vo.getPaymentStatus()));
+        }
         return vo == null ? R.fail("支付记录不存在") : R.ok(vo);
     }
 
@@ -78,8 +81,23 @@ public class PortalPaymentController extends BaseController {
         }
         Map<String, Object> result = new HashMap<>();
         result.put("orderId", payment.getOrderId());
+        result.put("orderNo", payment.getOrderNo());
         result.put("paymentStatus", payment.getPaymentStatus());
+        result.put("paymentStatusText", toPaymentStatusText(payment.getPaymentStatus()));
         result.put("paymentAmount", payment.getPaymentAmount());
         return R.ok(result);
     }
+
+    private String toPaymentStatusText(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        return switch (status) {
+            case 0 -> "待支付";
+            case 1 -> "支付中";
+            case 2 -> "支付成功";
+            case 3 -> "支付失败";
+            default -> "未知";
+        };
+    }
 }

+ 153 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalWithdrawController.java

@@ -0,0 +1,153 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.core.util.RandomUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.WithdrawAccount;
+import org.dromara.main.domain.bo.WithdrawBo;
+import org.dromara.main.domain.vo.WithdrawAccountVo;
+import org.dromara.main.domain.vo.WithdrawVo;
+import org.dromara.main.mapper.WithdrawAccountMapper;
+import org.dromara.main.service.IWithdrawService;
+import org.dromara.system.domain.vo.SysTenantVo;
+import org.dromara.system.service.ISysTenantService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 门户提现接口
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/portal/withdraw")
+public class PortalWithdrawController extends BaseController {
+
+    private final IWithdrawService withdrawService;
+    private final WithdrawAccountMapper withdrawAccountMapper;
+    private final ISysTenantService tenantService;
+
+    @GetMapping("/account/list")
+    public R<List<WithdrawAccountVo>> getAccountList() {
+        Long companyId = getCurrentCompany().getId();
+
+        LambdaQueryWrapper<WithdrawAccount> lqw = Wrappers.lambdaQuery();
+        lqw.eq(WithdrawAccount::getCompanyId, companyId);
+        lqw.eq(WithdrawAccount::getStatus, 1);
+        lqw.orderByDesc(WithdrawAccount::getIsDefault);
+        lqw.orderByDesc(WithdrawAccount::getCreateTime);
+        return R.ok(withdrawAccountMapper.selectVoList(lqw));
+    }
+
+    @PostMapping("/account/save")
+    public R<Void> saveAccount(@RequestBody WithdrawAccount account) {
+        Long companyId = getCurrentCompany().getId();
+        account.setCompanyId(companyId);
+        account.setStatus(1);
+        if (account.getIsDefault() == null) {
+            account.setIsDefault(1);
+        }
+
+        if (account.getIsDefault() == 1) {
+            WithdrawAccount reset = new WithdrawAccount();
+            reset.setIsDefault(0);
+            withdrawAccountMapper.update(reset, Wrappers.<WithdrawAccount>lambdaUpdate()
+                .eq(WithdrawAccount::getCompanyId, companyId));
+        }
+
+        if (account.getId() != null) {
+            WithdrawAccount exists = withdrawAccountMapper.selectById(account.getId());
+            if (exists == null || !companyId.equals(exists.getCompanyId())) {
+                return R.fail("收款账户不存在");
+            }
+            withdrawAccountMapper.updateById(account);
+        } else {
+            WithdrawAccount exists = withdrawAccountMapper.selectOne(Wrappers.<WithdrawAccount>lambdaQuery()
+                .eq(WithdrawAccount::getCompanyId, companyId)
+                .eq(WithdrawAccount::getAccountType, account.getAccountType())
+                .last("limit 1"));
+            if (exists != null) {
+                account.setId(exists.getId());
+                withdrawAccountMapper.updateById(account);
+            } else {
+                withdrawAccountMapper.insert(account);
+            }
+        }
+
+        return R.ok();
+    }
+
+    @RateLimiter(key = "#phonenumber", time = 60, count = 5)
+    @PostMapping("/sendSmsCode")
+    public R<Void> sendSmsCode(@RequestParam @NotBlank(message = "手机号不能为空") String phonenumber) {
+        String code = RandomUtil.randomNumbers(6);
+        RedisUtils.setCacheObject("captcha_codes:withdraw:" + phonenumber, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+        log.warn("提现验证码已生成,手机号: {}, code: {}", phonenumber, code);
+        return R.ok();
+    }
+
+    @GetMapping("/list")
+    public TableDataInfo<WithdrawVo> list(WithdrawBo bo, PageQuery pageQuery) {
+        bo.setCompanyId(getCurrentCompany().getId());
+        return withdrawService.queryPageList(bo, pageQuery);
+    }
+
+    @PostMapping("/apply")
+    public R<Long> apply(@Validated(AddGroup.class) @RequestBody WithdrawBo bo) {
+        Long id = withdrawService.applyWithdraw(bo);
+        return R.ok("提现申请已提交,等待审核", id);
+    }
+
+    @SaIgnore
+    @PostMapping("/alipay/notify")
+    public String alipayTransferNotify(HttpServletRequest request) {
+        Map<String, String> params = new HashMap<>();
+        Map<String, String[]> requestParams = request.getParameterMap();
+        for (String name : requestParams.keySet()) {
+            String[] values = requestParams.get(name);
+            String valueStr = "";
+            for (int i = 0; i < values.length; i++) {
+                valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
+            }
+            params.put(name, valueStr);
+        }
+        return withdrawService.handleAlipayTransferNotify(params);
+    }
+
+    private SysTenantVo getCurrentCompany() {
+        String tenantId = LoginHelper.getTenantId();
+        if (tenantId == null || tenantId.isBlank()) {
+            throw new org.dromara.common.core.exception.ServiceException("未登录或token已失效");
+        }
+        SysTenantVo company = tenantService.queryByTenantId(tenantId);
+        if (company == null) {
+            throw new org.dromara.common.core.exception.ServiceException("企业信息不存在");
+        }
+        return company;
+    }
+}

+ 72 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/WithdrawController.java

@@ -0,0 +1,72 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.bo.WithdrawBo;
+import org.dromara.main.domain.vo.WithdrawVo;
+import org.dromara.main.service.IWithdrawService;
+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.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 后台提现管理Controller
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/system/withdraw")
+public class WithdrawController extends BaseController {
+
+    private final IWithdrawService withdrawService;
+
+    @SaCheckPermission("system:withdraw:list")
+    @GetMapping("/list")
+    public TableDataInfo<WithdrawVo> list(WithdrawBo bo, PageQuery pageQuery) {
+        return withdrawService.queryPageList(bo, pageQuery);
+    }
+
+    @SaCheckPermission("system:withdraw:export")
+    @Log(title = "提现管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(WithdrawBo bo, HttpServletResponse response) {
+        List<WithdrawVo> list = withdrawService.queryList(bo);
+        ExcelUtil.exportExcel(list, "提现管理", WithdrawVo.class, response);
+    }
+
+    @SaCheckPermission("system:withdraw:query")
+    @GetMapping("/{id}")
+    public R<WithdrawVo> getInfo(@PathVariable Long id) {
+        return R.ok(withdrawService.queryById(id));
+    }
+
+    @SaCheckPermission("system:withdraw:audit")
+    @Log(title = "提现管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/audit/pass/{id}")
+    public R<Void> auditPass(@PathVariable Long id, @RequestParam(required = false) String remark) {
+        withdrawService.auditAndTransfer(id, remark);
+        return R.ok("审核通过,打款成功");
+    }
+
+    @SaCheckPermission("system:withdraw:audit")
+    @Log(title = "提现管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/audit/reject/{id}")
+    public R<Void> auditReject(@PathVariable Long id, @RequestParam String remark) {
+        withdrawService.auditReject(id, remark);
+        return R.ok("审核拒绝成功");
+    }
+}

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

@@ -0,0 +1,51 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+
+/**
+ * 企业账户流水对象 main_company_account_flow
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_company_account_flow")
+public class CompanyAccountFlow extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    private Long companyId;
+
+    private String flowNo;
+
+    private Integer flowType;
+
+    private BigDecimal amount;
+
+    private Integer balanceType;
+
+    private BigDecimal balanceBefore;
+
+    private BigDecimal balanceAfter;
+
+    private Integer businessType;
+
+    private Long businessId;
+
+    private String businessNo;
+
+    private String remark;
+
+    @TableLogic
+    private String delFlag;
+}

+ 52 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/Withdraw.java

@@ -0,0 +1,52 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 企业提现申请对象 main_withdraw
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_withdraw")
+public class Withdraw extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    private String withdrawNo;
+
+    private Long companyId;
+
+    private Long accountId;
+
+    private BigDecimal withdrawAmount;
+
+    private Integer withdrawStatus;
+
+    private String tradeNo;
+
+    private Long auditorId;
+
+    private Date auditTime;
+
+    private String auditRemark;
+
+    private String failReason;
+
+    private Date transferTime;
+
+    @TableLogic
+    private String delFlag;
+}

+ 47 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/WithdrawAccount.java

@@ -0,0 +1,47 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.util.Date;
+
+/**
+ * 企业收款账户对象 main_company_withdraw_account
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_company_withdraw_account")
+public class WithdrawAccount extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    private Long companyId;
+
+    private Integer accountType;
+
+    private String accountName;
+
+    private String accountNumber;
+
+    private String bankName;
+
+    private String bankBranch;
+
+    private Integer isDefault;
+
+    private Integer status;
+
+    private Long auditorId;
+
+    private Date auditTime;
+
+    private String auditRemark;
+}

+ 38 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/WithdrawBo.java

@@ -0,0 +1,38 @@
+package org.dromara.main.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.main.domain.Withdraw;
+
+import java.math.BigDecimal;
+
+/**
+ * 提现申请业务对象
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = Withdraw.class, reverseConvertGenerate = false)
+public class WithdrawBo extends BaseEntity {
+
+    @NotNull(message = "主键不能为空", groups = { EditGroup.class })
+    private Long id;
+
+    private String withdrawNo;
+
+    private Long companyId;
+
+    @NotNull(message = "收款账户不能为空", groups = { AddGroup.class })
+    private Long accountId;
+
+    @NotNull(message = "提现金额不能为空", groups = { AddGroup.class })
+    private BigDecimal amount;
+
+    private Integer withdrawStatus;
+
+    private String smsCode;
+}

+ 1 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/PaymentVo.java

@@ -22,6 +22,7 @@ public class PaymentVo implements Serializable {
     private String tradeNo;
     private String tradeStatus;
     private Integer paymentStatus;
+    private String paymentStatusText;
     private Date payTime;
     private String remark;
     private Date createTime;

+ 62 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/WithdrawAccountVo.java

@@ -0,0 +1,62 @@
+package org.dromara.main.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.main.domain.WithdrawAccount;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 收款账户视图对象
+ */
+@Data
+@AutoMapper(target = WithdrawAccount.class)
+public class WithdrawAccountVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "账户ID")
+    private Long id;
+
+    @ExcelProperty(value = "企业ID")
+    private Long companyId;
+
+    @ExcelProperty(value = "账户类型")
+    @ExcelDictFormat(dictType = "withdraw_account_type")
+    private Integer accountType;
+
+    @ExcelProperty(value = "开户人姓名")
+    private String accountName;
+
+    @ExcelProperty(value = "账号")
+    private String accountNumber;
+
+    @ExcelProperty(value = "开户银行")
+    private String bankName;
+
+    @ExcelProperty(value = "开户支行")
+    private String bankBranch;
+
+    @ExcelProperty(value = "是否默认")
+    private Integer isDefault;
+
+    @ExcelProperty(value = "状态")
+    private Integer status;
+
+    @ExcelProperty(value = "审核人ID")
+    private Long auditorId;
+
+    @ExcelProperty(value = "审核时间")
+    private Date auditTime;
+
+    @ExcelProperty(value = "审核备注")
+    private String auditRemark;
+
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+}

+ 66 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/WithdrawVo.java

@@ -0,0 +1,66 @@
+package org.dromara.main.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.main.domain.Withdraw;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 提现申请视图对象
+ */
+@Data
+@AutoMapper(target = Withdraw.class)
+public class WithdrawVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "提现ID")
+    private Long id;
+
+    @ExcelProperty(value = "提现单号")
+    private String withdrawNo;
+
+    @ExcelProperty(value = "企业ID")
+    private Long companyId;
+
+    @ExcelProperty(value = "企业名称")
+    private String companyName;
+
+    @ExcelProperty(value = "收款账户ID")
+    private Long accountId;
+
+    @ExcelProperty(value = "提现金额")
+    private BigDecimal withdrawAmount;
+
+    @ExcelProperty(value = "提现状态")
+    private Integer withdrawStatus;
+
+    @ExcelProperty(value = "支付宝流水号")
+    private String tradeNo;
+
+    @ExcelProperty(value = "审核人ID")
+    private Long auditorId;
+
+    @ExcelProperty(value = "审核时间")
+    private Date auditTime;
+
+    @ExcelProperty(value = "审核备注")
+    private String auditRemark;
+
+    @ExcelProperty(value = "失败原因")
+    private String failReason;
+
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+
+    @ExcelProperty(value = "打款时间")
+    private Date transferTime;
+
+    private WithdrawAccountVo account;
+}

+ 7 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/CompanyAccountFlowMapper.java

@@ -0,0 +1,7 @@
+package org.dromara.main.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.CompanyAccountFlow;
+
+public interface CompanyAccountFlowMapper extends BaseMapperPlus<CompanyAccountFlow, CompanyAccountFlow> {
+}

+ 8 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/WithdrawAccountMapper.java

@@ -0,0 +1,8 @@
+package org.dromara.main.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.WithdrawAccount;
+import org.dromara.main.domain.vo.WithdrawAccountVo;
+
+public interface WithdrawAccountMapper extends BaseMapperPlus<WithdrawAccount, WithdrawAccountVo> {
+}

+ 8 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/WithdrawMapper.java

@@ -0,0 +1,8 @@
+package org.dromara.main.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.Withdraw;
+import org.dromara.main.domain.vo.WithdrawVo;
+
+public interface WithdrawMapper extends BaseMapperPlus<Withdraw, WithdrawVo> {
+}

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

@@ -26,7 +26,7 @@ public interface IMainBackOrderService {
     /**
      * 查询门户订单详情
      */
-    MainBackOrderVo queryPortalDetail(Long orderId, String tenantId);
+    MainBackOrderVo queryPortalDetail(String orderNo, String tenantId);
 
     /**
      * 门户创建背调订单

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IPaymentConfigService.java

@@ -7,4 +7,6 @@ public interface IPaymentConfigService {
     PaymentConfig getEnabledAlipayConfig();
 
     PaymentConfig getEnabledReceiveConfig();
+
+    PaymentConfig getEnabledTransferConfig();
 }

+ 26 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IWithdrawService.java

@@ -0,0 +1,26 @@
+package org.dromara.main.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.main.domain.bo.WithdrawBo;
+import org.dromara.main.domain.vo.WithdrawVo;
+
+import java.util.List;
+import java.util.Map;
+
+public interface IWithdrawService {
+
+    WithdrawVo queryById(Long id);
+
+    TableDataInfo<WithdrawVo> queryPageList(WithdrawBo bo, PageQuery pageQuery);
+
+    List<WithdrawVo> queryList(WithdrawBo bo);
+
+    Long applyWithdraw(WithdrawBo bo);
+
+    Boolean auditAndTransfer(Long id, String auditRemark);
+
+    Boolean auditReject(Long id, String auditRemark);
+
+    String handleAlipayTransferNotify(Map<String, String> params);
+}

+ 27 - 4
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java

@@ -150,9 +150,14 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
     }
 
     @Override
-    public MainBackOrderVo queryPortalDetail(Long orderId, String tenantId) {
-        MainOrder mainOrder = mainOrderMapper.selectById(orderId);
-        if (mainOrder == null || !StringUtils.equals(mainOrder.getTenantId(), tenantId)) {
+    public MainBackOrderVo queryPortalDetail(String orderNo, String tenantId) {
+        MainOrder mainOrder = mainOrderMapper.selectOne(
+            Wrappers.<MainOrder>lambdaQuery()
+                .eq(MainOrder::getOrderNo, orderNo)
+                .eq(MainOrder::getTenantId, tenantId)
+                .last("limit 1")
+        );
+        if (mainOrder == null) {
             return null;
         }
         if (mainOrder.getBusinessId() == null) {
@@ -168,11 +173,14 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         fillPortalFields(vo, mainOrder);
         vo.setPayment(paymentMapper.selectVoOne(
             Wrappers.<Payment>lambdaQuery()
-                .eq(Payment::getOrderId, orderId)
+                .eq(Payment::getOrderId, mainOrder.getId())
                 .orderByDesc(Payment::getCreateTime)
                 .last("limit 1"),
             false
         ));
+        if (vo.getPayment() != null) {
+            vo.getPayment().setPaymentStatusText(toPaymentStatusText(vo.getPayment().getPaymentStatus()));
+        }
         vo.setCandidates(buildPortalCandidates(backOrder.getId()));
         vo.setClauses(buildPortalClauses(backOrder));
         return vo;
@@ -330,6 +338,8 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         vo.setCompanyId(mainOrder.getBuyerId());
         vo.setTenantId(mainOrder.getTenantId());
         vo.setTotalAmount(mainOrder.getTotalAmount());
+        vo.setCreateTime(mainOrder.getCreateTime() == null ? null :
+            mainOrder.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
         vo.setOrderType(vo.getCategoryId() != null ? 1 : 2);
         vo.setOrderStatus(parseIntOrDefault(vo.getStatus(), 0));
         vo.setMainOrderStatus(mainOrder.getOrderStatus());
@@ -477,6 +487,19 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         };
     }
 
+    private String toPaymentStatusText(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        return switch (status) {
+            case 0 -> "待支付";
+            case 1 -> "支付中";
+            case 2 -> "支付成功";
+            case 3 -> "支付失败";
+            default -> "未知";
+        };
+    }
+
     private Integer parseIntOrDefault(String value, int defaultValue) {
         if (StringUtils.isBlank(value)) {
             return defaultValue;

+ 12 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/PaymentConfigServiceImpl.java

@@ -29,4 +29,16 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
                 .last("limit 1")
         );
     }
+
+    @Override
+    public PaymentConfig getEnabledTransferConfig() {
+        return paymentConfigMapper.selectOne(
+            Wrappers.<PaymentConfig>lambdaQuery()
+                .eq(PaymentConfig::getConfigType, 2)
+                .eq(PaymentConfig::getPaymentType, 1)
+                .eq(PaymentConfig::getIsEnabled, 1)
+                .orderByDesc(PaymentConfig::getCreateTime)
+                .last("limit 1")
+        );
+    }
 }

+ 434 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WithdrawServiceImpl.java

@@ -0,0 +1,434 @@
+package org.dromara.main.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.internal.util.AlipaySignature;
+import com.alipay.api.request.AlipayFundTransUniTransferRequest;
+import com.alipay.api.response.AlipayFundTransUniTransferResponse;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.main.domain.CompanyAccountFlow;
+import org.dromara.main.domain.PaymentConfig;
+import org.dromara.main.domain.Withdraw;
+import org.dromara.main.domain.WithdrawAccount;
+import org.dromara.main.domain.bo.WithdrawBo;
+import org.dromara.main.domain.vo.WithdrawAccountVo;
+import org.dromara.main.domain.vo.WithdrawVo;
+import org.dromara.main.mapper.CompanyAccountFlowMapper;
+import org.dromara.main.mapper.WithdrawAccountMapper;
+import org.dromara.main.mapper.WithdrawMapper;
+import org.dromara.main.service.IPaymentConfigService;
+import org.dromara.main.service.IWithdrawService;
+import org.dromara.system.domain.SysTenant;
+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;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 门户提现申请服务
+ */
+@RequiredArgsConstructor
+@Service
+public class WithdrawServiceImpl implements IWithdrawService {
+
+    private final WithdrawMapper withdrawMapper;
+    private final WithdrawAccountMapper withdrawAccountMapper;
+    private final CompanyAccountFlowMapper companyAccountFlowMapper;
+    private final ISysTenantService tenantService;
+    private final SysTenantMapper sysTenantMapper;
+    private final IPaymentConfigService paymentConfigService;
+
+    @Override
+    public WithdrawVo queryById(Long id) {
+        WithdrawVo vo = withdrawMapper.selectVoById(id);
+        if (vo != null) {
+            fillVoList(List.of(vo));
+        }
+        return vo;
+    }
+
+    @Override
+    public TableDataInfo<WithdrawVo> queryPageList(WithdrawBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<Withdraw> lqw = buildQueryWrapper(bo);
+        Page<WithdrawVo> result = withdrawMapper.selectVoPage(pageQuery.build(), lqw);
+        fillVoList(result.getRecords());
+        return TableDataInfo.build(result);
+    }
+
+    @Override
+    public List<WithdrawVo> queryList(WithdrawBo bo) {
+        List<WithdrawVo> list = withdrawMapper.selectVoList(buildQueryWrapper(bo));
+        fillVoList(list);
+        return list;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long applyWithdraw(WithdrawBo bo) {
+        SysTenantVo company = getCurrentCompany();
+        BigDecimal amount = defaultAmount(bo.getAmount());
+        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("提现金额必须大于0");
+        }
+
+        BigDecimal availableBalance = defaultAmount(company.getAvailableBalance());
+        if (availableBalance.compareTo(amount) < 0) {
+            throw new ServiceException("可使用余额不足,当前可用余额:{}元", availableBalance);
+        }
+
+        WithdrawAccount account = withdrawAccountMapper.selectById(bo.getAccountId());
+        if (account == null) {
+            throw new ServiceException("收款账户不存在");
+        }
+        if (!Objects.equals(account.getCompanyId(), company.getId())) {
+            throw new ServiceException("收款账户不属于当前企业");
+        }
+        if (!Objects.equals(account.getStatus(), 1)) {
+            throw new ServiceException("收款账户未审核通过");
+        }
+
+        Withdraw withdraw = new Withdraw();
+        withdraw.setWithdrawNo(generateWithdrawNo());
+        withdraw.setCompanyId(company.getId());
+        withdraw.setAccountId(account.getId());
+        withdraw.setWithdrawAmount(amount);
+        withdraw.setWithdrawStatus(0);
+        withdrawMapper.insert(withdraw);
+
+        BigDecimal withdrawingBalance = defaultAmount(company.getWithdrawingBalance());
+
+        SysTenant update = new SysTenant();
+        update.setId(company.getId());
+        update.setAvailableBalance(availableBalance.subtract(amount));
+        update.setWithdrawingBalance(withdrawingBalance.add(amount));
+        sysTenantMapper.updateById(update);
+
+        insertFlow(company.getId(), 2, amount, 1, availableBalance, availableBalance.subtract(amount), 5, withdraw.getId(), withdraw.getWithdrawNo(), "申请提现:" + withdraw.getWithdrawNo());
+        insertFlow(company.getId(), 1, amount, 3, withdrawingBalance, withdrawingBalance.add(amount), 5, withdraw.getId(), withdraw.getWithdrawNo(), "申请提现(冻结):" + withdraw.getWithdrawNo());
+
+        return withdraw.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean auditAndTransfer(Long id, String auditRemark) {
+        Withdraw withdraw = withdrawMapper.selectById(id);
+        if (withdraw == null) {
+            throw new ServiceException("提现申请不存在");
+        }
+        if (!Objects.equals(withdraw.getWithdrawStatus(), 0)) {
+            throw new ServiceException("提现状态不正确,当前状态:{}", withdraw.getWithdrawStatus());
+        }
+
+        withdraw.setWithdrawStatus(1);
+        withdraw.setAuditorId(LoginHelper.getUserId());
+        withdraw.setAuditTime(new Date());
+        withdraw.setAuditRemark(auditRemark);
+        withdrawMapper.updateById(withdraw);
+
+        try {
+            String tradeNo = transferToAlipay(withdraw);
+
+            SysTenant tenant = sysTenantMapper.selectById(withdraw.getCompanyId());
+            if (tenant == null) {
+                throw new ServiceException("企业信息不存在");
+            }
+            BigDecimal withdrawingBalance = defaultAmount(tenant.getWithdrawingBalance());
+            if (withdrawingBalance.compareTo(withdraw.getWithdrawAmount()) < 0) {
+                throw new ServiceException("提现中余额不足");
+            }
+            SysTenant update = new SysTenant();
+            update.setId(tenant.getId());
+            update.setWithdrawingBalance(withdrawingBalance.subtract(withdraw.getWithdrawAmount()));
+            sysTenantMapper.updateById(update);
+
+            insertFlow(tenant.getId(), 2, withdraw.getWithdrawAmount(), 3, withdrawingBalance,
+                withdrawingBalance.subtract(withdraw.getWithdrawAmount()), 6, withdraw.getId(), withdraw.getWithdrawNo(), "提现成功:" + withdraw.getWithdrawNo());
+
+            withdraw.setWithdrawStatus(3);
+            withdraw.setTradeNo(tradeNo);
+            withdraw.setTransferTime(new Date());
+            withdrawMapper.updateById(withdraw);
+            return true;
+        } catch (Exception e) {
+            refundWithdrawToAvailable(withdraw, "提现失败,退回可使用余额");
+            withdraw.setWithdrawStatus(4);
+            withdraw.setFailReason(e.getMessage());
+            withdrawMapper.updateById(withdraw);
+            throw new ServiceException("打款失败:{}", e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean auditReject(Long id, String auditRemark) {
+        Withdraw withdraw = withdrawMapper.selectById(id);
+        if (withdraw == null) {
+            throw new ServiceException("提现申请不存在");
+        }
+        if (!Objects.equals(withdraw.getWithdrawStatus(), 0)) {
+            throw new ServiceException("提现状态不正确");
+        }
+
+        refundWithdrawToAvailable(withdraw, "提现审核拒绝,退回可使用余额");
+
+        withdraw.setWithdrawStatus(5);
+        withdraw.setAuditorId(LoginHelper.getUserId());
+        withdraw.setAuditTime(new Date());
+        withdraw.setAuditRemark(auditRemark);
+        withdrawMapper.updateById(withdraw);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String handleAlipayTransferNotify(Map<String, String> params) {
+        try {
+            PaymentConfig config = paymentConfigService.getEnabledTransferConfig();
+            if (config == null) {
+                return "failure";
+            }
+
+            boolean signVerified = AlipaySignature.rsaCheckV1(
+                params,
+                config.getPublicKey(),
+                defaultIfBlank(config.getCharset(), "UTF-8"),
+                defaultIfBlank(config.getSignType(), "RSA2")
+            );
+            if (!signVerified) {
+                return "failure";
+            }
+
+            String outBizNo = params.get("out_biz_no");
+            String orderId = params.get("order_id");
+            String status = params.get("status");
+            String transAmount = params.get("trans_amount");
+
+            Withdraw withdraw = withdrawMapper.selectOne(Wrappers.<Withdraw>lambdaQuery()
+                .eq(Withdraw::getWithdrawNo, outBizNo)
+                .last("limit 1"));
+            if (withdraw == null) {
+                return "failure";
+            }
+            if (Objects.equals(withdraw.getWithdrawStatus(), 3)) {
+                return "success";
+            }
+
+            BigDecimal notifyAmount = new BigDecimal(transAmount);
+            if (notifyAmount.compareTo(withdraw.getWithdrawAmount()) != 0) {
+                return "failure";
+            }
+
+            if ("SUCCESS".equals(status)) {
+                withdraw.setTradeNo(orderId);
+                withdraw.setWithdrawStatus(3);
+                withdraw.setTransferTime(new Date());
+                withdrawMapper.updateById(withdraw);
+                return "success";
+            }
+
+            if ("FAIL".equals(status)) {
+                refundWithdrawToAvailable(withdraw, "支付宝转账失败,退回可使用余额");
+                withdraw.setWithdrawStatus(4);
+                withdraw.setFailReason("支付宝转账失败");
+                withdrawMapper.updateById(withdraw);
+                return "success";
+            }
+            return "success";
+        } catch (Exception e) {
+            return "failure";
+        }
+    }
+
+    private LambdaQueryWrapper<Withdraw> buildQueryWrapper(WithdrawBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<Withdraw> lqw = Wrappers.lambdaQuery();
+        lqw.eq(bo.getCompanyId() != null, Withdraw::getCompanyId, bo.getCompanyId());
+        lqw.eq(bo.getAccountId() != null, Withdraw::getAccountId, bo.getAccountId());
+        lqw.eq(bo.getWithdrawStatus() != null, Withdraw::getWithdrawStatus, bo.getWithdrawStatus());
+        lqw.eq(bo.getWithdrawNo() != null && !bo.getWithdrawNo().isBlank(), Withdraw::getWithdrawNo, bo.getWithdrawNo());
+        lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
+            Withdraw::getCreateTime, params.get("beginTime"), params.get("endTime"));
+        lqw.orderByDesc(Withdraw::getCreateTime);
+        return lqw;
+    }
+
+    private void fillVoList(List<WithdrawVo> voList) {
+        if (voList == null || voList.isEmpty()) {
+            return;
+        }
+
+        List<Long> accountIds = voList.stream()
+            .map(WithdrawVo::getAccountId)
+            .filter(Objects::nonNull)
+            .distinct()
+            .toList();
+
+        Map<Long, WithdrawAccount> accountMap = accountIds.isEmpty()
+            ? Map.of()
+            : withdrawAccountMapper.selectList(
+                    Wrappers.<WithdrawAccount>lambdaQuery().in(WithdrawAccount::getId, accountIds))
+                .stream()
+                .collect(Collectors.toMap(WithdrawAccount::getId, Function.identity(), (left, right) -> left));
+
+        SysTenantVo company = getCurrentCompany();
+        for (WithdrawVo vo : voList) {
+            vo.setCompanyName(company.getCompanyName());
+            WithdrawAccount account = accountMap.get(vo.getAccountId());
+            if (account != null) {
+                WithdrawAccountVo accountVo = new WithdrawAccountVo();
+                accountVo.setId(account.getId());
+                accountVo.setCompanyId(account.getCompanyId());
+                accountVo.setAccountType(account.getAccountType());
+                accountVo.setAccountName(account.getAccountName());
+                accountVo.setAccountNumber(account.getAccountNumber());
+                accountVo.setBankName(account.getBankName());
+                accountVo.setBankBranch(account.getBankBranch());
+                accountVo.setIsDefault(account.getIsDefault());
+                accountVo.setStatus(account.getStatus());
+                accountVo.setAuditorId(account.getAuditorId());
+                accountVo.setAuditTime(account.getAuditTime());
+                accountVo.setAuditRemark(account.getAuditRemark());
+                accountVo.setCreateTime(account.getCreateTime());
+                vo.setAccount(accountVo);
+            }
+        }
+    }
+
+    private SysTenantVo getCurrentCompany() {
+        String tenantId = LoginHelper.getTenantId();
+        if (tenantId == null || tenantId.isBlank()) {
+            throw new ServiceException("未登录或token已失效");
+        }
+        SysTenantVo company = tenantService.queryByTenantId(tenantId);
+        if (company == null) {
+            throw new ServiceException("企业信息不存在");
+        }
+        return company;
+    }
+
+    private BigDecimal defaultAmount(BigDecimal amount) {
+        return amount == null ? BigDecimal.ZERO : amount;
+    }
+
+    private void refundWithdrawToAvailable(Withdraw withdraw, String remark) {
+        SysTenant tenant = sysTenantMapper.selectById(withdraw.getCompanyId());
+        if (tenant == null) {
+            throw new ServiceException("企业信息不存在");
+        }
+        BigDecimal withdrawingBalance = defaultAmount(tenant.getWithdrawingBalance());
+        BigDecimal availableBalance = defaultAmount(tenant.getAvailableBalance());
+        if (withdrawingBalance.compareTo(withdraw.getWithdrawAmount()) < 0) {
+            throw new ServiceException("提现中余额不足");
+        }
+
+        SysTenant update = new SysTenant();
+        update.setId(tenant.getId());
+        update.setWithdrawingBalance(withdrawingBalance.subtract(withdraw.getWithdrawAmount()));
+        update.setAvailableBalance(availableBalance.add(withdraw.getWithdrawAmount()));
+        sysTenantMapper.updateById(update);
+
+        insertFlow(tenant.getId(), 2, withdraw.getWithdrawAmount(), 3, withdrawingBalance,
+            withdrawingBalance.subtract(withdraw.getWithdrawAmount()), 7, withdraw.getId(), withdraw.getWithdrawNo(), remark + "(解冻)");
+        insertFlow(tenant.getId(), 1, withdraw.getWithdrawAmount(), 1, availableBalance,
+            availableBalance.add(withdraw.getWithdrawAmount()), 7, withdraw.getId(), withdraw.getWithdrawNo(), remark);
+    }
+
+    private String transferToAlipay(Withdraw withdraw) throws Exception {
+        PaymentConfig config = paymentConfigService.getEnabledTransferConfig();
+        if (config == null) {
+            throw new ServiceException("未找到启用的付款配置,请先配置支付宝付款参数");
+        }
+
+        WithdrawAccount account = withdrawAccountMapper.selectById(withdraw.getAccountId());
+        if (account == null) {
+            throw new ServiceException("收款账户不存在");
+        }
+
+        AlipayClient client = new DefaultAlipayClient(
+            config.getGatewayUrl(),
+            config.getAppId(),
+            config.getPrivateKey(),
+            defaultIfBlank(config.getFormat(), "json"),
+            defaultIfBlank(config.getCharset(), "UTF-8"),
+            config.getPublicKey(),
+            defaultIfBlank(config.getSignType(), "RSA2")
+        );
+
+        AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
+        if (config.getTransferNotifyUrl() != null) {
+            request.setNotifyUrl(config.getTransferNotifyUrl());
+        }
+
+        JSONObject bizContent = JSONUtil.createObj();
+        bizContent.set("out_biz_no", withdraw.getWithdrawNo());
+        bizContent.set("trans_amount", withdraw.getWithdrawAmount().toString());
+        bizContent.set("product_code", "TRANS_ACCOUNT_NO_PWD");
+        bizContent.set("biz_scene", "DIRECT_TRANSFER");
+
+        JSONObject payeeInfo = JSONUtil.createObj();
+        payeeInfo.set("identity", account.getAccountNumber());
+        payeeInfo.set("identity_type", "ALIPAY_LOGON_ID");
+        payeeInfo.set("name", account.getAccountName());
+        bizContent.set("payee_info", payeeInfo);
+        bizContent.set("remark", "平台背调佣金提现");
+
+        request.setBizContent(bizContent.toString());
+
+        AlipayFundTransUniTransferResponse response = client.execute(request);
+        if (response.isSuccess()) {
+            return response.getOrderId();
+        }
+        throw new ServiceException("支付宝返回错误:{}", response.getSubMsg());
+    }
+
+    private void insertFlow(Long companyId, Integer flowType, BigDecimal amount, Integer balanceType,
+                            BigDecimal balanceBefore, BigDecimal balanceAfter, Integer businessType,
+                            Long businessId, String businessNo, String remark) {
+        CompanyAccountFlow flow = new CompanyAccountFlow();
+        flow.setCompanyId(companyId);
+        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 String generateFlowNo() {
+        return "CF" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomNumbers(4);
+    }
+
+    private String generateWithdrawNo() {
+        return "WD" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomNumbers(4);
+    }
+
+    private String defaultIfBlank(String value, String defaultValue) {
+        return value == null || value.isBlank() ? defaultValue : value;
+    }
+}

+ 21 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysTenant.java

@@ -8,6 +8,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.io.Serial;
+import java.math.BigDecimal;
 import java.util.Date;
 
 /**
@@ -64,6 +65,26 @@ public class SysTenant extends BaseEntity {
      */
     private String domain;
 
+    /**
+     * 提现中余额
+     */
+    private BigDecimal withdrawingBalance;
+
+    /**
+     * 使用中余额
+     */
+    private BigDecimal inUseBalance;
+
+    /**
+     * 累计消费
+     */
+    private BigDecimal totalConsume;
+
+    /**
+     * 可使用余额
+     */
+    private BigDecimal availableBalance;
+
     /**
      * 企业简介
      */

+ 25 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysTenantVo.java

@@ -10,6 +10,7 @@ import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
 
 import java.io.Serial;
+import java.math.BigDecimal;
 import java.io.Serializable;
 
 
@@ -68,6 +69,30 @@ public class SysTenantVo implements Serializable {
     @ExcelProperty(value = "地址")
     private String address;
 
+    /**
+     * 提现中余额
+     */
+    @ExcelProperty(value = "提现中余额")
+    private BigDecimal withdrawingBalance;
+
+    /**
+     * 使用中余额
+     */
+    @ExcelProperty(value = "使用中余额")
+    private BigDecimal inUseBalance;
+
+    /**
+     * 累计消费
+     */
+    @ExcelProperty(value = "累计消费")
+    private BigDecimal totalConsume;
+
+    /**
+     * 可使用余额
+     */
+    @ExcelProperty(value = "可使用余额")
+    private BigDecimal availableBalance;
+
     /**
      * 域名
      */