Commit c05eb941aa9d153aa82dd1231085147a1f4e49f3

Authored by shaofan
1 parent 796589c3

新增骑手提现审核功能,补充提现申请/审核/打款接口及冻结余额支持

src/main/java/com/diligrp/rider/controller/AdminRiderWithdrawController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.WithdrawAuditDTO;
  5 +import com.diligrp.rider.dto.WithdrawMarkPaidDTO;
  6 +import com.diligrp.rider.service.RiderWithdrawService;
  7 +import com.diligrp.rider.vo.PageResultVO;
  8 +import com.diligrp.rider.vo.RiderWithdrawApplyVO;
  9 +import jakarta.servlet.http.HttpServletRequest;
  10 +import jakarta.validation.Valid;
  11 +import lombok.RequiredArgsConstructor;
  12 +import org.springframework.web.bind.annotation.GetMapping;
  13 +import org.springframework.web.bind.annotation.PathVariable;
  14 +import org.springframework.web.bind.annotation.PostMapping;
  15 +import org.springframework.web.bind.annotation.RequestBody;
  16 +import org.springframework.web.bind.annotation.RequestMapping;
  17 +import org.springframework.web.bind.annotation.RequestParam;
  18 +import org.springframework.web.bind.annotation.RestController;
  19 +
  20 +@RestController
  21 +@RequestMapping("/api/admin/rider/withdraw")
  22 +@RequiredArgsConstructor
  23 +public class AdminRiderWithdrawController {
  24 +
  25 + private final RiderWithdrawService withdrawService;
  26 +
  27 + @GetMapping("/list")
  28 + public Result<PageResultVO<RiderWithdrawApplyVO>> list(@RequestParam(required = false) Long cityId,
  29 + @RequestParam(required = false) Integer status,
  30 + @RequestParam(required = false) String keyword,
  31 + @RequestParam(required = false) String startDate,
  32 + @RequestParam(required = false) String endDate,
  33 + @RequestParam(defaultValue = "1") int page,
  34 + HttpServletRequest request) {
  35 + return Result.success(withdrawService.adminList(resolveQueryCityId(request, cityId), status, keyword, startDate, endDate, page));
  36 + }
  37 +
  38 + @GetMapping("/{id}")
  39 + public Result<RiderWithdrawApplyVO> detail(@PathVariable Long id, HttpServletRequest request) {
  40 + return Result.success(withdrawService.adminDetail(id, resolveScopedCityId(request)));
  41 + }
  42 +
  43 + @PostMapping("/{id}/approve")
  44 + public Result<Void> approve(@PathVariable Long id,
  45 + @RequestBody(required = false) WithdrawAuditDTO dto,
  46 + HttpServletRequest request) {
  47 + withdrawService.approve(id, dto, resolveScopedCityId(request), currentAdminId(request), currentRole(request));
  48 + return Result.success();
  49 + }
  50 +
  51 + @PostMapping("/{id}/reject")
  52 + public Result<Void> reject(@PathVariable Long id,
  53 + @RequestBody(required = false) WithdrawAuditDTO dto,
  54 + HttpServletRequest request) {
  55 + withdrawService.reject(id, dto, resolveScopedCityId(request), currentAdminId(request), currentRole(request));
  56 + return Result.success();
  57 + }
  58 +
  59 + @PostMapping("/{id}/mark-paid")
  60 + public Result<Void> markPaid(@PathVariable Long id,
  61 + @Valid @RequestBody WithdrawMarkPaidDTO dto,
  62 + HttpServletRequest request) {
  63 + withdrawService.markPaid(id, dto, resolveScopedCityId(request), currentAdminId(request), currentRole(request));
  64 + return Result.success();
  65 + }
  66 +
  67 + private Long resolveScopedCityId(HttpServletRequest request) {
  68 + return "substation".equals(request.getAttribute("role")) ? (Long) request.getAttribute("cityId") : null;
  69 + }
  70 +
  71 + private Long resolveQueryCityId(HttpServletRequest request, Long queryCityId) {
  72 + Long scopedCityId = resolveScopedCityId(request);
  73 + return scopedCityId != null ? scopedCityId : queryCityId;
  74 + }
  75 +
  76 + private Long currentAdminId(HttpServletRequest request) {
  77 + return (Long) request.getAttribute("adminId");
  78 + }
  79 +
  80 + private String currentRole(HttpServletRequest request) {
  81 + Object role = request.getAttribute("role");
  82 + return role == null ? "" : role.toString();
  83 + }
  84 +}
src/main/java/com/diligrp/rider/controller/RiderWithdrawController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.RiderWithdrawApplyDTO;
  5 +import com.diligrp.rider.service.RiderWithdrawService;
  6 +import com.diligrp.rider.vo.PageResultVO;
  7 +import com.diligrp.rider.vo.RiderWithdrawApplyVO;
  8 +import jakarta.servlet.http.HttpServletRequest;
  9 +import jakarta.validation.Valid;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.web.bind.annotation.GetMapping;
  12 +import org.springframework.web.bind.annotation.PostMapping;
  13 +import org.springframework.web.bind.annotation.RequestBody;
  14 +import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RequestParam;
  16 +import org.springframework.web.bind.annotation.RestController;
  17 +
  18 +@RestController
  19 +@RequestMapping("/api/rider/withdraw")
  20 +@RequiredArgsConstructor
  21 +public class RiderWithdrawController {
  22 +
  23 + private final RiderWithdrawService withdrawService;
  24 +
  25 + @PostMapping("/apply")
  26 + public Result<Void> apply(@Valid @RequestBody RiderWithdrawApplyDTO dto, HttpServletRequest request) {
  27 + Long riderId = (Long) request.getAttribute("riderId");
  28 + withdrawService.apply(riderId, dto);
  29 + return Result.success();
  30 + }
  31 +
  32 + @GetMapping("/list")
  33 + public Result<PageResultVO<RiderWithdrawApplyVO>> list(@RequestParam(required = false) Integer status,
  34 + @RequestParam(defaultValue = "1") int page,
  35 + HttpServletRequest request) {
  36 + Long riderId = (Long) request.getAttribute("riderId");
  37 + return Result.success(withdrawService.riderList(riderId, status, page));
  38 + }
  39 +}
src/main/java/com/diligrp/rider/dto/RiderWithdrawApplyDTO.java 0 → 100644
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.DecimalMin;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Data
  11 +public class RiderWithdrawApplyDTO {
  12 +
  13 + @NotNull(message = "提现金额不能为空")
  14 + @DecimalMin(value = "0.01", message = "提现金额必须大于0")
  15 + private BigDecimal amount;
  16 +
  17 + /** 收款账户类型:1=银行卡 2=支付宝 3=微信 */
  18 + @NotNull(message = "收款账户类型不能为空")
  19 + private Integer accountType;
  20 +
  21 + @NotBlank(message = "收款人不能为空")
  22 + private String accountName;
  23 +
  24 + private String bankName;
  25 +
  26 + private String bankBranch;
  27 +
  28 + @NotBlank(message = "收款账号不能为空")
  29 + private String accountNo;
  30 +
  31 + private String applyRemark;
  32 +}
src/main/java/com/diligrp/rider/dto/WithdrawAuditDTO.java 0 → 100644
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class WithdrawAuditDTO {
  7 + private String remark;
  8 +}
src/main/java/com/diligrp/rider/dto/WithdrawMarkPaidDTO.java 0 → 100644
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +public class WithdrawMarkPaidDTO {
  8 + @NotBlank(message = "打款流水号不能为空")
  9 + private String transferNo;
  10 +
  11 + private String remark;
  12 +}
src/main/java/com/diligrp/rider/entity/Rider.java
@@ -56,6 +56,9 @@ public class Rider { @@ -56,6 +56,9 @@ public class Rider {
56 /** 余额(兼职骑手用) */ 56 /** 余额(兼职骑手用) */
57 private BigDecimal balance; 57 private BigDecimal balance;
58 58
  59 + /** 冻结余额(提现审核中) */
  60 + private BigDecimal frozenBalance;
  61 +
59 /** 是否休息:0=否 1=是 */ 62 /** 是否休息:0=否 1=是 */
60 private Integer isRest; 63 private Integer isRest;
61 64
src/main/java/com/diligrp/rider/entity/RiderWithdrawApply.java 0 → 100644
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +/**
  11 + * 骑手提现申请表
  12 + */
  13 +@Data
  14 +@TableName("rider_withdraw_apply")
  15 +public class RiderWithdrawApply {
  16 +
  17 + @TableId(type = IdType.AUTO)
  18 + private Long id;
  19 +
  20 + /** 提现单号 */
  21 + private String withdrawNo;
  22 +
  23 + /** 骑手ID */
  24 + private Long riderId;
  25 +
  26 + /** 城市ID */
  27 + private Long cityId;
  28 +
  29 + /** 提现金额 */
  30 + private BigDecimal amount;
  31 +
  32 + /** 状态:0=待审核 1=审核通过待打款 2=审核拒绝 3=已打款 4=打款失败 */
  33 + private Integer status;
  34 +
  35 + /** 收款账户类型:1=银行卡 2=支付宝 3=微信 */
  36 + private Integer accountType;
  37 +
  38 + /** 收款人 */
  39 + private String accountName;
  40 +
  41 + /** 开户行 */
  42 + private String bankName;
  43 +
  44 + /** 开户支行 */
  45 + private String bankBranch;
  46 +
  47 + /** 收款账号 */
  48 + private String accountNo;
  49 +
  50 + /** 申请备注 */
  51 + private String applyRemark;
  52 +
  53 + /** 审核/打款备注 */
  54 + private String auditRemark;
  55 +
  56 + /** 审核人ID */
  57 + private Long auditorId;
  58 +
  59 + /** 审核人名称 */
  60 + private String auditorName;
  61 +
  62 + /** 申请时间 */
  63 + private Long applyTime;
  64 +
  65 + /** 审核时间 */
  66 + private Long auditTime;
  67 +
  68 + /** 打款时间 */
  69 + private Long payTime;
  70 +
  71 + /** 打款流水号 */
  72 + private String transferNo;
  73 +
  74 + private Long createTime;
  75 +
  76 + private Long updateTime;
  77 +}
src/main/java/com/diligrp/rider/mapper/RiderWithdrawApplyMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.Rider;
  5 +import com.diligrp.rider.entity.RiderWithdrawApply;
  6 +import com.diligrp.rider.vo.RiderWithdrawApplyVO;
  7 +import org.apache.ibatis.annotations.Mapper;
  8 +import org.apache.ibatis.annotations.Param;
  9 +
  10 +import java.util.List;
  11 +
  12 +@Mapper
  13 +public interface RiderWithdrawApplyMapper extends BaseMapper<RiderWithdrawApply> {
  14 + Rider selectRiderForUpdate(@Param("riderId") Long riderId);
  15 +
  16 + RiderWithdrawApply selectApplyForUpdate(@Param("id") Long id);
  17 +
  18 + long countRiderList(@Param("riderId") Long riderId,
  19 + @Param("status") Integer status);
  20 +
  21 + List<RiderWithdrawApplyVO> selectRiderList(@Param("riderId") Long riderId,
  22 + @Param("status") Integer status,
  23 + @Param("offset") int offset,
  24 + @Param("pageSize") int pageSize);
  25 +
  26 + long countAdminList(@Param("cityId") Long cityId,
  27 + @Param("status") Integer status,
  28 + @Param("keyword") String keyword,
  29 + @Param("startTime") Long startTime,
  30 + @Param("endTime") Long endTime);
  31 +
  32 + List<RiderWithdrawApplyVO> selectAdminList(@Param("cityId") Long cityId,
  33 + @Param("status") Integer status,
  34 + @Param("keyword") String keyword,
  35 + @Param("startTime") Long startTime,
  36 + @Param("endTime") Long endTime,
  37 + @Param("offset") int offset,
  38 + @Param("pageSize") int pageSize);
  39 +
  40 + RiderWithdrawApplyVO selectAdminDetail(@Param("id") Long id,
  41 + @Param("cityId") Long cityId);
  42 +}
src/main/java/com/diligrp/rider/service/RiderWithdrawService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.RiderWithdrawApplyDTO;
  4 +import com.diligrp.rider.dto.WithdrawAuditDTO;
  5 +import com.diligrp.rider.dto.WithdrawMarkPaidDTO;
  6 +import com.diligrp.rider.vo.PageResultVO;
  7 +import com.diligrp.rider.vo.RiderWithdrawApplyVO;
  8 +
  9 +public interface RiderWithdrawService {
  10 + void apply(Long riderId, RiderWithdrawApplyDTO dto);
  11 +
  12 + PageResultVO<RiderWithdrawApplyVO> riderList(Long riderId, Integer status, int page);
  13 +
  14 + PageResultVO<RiderWithdrawApplyVO> adminList(Long cityId, Integer status, String keyword, String startDate, String endDate, int page);
  15 +
  16 + RiderWithdrawApplyVO adminDetail(Long id, Long cityId);
  17 +
  18 + void approve(Long id, WithdrawAuditDTO dto, Long cityId, Long adminId, String adminRole);
  19 +
  20 + void reject(Long id, WithdrawAuditDTO dto, Long cityId, Long adminId, String adminRole);
  21 +
  22 + void markPaid(Long id, WithdrawMarkPaidDTO dto, Long cityId, Long adminId, String adminRole);
  23 +}
src/main/java/com/diligrp/rider/service/impl/AdminRiderServiceImpl.java
@@ -65,6 +65,7 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -65,6 +65,7 @@ public class AdminRiderServiceImpl implements AdminRiderService {
65 rider.setType(1); 65 rider.setType(1);
66 rider.setIsRest(0); 66 rider.setIsRest(0);
67 rider.setBalance(BigDecimal.ZERO); 67 rider.setBalance(BigDecimal.ZERO);
  68 + rider.setFrozenBalance(BigDecimal.ZERO);
68 rider.setUserLogin("phone_" + System.currentTimeMillis()); 69 rider.setUserLogin("phone_" + System.currentTimeMillis());
69 rider.setCreateTime(System.currentTimeMillis() / 1000); 70 rider.setCreateTime(System.currentTimeMillis() / 1000);
70 riderMapper.insert(rider); 71 riderMapper.insert(rider);
@@ -163,6 +164,9 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -163,6 +164,9 @@ public class AdminRiderServiceImpl implements AdminRiderService {
163 if (rider.getBalance() != null && rider.getBalance().compareTo(BigDecimal.ZERO) > 0) { 164 if (rider.getBalance() != null && rider.getBalance().compareTo(BigDecimal.ZERO) > 0) {
164 throw new BizException("变更为全职前要保证余额为0"); 165 throw new BizException("变更为全职前要保证余额为0");
165 } 166 }
  167 + if (rider.getFrozenBalance() != null && rider.getFrozenBalance().compareTo(BigDecimal.ZERO) > 0) {
  168 + throw new BizException("变更为全职前要保证无提现冻结余额");
  169 + }
166 } 170 }
167 LambdaUpdateWrapper<Rider> wrapper = new LambdaUpdateWrapper<Rider>() 171 LambdaUpdateWrapper<Rider> wrapper = new LambdaUpdateWrapper<Rider>()
168 .eq(Rider::getId, riderId) 172 .eq(Rider::getId, riderId)
src/main/java/com/diligrp/rider/service/impl/MenuBootstrapServiceImpl.java
@@ -67,6 +67,7 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService { @@ -67,6 +67,7 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
67 defaults.add(menu("merchant.fund", "资金对账", "MENU", "/merchant/fund", "", 0L, MenuScopeEnum.BOTH, 43)); 67 defaults.add(menu("merchant.fund", "资金对账", "MENU", "/merchant/fund", "", 0L, MenuScopeEnum.BOTH, 43));
68 defaults.add(menu("rider.list", "骑手管理", "MENU", "/rider", "UserOutlined", 0L, MenuScopeEnum.BOTH, 50)); 68 defaults.add(menu("rider.list", "骑手管理", "MENU", "/rider", "UserOutlined", 0L, MenuScopeEnum.BOTH, 50));
69 defaults.add(menu("rider.level", "骑手等级", "MENU", "/rider/level", "TrophyOutlined", 0L, MenuScopeEnum.BOTH, 55)); 69 defaults.add(menu("rider.level", "骑手等级", "MENU", "/rider/level", "TrophyOutlined", 0L, MenuScopeEnum.BOTH, 55));
  70 + defaults.add(menu("rider.withdraw", "骑手提现审核", "MENU", "/rider/withdraw", "", 0L, MenuScopeEnum.BOTH, 56));
70 defaults.add(menu("rider.evaluate", "骑手评价", "MENU", "/rider/evaluate", "StarOutlined", 0L, MenuScopeEnum.BOTH, 60)); 71 defaults.add(menu("rider.evaluate", "骑手评价", "MENU", "/rider/evaluate", "StarOutlined", 0L, MenuScopeEnum.BOTH, 60));
71 defaults.add(menu("order.root", "订单管理", "DIR", "", "UnorderedListOutlined", 0L, MenuScopeEnum.BOTH, 70)); 72 defaults.add(menu("order.root", "订单管理", "DIR", "", "UnorderedListOutlined", 0L, MenuScopeEnum.BOTH, 70));
72 defaults.add(menu("order.list", "订单列表", "MENU", "/order", "", 0L, MenuScopeEnum.BOTH, 71)); 73 defaults.add(menu("order.list", "订单列表", "MENU", "/order", "", 0L, MenuScopeEnum.BOTH, 71));
src/main/java/com/diligrp/rider/service/impl/RiderWithdrawServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  4 +import com.diligrp.rider.common.exception.BizException;
  5 +import com.diligrp.rider.dto.RiderWithdrawApplyDTO;
  6 +import com.diligrp.rider.dto.WithdrawAuditDTO;
  7 +import com.diligrp.rider.dto.WithdrawMarkPaidDTO;
  8 +import com.diligrp.rider.entity.Rider;
  9 +import com.diligrp.rider.entity.RiderBalance;
  10 +import com.diligrp.rider.entity.RiderWithdrawApply;
  11 +import com.diligrp.rider.mapper.RiderBalanceMapper;
  12 +import com.diligrp.rider.mapper.RiderMapper;
  13 +import com.diligrp.rider.mapper.RiderWithdrawApplyMapper;
  14 +import com.diligrp.rider.service.RiderWithdrawService;
  15 +import com.diligrp.rider.vo.PageResultVO;
  16 +import com.diligrp.rider.vo.RiderWithdrawApplyVO;
  17 +import lombok.RequiredArgsConstructor;
  18 +import org.springframework.stereotype.Service;
  19 +import org.springframework.transaction.annotation.Transactional;
  20 +
  21 +import java.math.BigDecimal;
  22 +import java.time.LocalDate;
  23 +import java.time.ZoneId;
  24 +import java.time.format.DateTimeParseException;
  25 +import java.util.List;
  26 +
  27 +@Service
  28 +@RequiredArgsConstructor
  29 +public class RiderWithdrawServiceImpl implements RiderWithdrawService {
  30 +
  31 + private static final int PAGE_SIZE = 20;
  32 + private static final int STATUS_PENDING = 0;
  33 + private static final int STATUS_APPROVED = 1;
  34 + private static final int STATUS_REJECTED = 2;
  35 + private static final int STATUS_PAID = 3;
  36 + private static final ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");
  37 +
  38 + private final RiderWithdrawApplyMapper withdrawMapper;
  39 + private final RiderMapper riderMapper;
  40 + private final RiderBalanceMapper balanceMapper;
  41 +
  42 + @Override
  43 + @Transactional
  44 + public void apply(Long riderId, RiderWithdrawApplyDTO dto) {
  45 + if (riderId == null || riderId < 1) {
  46 + throw new BizException("骑手身份无效");
  47 + }
  48 + validateApply(dto);
  49 +
  50 + Rider rider = withdrawMapper.selectRiderForUpdate(riderId);
  51 + if (rider == null) {
  52 + throw new BizException("骑手信息不存在");
  53 + }
  54 + BigDecimal amount = dto.getAmount().setScale(2, java.math.RoundingMode.HALF_UP);
  55 + BigDecimal balance = money(rider.getBalance());
  56 + BigDecimal frozenBalance = money(rider.getFrozenBalance());
  57 + if (balance.compareTo(amount) < 0) {
  58 + throw new BizException("可提现余额不足");
  59 + }
  60 +
  61 + long now = now();
  62 + RiderWithdrawApply apply = new RiderWithdrawApply();
  63 + apply.setWithdrawNo(buildWithdrawNo(riderId));
  64 + apply.setRiderId(riderId);
  65 + apply.setCityId(rider.getCityId());
  66 + apply.setAmount(amount);
  67 + apply.setStatus(STATUS_PENDING);
  68 + apply.setAccountType(dto.getAccountType());
  69 + apply.setAccountName(trim(dto.getAccountName()));
  70 + apply.setBankName(trim(dto.getBankName()));
  71 + apply.setBankBranch(trim(dto.getBankBranch()));
  72 + apply.setAccountNo(trim(dto.getAccountNo()));
  73 + apply.setApplyRemark(trim(dto.getApplyRemark()));
  74 + apply.setAuditRemark("");
  75 + apply.setAuditorId(0L);
  76 + apply.setAuditorName("");
  77 + apply.setApplyTime(now);
  78 + apply.setAuditTime(0L);
  79 + apply.setPayTime(0L);
  80 + apply.setTransferNo("");
  81 + apply.setCreateTime(now);
  82 + apply.setUpdateTime(now);
  83 + withdrawMapper.insert(apply);
  84 +
  85 + updateRiderMoney(riderId, balance.subtract(amount), frozenBalance.add(amount));
  86 + }
  87 +
  88 + @Override
  89 + public PageResultVO<RiderWithdrawApplyVO> riderList(Long riderId, Integer status, int page) {
  90 + int currentPage = normalizePage(page);
  91 + int offset = (currentPage - 1) * PAGE_SIZE;
  92 + PageResultVO<RiderWithdrawApplyVO> result = new PageResultVO<>();
  93 + result.setList(withdrawMapper.selectRiderList(riderId, normalizeStatus(status), offset, PAGE_SIZE));
  94 + result.setPage(currentPage);
  95 + result.setPageSize(PAGE_SIZE);
  96 + result.setTotal(withdrawMapper.countRiderList(riderId, normalizeStatus(status)));
  97 + return result;
  98 + }
  99 +
  100 + @Override
  101 + public PageResultVO<RiderWithdrawApplyVO> adminList(Long cityId, Integer status, String keyword, String startDate, String endDate, int page) {
  102 + int currentPage = normalizePage(page);
  103 + int offset = (currentPage - 1) * PAGE_SIZE;
  104 + Long startTime = parseStartTime(startDate);
  105 + Long endTime = parseEndTime(endDate);
  106 + PageResultVO<RiderWithdrawApplyVO> result = new PageResultVO<>();
  107 + result.setList(withdrawMapper.selectAdminList(cityId, normalizeStatus(status), normalizeKeyword(keyword), startTime, endTime, offset, PAGE_SIZE));
  108 + result.setPage(currentPage);
  109 + result.setPageSize(PAGE_SIZE);
  110 + result.setTotal(withdrawMapper.countAdminList(cityId, normalizeStatus(status), normalizeKeyword(keyword), startTime, endTime));
  111 + return result;
  112 + }
  113 +
  114 + @Override
  115 + public RiderWithdrawApplyVO adminDetail(Long id, Long cityId) {
  116 + if (id == null || id < 1) {
  117 + throw new BizException("提现申请ID不能为空");
  118 + }
  119 + RiderWithdrawApplyVO detail = withdrawMapper.selectAdminDetail(id, cityId);
  120 + if (detail == null) {
  121 + throw new BizException("提现申请不存在");
  122 + }
  123 + return detail;
  124 + }
  125 +
  126 + @Override
  127 + @Transactional
  128 + public void approve(Long id, WithdrawAuditDTO dto, Long cityId, Long adminId, String adminRole) {
  129 + RiderWithdrawApply apply = lockApply(id, cityId);
  130 + if (apply.getStatus() == null || apply.getStatus() != STATUS_PENDING) {
  131 + throw new BizException("只有待审核申请可以审核通过");
  132 + }
  133 + Rider rider = lockRider(apply.getRiderId());
  134 + ensureFrozenEnough(rider, apply.getAmount());
  135 + long now = now();
  136 + apply.setStatus(STATUS_APPROVED);
  137 + apply.setAuditRemark(trim(dto == null ? null : dto.getRemark()));
  138 + apply.setAuditorId(adminId == null ? 0L : adminId);
  139 + apply.setAuditorName(buildAdminName(adminId, adminRole));
  140 + apply.setAuditTime(now);
  141 + apply.setUpdateTime(now);
  142 + withdrawMapper.updateById(apply);
  143 + }
  144 +
  145 + @Override
  146 + @Transactional
  147 + public void reject(Long id, WithdrawAuditDTO dto, Long cityId, Long adminId, String adminRole) {
  148 + RiderWithdrawApply apply = lockApply(id, cityId);
  149 + if (apply.getStatus() == null || apply.getStatus() != STATUS_PENDING) {
  150 + throw new BizException("只有待审核申请可以拒绝");
  151 + }
  152 + String remark = trim(dto == null ? null : dto.getRemark());
  153 + if (remark.isBlank()) {
  154 + throw new BizException("拒绝原因不能为空");
  155 + }
  156 + Rider rider = lockRider(apply.getRiderId());
  157 + BigDecimal amount = money(apply.getAmount());
  158 + ensureFrozenEnough(rider, amount);
  159 + updateRiderMoney(rider.getId(), money(rider.getBalance()).add(amount), money(rider.getFrozenBalance()).subtract(amount));
  160 +
  161 + long now = now();
  162 + apply.setStatus(STATUS_REJECTED);
  163 + apply.setAuditRemark(remark);
  164 + apply.setAuditorId(adminId == null ? 0L : adminId);
  165 + apply.setAuditorName(buildAdminName(adminId, adminRole));
  166 + apply.setAuditTime(now);
  167 + apply.setUpdateTime(now);
  168 + withdrawMapper.updateById(apply);
  169 + }
  170 +
  171 + @Override
  172 + @Transactional
  173 + public void markPaid(Long id, WithdrawMarkPaidDTO dto, Long cityId, Long adminId, String adminRole) {
  174 + if (dto == null || trim(dto.getTransferNo()).isBlank()) {
  175 + throw new BizException("打款流水号不能为空");
  176 + }
  177 + RiderWithdrawApply apply = lockApply(id, cityId);
  178 + if (apply.getStatus() == null || apply.getStatus() != STATUS_APPROVED) {
  179 + throw new BizException("只有审核通过待打款申请可以标记已打款");
  180 + }
  181 + Rider rider = lockRider(apply.getRiderId());
  182 + BigDecimal amount = money(apply.getAmount());
  183 + ensureFrozenEnough(rider, amount);
  184 + BigDecimal nextFrozenBalance = money(rider.getFrozenBalance()).subtract(amount);
  185 + BigDecimal currentBalance = money(rider.getBalance());
  186 + updateRiderMoney(rider.getId(), currentBalance, nextFrozenBalance);
  187 +
  188 + long now = now();
  189 + RiderBalance record = new RiderBalance();
  190 + record.setUid(rider.getId());
  191 + record.setType(2);
  192 + record.setAction("withdraw_paid");
  193 + record.setActionId(apply.getId());
  194 + record.setOrderNo(apply.getWithdrawNo());
  195 + record.setNums(amount.negate());
  196 + record.setTotal(currentBalance);
  197 + record.setAddTime(now);
  198 + balanceMapper.insert(record);
  199 +
  200 + String remark = trim(dto.getRemark());
  201 + apply.setStatus(STATUS_PAID);
  202 + apply.setAuditRemark(remark.isBlank() ? apply.getAuditRemark() : remark);
  203 + apply.setAuditorId(adminId == null ? apply.getAuditorId() : adminId);
  204 + apply.setAuditorName(buildAdminName(adminId, adminRole));
  205 + apply.setPayTime(now);
  206 + apply.setTransferNo(trim(dto.getTransferNo()));
  207 + apply.setUpdateTime(now);
  208 + withdrawMapper.updateById(apply);
  209 + }
  210 +
  211 + private void validateApply(RiderWithdrawApplyDTO dto) {
  212 + if (dto == null) {
  213 + throw new BizException("提现申请不能为空");
  214 + }
  215 + if (dto.getAccountType() == null || dto.getAccountType() < 1 || dto.getAccountType() > 3) {
  216 + throw new BizException("收款账户类型不正确");
  217 + }
  218 + if (dto.getAmount() == null || dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
  219 + throw new BizException("提现金额必须大于0");
  220 + }
  221 + if (trim(dto.getAccountName()).isBlank()) {
  222 + throw new BizException("收款人不能为空");
  223 + }
  224 + if (trim(dto.getAccountNo()).isBlank()) {
  225 + throw new BizException("收款账号不能为空");
  226 + }
  227 + if (dto.getAccountType() == 1 && trim(dto.getBankName()).isBlank()) {
  228 + throw new BizException("银行卡提现需填写开户行");
  229 + }
  230 + }
  231 +
  232 + private RiderWithdrawApply lockApply(Long id, Long cityId) {
  233 + if (id == null || id < 1) {
  234 + throw new BizException("提现申请ID不能为空");
  235 + }
  236 + RiderWithdrawApply apply = withdrawMapper.selectApplyForUpdate(id);
  237 + if (apply == null) {
  238 + throw new BizException("提现申请不存在");
  239 + }
  240 + if (cityId != null && !cityId.equals(apply.getCityId())) {
  241 + throw new BizException("只能操作当前租户提现申请");
  242 + }
  243 + return apply;
  244 + }
  245 +
  246 + private Rider lockRider(Long riderId) {
  247 + Rider rider = withdrawMapper.selectRiderForUpdate(riderId);
  248 + if (rider == null) {
  249 + throw new BizException("骑手信息不存在");
  250 + }
  251 + return rider;
  252 + }
  253 +
  254 + private void ensureFrozenEnough(Rider rider, BigDecimal amount) {
  255 + if (money(rider.getFrozenBalance()).compareTo(money(amount)) < 0) {
  256 + throw new BizException("冻结余额不足,请核对提现申请状态");
  257 + }
  258 + }
  259 +
  260 + private void updateRiderMoney(Long riderId, BigDecimal balance, BigDecimal frozenBalance) {
  261 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  262 + .eq(Rider::getId, riderId)
  263 + .set(Rider::getBalance, money(balance))
  264 + .set(Rider::getFrozenBalance, money(frozenBalance)));
  265 + }
  266 +
  267 + private BigDecimal money(BigDecimal value) {
  268 + return value == null ? BigDecimal.ZERO : value.setScale(2, java.math.RoundingMode.HALF_UP);
  269 + }
  270 +
  271 + private long now() {
  272 + return System.currentTimeMillis() / 1000;
  273 + }
  274 +
  275 + private String trim(String value) {
  276 + return value == null ? "" : value.trim();
  277 + }
  278 +
  279 + private String buildWithdrawNo(Long riderId) {
  280 + return "WD" + System.currentTimeMillis() + String.format("%04d", riderId % 10000);
  281 + }
  282 +
  283 + private String buildAdminName(Long adminId, String adminRole) {
  284 + if (adminId == null) {
  285 + return "系统";
  286 + }
  287 + return ("substation".equals(adminRole) ? "分站管理员" : "平台管理员") + "#" + adminId;
  288 + }
  289 +
  290 + private int normalizePage(int page) {
  291 + return Math.max(page, 1);
  292 + }
  293 +
  294 + private Integer normalizeStatus(Integer status) {
  295 + return status == null || status < 0 ? null : status;
  296 + }
  297 +
  298 + private String normalizeKeyword(String keyword) {
  299 + return keyword == null || keyword.isBlank() ? null : keyword.trim();
  300 + }
  301 +
  302 + private Long parseStartTime(String startDate) {
  303 + if (startDate == null || startDate.isBlank()) {
  304 + return null;
  305 + }
  306 + return parseDate(startDate, "开始日期格式不正确").atStartOfDay(ZONE_ID).toEpochSecond();
  307 + }
  308 +
  309 + private Long parseEndTime(String endDate) {
  310 + if (endDate == null || endDate.isBlank()) {
  311 + return null;
  312 + }
  313 + return parseDate(endDate, "结束日期格式不正确").plusDays(1).atStartOfDay(ZONE_ID).toEpochSecond();
  314 + }
  315 +
  316 + private LocalDate parseDate(String value, String errorMessage) {
  317 + try {
  318 + return LocalDate.parse(value);
  319 + } catch (DateTimeParseException ex) {
  320 + throw new BizException(errorMessage);
  321 + }
  322 + }
  323 +}
src/main/java/com/diligrp/rider/vo/RiderWithdrawApplyVO.java 0 → 100644
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +
  7 +@Data
  8 +public class RiderWithdrawApplyVO {
  9 + private Long id;
  10 + private String withdrawNo;
  11 + private Long riderId;
  12 + private String riderName;
  13 + private String mobile;
  14 + private Long cityId;
  15 + private String cityName;
  16 + private BigDecimal riderBalance;
  17 + private BigDecimal riderFrozenBalance;
  18 + private BigDecimal amount;
  19 + private Integer status;
  20 + private Integer accountType;
  21 + private String accountName;
  22 + private String bankName;
  23 + private String bankBranch;
  24 + private String accountNo;
  25 + private String applyRemark;
  26 + private String auditRemark;
  27 + private Long auditorId;
  28 + private String auditorName;
  29 + private Long applyTime;
  30 + private Long auditTime;
  31 + private Long payTime;
  32 + private String transferNo;
  33 + private Long createTime;
  34 + private Long updateTime;
  35 +}
src/main/resources/20260509-rider-withdraw.sql 0 → 100644
  1 +-- 骑手提现审核增量脚本
  2 +-- 在已有环境执行;全新环境可直接使用 schema.sql。
  3 +
  4 +ALTER TABLE `rider`
  5 + ADD COLUMN `frozen_balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额(提现审核中)' AFTER `balance`;
  6 +
  7 +CREATE TABLE `rider_withdraw_apply` (
  8 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  9 + `withdraw_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '提现单号',
  10 + `rider_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '骑手ID',
  11 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID',
  12 + `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '提现金额',
  13 + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=待审核 1=审核通过待打款 2=审核拒绝 3=已打款 4=打款失败',
  14 + `account_type` TINYINT NOT NULL DEFAULT 1 COMMENT '收款账户类型:1=银行卡 2=支付宝 3=微信',
  15 + `account_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '收款人',
  16 + `bank_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '开户行',
  17 + `bank_branch` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '开户支行',
  18 + `account_no` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '收款账号',
  19 + `apply_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '申请备注',
  20 + `audit_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '审核/打款备注',
  21 + `auditor_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核人ID',
  22 + `auditor_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审核人名称',
  23 + `apply_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请时间',
  24 + `audit_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核时间',
  25 + `pay_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '打款时间',
  26 + `transfer_no` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '打款流水号',
  27 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  28 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  29 + PRIMARY KEY (`id`),
  30 + UNIQUE KEY `uk_withdraw_no` (`withdraw_no`),
  31 + KEY `idx_rider_status` (`rider_id`, `status`),
  32 + KEY `idx_city_status_time` (`city_id`, `status`, `apply_time`)
  33 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手提现申请表';
  34 +
  35 +INSERT INTO `sys_menu` (`code`, `name`, `type`, `path`, `icon`, `parent_id`, `menu_scope`, `list_order`, `visible`, `status`, `create_time`)
  36 +SELECT 'rider.withdraw', '骑手提现审核', 'MENU', '/rider/withdraw', '', 0, 'BOTH', 56, 1, 1, UNIX_TIMESTAMP()
  37 +WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `code` = 'rider.withdraw');
  38 +
  39 +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `create_time`)
  40 +SELECT r.id, m.id, UNIX_TIMESTAMP()
  41 +FROM `sys_role` r
  42 +INNER JOIN `sys_menu` m ON m.code = 'rider.withdraw'
  43 +WHERE r.code IN ('platform_admin', 'substation_admin')
  44 + AND NOT EXISTS (
  45 + SELECT 1 FROM `sys_role_menu` rm WHERE rm.role_id = r.id AND rm.menu_id = m.id
  46 + );
src/main/resources/data-init.sql
@@ -90,7 +90,8 @@ INSERT INTO `sys_menu` (`code`, `name`, `type`, `path`, `icon`, `parent_id`, `me @@ -90,7 +90,8 @@ INSERT INTO `sys_menu` (`code`, `name`, `type`, `path`, `icon`, `parent_id`, `me
90 ('system.root', '系统管理', 'DIR', '', 'ControlOutlined', 0, 'PLATFORM', 100, 1, 1, UNIX_TIMESTAMP()), 90 ('system.root', '系统管理', 'DIR', '', 'ControlOutlined', 0, 'PLATFORM', 100, 1, 1, UNIX_TIMESTAMP()),
91 ('system.menu', '菜单管理', 'MENU', '/system/menu', '', 19, 'PLATFORM', 101, 1, 1, UNIX_TIMESTAMP()), 91 ('system.menu', '菜单管理', 'MENU', '/system/menu', '', 19, 'PLATFORM', 101, 1, 1, UNIX_TIMESTAMP()),
92 ('system.role_menu', '角色菜单', 'MENU', '/system/role-menu', '', 19, 'PLATFORM', 102, 1, 1, UNIX_TIMESTAMP()), 92 ('system.role_menu', '角色菜单', 'MENU', '/system/role-menu', '', 19, 'PLATFORM', 102, 1, 1, UNIX_TIMESTAMP()),
93 -('admin.user', '平台账号', 'MENU', '/admin-user', '', 19, 'PLATFORM', 103, 1, 1, UNIX_TIMESTAMP()); 93 +('admin.user', '平台账号', 'MENU', '/admin-user', '', 19, 'PLATFORM', 103, 1, 1, UNIX_TIMESTAMP()),
  94 +('rider.withdraw', '骑手提现审核', 'MENU', '/rider/withdraw', '', 0, 'BOTH', 56, 1, 1, UNIX_TIMESTAMP());
94 95
95 INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `create_time`) 96 INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `create_time`)
96 SELECT 1, id, UNIX_TIMESTAMP() FROM `sys_menu` WHERE `menu_scope` IN ('PLATFORM', 'BOTH'); 97 SELECT 1, id, UNIX_TIMESTAMP() FROM `sys_menu` WHERE `menu_scope` IN ('PLATFORM', 'BOTH');
src/main/resources/mapper/RiderWithdrawApplyMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="com.diligrp.rider.mapper.RiderWithdrawApplyMapper">
  4 +
  5 + <sql id="adminListFilter">
  6 + FROM rider_withdraw_apply w
  7 + INNER JOIN rider r ON r.id = w.rider_id AND r.is_del = 0
  8 + LEFT JOIN city c ON c.id = w.city_id
  9 + WHERE 1 = 1
  10 + <if test="cityId != null">
  11 + AND w.city_id = #{cityId}
  12 + </if>
  13 + <if test="status != null">
  14 + AND w.status = #{status}
  15 + </if>
  16 + <if test="keyword != null and keyword != ''">
  17 + AND (w.withdraw_no LIKE CONCAT('%', #{keyword}, '%')
  18 + OR r.user_nickname LIKE CONCAT('%', #{keyword}, '%')
  19 + OR r.mobile LIKE CONCAT('%', #{keyword}, '%'))
  20 + </if>
  21 + <if test="startTime != null">
  22 + AND w.apply_time &gt;= #{startTime}
  23 + </if>
  24 + <if test="endTime != null">
  25 + AND w.apply_time &lt; #{endTime}
  26 + </if>
  27 + </sql>
  28 +
  29 + <select id="selectRiderForUpdate" resultType="com.diligrp.rider.entity.Rider">
  30 + SELECT * FROM rider WHERE id = #{riderId} AND is_del = 0 FOR UPDATE
  31 + </select>
  32 +
  33 + <select id="selectApplyForUpdate" resultType="com.diligrp.rider.entity.RiderWithdrawApply">
  34 + SELECT * FROM rider_withdraw_apply WHERE id = #{id} FOR UPDATE
  35 + </select>
  36 +
  37 + <select id="countRiderList" resultType="long">
  38 + SELECT COUNT(1)
  39 + FROM rider_withdraw_apply
  40 + WHERE rider_id = #{riderId}
  41 + <if test="status != null">
  42 + AND status = #{status}
  43 + </if>
  44 + </select>
  45 +
  46 + <select id="selectRiderList" resultType="com.diligrp.rider.vo.RiderWithdrawApplyVO">
  47 + SELECT
  48 + w.id,
  49 + w.withdraw_no AS withdrawNo,
  50 + w.rider_id AS riderId,
  51 + w.city_id AS cityId,
  52 + w.amount,
  53 + w.status,
  54 + w.account_type AS accountType,
  55 + w.account_name AS accountName,
  56 + w.bank_name AS bankName,
  57 + w.bank_branch AS bankBranch,
  58 + w.account_no AS accountNo,
  59 + w.apply_remark AS applyRemark,
  60 + w.audit_remark AS auditRemark,
  61 + w.auditor_id AS auditorId,
  62 + w.auditor_name AS auditorName,
  63 + w.apply_time AS applyTime,
  64 + w.audit_time AS auditTime,
  65 + w.pay_time AS payTime,
  66 + w.transfer_no AS transferNo,
  67 + w.create_time AS createTime,
  68 + w.update_time AS updateTime
  69 + FROM rider_withdraw_apply w
  70 + WHERE w.rider_id = #{riderId}
  71 + <if test="status != null">
  72 + AND w.status = #{status}
  73 + </if>
  74 + ORDER BY w.id DESC
  75 + LIMIT #{offset}, #{pageSize}
  76 + </select>
  77 +
  78 + <select id="countAdminList" resultType="long">
  79 + SELECT COUNT(1)
  80 + <include refid="adminListFilter"/>
  81 + </select>
  82 +
  83 + <select id="selectAdminList" resultType="com.diligrp.rider.vo.RiderWithdrawApplyVO">
  84 + SELECT
  85 + w.id,
  86 + w.withdraw_no AS withdrawNo,
  87 + w.rider_id AS riderId,
  88 + r.user_nickname AS riderName,
  89 + r.mobile,
  90 + w.city_id AS cityId,
  91 + c.name AS cityName,
  92 + r.balance AS riderBalance,
  93 + r.frozen_balance AS riderFrozenBalance,
  94 + w.amount,
  95 + w.status,
  96 + w.account_type AS accountType,
  97 + w.account_name AS accountName,
  98 + w.bank_name AS bankName,
  99 + w.bank_branch AS bankBranch,
  100 + w.account_no AS accountNo,
  101 + w.apply_remark AS applyRemark,
  102 + w.audit_remark AS auditRemark,
  103 + w.auditor_id AS auditorId,
  104 + w.auditor_name AS auditorName,
  105 + w.apply_time AS applyTime,
  106 + w.audit_time AS auditTime,
  107 + w.pay_time AS payTime,
  108 + w.transfer_no AS transferNo,
  109 + w.create_time AS createTime,
  110 + w.update_time AS updateTime
  111 + <include refid="adminListFilter"/>
  112 + ORDER BY w.id DESC
  113 + LIMIT #{offset}, #{pageSize}
  114 + </select>
  115 +
  116 + <select id="selectAdminDetail" resultType="com.diligrp.rider.vo.RiderWithdrawApplyVO">
  117 + SELECT
  118 + w.id,
  119 + w.withdraw_no AS withdrawNo,
  120 + w.rider_id AS riderId,
  121 + r.user_nickname AS riderName,
  122 + r.mobile,
  123 + w.city_id AS cityId,
  124 + c.name AS cityName,
  125 + r.balance AS riderBalance,
  126 + r.frozen_balance AS riderFrozenBalance,
  127 + w.amount,
  128 + w.status,
  129 + w.account_type AS accountType,
  130 + w.account_name AS accountName,
  131 + w.bank_name AS bankName,
  132 + w.bank_branch AS bankBranch,
  133 + w.account_no AS accountNo,
  134 + w.apply_remark AS applyRemark,
  135 + w.audit_remark AS auditRemark,
  136 + w.auditor_id AS auditorId,
  137 + w.auditor_name AS auditorName,
  138 + w.apply_time AS applyTime,
  139 + w.audit_time AS auditTime,
  140 + w.pay_time AS payTime,
  141 + w.transfer_no AS transferNo,
  142 + w.create_time AS createTime,
  143 + w.update_time AS updateTime
  144 + FROM rider_withdraw_apply w
  145 + INNER JOIN rider r ON r.id = w.rider_id AND r.is_del = 0
  146 + LEFT JOIN city c ON c.id = w.city_id
  147 + WHERE w.id = #{id}
  148 + <if test="cityId != null">
  149 + AND w.city_id = #{cityId}
  150 + </if>
  151 + LIMIT 1
  152 + </select>
  153 +
  154 +</mapper>
src/main/resources/schema.sql
@@ -19,6 +19,7 @@ CREATE TABLE `rider` ( @@ -19,6 +19,7 @@ CREATE TABLE `rider` (
19 `user_status` TINYINT NOT NULL DEFAULT 2 COMMENT '审核状态:0=拒绝 1=通过 2=待审核', 19 `user_status` TINYINT NOT NULL DEFAULT 2 COMMENT '审核状态:0=拒绝 1=通过 2=待审核',
20 `status` TINYINT NOT NULL DEFAULT 1 COMMENT '账号状态:0=禁用 1=正常', 20 `status` TINYINT NOT NULL DEFAULT 1 COMMENT '账号状态:0=禁用 1=正常',
21 `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '余额(兼职用)', 21 `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '余额(兼职用)',
  22 + `frozen_balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额(提现审核中)',
22 `is_rest` TINYINT NOT NULL DEFAULT 0 COMMENT '是否休息:0=否 1=是', 23 `is_rest` TINYINT NOT NULL DEFAULT 0 COMMENT '是否休息:0=否 1=是',
23 `hold_order_limit` INT NOT NULL DEFAULT 0 COMMENT '个人持单上限,0=不限制', 24 `hold_order_limit` INT NOT NULL DEFAULT 0 COMMENT '个人持单上限,0=不限制',
24 `id_no` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '身份证号', 25 `id_no` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '身份证号',
@@ -80,6 +81,35 @@ CREATE TABLE `rider_balance` ( @@ -80,6 +81,35 @@ CREATE TABLE `rider_balance` (
80 KEY `idx_action_id` (`action_id`) 81 KEY `idx_action_id` (`action_id`)
81 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手余额流水表'; 82 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手余额流水表';
82 83
  84 +-- 骑手提现申请表
  85 +CREATE TABLE `rider_withdraw_apply` (
  86 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  87 + `withdraw_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '提现单号',
  88 + `rider_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '骑手ID',
  89 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID',
  90 + `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '提现金额',
  91 + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=待审核 1=审核通过待打款 2=审核拒绝 3=已打款 4=打款失败',
  92 + `account_type` TINYINT NOT NULL DEFAULT 1 COMMENT '收款账户类型:1=银行卡 2=支付宝 3=微信',
  93 + `account_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '收款人',
  94 + `bank_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '开户行',
  95 + `bank_branch` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '开户支行',
  96 + `account_no` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '收款账号',
  97 + `apply_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '申请备注',
  98 + `audit_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '审核/打款备注',
  99 + `auditor_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核人ID',
  100 + `auditor_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审核人名称',
  101 + `apply_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请时间',
  102 + `audit_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核时间',
  103 + `pay_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '打款时间',
  104 + `transfer_no` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '打款流水号',
  105 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  106 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  107 + PRIMARY KEY (`id`),
  108 + UNIQUE KEY `uk_withdraw_no` (`withdraw_no`),
  109 + KEY `idx_rider_status` (`rider_id`, `status`),
  110 + KEY `idx_city_status_time` (`city_id`, `status`, `apply_time`)
  111 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手提现申请表';
  112 +
83 -- 骑手订单统计表 113 -- 骑手订单统计表
84 CREATE TABLE `rider_order_count` ( 114 CREATE TABLE `rider_order_count` (
85 `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 115 `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -598,4 +628,4 @@ CREATE TABLE `rider_device` ( @@ -598,4 +628,4 @@ CREATE TABLE `rider_device` (
598 PRIMARY KEY (`id`), 628 PRIMARY KEY (`id`),
599 UNIQUE KEY `uk_registration_id` (`registration_id`), 629 UNIQUE KEY `uk_registration_id` (`registration_id`),
600 KEY `idx_rider_status` (`rider_id`, `status`) 630 KEY `idx_rider_status` (`rider_id`, `status`)
601 -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手设备推送绑定表';  
602 \ No newline at end of file 631 \ No newline at end of file
  632 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手设备推送绑定表';