puzhibing
2026-09-09 4a5e1477afc559cf870b34e4693f41d85e9580d1
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
@@ -1,14 +1,18 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
import com.ruoyi.account.api.dto.ChargingCardQueryDTO;
import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
import com.ruoyi.account.api.model.TChargingCard;
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
import com.ruoyi.account.mapper.TChargingCardMapper;
import com.ruoyi.account.service.TChargingCardService;
import com.ruoyi.common.core.domain.R;
@@ -19,9 +23,13 @@
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
import com.ruoyi.system.api.model.LoginUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -33,17 +41,29 @@
public class TChargingCardServiceImpl extends ServiceImpl<TChargingCardMapper, TChargingCard>
        implements TChargingCardService {
    private static final Logger log = LoggerFactory.getLogger(TChargingCardServiceImpl.class);
    private static final int ENABLED = 1;
    private static final int DISABLED = 2;
    private static final int BALANCE_ADD = 1;
    private static final int BALANCE_SUBTRACT = 2;
    /** 余额变动类型:3=刷卡订单充电结算扣款(V0.2 定稿,订单扣款专用) */
    private static final int BALANCE_TYPE_ORDER_DEDUCT = 3;
    /** 充电锁定标记:1=充电中(业务锁定,管理操作互斥) */
    private static final int CHARGING_FLAG_LOCKED = 1;
    /** 设备链路无登录操作人,流水统一记 0 */
    private static final long SYSTEM_OPERATOR = 0L;
    private static final BigDecimal MAX_AMOUNT = new BigDecimal("9999999999.99");
    private static final int PASSWORD_MAX_LENGTH = 64;
    @Autowired
    private TokenService tokenService;
    @Autowired
    private SysUserClient sysUserClient;
    @Autowired
    private TransactionTemplate transactionTemplate;
    @Override
    public IPage<TChargingCard> selectCardPage(ChargingCardQueryDTO query) {
@@ -61,6 +81,9 @@
        card.setCardNo(dto.getCardNo().trim());
        card.setBalance(dto.getBalance() == null ? BigDecimal.ZERO : dto.getBalance());
        card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus());
        card.setPassword(encodePasswordIfPresent(dto.getPassword()));
        card.setChargingFlag(0);
        card.setChargingOrderCode(null);
        card.setDelFlag(0);
        card.setCreateTime(LocalDateTime.now());
        card.setCreateBy(SecurityUtils.getUserId());
@@ -73,13 +96,20 @@
    @Override
    public boolean updateCard(ChargingCardSaveDTO dto) {
        validateCard(dto, true);
        if (getById(dto.getId()) == null) {
        TChargingCard existing = getById(dto.getId());
        if (existing == null) {
            throw new ServiceException("卡片不存在或已删除");
        }
        assertNotChargingLocked(existing, "修改卡片或重置卡密码");
        TChargingCard card = new TChargingCard();
        card.setId(dto.getId());
        card.setCardNo(dto.getCardNo().trim());
        card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus());
        // 仅当传入新密码时才重置卡密码;留空表示不修改(V0.2 定稿)
        String encodedPassword = encodePasswordIfPresent(dto.getPassword());
        if (encodedPassword != null) {
            card.setPassword(encodedPassword);
        }
        card.setUpdateTime(LocalDateTime.now());
        card.setUpdateBy(SecurityUtils.getUserId());
        if (!updateById(card)) {
@@ -94,9 +124,11 @@
            throw new ServiceException("请选择需要删除的卡片");
        }
        List<Long> distinctIds = new java.util.ArrayList<>(new HashSet<>(ids));
        if (listByIds(distinctIds).size() != distinctIds.size()) {
        List<TChargingCard> cards = listByIds(distinctIds);
        if (cards.size() != distinctIds.size()) {
            throw new ServiceException("部分卡片不存在或已删除");
        }
        assertNotChargingLocked(cards, "删除卡片");
        if (!removeByIds(distinctIds)) {
            throw new ServiceException("删除卡片失败");
        }
@@ -108,9 +140,11 @@
        if (dto == null || dto.getId() == null || !isValidStatus(dto.getStatus())) {
            throw new ServiceException("卡片状态参数错误");
        }
        if (getById(dto.getId()) == null) {
        TChargingCard existing = getById(dto.getId());
        if (existing == null) {
            throw new ServiceException("卡片不存在或已删除");
        }
        assertNotChargingLocked(existing, "启用/禁用卡片");
        TChargingCard card = new TChargingCard();
        card.setId(dto.getId());
        card.setStatus(dto.getStatus());
@@ -132,12 +166,13 @@
        if (cards.size() != new HashSet<>(dto.getIds()).size()) {
            throw new ServiceException("部分卡片不存在或已删除");
        }
        assertNotChargingLocked(cards, "调整余额");
        int updated = baseMapper.adjustBalance(dto.getIds(), dto.getAmount(), dto.getType(),
                MAX_AMOUNT, SecurityUtils.getUserId());
        if (updated != cards.size()) {
            throw new ServiceException(dto.getType() == BALANCE_SUBTRACT
                    ? "部分卡片余额不足,无法完成扣减"
                    : "部分卡片余额超过上限,无法完成增加");
                    ? "部分卡片正在充电中或余额不足,无法完成扣减"
                    : "部分卡片正在充电中或余额超过上限,无法完成增加");
        }
        for (TChargingCard card : cards) {
            baseMapper.insertBalanceRecord(card.getId(), dto.getType(), dto.getAmount(),
@@ -187,12 +222,104 @@
            card.setCardNo(item.getCardNo().trim());
            card.setBalance(item.getBalance() == null ? BigDecimal.ZERO : item.getBalance());
            card.setStatus(parseExcelStatus(item.getStatus(), 0));
            card.setChargingFlag(0);
            card.setDelFlag(0);
            card.setCreateTime(now);
            card.setCreateBy(userId);
            save(card);
        }
        return "成功导入" + cards.size() + "条卡片数据";
    }
    @Override
    public TChargingCard verifyCardForCharging(ChargingCardAuthDTO dto) {
        if (dto == null || StringUtils.isEmpty(dto.getCardNo())) {
            throw new ServiceException("卡号不能为空");
        }
        TChargingCard card = getOne(new LambdaQueryWrapper<TChargingCard>()
                .eq(TChargingCard::getCardNo, dto.getCardNo().trim()));
        if (card == null) {
            throw new ServiceException("卡片不存在或已删除");
        }
        if (card.getStatus() == null || card.getStatus() != ENABLED) {
            throw new ServiceException("卡片已禁用,无法启动充电");
        }
        // 条件密码校验(V0.2 定稿):卡片配置了密码才校验;未配置密码不要求、不校验
        if (StringUtils.isNotEmpty(card.getPassword())) {
            if (StringUtils.isEmpty(dto.getPassword())) {
                throw new ServiceException("卡片已配置密码,请输入卡密码");
            }
            if (!SecurityUtils.matchesPassword(dto.getPassword(), card.getPassword())) {
                throw new ServiceException("卡密码错误");
            }
        }
        return card;
    }
    @Override
    public boolean lockCardForCharging(Long cardId, String orderCode) {
        if (cardId == null || StringUtils.isEmpty(orderCode)) {
            throw new ServiceException("卡片ID与订单号不能为空");
        }
        return baseMapper.lockCardForCharging(cardId, orderCode.trim()) == 1;
    }
    @Override
    public boolean unlockCardForCharging(Long cardId, String orderCode) {
        if (cardId == null || StringUtils.isEmpty(orderCode)) {
            throw new ServiceException("卡片ID与订单号不能为空");
        }
        if (baseMapper.unlockCardForCharging(cardId, orderCode.trim()) == 1) {
            return true;
        }
        TChargingCard card = getById(cardId);
        if (card == null) {
            throw new ServiceException("卡片不存在或已删除");
        }
        // 已解锁(订单号已被清空)视为幂等成功;仍被其他订单占用则返回 false,防止误释放
        return card.getChargingOrderCode() == null || orderCode.trim().equals(card.getChargingOrderCode());
    }
    @Override
    public ChargingCardDeductVO deductBalanceForCharging(ChargingCardDeductDTO dto) {
        validateDeductRequest(dto);
        try {
            return transactionTemplate.execute(status -> {
                TChargingCard card = getById(dto.getCardId());
                if (card == null) {
                    throw new ServiceException("卡片不存在或已删除");
                }
                BigDecimal before = card.getBalance() == null ? BigDecimal.ZERO : card.getBalance();
                // 第一步先占扣款幂等键:同一订单并发结算时,唯一索引 uk_order_code 只允许一个事务插入成功
                baseMapper.insertOrderDeductRecord(dto.getCardId(), dto.getAmount(),
                        dto.getOrderCode(), dto.getOperatorId() == null ? SYSTEM_OPERATOR : dto.getOperatorId());
                // 第二步原子扣减:允许扣成负余额,禁止携带 balance >= amount 下限条件(V0.2 定稿)
                int updated = baseMapper.deductBalanceByCardId(dto.getCardId(), dto.getAmount());
                if (updated != 1) {
                    throw new ServiceException("卡片不存在或已删除,扣款失败");
                }
                TChargingCard afterCard = getById(dto.getCardId());
                BigDecimal after = afterCard == null || afterCard.getBalance() == null
                        ? BigDecimal.ZERO : afterCard.getBalance();
                ChargingCardDeductVO vo = new ChargingCardDeductVO();
                vo.setCardId(dto.getCardId());
                vo.setOrderCode(dto.getOrderCode());
                vo.setAmount(dto.getAmount());
                vo.setBeforeBalance(before);
                vo.setAfterBalance(after);
                vo.setDuplicated(false);
                return vo;
            });
        } catch (DuplicateKeyException e) {
            // 唯一键冲突:该订单此前已扣款成功(含并发下慢事务先提交),按幂等重复返回,不重复扣款
            log.warn("刷卡订单结算扣款幂等命中,不重复扣款。订单号={}, 卡片ID={}", dto.getOrderCode(), dto.getCardId());
            ChargingCardDeductVO vo = new ChargingCardDeductVO();
            vo.setCardId(dto.getCardId());
            vo.setOrderCode(dto.getOrderCode());
            vo.setAmount(dto.getAmount());
            vo.setDuplicated(true);
            return vo;
        }
    }
    private void validateCard(ChargingCardSaveDTO dto, boolean update) {
@@ -206,6 +333,11 @@
        if (cardNo.length() > 64) {
            throw new ServiceException("卡号不能超过64个字符");
        }
        if (dto.getPassword() != null && !dto.getPassword().trim().isEmpty()) {
            if (dto.getPassword().length() > PASSWORD_MAX_LENGTH) {
                throw new ServiceException("卡密码不能超过" + PASSWORD_MAX_LENGTH + "个字符");
            }
        }
        if (!update && dto.getBalance() != null) {
            validateAmount(dto.getBalance(), true, "卡余额");
        }
@@ -214,6 +346,35 @@
        }
        if (existsCardNo(cardNo, dto.getId())) {
            throw new ServiceException("卡号已存在");
        }
    }
    private String encodePasswordIfPresent(String rawPassword) {
        if (rawPassword == null || rawPassword.trim().isEmpty()) {
            return null;
        }
        return SecurityUtils.encryptPassword(rawPassword.trim());
    }
    /**
     * 卡片处于充电锁定状态时拒绝管理操作(V0.2 定稿)。
     *
     * @param cards  待操作的卡片
     * @param action 操作描述,用于异常提示
     */
    private void assertNotChargingLocked(List<TChargingCard> cards, String action) {
        if (cards == null) {
            return;
        }
        for (TChargingCard card : cards) {
            assertNotChargingLocked(card, action);
        }
    }
    private void assertNotChargingLocked(TChargingCard card, String action) {
        if (card.getChargingFlag() != null && card.getChargingFlag() == CHARGING_FLAG_LOCKED) {
            throw new ServiceException("卡片【" + card.getCardNo() + "】正在充电中,禁止" + action
                    + ",充电结束后可重试");
        }
    }
@@ -237,6 +398,22 @@
        }
    }
    private void validateDeductRequest(ChargingCardDeductDTO dto) {
        if (dto == null) {
            throw new ServiceException("扣款参数不能为空");
        }
        if (dto.getCardId() == null) {
            throw new ServiceException("卡片ID不能为空");
        }
        if (StringUtils.isEmpty(dto.getOrderCode())) {
            throw new ServiceException("订单号不能为空");
        }
        if (dto.getOrderCode().length() > 64) {
            throw new ServiceException("订单号不能超过64个字符");
        }
        validateAmount(dto.getAmount(), false, "扣款金额");
    }
    private void validateAmount(BigDecimal amount, boolean allowZero, String fieldName) {
        if (amount == null || (allowZero ? amount.compareTo(BigDecimal.ZERO) < 0 : amount.compareTo(BigDecimal.ZERO) <= 0)) {
            throw new ServiceException(fieldName + (allowZero ? "不能小于0" : "必须大于0"));