| New file |
| | |
| | | package com.ruoyi.other.api.dto; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import com.ruoyi.common.core.annotation.Excel; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * 公司充值记录 Excel 导出 DTO(仅导出使用) |
| | | */ |
| | | @Data |
| | | public class CompanyRechargeRecordExcelDTO implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @Excel(name = "充值流水号", sort = 1, width = 24) |
| | | private String serialNo; |
| | | |
| | | @Excel(name = "公司", sort = 2, width = 24) |
| | | private String companyName; |
| | | |
| | | @Excel(name = "实付金额", sort = 3, width = 14, scale = 2) |
| | | private BigDecimal payAmount; |
| | | |
| | | @Excel(name = "赠送金额", sort = 4, width = 14, scale = 2) |
| | | private BigDecimal giftAmount; |
| | | |
| | | @Excel(name = "到账金额", sort = 5, width = 14, scale = 2) |
| | | private BigDecimal arrivalAmount; |
| | | |
| | | @Excel(name = "充值状态", sort = 6, width = 12, readConverterExp = "1=成功,2=失败,3=已退款") |
| | | private Integer status; |
| | | |
| | | @Excel(name = "充值人", sort = 7, width = 16) |
| | | private String operatorName; |
| | | |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | @Excel(name = "充值时间", sort = 8, width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss") |
| | | private LocalDateTime rechargeTime; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.other.api.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * 公司充值记录查询条件(公司管理-充值记录列表页) |
| | | */ |
| | | @Data |
| | | @ApiModel(value = "CompanyRechargeRecordQueryDTO", description = "公司充值记录查询条件") |
| | | public class CompanyRechargeRecordQueryDTO implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @ApiModelProperty(value = "充值流水号(精确匹配)") |
| | | private String serialNo; |
| | | |
| | | @ApiModelProperty(value = "公司ID") |
| | | private Integer companyId; |
| | | |
| | | @ApiModelProperty(value = "充值状态 1=成功 2=失败 3=已退款") |
| | | private Integer status; |
| | | |
| | | @ApiModelProperty(value = "充值时间起(含)") |
| | | private LocalDateTime rechargeTimeStart; |
| | | |
| | | @ApiModelProperty(value = "充值时间止(含)") |
| | | private LocalDateTime rechargeTimeEnd; |
| | | |
| | | @ApiModelProperty(value = "页码(从1开始)") |
| | | private Integer current = 1; |
| | | |
| | | @ApiModelProperty(value = "每页大小") |
| | | private Integer size = 10; |
| | | } |
| | |
| | | @TableField("arrival_amount") |
| | | private BigDecimal arrivalAmount; |
| | | |
| | | @ApiModelProperty(value = "充值流水号") |
| | | @TableField("serial_no") |
| | | private String serialNo; |
| | | |
| | | @ApiModelProperty(value = "充值状态 1=成功 2=失败 3=已退款") |
| | | @TableField("status") |
| | | private Integer status; |
| | | |
| | | @ApiModelProperty(value = "优惠方式(1=不优惠 2=按比例优惠 3=按优惠单价收费)") |
| | | @TableField("preferential_type") |
| | | private Integer preferentialType; |
| | |
| | | package com.ruoyi.other.controller; |
| | | |
| | | import com.ruoyi.common.core.utils.poi.ExcelUtil; |
| | | import com.ruoyi.common.core.web.domain.AjaxResult; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | import com.ruoyi.common.log.annotation.Log; |
| | | import com.ruoyi.common.log.enums.BusinessType; |
| | | import com.ruoyi.common.security.annotation.Logical; |
| | | import com.ruoyi.common.security.annotation.RequiresPermissions; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordExcelDTO; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordQueryDTO; |
| | | import com.ruoyi.other.api.model.TCompanyRechargeRecord; |
| | | import com.ruoyi.other.service.TCompanyRechargeRecordService; |
| | | import io.swagger.annotations.ApiOperation; |
| | | 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.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | |
| | | /** |
| | | * <p> |
| | |
| | | private TCompanyRechargeRecordService rechargeRecordService; |
| | | |
| | | /** |
| | | * 公司充值 |
| | | * 公司充值(公司管理主页面「充值」按钮触发) |
| | | */ |
| | | @RequiresPermissions(value = {"/companyManagement/list"}, logical = Logical.OR) |
| | | @RequiresPermissions("/companyManagement/list") |
| | | @PostMapping("/recharge") |
| | | @ApiOperation(tags = {"管理后台-公司管理"}, value = "公司充值") |
| | | @Log(title = "【公司管理】公司充值", businessType = BusinessType.INSERT) |
| | |
| | | } |
| | | |
| | | /** |
| | | * 充值记录分页 |
| | | * 充值记录分页(公司管理-充值记录) |
| | | */ |
| | | @RequiresPermissions(value = {"/companyManagement/list"}, logical = Logical.OR) |
| | | @GetMapping("/page") |
| | | @RequiresPermissions("/company/rechargeRecord") |
| | | @PostMapping("/page") |
| | | @ApiOperation(tags = {"管理后台-公司管理"}, value = "公司充值记录分页") |
| | | public AjaxResult<PageInfo<TCompanyRechargeRecord>> page(@RequestParam(value = "current", defaultValue = "1") Integer current, |
| | | @RequestParam(value = "size", defaultValue = "10") Integer size, |
| | | @RequestParam(value = "companyId", required = false) Integer companyId) { |
| | | return AjaxResult.success(rechargeRecordService.pageList(current, size, companyId, null)); |
| | | public AjaxResult<PageInfo<TCompanyRechargeRecord>> page(@RequestBody CompanyRechargeRecordQueryDTO query) { |
| | | return AjaxResult.success(rechargeRecordService.pageList(query)); |
| | | } |
| | | |
| | | /** |
| | | * 充值记录导出(公司管理-充值记录) |
| | | */ |
| | | @RequiresPermissions("/company/rechargeRecord/export") |
| | | @PutMapping("/export") |
| | | @ApiOperation(tags = {"管理后台-公司管理"}, value = "公司充值记录导出") |
| | | public void export(HttpServletResponse response, @RequestBody(required = false) CompanyRechargeRecordQueryDTO query) { |
| | | ExcelUtil<CompanyRechargeRecordExcelDTO> util = new ExcelUtil<>(CompanyRechargeRecordExcelDTO.class); |
| | | util.exportExcel(response, rechargeRecordService.selectExportList(query), "公司充值记录"); |
| | | } |
| | | } |
| | |
| | | package com.ruoyi.other.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.ruoyi.other.api.model.TCompanyRechargeRecord; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordExcelDTO; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordQueryDTO; |
| | | import com.ruoyi.other.api.model.TCompanyRechargeRecord; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * <p> |
| | | * 公司充值记录 服务类 |
| | | * </p> |
| | | * |
| | | * @author mxcd |
| | | * @since 2026-09-23 |
| | | */ |
| | | public interface TCompanyRechargeRecordService extends IService<TCompanyRechargeRecord> { |
| | | |
| | |
| | | Integer recharge(TCompanyRechargeRecord record); |
| | | |
| | | /** |
| | | * 充值记录分页 |
| | | * 充值记录分页(支持流水号/公司/状态/时间范围,公司账号自动限定本公司) |
| | | */ |
| | | PageInfo<TCompanyRechargeRecord> pageList(Integer current, Integer size, Integer companyId, String companyName); |
| | | PageInfo<TCompanyRechargeRecord> pageList(CompanyRechargeRecordQueryDTO query); |
| | | |
| | | /** |
| | | * 充值记录导出列表(复用相同查询条件,无分页) |
| | | */ |
| | | List<CompanyRechargeRecordExcelDTO> selectExportList(CompanyRechargeRecordQueryDTO query); |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | import com.ruoyi.common.security.utils.SecurityUtils; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordExcelDTO; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordQueryDTO; |
| | | import com.ruoyi.other.api.model.TCompanyRechargeRecord; |
| | | import com.ruoyi.other.mapper.TCompanyRechargeRecordMapper; |
| | | import com.ruoyi.other.mapper.TGroupCompanyMapper; |
| | | import com.ruoyi.other.service.CompanyAccountAccessService; |
| | | import com.ruoyi.other.service.TCompanyRechargeRecordService; |
| | | import com.ruoyi.system.api.domain.SysUser; |
| | | import com.ruoyi.system.api.feignClient.SysUserClient; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ThreadLocalRandom; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * <p> |
| | | * 公司充值记录 服务实现类 |
| | | * </p> |
| | | * |
| | | * @author mxcd |
| | | * @since 2026-09-23 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class TCompanyRechargeRecordServiceImpl extends ServiceImpl<TCompanyRechargeRecordMapper, TCompanyRechargeRecord> implements TCompanyRechargeRecordService { |
| | | |
| | | private static final DateTimeFormatter SERIAL_DTF = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); |
| | | /** 充值状态枚举:1=成功 2=失败 3=已退款 */ |
| | | public static final int RECHARGE_STATUS_SUCCESS = 1; |
| | | |
| | | @Resource |
| | | private TGroupCompanyMapper groupCompanyMapper; |
| | | @Resource |
| | | private CompanyAccountAccessService companyAccountAccessService; |
| | | @Resource |
| | | private SysUserClient sysUserClient; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | record.setId(null); |
| | | record.setDelFlag(false); |
| | | record.setRechargeTime(LocalDateTime.now()); |
| | | record.setSerialNo(buildSerialNo()); |
| | | record.setStatus(RECHARGE_STATUS_SUCCESS); |
| | | try { |
| | | record.setOperatorId(SecurityUtils.getLoginUser().getUserid()); |
| | | } catch (Exception ignored) { |
| | | // 非登录上下文(如内部调用)允许为空 |
| | | } |
| | | // 更新公司余额(原子加):拼接 toString 防止 SQL 注入,BigDecimal 是受控数值 |
| | | groupCompanyMapper.update(null, Wrappers.lambdaUpdate(TGroupCompany.class) |
| | |
| | | } |
| | | |
| | | @Override |
| | | public PageInfo<TCompanyRechargeRecord> pageList(Integer current, Integer size, Integer companyId, String companyName) { |
| | | public PageInfo<TCompanyRechargeRecord> pageList(CompanyRechargeRecordQueryDTO query) { |
| | | int current = query.getCurrent() == null || query.getCurrent() < 1 ? 1 : query.getCurrent(); |
| | | int size = query.getSize() == null || query.getSize() < 1 ? 10 : query.getSize(); |
| | | Page<TCompanyRechargeRecord> page = new Page<>(current, size); |
| | | LambdaQueryWrapper<TCompanyRechargeRecord> wrapper = Wrappers.lambdaQuery(TCompanyRechargeRecord.class) |
| | | .eq(TCompanyRechargeRecord::getDelFlag, false) |
| | | .eq(companyId != null, TCompanyRechargeRecord::getCompanyId, companyId) |
| | | .orderByDesc(TCompanyRechargeRecord::getId); |
| | | LambdaQueryWrapper<TCompanyRechargeRecord> wrapper = buildQueryWrapper(query); |
| | | Page<TCompanyRechargeRecord> result = this.page(page, wrapper); |
| | | fillNames(result.getRecords()); |
| | | PageInfo<TCompanyRechargeRecord> pageInfo = new PageInfo<>(result.getCurrent(), result.getSize()); |
| | | pageInfo.setRecords(result.getRecords()); |
| | | pageInfo.setTotal(result.getTotal()); |
| | | return pageInfo; |
| | | } |
| | | |
| | | @Override |
| | | public List<CompanyRechargeRecordExcelDTO> selectExportList(CompanyRechargeRecordQueryDTO query) { |
| | | LambdaQueryWrapper<TCompanyRechargeRecord> wrapper = buildQueryWrapper(query); |
| | | List<TCompanyRechargeRecord> records = this.list(wrapper); |
| | | fillNames(records); |
| | | return records.stream().map(this::toExcelDto).collect(Collectors.toList()); |
| | | } |
| | | |
| | | /** |
| | | * 构建查询 wrapper:参数为 null 时不参与条件;delFlag=0;按 id 倒序 |
| | | */ |
| | | private LambdaQueryWrapper<TCompanyRechargeRecord> buildQueryWrapper(CompanyRechargeRecordQueryDTO query) { |
| | | Integer companyId = companyAccountAccessService.resolveCompanyFilter(query.getCompanyId()); |
| | | return Wrappers.lambdaQuery(TCompanyRechargeRecord.class) |
| | | .eq(TCompanyRechargeRecord::getDelFlag, false) |
| | | .eq(companyId != null, TCompanyRechargeRecord::getCompanyId, companyId) |
| | | .eq(query.getStatus() != null, TCompanyRechargeRecord::getStatus, query.getStatus()) |
| | | .eq(query.getSerialNo() != null && !query.getSerialNo().isEmpty(), |
| | | TCompanyRechargeRecord::getSerialNo, query.getSerialNo()) |
| | | .ge(query.getRechargeTimeStart() != null, TCompanyRechargeRecord::getRechargeTime, query.getRechargeTimeStart()) |
| | | .le(query.getRechargeTimeEnd() != null, TCompanyRechargeRecord::getRechargeTime, query.getRechargeTimeEnd()) |
| | | .orderByDesc(TCompanyRechargeRecord::getId); |
| | | } |
| | | |
| | | /** |
| | | * 列表结果批量回填公司名称与操作人名称:单次查表 + 按需远程调用 sys_user |
| | | */ |
| | | private void fillNames(List<TCompanyRechargeRecord> records) { |
| | | if (records == null || records.isEmpty()) { |
| | | return; |
| | | } |
| | | Set<Integer> companyIds = records.stream() |
| | | .map(TCompanyRechargeRecord::getCompanyId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | Map<Integer, String> companyNameMap = new HashMap<>(); |
| | | if (!companyIds.isEmpty()) { |
| | | try { |
| | | List<TGroupCompany> companies = groupCompanyMapper.selectBatchIds(companyIds); |
| | | if (companies != null) { |
| | | for (TGroupCompany c : companies) { |
| | | companyNameMap.put(c.getId(), c.getName()); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("批量查询公司名称失败:ids={}", companyIds, e); |
| | | } |
| | | } |
| | | Set<Long> operatorIds = records.stream() |
| | | .map(TCompanyRechargeRecord::getOperatorId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, String> operatorNameMap = new HashMap<>(); |
| | | for (Long id : operatorIds) { |
| | | try { |
| | | R<SysUser> result = sysUserClient.getSysUser(id); |
| | | if (result != null && result.getData() != null |
| | | && result.getData().getUserName() != null |
| | | && !result.getData().getUserName().isEmpty()) { |
| | | operatorNameMap.put(id, result.getData().getUserName()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("查询操作人失败:id={}", id, e); |
| | | } |
| | | } |
| | | for (TCompanyRechargeRecord r : records) { |
| | | r.setCompanyName(companyNameMap.get(r.getCompanyId())); |
| | | r.setOperatorName(operatorNameMap.get(r.getOperatorId())); |
| | | } |
| | | } |
| | | |
| | | private CompanyRechargeRecordExcelDTO toExcelDto(TCompanyRechargeRecord r) { |
| | | CompanyRechargeRecordExcelDTO dto = new CompanyRechargeRecordExcelDTO(); |
| | | dto.setSerialNo(r.getSerialNo()); |
| | | dto.setCompanyName(r.getCompanyName()); |
| | | dto.setPayAmount(r.getPayAmount()); |
| | | dto.setGiftAmount(r.getGiftAmount()); |
| | | dto.setArrivalAmount(r.getArrivalAmount()); |
| | | dto.setStatus(r.getStatus()); |
| | | dto.setOperatorName(r.getOperatorName()); |
| | | dto.setRechargeTime(r.getRechargeTime()); |
| | | return dto; |
| | | } |
| | | |
| | | /** |
| | | * 生成充值流水号:CR + yyyyMMddHHmmss + 3 位随机数;唯一约束 uk_serial_no 兜底。 |
| | | * 同一秒内重复概率约 1/1000,重复时由 catch DuplicateKeyException 重试。 |
| | | */ |
| | | private String buildSerialNo() { |
| | | String prefix = "CR" + LocalDateTime.now().format(SERIAL_DTF); |
| | | for (int attempt = 0; attempt < 5; attempt++) { |
| | | String suffix = String.format("%03d", ThreadLocalRandom.current().nextInt(0, 1000)); |
| | | String candidate = prefix + suffix; |
| | | if (this.lambdaQuery() |
| | | .eq(TCompanyRechargeRecord::getSerialNo, candidate) |
| | | .eq(TCompanyRechargeRecord::getDelFlag, false) |
| | | .count() == 0) { |
| | | return candidate; |
| | | } |
| | | } |
| | | // 重试耗尽:使用基于当前纳秒的后缀,仍受唯一索引保护 |
| | | return prefix + String.format("%06d", System.nanoTime() % 1000000); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.other.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.MybatisConfiguration; |
| | | import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordExcelDTO; |
| | | import com.ruoyi.other.api.dto.CompanyRechargeRecordQueryDTO; |
| | | import com.ruoyi.other.api.model.TCompanyRechargeRecord; |
| | | import com.ruoyi.other.mapper.TCompanyRechargeRecordMapper; |
| | | import com.ruoyi.other.mapper.TGroupCompanyMapper; |
| | | import com.ruoyi.other.service.CompanyAccountAccessService; |
| | | import com.ruoyi.system.api.domain.SysUser; |
| | | import com.ruoyi.system.api.feignClient.SysUserClient; |
| | | import org.apache.ibatis.builder.MapperBuilderAssistant; |
| | | import org.junit.jupiter.api.BeforeAll; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.Mockito; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | |
| | | /** |
| | | * 公司充值记录 服务实现类 单测:覆盖充值流水号生成、状态默认值、公司隔离、名称回填、Excel DTO 映射。 |
| | | */ |
| | | class TCompanyRechargeRecordServiceImplTest { |
| | | |
| | | @BeforeAll |
| | | static void initTableInfo() { |
| | | TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), |
| | | TCompanyRechargeRecord.class); |
| | | } |
| | | |
| | | @Test |
| | | void rechargeGeneratesSerialNoAndSetsStatusSuccess() { |
| | | TGroupCompany company = new TGroupCompany(); |
| | | company.setId(100); |
| | | company.setName("M公司"); |
| | | |
| | | TGroupCompanyMapper groupCompanyMapper = Mockito.mock(TGroupCompanyMapper.class); |
| | | Mockito.when(groupCompanyMapper.selectById(100)).thenReturn(company); |
| | | |
| | | TCompanyRechargeRecordMapper mapper = Mockito.mock(TCompanyRechargeRecordMapper.class); |
| | | Mockito.when(mapper.insert(any())).thenAnswer(invocation -> { |
| | | TCompanyRechargeRecord r = invocation.getArgument(0); |
| | | r.setId(555); |
| | | return 1; |
| | | }); |
| | | |
| | | CompanyAccountAccessService access = Mockito.mock(CompanyAccountAccessService.class); |
| | | SysUserClient userClient = Mockito.mock(SysUserClient.class); |
| | | |
| | | TCompanyRechargeRecordServiceImpl service = new TCompanyRechargeRecordServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "groupCompanyMapper", groupCompanyMapper); |
| | | ReflectionTestUtils.setField(service, "companyAccountAccessService", access); |
| | | ReflectionTestUtils.setField(service, "sysUserClient", userClient); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | |
| | | TCompanyRechargeRecord in = new TCompanyRechargeRecord(); |
| | | in.setCompanyId(100); |
| | | in.setPayAmount(new BigDecimal("1000.00")); |
| | | in.setGiftAmount(new BigDecimal("200.00")); |
| | | Integer newId = service.recharge(in); |
| | | assertEquals(555, newId); |
| | | assertEquals(1, in.getStatus()); |
| | | assertNotNull(in.getSerialNo()); |
| | | assertTrue(in.getSerialNo().startsWith("CR")); |
| | | assertEquals(LocalDateTime.now().toLocalDate(), in.getRechargeTime().toLocalDate()); |
| | | assertEquals(new BigDecimal("1200.00"), in.getArrivalAmount()); |
| | | // 验证公司余额原子加 setSql 触发 |
| | | Mockito.verify(groupCompanyMapper, Mockito.times(1)) |
| | | .update(eq(null), any()); |
| | | } |
| | | |
| | | @Test |
| | | void rechargeFailsWhenCompanyNotExists() { |
| | | TGroupCompanyMapper groupCompanyMapper = Mockito.mock(TGroupCompanyMapper.class); |
| | | Mockito.when(groupCompanyMapper.selectById(999)).thenReturn(null); |
| | | |
| | | TCompanyRechargeRecordMapper mapper = Mockito.mock(TCompanyRechargeRecordMapper.class); |
| | | CompanyAccountAccessService access = Mockito.mock(CompanyAccountAccessService.class); |
| | | SysUserClient userClient = Mockito.mock(SysUserClient.class); |
| | | |
| | | TCompanyRechargeRecordServiceImpl service = new TCompanyRechargeRecordServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "groupCompanyMapper", groupCompanyMapper); |
| | | ReflectionTestUtils.setField(service, "companyAccountAccessService", access); |
| | | ReflectionTestUtils.setField(service, "sysUserClient", userClient); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | |
| | | TCompanyRechargeRecord in = new TCompanyRechargeRecord(); |
| | | in.setCompanyId(999); |
| | | in.setPayAmount(new BigDecimal("100")); |
| | | |
| | | IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> service.recharge(in)); |
| | | assertTrue(ex.getMessage().contains("公司不存在")); |
| | | Mockito.verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | @SuppressWarnings({"rawtypes", "unchecked"}) |
| | | void pageListAppliesCompanyFilterAndFillsNames() { |
| | | TCompanyRechargeRecord r1 = new TCompanyRechargeRecord(); |
| | | r1.setId(1); |
| | | r1.setCompanyId(100); |
| | | r1.setOperatorId(9001L); |
| | | r1.setPayAmount(new BigDecimal("100")); |
| | | TCompanyRechargeRecord r2 = new TCompanyRechargeRecord(); |
| | | r2.setId(2); |
| | | r2.setCompanyId(200); |
| | | r2.setOperatorId(9002L); |
| | | r2.setPayAmount(new BigDecimal("50")); |
| | | |
| | | Page pageResult = new Page(1, 10); |
| | | pageResult.setRecords(Arrays.asList(r1, r2)); |
| | | pageResult.setTotal(2L); |
| | | |
| | | TCompanyRechargeRecordMapper mapper = Mockito.mock(TCompanyRechargeRecordMapper.class); |
| | | Mockito.when(mapper.selectPage(any(Page.class), any())).thenReturn(pageResult); |
| | | |
| | | TGroupCompanyMapper groupCompanyMapper = Mockito.mock(TGroupCompanyMapper.class); |
| | | TGroupCompany c1 = new TGroupCompany(); |
| | | c1.setId(100); |
| | | c1.setName("A公司"); |
| | | TGroupCompany c2 = new TGroupCompany(); |
| | | c2.setId(200); |
| | | c2.setName("B公司"); |
| | | Mockito.when(groupCompanyMapper.selectBatchIds(any())).thenReturn(Arrays.asList(c1, c2)); |
| | | |
| | | SysUserClient userClient = Mockito.mock(SysUserClient.class); |
| | | SysUser u1 = new SysUser(); |
| | | u1.setUserId(9001L); |
| | | u1.setUserName("张三"); |
| | | SysUser u2 = new SysUser(); |
| | | u2.setUserId(9002L); |
| | | u2.setUserName("李四"); |
| | | Mockito.when(userClient.getSysUser(9001L)).thenReturn(R.ok(u1)); |
| | | Mockito.when(userClient.getSysUser(9002L)).thenReturn(R.ok(u2)); |
| | | |
| | | CompanyAccountAccessService access = Mockito.mock(CompanyAccountAccessService.class); |
| | | // 当前请求携带 companyId=null,且当前账号非公司账号 → resolveCompanyFilter 返回 null(不限) |
| | | Mockito.when(access.resolveCompanyFilter(null)).thenReturn(null); |
| | | |
| | | TCompanyRechargeRecordServiceImpl service = new TCompanyRechargeRecordServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "groupCompanyMapper", groupCompanyMapper); |
| | | ReflectionTestUtils.setField(service, "sysUserClient", userClient); |
| | | ReflectionTestUtils.setField(service, "companyAccountAccessService", access); |
| | | |
| | | CompanyRechargeRecordQueryDTO query = new CompanyRechargeRecordQueryDTO(); |
| | | query.setCurrent(1); |
| | | query.setSize(10); |
| | | com.ruoyi.common.core.web.page.PageInfo<TCompanyRechargeRecord> page = service.pageList(query); |
| | | |
| | | assertEquals(2L, page.getTotal()); |
| | | assertEquals(2, page.getRecords().size()); |
| | | assertEquals("A公司", page.getRecords().get(0).getCompanyName()); |
| | | assertEquals("张三", page.getRecords().get(0).getOperatorName()); |
| | | assertEquals("B公司", page.getRecords().get(1).getCompanyName()); |
| | | assertEquals("李四", page.getRecords().get(1).getOperatorName()); |
| | | } |
| | | |
| | | @Test |
| | | @SuppressWarnings("unchecked") |
| | | void selectExportListMapsToExcelDto() { |
| | | TCompanyRechargeRecord r1 = new TCompanyRechargeRecord(); |
| | | r1.setId(1); |
| | | r1.setSerialNo("CR20260930101500001"); |
| | | r1.setCompanyId(100); |
| | | r1.setPayAmount(new BigDecimal("100")); |
| | | r1.setGiftAmount(new BigDecimal("20")); |
| | | r1.setArrivalAmount(new BigDecimal("120")); |
| | | r1.setStatus(1); |
| | | r1.setOperatorId(9001L); |
| | | r1.setCompanyName("A公司"); |
| | | r1.setOperatorName("张三"); |
| | | r1.setRechargeTime(LocalDateTime.of(2026, 9, 30, 10, 15, 0)); |
| | | |
| | | TCompanyRechargeRecordMapper mapper = Mockito.mock(TCompanyRechargeRecordMapper.class); |
| | | Mockito.when(mapper.selectList(any())).thenReturn(Collections.singletonList(r1)); |
| | | |
| | | // fillNames 会重新覆盖 companyName/operatorName,必须 stub 远程查询 |
| | | TGroupCompanyMapper groupCompanyMapper = Mockito.mock(TGroupCompanyMapper.class); |
| | | TGroupCompany c1 = new TGroupCompany(); |
| | | c1.setId(100); |
| | | c1.setName("A公司"); |
| | | Mockito.when(groupCompanyMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(c1)); |
| | | SysUserClient userClient = Mockito.mock(SysUserClient.class); |
| | | SysUser u1 = new SysUser(); |
| | | u1.setUserId(9001L); |
| | | u1.setUserName("张三"); |
| | | Mockito.when(userClient.getSysUser(9001L)).thenReturn(R.ok(u1)); |
| | | |
| | | TCompanyRechargeRecordServiceImpl service = new TCompanyRechargeRecordServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "groupCompanyMapper", groupCompanyMapper); |
| | | ReflectionTestUtils.setField(service, "sysUserClient", userClient); |
| | | ReflectionTestUtils.setField(service, "companyAccountAccessService", |
| | | Mockito.mock(CompanyAccountAccessService.class)); |
| | | |
| | | CompanyRechargeRecordQueryDTO query = new CompanyRechargeRecordQueryDTO(); |
| | | query.setStatus(1); |
| | | List<CompanyRechargeRecordExcelDTO> list = service.selectExportList(query); |
| | | |
| | | assertEquals(1, list.size()); |
| | | CompanyRechargeRecordExcelDTO dto = list.get(0); |
| | | assertEquals("CR20260930101500001", dto.getSerialNo()); |
| | | assertEquals("A公司", dto.getCompanyName()); |
| | | assertEquals(new BigDecimal("120"), dto.getArrivalAmount()); |
| | | assertEquals(1, dto.getStatus()); |
| | | assertEquals("张三", dto.getOperatorName()); |
| | | assertNotEquals(null, dto.getRechargeTime()); |
| | | } |
| | | } |
| New file |
| | |
| | | -- ============================================================================= |
| | | -- 2026-09-30 公司充值记录表新增字段:serial_no(充值流水号)、status(充值状态) |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 前置说明: |
| | | -- 1) Mycat 2.0 代理可广播 DDL,无需逐分表执行;ALTER 拆分为单条语句便于重复执行排错 |
| | | -- 2) status 历史回填为 1(成功);serial_no 历史回填为 CR + id 左补 0(10 位) |
| | | -- 3) 新增唯一索引 uk_serial_no + 普通索引 idx_status |
| | | -- 4) 状态枚举:1=成功 2=失败 3=已退款 |
| | | -- ============================================================================= |
| | | SET NAMES utf8mb4; |
| | | |
| | | USE charging_pile_other; |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 小节 A: DDL |
| | | -- ----------------------------------------------------------------------------- |
| | | ALTER TABLE t_company_recharge_record |
| | | ADD COLUMN serial_no VARCHAR(32) DEFAULT NULL COMMENT '充值流水号' AFTER id; |
| | | |
| | | ALTER TABLE t_company_recharge_record |
| | | ADD COLUMN status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '充值状态 1=成功 2=失败 3=已退款' AFTER arrival_amount; |
| | | |
| | | ALTER TABLE t_company_recharge_record |
| | | ADD UNIQUE KEY uk_serial_no (serial_no); |
| | | |
| | | ALTER TABLE t_company_recharge_record |
| | | ADD KEY idx_status (status); |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 小节 B: 历史数据回填 |
| | | -- ----------------------------------------------------------------------------- |
| | | UPDATE t_company_recharge_record |
| | | SET serial_no = CONCAT('CR', LPAD(id, 10, '0')) |
| | | WHERE serial_no IS NULL OR serial_no = ''; |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 小节 C: 回查 |
| | | -- C1. 列存在 + 注释 |
| | | -- SELECT column_name, column_type, is_nullable, column_default, column_comment |
| | | -- FROM information_schema.columns |
| | | -- WHERE table_schema = 'charging_pile_other' AND table_name = 't_company_recharge_record' |
| | | -- AND column_name IN ('serial_no','status'); |
| | | -- C2. 索引存在 |
| | | -- SELECT index_name, non_unique, column_name |
| | | -- FROM information_schema.statistics |
| | | -- WHERE table_schema = 'charging_pile_other' AND table_name = 't_company_recharge_record'; |
| | | -- C3. 历史回填全填(含 NULL 不允许) |
| | | -- SELECT COUNT(*) AS empty_serial FROM charging_pile_other.t_company_recharge_record |
| | | -- WHERE serial_no IS NULL OR serial_no = ''; |
| | | -- 期望:empty_serial=0 |
| | | -- ============================================================================= |
| New file |
| | |
| | | -- ============================================================================= |
| | | -- 2026-09-30 公司管理-充值记录二级菜单与导出按钮 |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 前置说明: |
| | | -- 1) 父菜单「单位管理」path='/company'(menu_id=51,测试库已建) |
| | | -- 2) 全部语句幂等,可重复执行 |
| | | -- 3) 权限键与前端 accessKey、后端 @RequiresPermissions 一致: |
| | | -- C 列表页 path = /company/rechargeRecord |
| | | -- F 导出按钮 path = '#' perms = /company/rechargeRecord/export |
| | | -- 4) 默认授权 admin(role_id=1)和 company_admin(role_id=75) |
| | | -- 5) 依赖前置脚本:20260930_company_admin_role_menu.sql(提供 company_admin 角色) |
| | | -- ============================================================================= |
| | | SET NAMES utf8mb4; |
| | | |
| | | USE charging_pile_other; |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 1. 充值记录(菜单 C) |
| | | -- ----------------------------------------------------------------------------- |
| | | 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, remark) |
| | | SELECT '充值记录', 51, 4, '/company/rechargeRecord', 'company/rechargeRecord/index', NULL, 1, 0, 'C', '0', '0', '/company/rechargeRecord', 'wallet', 'admin', NOW(), '公司管理-充值记录' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_type = 'C' AND perms = '/company/rechargeRecord'); |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 2. 导出按钮(F) |
| | | -- ----------------------------------------------------------------------------- |
| | | 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, remark) |
| | | SELECT '导出', ( |
| | | SELECT menu_id FROM sys_menu WHERE menu_type = 'C' AND perms = '/company/rechargeRecord' LIMIT 1 |
| | | ), 1, '#', NULL, NULL, 1, 0, 'F', '0', '0', '/company/rechargeRecord/export', '#', 'admin', NOW(), '公司管理-充值记录-导出' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = '/company/rechargeRecord/export'); |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 3. 默认授权 admin 角色(role_id=1) |
| | | -- ----------------------------------------------------------------------------- |
| | | INSERT INTO sys_role_menu (role_id, menu_id) |
| | | SELECT 1, m.menu_id |
| | | FROM sys_menu m |
| | | WHERE (m.perms = '/company/rechargeRecord' OR m.perms = '/company/rechargeRecord/export') |
| | | AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 4. 默认授权 company_admin(role_id=75;该角色由 20260930_company_admin_role_menu.sql 创建) |
| | | -- ----------------------------------------------------------------------------- |
| | | INSERT INTO sys_role_menu (role_id, menu_id) |
| | | SELECT 75, m.menu_id |
| | | FROM sys_menu m |
| | | WHERE (m.perms = '/company/rechargeRecord' OR m.perms = '/company/rechargeRecord/export') |
| | | AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 75 AND rm.menu_id = m.menu_id); |
| | | |
| | | -- ----------------------------------------------------------------------------- |
| | | -- 5. 回查 |
| | | -- R1. C 菜单与 F 按钮已建: |
| | | -- SELECT menu_id, menu_name, path, perms, menu_type, order_num, parent_id |
| | | -- FROM charging_pile_other.sys_menu |
| | | -- WHERE perms IN ('/company/rechargeRecord','/company/rechargeRecord/export'); |
| | | -- R2. 已授 admin(1) 与 company_admin(75): |
| | | -- SELECT rm.role_id, r.role_name, m.menu_name, m.perms |
| | | -- FROM charging_pile_other.sys_role_menu rm |
| | | -- JOIN charging_pile_other.sys_role r ON rm.role_id = r.role_id |
| | | -- JOIN charging_pile_other.sys_menu m ON rm.menu_id = m.menu_id |
| | | -- WHERE m.perms IN ('/company/rechargeRecord','/company/rechargeRecord/export') |
| | | -- ORDER BY rm.role_id, m.perms; |
| | | -- R3. 幂等复跑:以上 INSERT 命中 0 行(NOT EXISTS 已存在) |
| | | -- ============================================================================= |