puzhibing
2026-08-27 768ae5d15078c31afad2bc6781ca570cea52fa6c
feat: 瀹屽杽鍗$墖绠$悊鎿嶄綔鍔熻兘
5个文件已添加
6个文件已修改
598 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardBalanceDTO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardStatusDTO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260827_charging_card_operations.sql 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardBalanceDTO.java
New file
@@ -0,0 +1,31 @@
package com.ruoyi.account.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
@Data
@ApiModel(value = "充电卡余额更新参数")
public class ChargingCardBalanceDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "卡片ID集合", required = true)
    private List<Long> ids;
    @ApiModelProperty(value = "调整类型:1增加,2减少", required = true)
    private Integer type;
    @ApiModelProperty(value = "调整金额", required = true)
    private BigDecimal amount;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "操作密码", required = true)
    private String operationPassword;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java
New file
@@ -0,0 +1,24 @@
package com.ruoyi.account.api.dto;
import com.ruoyi.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@ApiModel(value = "充电卡导入导出数据")
public class ChargingCardExcelDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @Excel(name = "卡号", width = 22, prompt = "必填,最多64个字符")
    private String cardNo;
    @Excel(name = "卡余额", width = 16, scale = 2, prompt = "选填,必须大于等于0")
    private BigDecimal balance;
    @Excel(name = "启用状态", width = 14, readConverterExp = "1=启用,2=禁用", combo = {"启用", "禁用"})
    private String status;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardSaveDTO.java
New file
@@ -0,0 +1,27 @@
package com.ruoyi.account.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@ApiModel(value = "充电卡保存参数")
public class ChargingCardSaveDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "主键ID,编辑时必填")
    private Long id;
    @ApiModelProperty(value = "卡号", required = true)
    private String cardNo;
    @ApiModelProperty(value = "卡余额")
    private BigDecimal balance;
    @ApiModelProperty(value = "状态:1启用,2禁用")
    private Integer status;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardStatusDTO.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.account.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(value = "充电卡状态更新参数")
public class ChargingCardStatusDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "卡片ID", required = true)
    private Long id;
    @ApiModelProperty(value = "状态:1启用,2禁用", required = true)
    private Integer status;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -24,16 +25,19 @@
    @ApiModelProperty(value = "主键ID")
    private Long id;
    @Excel(name = "卡号", width = 22)
    @ApiModelProperty(value = "卡号")
    private String cardNo;
    @Excel(name = "卡余额", width = 16, scale = 2)
    @ApiModelProperty(value = "卡余额")
    private BigDecimal balance;
    @Excel(name = "启用状态", width = 14, readConverterExp = "1=启用,2=禁用")
    @ApiModelProperty(value = "状态:1启用,2禁用")
    private Integer status;
    @TableLogic
    @TableLogic(value = "0", delval = "2")
    @ApiModelProperty(value = "删除状态:0正常,2删除")
    private Integer delFlag;
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TChargingCardController.java
@@ -1,20 +1,33 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
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.service.TChargingCardService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.poi.ExcelUtil;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Api(tags = "卡片管理")
@RestController
@@ -30,4 +43,64 @@
    public R<IPage<TChargingCard>> page(@RequestBody ChargingCardQueryDTO query) {
        return R.ok(chargingCardService.selectCardPage(query));
    }
    @ApiOperation(value = "新增卡片")
    @RequiresPermissions(value = {"/chargingCard/add"}, logical = Logical.OR)
    @PostMapping
    public R<Boolean> add(@RequestBody ChargingCardSaveDTO dto) {
        return R.ok(chargingCardService.saveCard(dto));
    }
    @ApiOperation(value = "编辑卡片")
    @RequiresPermissions(value = {"/chargingCard/edit"}, logical = Logical.OR)
    @PutMapping
    public R<Boolean> edit(@RequestBody ChargingCardSaveDTO dto) {
        return R.ok(chargingCardService.updateCard(dto));
    }
    @ApiOperation(value = "删除卡片")
    @RequiresPermissions(value = {"/chargingCard/delete"}, logical = Logical.OR)
    @DeleteMapping
    public R<Boolean> delete(@RequestBody List<Long> ids) {
        return R.ok(chargingCardService.deleteCards(ids));
    }
    @ApiOperation(value = "更新卡片启用状态")
    @RequiresPermissions(value = {"/chargingCard/status"}, logical = Logical.OR)
    @PutMapping("/status")
    public R<Boolean> status(@RequestBody ChargingCardStatusDTO dto) {
        return R.ok(chargingCardService.updateStatus(dto));
    }
    @ApiOperation(value = "批量更新卡片余额")
    @RequiresPermissions(value = {"/chargingCard/balance"}, logical = Logical.OR)
    @PutMapping("/balance")
    public R<Boolean> balance(@RequestBody ChargingCardBalanceDTO dto) {
        return R.ok(chargingCardService.updateBalance(dto));
    }
    @ApiOperation(value = "导出卡片")
    @RequiresPermissions(value = {"/chargingCard/export"}, logical = Logical.OR)
    @PutMapping("/export")
    public void export(HttpServletResponse response, @RequestBody ChargingCardQueryDTO query) {
        ExcelUtil<ChargingCardExcelDTO> util = new ExcelUtil<>(ChargingCardExcelDTO.class);
        util.exportExcel(response, chargingCardService.selectExportList(query), "卡片数据");
    }
    @ApiOperation(value = "下载卡片导入模板")
    @RequiresPermissions(value = {"/chargingCard/import"}, logical = Logical.OR)
    @GetMapping("/import-template")
    public void importTemplate(HttpServletResponse response) {
        ExcelUtil<ChargingCardExcelDTO> util = new ExcelUtil<>(ChargingCardExcelDTO.class);
        util.importTemplateExcel(response, "卡片数据");
    }
    @ApiOperation(value = "导入卡片")
    @RequiresPermissions(value = {"/chargingCard/import"}, logical = Logical.OR)
    @PostMapping("/import")
    public R<String> importData(@RequestPart("file") MultipartFile file) throws Exception {
        ExcelUtil<ChargingCardExcelDTO> util = new ExcelUtil<>(ChargingCardExcelDTO.class);
        List<ChargingCardExcelDTO> cards = util.importExcel(file.getInputStream());
        return R.ok(chargingCardService.importCards(cards));
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/mapper/TChargingCardMapper.java
@@ -3,12 +3,32 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.api.dto.ChargingCardExcelDTO;
import com.ruoyi.account.api.dto.ChargingCardQueryDTO;
import com.ruoyi.account.api.model.TChargingCard;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
public interface TChargingCardMapper extends BaseMapper<TChargingCard> {
    IPage<TChargingCard> selectCardPage(Page<TChargingCard> page,
                                        @Param("query") ChargingCardQueryDTO query);
    List<ChargingCardExcelDTO> selectExportList(@Param("query") ChargingCardQueryDTO query);
    int countByCardNo(@Param("cardNo") String cardNo, @Param("excludeId") Long excludeId);
    int adjustBalance(@Param("ids") List<Long> ids,
                      @Param("amount") BigDecimal amount,
                      @Param("type") Integer type,
                      @Param("maxAmount") BigDecimal maxAmount,
                      @Param("updateBy") Long updateBy);
    int insertBalanceRecord(@Param("cardId") Long cardId,
                            @Param("type") Integer type,
                            @Param("amount") BigDecimal amount,
                            @Param("remark") String remark,
                            @Param("createBy") Long createBy);
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TChargingCardService.java
@@ -2,10 +2,30 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.api.dto.ChargingCardBalanceDTO;
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 java.util.List;
public interface TChargingCardService extends IService<TChargingCard> {
    IPage<TChargingCard> selectCardPage(ChargingCardQueryDTO query);
    boolean saveCard(ChargingCardSaveDTO dto);
    boolean updateCard(ChargingCardSaveDTO dto);
    boolean deleteCards(List<Long> ids);
    boolean updateStatus(ChargingCardStatusDTO dto);
    boolean updateBalance(ChargingCardBalanceDTO dto);
    List<ChargingCardExcelDTO> selectExportList(ChargingCardQueryDTO query);
    String importCards(List<ChargingCardExcelDTO> cards);
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
@@ -3,19 +3,283 @@
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.ChargingCardBalanceDTO;
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.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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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 int ENABLED = 1;
    private static final int DISABLED = 2;
    private static final int BALANCE_ADD = 1;
    private static final int BALANCE_SUBTRACT = 2;
    private static final BigDecimal MAX_AMOUNT = new BigDecimal("9999999999.99");
    @Autowired
    private TokenService tokenService;
    @Autowired
    private SysUserClient sysUserClient;
    @Override
    public IPage<TChargingCard> selectCardPage(ChargingCardQueryDTO query) {
        Page<TChargingCard> page = new Page<>(query.getPageCurr(), query.getPageSize());
        return baseMapper.selectCardPage(page, 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.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);
        if (getById(dto.getId()) == null) {
            throw new ServiceException("卡片不存在或已删除");
        }
        TChargingCard card = new TChargingCard();
        card.setId(dto.getId());
        card.setCardNo(dto.getCardNo().trim());
        card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus());
        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));
        if (listByIds(distinctIds).size() != distinctIds.size()) {
            throw new ServiceException("部分卡片不存在或已删除");
        }
        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("卡片状态参数错误");
        }
        if (getById(dto.getId()) == null) {
            throw new ServiceException("卡片不存在或已删除");
        }
        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("部分卡片不存在或已删除");
        }
        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.setDelFlag(0);
            card.setCreateTime(now);
            card.setCreateBy(userId);
            save(card);
        }
        return "成功导入" + cards.size() + "条卡片数据";
    }
    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 (!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 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 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);
    }
}
ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
@@ -25,4 +25,59 @@
        ORDER BY create_time DESC, id DESC
    </select>
    <select id="selectExportList" resultType="com.ruoyi.account.api.dto.ChargingCardExcelDTO">
        SELECT card_no,
               balance,
               status
        FROM t_charging_card
        WHERE del_flag = 0
        <if test="query.cardNo != null and query.cardNo != ''">
            AND card_no LIKE CONCAT('%', #{query.cardNo}, '%')
        </if>
        <if test="query.status != null">
            AND status = #{query.status}
        </if>
        ORDER BY create_time DESC, id DESC
    </select>
    <select id="countByCardNo" resultType="int">
        SELECT COUNT(1)
        FROM t_charging_card
        WHERE card_no = #{cardNo}
        <if test="excludeId != null">
            AND id != #{excludeId}
        </if>
    </select>
    <update id="adjustBalance">
        UPDATE t_charging_card
        SET balance = balance
            <choose>
                <when test="type == 1">+ #{amount}</when>
                <otherwise>- #{amount}</otherwise>
            </choose>,
            update_time = NOW(),
            update_by = #{updateBy}
        WHERE del_flag = 0
          AND id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
        <choose>
            <when test="type == 1">
                AND balance &lt;= #{maxAmount} - #{amount}
            </when>
            <otherwise>
                AND balance &gt;= #{amount}
            </otherwise>
        </choose>
    </update>
    <insert id="insertBalanceRecord">
        INSERT INTO t_charging_card_balance_record
            (card_id, change_type, amount, remark, create_time, create_by)
        VALUES
            (#{cardId}, #{type}, #{amount}, #{remark}, NOW(), #{createBy})
    </insert>
</mapper>
sql/20260827_charging_card_operations.sql
New file
@@ -0,0 +1,54 @@
-- 卡片管理操作功能增量脚本
-- 先在 charging_pile_account 逻辑库执行余额变更记录表,再在 charging_pile_other 逻辑库执行菜单权限。
USE charging_pile_account;
CREATE TABLE IF NOT EXISTS `t_charging_card_balance_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `card_id` bigint NOT NULL COMMENT '卡片ID',
  `change_type` tinyint NOT NULL COMMENT '变更类型:1增加,2减少',
  `amount` decimal(12,2) NOT NULL COMMENT '变更金额',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
  `create_by` bigint DEFAULT NULL COMMENT '操作人ID',
  PRIMARY KEY (`id`),
  KEY `idx_card_id` (`card_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='充电卡余额变更记录';
USE charging_pile_other;
-- 卡片管理操作按钮权限;父菜单按现有卡片管理菜单定位。
INSERT INTO `sys_menu`
(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
SELECT permissions.menu_name, parent.menu_id, permissions.order_num, '#', '', '', 1, 0, 'F', '0', '0', permissions.perms, '#', 'admin', NOW(), '', NULL, ''
FROM `sys_menu` parent
JOIN (
    SELECT '卡片新增' AS menu_name, 1 AS order_num, '/chargingCard/add' AS perms
    UNION ALL SELECT '卡片编辑', 2, '/chargingCard/edit'
    UNION ALL SELECT '卡片删除', 3, '/chargingCard/delete'
    UNION ALL SELECT '卡片启停', 4, '/chargingCard/status'
    UNION ALL SELECT '余额更新', 5, '/chargingCard/balance'
    UNION ALL SELECT '卡片导入', 6, '/chargingCard/import'
    UNION ALL SELECT '卡片导出', 7, '/chargingCard/export'
) permissions
WHERE parent.perms = '/chargingCard/list'
  AND NOT EXISTS (
      SELECT 1 FROM `sys_menu` existing WHERE existing.perms = permissions.perms
  );
-- 默认仅授予管理员角色;其他角色由后台按职责单独授权。
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, child.menu_id
FROM `sys_menu` child
WHERE child.parent_id = (
    SELECT parent.menu_id FROM `sys_menu` parent WHERE parent.perms = '/chargingCard/list' LIMIT 1
  )
  AND child.perms IN (
      '/chargingCard/add', '/chargingCard/edit', '/chargingCard/delete', '/chargingCard/status',
      '/chargingCard/balance', '/chargingCard/import', '/chargingCard/export'
  )
  AND NOT EXISTS (
      SELECT 1 FROM `sys_role_menu` existing
      WHERE existing.role_id = 1 AND existing.menu_id = child.menu_id
  );