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.common.core.exception.ServiceException;
|
import com.ruoyi.common.core.utils.StringUtils;
|
import com.ruoyi.common.security.service.TokenService;
|
import com.ruoyi.common.security.utils.SecurityUtils;
|
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;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Set;
|
|
@Service
|
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) {
|
ChargingCardQueryDTO safeQuery = query == null ? new ChargingCardQueryDTO() : query;
|
long pageNum = safeQuery.getPageCurr() == null || safeQuery.getPageCurr() < 1 ? 1 : safeQuery.getPageCurr();
|
long pageSize = safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1 ? 10 : safeQuery.getPageSize();
|
Page<TChargingCard> page = new Page<>(pageNum, pageSize);
|
return baseMapper.selectCardPage(page, safeQuery);
|
}
|
|
@Override
|
public boolean saveCard(ChargingCardSaveDTO dto) {
|
validateCard(dto, false);
|
TChargingCard card = new TChargingCard();
|
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());
|
if (!save(card)) {
|
throw new ServiceException("新增卡片失败");
|
}
|
return true;
|
}
|
|
@Override
|
public boolean updateCard(ChargingCardSaveDTO dto) {
|
validateCard(dto, true);
|
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("修改卡片失败");
|
}
|
return true;
|
}
|
|
@Override
|
public boolean deleteCards(List<Long> ids) {
|
if (ids == null || ids.isEmpty()) {
|
throw new ServiceException("请选择需要删除的卡片");
|
}
|
List<Long> distinctIds = new java.util.ArrayList<>(new HashSet<>(ids));
|
List<TChargingCard> cards = listByIds(distinctIds);
|
if (cards.size() != distinctIds.size()) {
|
throw new ServiceException("部分卡片不存在或已删除");
|
}
|
assertNotChargingLocked(cards, "删除卡片");
|
if (!removeByIds(distinctIds)) {
|
throw new ServiceException("删除卡片失败");
|
}
|
return true;
|
}
|
|
@Override
|
public boolean updateStatus(ChargingCardStatusDTO dto) {
|
if (dto == null || dto.getId() == null || !isValidStatus(dto.getStatus())) {
|
throw new ServiceException("卡片状态参数错误");
|
}
|
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());
|
card.setUpdateTime(LocalDateTime.now());
|
card.setUpdateBy(SecurityUtils.getUserId());
|
if (!updateById(card)) {
|
throw new ServiceException("更新卡片状态失败");
|
}
|
return true;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public boolean updateBalance(ChargingCardBalanceDTO dto) {
|
validateBalanceRequest(dto);
|
verifyOperationPassword(dto.getOperationPassword());
|
|
List<TChargingCard> cards = listByIds(dto.getIds());
|
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(),
|
StringUtils.isEmpty(dto.getRemark()) ? null : dto.getRemark().trim(), SecurityUtils.getUserId());
|
}
|
return true;
|
}
|
|
@Override
|
public List<ChargingCardExcelDTO> selectExportList(ChargingCardQueryDTO query) {
|
return baseMapper.selectExportList(query == null ? new ChargingCardQueryDTO() : query);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public String importCards(List<ChargingCardExcelDTO> cards) {
|
if (cards == null || cards.isEmpty()) {
|
throw new ServiceException("导入数据不能为空");
|
}
|
Set<String> importCardNos = new HashSet<>();
|
int row = 1;
|
for (ChargingCardExcelDTO item : cards) {
|
row++;
|
if (item == null || StringUtils.isEmpty(item.getCardNo())) {
|
throw new ServiceException("第" + row + "行卡号不能为空");
|
}
|
String cardNo = item.getCardNo().trim();
|
if (cardNo.length() > 64) {
|
throw new ServiceException("第" + row + "行卡号不能超过64个字符");
|
}
|
if (!importCardNos.add(cardNo)) {
|
throw new ServiceException("第" + row + "行卡号在文件中重复");
|
}
|
if (existsCardNo(cardNo, null)) {
|
throw new ServiceException("第" + row + "行卡号已存在");
|
}
|
if (item.getBalance() != null) {
|
validateAmount(item.getBalance(), true, "第" + row + "行卡余额");
|
}
|
parseExcelStatus(item.getStatus(), row);
|
}
|
|
Long userId = SecurityUtils.getUserId();
|
LocalDateTime now = LocalDateTime.now();
|
for (ChargingCardExcelDTO item : cards) {
|
TChargingCard card = new TChargingCard();
|
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 (dto == null || StringUtils.isEmpty(dto.getCardNo())) {
|
throw new ServiceException("卡号不能为空");
|
}
|
if (update && dto.getId() == null) {
|
throw new ServiceException("卡片ID不能为空");
|
}
|
String cardNo = dto.getCardNo().trim();
|
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 (dto.getStatus() != null && !isValidStatus(dto.getStatus())) {
|
throw new ServiceException("卡片状态参数错误");
|
}
|
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 boolean existsCardNo(String cardNo, Long excludeId) {
|
return baseMapper.countByCardNo(cardNo, excludeId) > 0;
|
}
|
|
private void validateBalanceRequest(ChargingCardBalanceDTO dto) {
|
if (dto == null || dto.getIds() == null || dto.getIds().isEmpty()) {
|
throw new ServiceException("请选择需要更新余额的卡片");
|
}
|
if (dto.getType() == null || (dto.getType() != BALANCE_ADD && dto.getType() != BALANCE_SUBTRACT)) {
|
throw new ServiceException("余额调整类型错误");
|
}
|
validateAmount(dto.getAmount(), false, "调整金额");
|
if (dto.getRemark() != null && dto.getRemark().trim().length() > 255) {
|
throw new ServiceException("备注不能超过255个字符");
|
}
|
if (StringUtils.isEmpty(dto.getOperationPassword())) {
|
throw new ServiceException("请输入操作密码");
|
}
|
}
|
|
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"));
|
}
|
if (amount.scale() > 2) {
|
throw new ServiceException(fieldName + "最多保留2位小数");
|
}
|
if (amount.compareTo(MAX_AMOUNT) > 0) {
|
throw new ServiceException(fieldName + "不能超过" + MAX_AMOUNT.toPlainString());
|
}
|
}
|
|
private Integer parseExcelStatus(String status, int row) {
|
if (StringUtils.isEmpty(status)) {
|
return ENABLED;
|
}
|
String value = status.trim();
|
if ("1".equals(value) || "启用".equals(value)) {
|
return ENABLED;
|
}
|
if ("2".equals(value) || "禁用".equals(value)) {
|
return DISABLED;
|
}
|
String prefix = row > 0 ? "第" + row + "行" : "";
|
throw new ServiceException(prefix + "启用状态错误");
|
}
|
|
private void verifyOperationPassword(String operationPassword) {
|
LoginUser loginUser = tokenService.getLoginUser();
|
if (loginUser == null || loginUser.getUserid() == null) {
|
throw new ServiceException("登录状态已失效,请重新登录");
|
}
|
R<SysUser> response = sysUserClient.getSysUser(loginUser.getUserid());
|
SysUser user = response == null ? null : response.getData();
|
if (user == null || StringUtils.isEmpty(user.getOperationPassword())) {
|
throw new ServiceException("当前账号未设置操作密码");
|
}
|
if (!SecurityUtils.matchesPassword(operationPassword, user.getOperationPassword())) {
|
throw new ServiceException("操作密码错误");
|
}
|
}
|
|
private boolean isValidStatus(Integer status) {
|
return status != null && (status == ENABLED || status == DISABLED);
|
}
|
}
|