RiderWithdrawServiceImpl.java 12.9 KB
package com.diligrp.rider.service.impl;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.diligrp.rider.common.exception.BizException;
import com.diligrp.rider.dto.RiderWithdrawApplyDTO;
import com.diligrp.rider.dto.WithdrawAuditDTO;
import com.diligrp.rider.dto.WithdrawMarkPaidDTO;
import com.diligrp.rider.entity.Rider;
import com.diligrp.rider.entity.RiderBalance;
import com.diligrp.rider.entity.RiderWithdrawApply;
import com.diligrp.rider.mapper.RiderBalanceMapper;
import com.diligrp.rider.mapper.RiderMapper;
import com.diligrp.rider.mapper.RiderWithdrawApplyMapper;
import com.diligrp.rider.service.RiderWithdrawService;
import com.diligrp.rider.vo.PageResultVO;
import com.diligrp.rider.vo.RiderWithdrawApplyVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.List;

@Service
@RequiredArgsConstructor
public class RiderWithdrawServiceImpl implements RiderWithdrawService {

    private static final int PAGE_SIZE = 20;
    private static final int STATUS_PENDING = 0;
    private static final int STATUS_APPROVED = 1;
    private static final int STATUS_REJECTED = 2;
    private static final int STATUS_PAID = 3;
    private static final ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");

    private final RiderWithdrawApplyMapper withdrawMapper;
    private final RiderMapper riderMapper;
    private final RiderBalanceMapper balanceMapper;

    @Override
    @Transactional
    public void apply(Long riderId, RiderWithdrawApplyDTO dto) {
        if (riderId == null || riderId < 1) {
            throw new BizException("骑手身份无效");
        }
        validateApply(dto);

        Rider rider = withdrawMapper.selectRiderForUpdate(riderId);
        if (rider == null) {
            throw new BizException("骑手信息不存在");
        }
        BigDecimal amount = dto.getAmount().setScale(2, java.math.RoundingMode.HALF_UP);
        BigDecimal balance = money(rider.getBalance());
        BigDecimal frozenBalance = money(rider.getFrozenBalance());
        if (balance.compareTo(amount) < 0) {
            throw new BizException("可提现余额不足");
        }

        long now = now();
        RiderWithdrawApply apply = new RiderWithdrawApply();
        apply.setWithdrawNo(buildWithdrawNo(riderId));
        apply.setRiderId(riderId);
        apply.setCityId(rider.getCityId());
        apply.setAmount(amount);
        apply.setStatus(STATUS_PENDING);
        apply.setAccountType(dto.getAccountType());
        apply.setAccountName(trim(dto.getAccountName()));
        apply.setBankName(trim(dto.getBankName()));
        apply.setBankBranch(trim(dto.getBankBranch()));
        apply.setAccountNo(trim(dto.getAccountNo()));
        apply.setApplyRemark(trim(dto.getApplyRemark()));
        apply.setAuditRemark("");
        apply.setAuditorId(0L);
        apply.setAuditorName("");
        apply.setApplyTime(now);
        apply.setAuditTime(0L);
        apply.setPayTime(0L);
        apply.setTransferNo("");
        apply.setCreateTime(now);
        apply.setUpdateTime(now);
        withdrawMapper.insert(apply);

        updateRiderMoney(riderId, balance.subtract(amount), frozenBalance.add(amount));
    }

    @Override
    public PageResultVO<RiderWithdrawApplyVO> riderList(Long riderId, Integer status, int page) {
        int currentPage = normalizePage(page);
        int offset = (currentPage - 1) * PAGE_SIZE;
        PageResultVO<RiderWithdrawApplyVO> result = new PageResultVO<>();
        result.setList(withdrawMapper.selectRiderList(riderId, normalizeStatus(status), offset, PAGE_SIZE));
        result.setPage(currentPage);
        result.setPageSize(PAGE_SIZE);
        result.setTotal(withdrawMapper.countRiderList(riderId, normalizeStatus(status)));
        return result;
    }

    @Override
    public PageResultVO<RiderWithdrawApplyVO> adminList(Long cityId, Integer status, String keyword, String startDate, String endDate, int page) {
        int currentPage = normalizePage(page);
        int offset = (currentPage - 1) * PAGE_SIZE;
        Long startTime = parseStartTime(startDate);
        Long endTime = parseEndTime(endDate);
        PageResultVO<RiderWithdrawApplyVO> result = new PageResultVO<>();
        result.setList(withdrawMapper.selectAdminList(cityId, normalizeStatus(status), normalizeKeyword(keyword), startTime, endTime, offset, PAGE_SIZE));
        result.setPage(currentPage);
        result.setPageSize(PAGE_SIZE);
        result.setTotal(withdrawMapper.countAdminList(cityId, normalizeStatus(status), normalizeKeyword(keyword), startTime, endTime));
        return result;
    }

    @Override
    public RiderWithdrawApplyVO adminDetail(Long id, Long cityId) {
        if (id == null || id < 1) {
            throw new BizException("提现申请ID不能为空");
        }
        RiderWithdrawApplyVO detail = withdrawMapper.selectAdminDetail(id, cityId);
        if (detail == null) {
            throw new BizException("提现申请不存在");
        }
        return detail;
    }

    @Override
    @Transactional
    public void approve(Long id, WithdrawAuditDTO dto, Long cityId, Long adminId, String adminRole) {
        RiderWithdrawApply apply = lockApply(id, cityId);
        if (apply.getStatus() == null || apply.getStatus() != STATUS_PENDING) {
            throw new BizException("只有待审核申请可以审核通过");
        }
        Rider rider = lockRider(apply.getRiderId());
        ensureFrozenEnough(rider, apply.getAmount());
        long now = now();
        apply.setStatus(STATUS_APPROVED);
        apply.setAuditRemark(trim(dto == null ? null : dto.getRemark()));
        apply.setAuditorId(adminId == null ? 0L : adminId);
        apply.setAuditorName(buildAdminName(adminId, adminRole));
        apply.setAuditTime(now);
        apply.setUpdateTime(now);
        withdrawMapper.updateById(apply);
    }

    @Override
    @Transactional
    public void reject(Long id, WithdrawAuditDTO dto, Long cityId, Long adminId, String adminRole) {
        RiderWithdrawApply apply = lockApply(id, cityId);
        if (apply.getStatus() == null || apply.getStatus() != STATUS_PENDING) {
            throw new BizException("只有待审核申请可以拒绝");
        }
        String remark = trim(dto == null ? null : dto.getRemark());
        if (remark.isBlank()) {
            throw new BizException("拒绝原因不能为空");
        }
        Rider rider = lockRider(apply.getRiderId());
        BigDecimal amount = money(apply.getAmount());
        ensureFrozenEnough(rider, amount);
        updateRiderMoney(rider.getId(), money(rider.getBalance()).add(amount), money(rider.getFrozenBalance()).subtract(amount));

        long now = now();
        apply.setStatus(STATUS_REJECTED);
        apply.setAuditRemark(remark);
        apply.setAuditorId(adminId == null ? 0L : adminId);
        apply.setAuditorName(buildAdminName(adminId, adminRole));
        apply.setAuditTime(now);
        apply.setUpdateTime(now);
        withdrawMapper.updateById(apply);
    }

    @Override
    @Transactional
    public void markPaid(Long id, WithdrawMarkPaidDTO dto, Long cityId, Long adminId, String adminRole) {
        if (dto == null || trim(dto.getTransferNo()).isBlank()) {
            throw new BizException("打款流水号不能为空");
        }
        RiderWithdrawApply apply = lockApply(id, cityId);
        if (apply.getStatus() == null || apply.getStatus() != STATUS_APPROVED) {
            throw new BizException("只有审核通过待打款申请可以标记已打款");
        }
        Rider rider = lockRider(apply.getRiderId());
        BigDecimal amount = money(apply.getAmount());
        ensureFrozenEnough(rider, amount);
        BigDecimal nextFrozenBalance = money(rider.getFrozenBalance()).subtract(amount);
        BigDecimal currentBalance = money(rider.getBalance());
        updateRiderMoney(rider.getId(), currentBalance, nextFrozenBalance);

        long now = now();
        RiderBalance record = new RiderBalance();
        record.setUid(rider.getId());
        record.setType(2);
        record.setAction("withdraw_paid");
        record.setActionId(apply.getId());
        record.setOrderNo(apply.getWithdrawNo());
        record.setNums(amount.negate());
        record.setTotal(currentBalance);
        record.setAddTime(now);
        balanceMapper.insert(record);

        String remark = trim(dto.getRemark());
        apply.setStatus(STATUS_PAID);
        apply.setAuditRemark(remark.isBlank() ? apply.getAuditRemark() : remark);
        apply.setAuditorId(adminId == null ? apply.getAuditorId() : adminId);
        apply.setAuditorName(buildAdminName(adminId, adminRole));
        apply.setPayTime(now);
        apply.setTransferNo(trim(dto.getTransferNo()));
        apply.setUpdateTime(now);
        withdrawMapper.updateById(apply);
    }

    private void validateApply(RiderWithdrawApplyDTO dto) {
        if (dto == null) {
            throw new BizException("提现申请不能为空");
        }
        if (dto.getAccountType() == null || dto.getAccountType() < 1 || dto.getAccountType() > 3) {
            throw new BizException("收款账户类型不正确");
        }
        if (dto.getAmount() == null || dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BizException("提现金额必须大于0");
        }
        if (trim(dto.getAccountName()).isBlank()) {
            throw new BizException("收款人不能为空");
        }
        if (trim(dto.getAccountNo()).isBlank()) {
            throw new BizException("收款账号不能为空");
        }
        if (dto.getAccountType() == 1 && trim(dto.getBankName()).isBlank()) {
            throw new BizException("银行卡提现需填写开户行");
        }
    }

    private RiderWithdrawApply lockApply(Long id, Long cityId) {
        if (id == null || id < 1) {
            throw new BizException("提现申请ID不能为空");
        }
        RiderWithdrawApply apply = withdrawMapper.selectApplyForUpdate(id);
        if (apply == null) {
            throw new BizException("提现申请不存在");
        }
        if (cityId != null && !cityId.equals(apply.getCityId())) {
            throw new BizException("只能操作当前租户提现申请");
        }
        return apply;
    }

    private Rider lockRider(Long riderId) {
        Rider rider = withdrawMapper.selectRiderForUpdate(riderId);
        if (rider == null) {
            throw new BizException("骑手信息不存在");
        }
        return rider;
    }

    private void ensureFrozenEnough(Rider rider, BigDecimal amount) {
        if (money(rider.getFrozenBalance()).compareTo(money(amount)) < 0) {
            throw new BizException("冻结余额不足,请核对提现申请状态");
        }
    }

    private void updateRiderMoney(Long riderId, BigDecimal balance, BigDecimal frozenBalance) {
        riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
                .eq(Rider::getId, riderId)
                .set(Rider::getBalance, money(balance))
                .set(Rider::getFrozenBalance, money(frozenBalance)));
    }

    private BigDecimal money(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value.setScale(2, java.math.RoundingMode.HALF_UP);
    }

    private long now() {
        return System.currentTimeMillis() / 1000;
    }

    private String trim(String value) {
        return value == null ? "" : value.trim();
    }

    private String buildWithdrawNo(Long riderId) {
        return "WD" + System.currentTimeMillis() + String.format("%04d", riderId % 10000);
    }

    private String buildAdminName(Long adminId, String adminRole) {
        if (adminId == null) {
            return "系统";
        }
        return ("substation".equals(adminRole) ? "分站管理员" : "平台管理员") + "#" + adminId;
    }

    private int normalizePage(int page) {
        return Math.max(page, 1);
    }

    private Integer normalizeStatus(Integer status) {
        return status == null || status < 0 ? null : status;
    }

    private String normalizeKeyword(String keyword) {
        return keyword == null || keyword.isBlank() ? null : keyword.trim();
    }

    private Long parseStartTime(String startDate) {
        if (startDate == null || startDate.isBlank()) {
            return null;
        }
        return parseDate(startDate, "开始日期格式不正确").atStartOfDay(ZONE_ID).toEpochSecond();
    }

    private Long parseEndTime(String endDate) {
        if (endDate == null || endDate.isBlank()) {
            return null;
        }
        return parseDate(endDate, "结束日期格式不正确").plusDays(1).atStartOfDay(ZONE_ID).toEpochSecond();
    }

    private LocalDate parseDate(String value, String errorMessage) {
        try {
            return LocalDate.parse(value);
        } catch (DateTimeParseException ex) {
            throw new BizException(errorMessage);
        }
    }
}