| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | 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()); |
| | |
| | | @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)) { |
| | |
| | | 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("删除卡片失败"); |
| | | } |
| | |
| | | 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()); |
| | |
| | | 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(), |
| | |
| | | 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) { |
| | |
| | | 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, "卡余额"); |
| | | } |
| | |
| | | } |
| | | 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 |
| | | + ",充电结束后可重试"); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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")); |