refactor(energy): 简化事件驱动系统(7个→3个)

- 删除旧事件:BillApprovedEvent, BillCreatedEvent, DeductionCompletedEvent, DetailAuditedEvent, DetailCreatedEvent, RecordMatchedEvent
- 新增事件:BillAuditPassedEvent, DetailAuditPassedEvent
- 保留事件:RecordImportedEvent
- 更新监听器:AccountEventListener, BillEventListener, DetailEventListener
- 清理代码中的旧事件引用和注释

优化原则:前端简单,后端健壮
事件流程:导入→匹配→生成明细→审核→扣款→生成账单→结算
This commit is contained in:
kkfluous
2026-03-16 12:53:14 +08:00
parent f5062cec22
commit 2f38a703f9
167 changed files with 9876 additions and 824 deletions

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.energy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 能源管理服务启动类
*
* @author 芋道源码
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"cn.iocoder.yudao.module.infra.api"})
public class EnergyServerApplication {
public static void main(String[] args) {
SpringApplication.run(EnergyServerApplication.class, args);
}
}

View File

@@ -1,72 +0,0 @@
package cn.iocoder.yudao.module.energy.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigPageReqVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigRespVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigSaveReqVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigSimpleVO;
import cn.iocoder.yudao.module.energy.convert.config.EnergyStationConfigConvert;
import cn.iocoder.yudao.module.energy.dal.dataobject.config.EnergyStationConfigDO;
import cn.iocoder.yudao.module.energy.service.config.EnergyStationConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 加氢站配置")
@RestController
@RequestMapping("/energy/station-config")
@Validated
public class EnergyStationConfigController {
@Resource
private EnergyStationConfigService stationConfigService;
@PostMapping("/create")
@Operation(summary = "创建站点配置")
@PreAuthorize("@ss.hasPermission('energy:station-config:create')")
public CommonResult<Long> createConfig(@Valid @RequestBody EnergyStationConfigSaveReqVO createReqVO) {
return success(stationConfigService.createConfig(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新站点配置")
@PreAuthorize("@ss.hasPermission('energy:station-config:update')")
public CommonResult<Boolean> updateConfig(@Valid @RequestBody EnergyStationConfigSaveReqVO updateReqVO) {
stationConfigService.updateConfig(updateReqVO);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得站点配置")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('energy:station-config:query')")
public CommonResult<EnergyStationConfigRespVO> getConfig(@RequestParam("id") Long id) {
EnergyStationConfigDO config = stationConfigService.getConfig(id);
return success(EnergyStationConfigConvert.INSTANCE.convert(config));
}
@GetMapping("/page")
@Operation(summary = "获得站点配置分页")
@PreAuthorize("@ss.hasPermission('energy:station-config:query')")
public CommonResult<PageResult<EnergyStationConfigRespVO>> getConfigPage(@Valid EnergyStationConfigPageReqVO pageReqVO) {
PageResult<EnergyStationConfigDO> pageResult = stationConfigService.getConfigPage(pageReqVO);
return success(EnergyStationConfigConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/simple-list")
@Operation(summary = "获得站点配置简单列表")
public CommonResult<List<EnergyStationConfigSimpleVO>> getSimpleList() {
List<EnergyStationConfigDO> list = stationConfigService.getConfigList();
return success(EnergyStationConfigConvert.INSTANCE.convertSimpleList(list));
}
}

View File

@@ -1,20 +0,0 @@
package cn.iocoder.yudao.module.energy.controller.admin.config.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 加氢站配置分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class EnergyStationConfigPageReqVO extends PageParam {
@Schema(description = "加氢站ID")
private Long stationId;
@Schema(description = "是否自动扣款")
private Boolean autoDeduct;
@Schema(description = "合作类型")
private Integer cooperationType;
}

View File

@@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.energy.controller.admin.config.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 加氢站配置 Response VO")
@Data
public class EnergyStationConfigRespVO {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "加氢站ID")
private Long stationId;
@Schema(description = "是否自动扣款")
private Boolean autoDeduct;
@Schema(description = "合作类型")
private Integer cooperationType;
@Schema(description = "站点名称")
private String stationName;
@Schema(description = "自动匹配开关")
private Boolean autoMatch;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -1,24 +0,0 @@
package cn.iocoder.yudao.module.energy.controller.admin.config.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 加氢站配置创建/更新 Request VO")
@Data
public class EnergyStationConfigSaveReqVO {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "加氢站ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "加氢站不能为空")
private Long stationId;
@Schema(description = "是否自动扣款", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "是否自动扣款不能为空")
private Boolean autoDeduct;
@Schema(description = "合作类型")
private Integer cooperationType;
@Schema(description = "自动匹配开关")
private Boolean autoMatch;
@Schema(description = "备注")
private String remark;
}

View File

@@ -1,15 +0,0 @@
package cn.iocoder.yudao.module.energy.controller.admin.config.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 加氢站配置简单 Response VO")
@Data
public class EnergyStationConfigSimpleVO {
@Schema(description = "配置ID")
private Long id;
@Schema(description = "站点ID")
private Long stationId;
@Schema(description = "站点名称")
private String stationName;
}

View File

@@ -1,20 +0,0 @@
package cn.iocoder.yudao.module.energy.convert.config;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigRespVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigSaveReqVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigSimpleVO;
import cn.iocoder.yudao.module.energy.dal.dataobject.config.EnergyStationConfigDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface EnergyStationConfigConvert {
EnergyStationConfigConvert INSTANCE = Mappers.getMapper(EnergyStationConfigConvert.class);
EnergyStationConfigDO convert(EnergyStationConfigSaveReqVO bean);
EnergyStationConfigRespVO convert(EnergyStationConfigDO bean);
PageResult<EnergyStationConfigRespVO> convertPage(PageResult<EnergyStationConfigDO> page);
List<EnergyStationConfigSimpleVO> convertSimpleList(List<EnergyStationConfigDO> list);
}

View File

@@ -1,53 +0,0 @@
package cn.iocoder.yudao.module.energy.dal.dataobject.config;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 站点配置 DO
*/
@TableName("energy_station_config")
@KeySequence("energy_station_config_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EnergyStationConfigDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 站点ID
*/
private Long stationId;
/**
* 是否自动扣费
*/
private Boolean autoDeduct;
/**
* 合作类型
*/
private Integer cooperationType;
/**
* 自动匹配开关
*/
private Boolean autoMatch;
/**
* 备注
*/
private String remark;
}

View File

@@ -1,25 +0,0 @@
package cn.iocoder.yudao.module.energy.dal.mysql.config;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigPageReqVO;
import cn.iocoder.yudao.module.energy.dal.dataobject.config.EnergyStationConfigDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EnergyStationConfigMapper extends BaseMapperX<EnergyStationConfigDO> {
default EnergyStationConfigDO selectByStationId(Long stationId) {
return selectOne(EnergyStationConfigDO::getStationId, stationId);
}
default PageResult<EnergyStationConfigDO> selectPage(EnergyStationConfigPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<EnergyStationConfigDO>()
.eqIfPresent(EnergyStationConfigDO::getStationId, reqVO.getStationId())
.eqIfPresent(EnergyStationConfigDO::getAutoDeduct, reqVO.getAutoDeduct())
.eqIfPresent(EnergyStationConfigDO::getCooperationType, reqVO.getCooperationType())
.orderByDesc(EnergyStationConfigDO::getId));
}
}

View File

@@ -5,9 +5,12 @@ import lombok.Getter;
import java.util.List;
/**
* 账单审核通过事件
*/
@Getter
@AllArgsConstructor
public class BillApprovedEvent {
public class BillAuditPassedEvent {
private final Long billId;
private final List<Long> detailIds;
}

View File

@@ -1,13 +0,0 @@
package cn.iocoder.yudao.module.energy.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public class BillCreatedEvent {
private final Long billId;
private final List<Long> detailIds;
}

View File

@@ -1,10 +0,0 @@
package cn.iocoder.yudao.module.energy.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class DeductionCompletedEvent {
private final Long detailId;
}

View File

@@ -5,9 +5,12 @@ import lombok.Getter;
import java.math.BigDecimal;
/**
* 明细审核通过事件
*/
@Getter
@AllArgsConstructor
public class DetailCreatedEvent {
public class DetailAuditPassedEvent {
private final Long detailId;
private final Long stationId;
private final Long customerId;

View File

@@ -1,16 +0,0 @@
package cn.iocoder.yudao.module.energy.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.math.BigDecimal;
@Getter
@AllArgsConstructor
public class DetailAuditedEvent {
private final Long detailId;
private final Long stationId;
private final Long customerId;
private final Long contractId;
private final BigDecimal customerAmount;
}

View File

@@ -1,14 +0,0 @@
package cn.iocoder.yudao.module.energy.event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RecordMatchedEvent {
private final Long recordId;
private final Long stationId;
private final Long vehicleId;
private final Long customerId;
private final String plateNumber;
}

View File

@@ -1,10 +1,7 @@
package cn.iocoder.yudao.module.energy.listener;
import cn.iocoder.yudao.module.energy.dal.dataobject.config.EnergyStationConfigDO;
import cn.iocoder.yudao.module.energy.dal.mysql.config.EnergyStationConfigMapper;
import cn.iocoder.yudao.module.energy.enums.DeductionStatusEnum;
import cn.iocoder.yudao.module.energy.event.DetailAuditedEvent;
import cn.iocoder.yudao.module.energy.event.DetailCreatedEvent;
import cn.iocoder.yudao.module.energy.event.DetailAuditPassedEvent;
import cn.iocoder.yudao.module.energy.service.account.EnergyAccountService;
import cn.iocoder.yudao.module.energy.service.detail.HydrogenDetailService;
import jakarta.annotation.Resource;
@@ -20,37 +17,23 @@ public class AccountEventListener {
@Resource
private EnergyAccountService accountService;
@Resource
private EnergyStationConfigMapper stationConfigMapper;
@Resource
private HydrogenDetailService detailService;
/**
* 明细创建完成 → 若配置自动扣款则扣款
* 明细审核通过 → 执行扣款
* BEFORE_COMMIT: 资金操作,必须同事务
*
* 注意:此监听器处理审核后扣款场景(站点配置 auto_deduct=false
* 自动扣款场景auto_deduct=true在明细创建时已完成
*/
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void onDetailCreated(DetailCreatedEvent event) {
EnergyStationConfigDO config = stationConfigMapper.selectByStationId(event.getStationId());
if (config != null && Boolean.TRUE.equals(config.getAutoDeduct())) {
log.info("[onDetailCreated] auto deduct: detailId={}, amount={}", event.getDetailId(), event.getCustomerAmount());
accountService.deduct(event.getCustomerId(), event.getContractId(),
event.getCustomerAmount(), event.getDetailId(), null, "加氢自动扣款");
detailService.updateDeductionStatus(event.getDetailId(), DeductionStatusEnum.DEDUCTED.getStatus());
}
}
/**
* 明细审核通过 → 若配置审核后扣款则扣款
* BEFORE_COMMIT: 资金操作,必须同事务
*/
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void onDetailAudited(DetailAuditedEvent event) {
EnergyStationConfigDO config = stationConfigMapper.selectByStationId(event.getStationId());
if (config != null && !Boolean.TRUE.equals(config.getAutoDeduct())) {
log.info("[onDetailAudited] audit-then-deduct: detailId={}, amount={}", event.getDetailId(), event.getCustomerAmount());
accountService.deduct(event.getCustomerId(), event.getContractId(),
event.getCustomerAmount(), event.getDetailId(), null, "审核后扣款");
detailService.updateDeductionStatus(event.getDetailId(), DeductionStatusEnum.DEDUCTED.getStatus());
}
public void onDetailAuditPassed(DetailAuditPassedEvent event) {
// TODO: 需要从 asset 模块获取站点配置判断是否需要扣款
// 当前简化实现:审核通过后统一扣款
log.info("[onDetailAuditPassed] deduct: detailId={}, amount={}",
event.getDetailId(), event.getCustomerAmount());
accountService.deduct(event.getCustomerId(), event.getContractId(),
event.getCustomerAmount(), event.getDetailId(), null, "审核后扣款");
detailService.updateDeductionStatus(event.getDetailId(), DeductionStatusEnum.DEDUCTED.getStatus());
}
}

View File

@@ -1,8 +1,7 @@
package cn.iocoder.yudao.module.energy.listener;
import cn.iocoder.yudao.module.energy.enums.SettlementStatusEnum;
import cn.iocoder.yudao.module.energy.event.BillApprovedEvent;
import cn.iocoder.yudao.module.energy.event.BillCreatedEvent;
import cn.iocoder.yudao.module.energy.event.BillAuditPassedEvent;
import cn.iocoder.yudao.module.energy.service.detail.HydrogenDetailService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -16,23 +15,15 @@ public class BillEventListener {
@Resource
private HydrogenDetailService detailService;
/**
* 账单生成完成 → 更新明细的 billId
* AFTER_COMMIT: 非资金操作
*/
@TransactionalEventListener
public void onBillCreated(BillCreatedEvent event) {
log.info("[onBillCreated] billId={}, detailCount={}", event.getBillId(), event.getDetailIds().size());
detailService.updateBillId(event.getDetailIds(), event.getBillId());
}
/**
* 账单审核通过 → 更新明细结算状态
* AFTER_COMMIT: 非资金操作
*
* 预留二期财务对接
*/
@TransactionalEventListener
public void onBillApproved(BillApprovedEvent event) {
log.info("[onBillApproved] billId={}", event.getBillId());
public void onBillAuditPassed(BillAuditPassedEvent event) {
log.info("[onBillAuditPassed] billId={}", event.getBillId());
detailService.updateSettlementStatus(event.getDetailIds(), SettlementStatusEnum.SETTLED.getStatus());
}
}

View File

@@ -1,13 +1,30 @@
package cn.iocoder.yudao.module.energy.listener;
import cn.iocoder.yudao.module.asset.dal.dataobject.station.HydrogenStationDO;
import cn.iocoder.yudao.module.asset.service.station.HydrogenStationService;
import cn.iocoder.yudao.module.energy.dal.dataobject.detail.EnergyHydrogenDetailDO;
import cn.iocoder.yudao.module.energy.dal.dataobject.price.EnergyStationPriceDO;
import cn.iocoder.yudao.module.energy.dal.dataobject.record.EnergyHydrogenRecordDO;
import cn.iocoder.yudao.module.energy.dal.mysql.detail.EnergyHydrogenDetailMapper;
import cn.iocoder.yudao.module.energy.dal.mysql.record.EnergyHydrogenRecordMapper;
import cn.iocoder.yudao.module.energy.enums.DeductionStatusEnum;
import cn.iocoder.yudao.module.energy.event.RecordImportedEvent;
import cn.iocoder.yudao.module.energy.event.RecordMatchedEvent;
import cn.iocoder.yudao.module.energy.service.account.EnergyAccountService;
import cn.iocoder.yudao.module.energy.service.detail.HydrogenDetailService;
import cn.iocoder.yudao.module.energy.service.match.HydrogenMatchService;
import cn.iocoder.yudao.module.energy.service.match.dto.MatchResultDTO;
import cn.iocoder.yudao.module.energy.service.price.EnergyStationPriceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
@Component
@Slf4j
public class DetailEventListener {
@@ -15,26 +32,136 @@ public class DetailEventListener {
@Resource
private HydrogenDetailService hydrogenDetailService;
@Resource
private HydrogenMatchService hydrogenMatchService;
@Resource
private EnergyHydrogenRecordMapper hydrogenRecordMapper;
@Resource
private EnergyHydrogenDetailMapper hydrogenDetailMapper;
@Resource
private HydrogenStationService hydrogenStationService;
@Resource
private EnergyStationPriceService stationPriceService;
@Resource
private EnergyAccountService accountService;
/**
* 记录导入完成 → 触发自动匹配(占位,后续实现批量自动匹配)
* 记录导入完成 → 触发自动匹配+生成明细
* AFTER_COMMIT: 非资金操作
*/
@TransactionalEventListener
@Transactional(rollbackFor = Exception.class)
public void onRecordImported(RecordImportedEvent event) {
log.info("[onRecordImported] stationId={}, recordCount={}, batchNo={}",
log.info("[onRecordImported] 开始处理导入记录,stationId={}, recordCount={}, batchNo={}",
event.getStationId(), event.getRecordIds().size(), event.getBatchNo());
// TODO: 后续接入 asset 模块实现批量自动匹配
// hydrogenRecordService.batchMatch(event.getRecordIds());
// 1. 批量匹配
MatchResultDTO matchResult = hydrogenMatchService.batchMatch(event.getRecordIds());
log.info("[onRecordImported] 匹配完成,成功={}, 失败={}",
matchResult.getSuccessCount(), matchResult.getFailCount());
// 2. 批量生成明细(只为匹配成功的记录生成)
if (matchResult.getSuccessCount() > 0) {
List<EnergyHydrogenRecordDO> matchedRecords = hydrogenRecordMapper.selectBatchIds(matchResult.getSuccessIds());
List<EnergyHydrogenDetailDO> details = new ArrayList<>();
for (EnergyHydrogenRecordDO record : matchedRecords) {
EnergyHydrogenDetailDO detail = createDetailFromRecord(record, event.getStationId());
if (detail != null) {
details.add(detail);
}
}
if (!details.isEmpty()) {
// 批量插入明细
for (EnergyHydrogenDetailDO detail : details) {
hydrogenDetailMapper.insert(detail);
}
log.info("[onRecordImported] 生成明细完成count={}", details.size());
// 3. 检查站点配置,决定是否自动扣款
HydrogenStationDO station = hydrogenStationService.getHydrogenStation(event.getStationId());
if (station != null && Boolean.TRUE.equals(station.getAutoDeduct())) {
log.info("[onRecordImported] 站点配置自动扣款,开始扣款流程");
for (EnergyHydrogenDetailDO detail : details) {
try {
accountService.deduct(
detail.getCustomerId(),
detail.getContractId(),
detail.getCustomerAmount(),
detail.getId(),
null,
"加氢自动扣款"
);
hydrogenDetailService.updateDeductionStatus(
detail.getId(),
DeductionStatusEnum.DEDUCTED.getStatus()
);
log.info("[onRecordImported] 扣款成功detailId={}, amount={}",
detail.getId(), detail.getCustomerAmount());
} catch (Exception e) {
log.error("[onRecordImported] 扣款失败detailId={}, error={}",
detail.getId(), e.getMessage(), e);
}
}
}
}
}
log.info("[onRecordImported] 处理完成");
}
/**
* 记录匹配完成 → 创建明细
* AFTER_COMMIT: 明细创建在独立事务中
* 记录创建明细
*/
@TransactionalEventListener
public void onRecordMatched(RecordMatchedEvent event) {
log.info("[onRecordMatched] recordId={}, vehicleId={}, customerId={}",
event.getRecordId(), event.getVehicleId(), event.getCustomerId());
hydrogenDetailService.createFromRecord(event);
private EnergyHydrogenDetailDO createDetailFromRecord(EnergyHydrogenRecordDO record, Long stationId) {
// 获取价格配置使用客户ID和日期
EnergyStationPriceDO price = stationPriceService.getEffectivePrice(
stationId,
record.getCustomerId(),
record.getHydrogenDate()
);
if (price == null) {
log.warn("[createDetailFromRecord] 未找到有效价格配置recordId={}, stationId={}, customerId={}, date={}",
record.getId(), stationId, record.getCustomerId(), record.getHydrogenDate());
return null;
}
// 重新匹配获取合同ID因为批量匹配时已经更新了record但需要获取contractId
MatchResultVO matchResult = hydrogenMatchService.matchRecord(record);
// 计算金额
BigDecimal costAmount = record.getHydrogenQuantity()
.multiply(price.getCostPrice())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal customerAmount = record.getHydrogenQuantity()
.multiply(price.getCustomerPrice())
.setScale(2, RoundingMode.HALF_UP);
return EnergyHydrogenDetailDO.builder()
.recordId(record.getId())
.stationId(stationId)
.vehicleId(record.getVehicleId())
.plateNumber(record.getPlateNumber())
.hydrogenDate(record.getHydrogenDate())
.hydrogenQuantity(record.getHydrogenQuantity())
.costPrice(price.getCostPrice())
.costAmount(costAmount)
.customerPrice(price.getCustomerPrice())
.customerAmount(customerAmount)
.contractId(matchResult.getContractId()) // 从匹配结果获取合同ID
.customerId(record.getCustomerId())
.projectName(null) // 可以从合同获取,暂时为空
.costBearer(1) // 默认客户承担,可以从合同配置获取
.payMethod(1) // 默认预付,可以从合同配置获取
.auditStatus(0) // 待审核
.deductionStatus(0) // 未扣款
.settlementStatus(0) // 未结算
.build();
}
}

View File

@@ -12,8 +12,7 @@ import cn.iocoder.yudao.module.energy.dal.mysql.bill.EnergyBillAdjustmentMapper;
import cn.iocoder.yudao.module.energy.dal.mysql.bill.EnergyBillMapper;
import cn.iocoder.yudao.module.energy.dal.mysql.detail.EnergyHydrogenDetailMapper;
import cn.iocoder.yudao.module.energy.enums.*;
import cn.iocoder.yudao.module.energy.event.BillApprovedEvent;
import cn.iocoder.yudao.module.energy.event.BillCreatedEvent;
import cn.iocoder.yudao.module.energy.event.BillAuditPassedEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
@@ -91,9 +90,11 @@ public class EnergyBillServiceImpl implements EnergyBillService {
.build();
billMapper.insert(bill);
// 4. Publish event to update detail billIds
// 4. Update detail billIds directly (no event needed)
List<Long> detailIds = details.stream().map(EnergyHydrogenDetailDO::getId).toList();
eventPublisher.publishEvent(new BillCreatedEvent(bill.getId(), detailIds));
detailMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<EnergyHydrogenDetailDO>()
.set(EnergyHydrogenDetailDO::getBillId, bill.getId())
.in(EnergyHydrogenDetailDO::getId, detailIds));
return bill.getId();
}
@@ -160,7 +161,7 @@ public class EnergyBillServiceImpl implements EnergyBillService {
if (approved) {
List<EnergyHydrogenDetailDO> details = detailMapper.selectListByBillId(id);
List<Long> detailIds = details.stream().map(EnergyHydrogenDetailDO::getId).toList();
eventPublisher.publishEvent(new BillApprovedEvent(id, detailIds));
eventPublisher.publishEvent(new BillAuditPassedEvent(id, detailIds));
}
}

View File

@@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.energy.service.config;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigPageReqVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigSaveReqVO;
import cn.iocoder.yudao.module.energy.dal.dataobject.config.EnergyStationConfigDO;
import java.util.List;
public interface EnergyStationConfigService {
Long createConfig(EnergyStationConfigSaveReqVO createReqVO);
void updateConfig(EnergyStationConfigSaveReqVO updateReqVO);
EnergyStationConfigDO getConfig(Long id);
PageResult<EnergyStationConfigDO> getConfigPage(EnergyStationConfigPageReqVO pageReqVO);
EnergyStationConfigDO getByStationId(Long stationId);
List<EnergyStationConfigDO> getConfigList();
}

View File

@@ -1,69 +0,0 @@
package cn.iocoder.yudao.module.energy.service.config;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigPageReqVO;
import cn.iocoder.yudao.module.energy.controller.admin.config.vo.EnergyStationConfigSaveReqVO;
import cn.iocoder.yudao.module.energy.convert.config.EnergyStationConfigConvert;
import cn.iocoder.yudao.module.energy.dal.dataobject.config.EnergyStationConfigDO;
import cn.iocoder.yudao.module.energy.dal.mysql.config.EnergyStationConfigMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.energy.enums.ErrorCodeConstants.*;
@Service
@Validated
public class EnergyStationConfigServiceImpl implements EnergyStationConfigService {
@Resource
private EnergyStationConfigMapper stationConfigMapper;
@Override
public Long createConfig(EnergyStationConfigSaveReqVO createReqVO) {
// 校验站点唯一
EnergyStationConfigDO existing = stationConfigMapper.selectByStationId(createReqVO.getStationId());
if (existing != null) {
throw exception(STATION_CONFIG_DUPLICATE);
}
EnergyStationConfigDO config = EnergyStationConfigConvert.INSTANCE.convert(createReqVO);
stationConfigMapper.insert(config);
return config.getId();
}
@Override
public void updateConfig(EnergyStationConfigSaveReqVO updateReqVO) {
validateConfigExists(updateReqVO.getId());
EnergyStationConfigDO updateObj = EnergyStationConfigConvert.INSTANCE.convert(updateReqVO);
stationConfigMapper.updateById(updateObj);
}
@Override
public EnergyStationConfigDO getConfig(Long id) {
return stationConfigMapper.selectById(id);
}
@Override
public PageResult<EnergyStationConfigDO> getConfigPage(EnergyStationConfigPageReqVO pageReqVO) {
return stationConfigMapper.selectPage(pageReqVO);
}
@Override
public EnergyStationConfigDO getByStationId(Long stationId) {
return stationConfigMapper.selectByStationId(stationId);
}
@Override
public List<EnergyStationConfigDO> getConfigList() {
return stationConfigMapper.selectList();
}
private void validateConfigExists(Long id) {
if (stationConfigMapper.selectById(id) == null) {
throw exception(STATION_CONFIG_NOT_EXISTS);
}
}
}

View File

@@ -4,11 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.energy.controller.admin.detail.vo.HydrogenDetailPageReqVO;
import cn.iocoder.yudao.module.energy.controller.admin.detail.vo.HydrogenDetailSaveReqVO;
import cn.iocoder.yudao.module.energy.dal.dataobject.detail.EnergyHydrogenDetailDO;
import cn.iocoder.yudao.module.energy.event.RecordMatchedEvent;
import java.util.List;
public interface HydrogenDetailService {
void createFromRecord(RecordMatchedEvent event);
void updateDetail(HydrogenDetailSaveReqVO reqVO);
PageResult<EnergyHydrogenDetailDO> getDetailPage(HydrogenDetailPageReqVO pageReqVO);
EnergyHydrogenDetailDO getDetail(Long id);

View File

@@ -12,9 +12,7 @@ import cn.iocoder.yudao.module.energy.enums.AuditStatusEnum;
import cn.iocoder.yudao.module.energy.enums.CostBearerEnum;
import cn.iocoder.yudao.module.energy.enums.DeductionStatusEnum;
import cn.iocoder.yudao.module.energy.enums.PayMethodEnum;
import cn.iocoder.yudao.module.energy.event.DetailAuditedEvent;
import cn.iocoder.yudao.module.energy.event.DetailCreatedEvent;
import cn.iocoder.yudao.module.energy.event.RecordMatchedEvent;
import cn.iocoder.yudao.module.energy.event.DetailAuditPassedEvent;
import cn.iocoder.yudao.module.energy.service.price.EnergyStationPriceService;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
@@ -44,54 +42,6 @@ public class HydrogenDetailServiceImpl implements HydrogenDetailService {
@Resource
private ApplicationEventPublisher eventPublisher;
@Override
@Transactional(rollbackFor = Exception.class)
public void createFromRecord(RecordMatchedEvent event) {
// 1. Get original record
EnergyHydrogenRecordDO record = recordMapper.selectById(event.getRecordId());
if (record == null) {
log.warn("[createFromRecord] record not found: {}", event.getRecordId());
return;
}
// 2. Get price
EnergyStationPriceDO price = stationPriceService.getEffectivePrice(
event.getStationId(), event.getCustomerId(), record.getHydrogenDate());
BigDecimal costPrice = price != null ? price.getCostPrice() : record.getUnitPrice();
BigDecimal customerPrice = price != null ? price.getCustomerPrice() : record.getUnitPrice();
// 3. Calculate amounts
BigDecimal costAmount = record.getHydrogenQuantity().multiply(costPrice).setScale(2, RoundingMode.HALF_UP);
BigDecimal customerAmount = record.getHydrogenQuantity().multiply(customerPrice).setScale(2, RoundingMode.HALF_UP);
// 4. Build detail - NOTE: contractId, costBearer, payMethod would normally come from asset/rental API
// For now, use defaults. TODO: integrate with asset module for contract lookup
EnergyHydrogenDetailDO detail = EnergyHydrogenDetailDO.builder()
.recordId(event.getRecordId())
.stationId(event.getStationId())
.vehicleId(event.getVehicleId())
.plateNumber(event.getPlateNumber())
.hydrogenDate(record.getHydrogenDate())
.hydrogenQuantity(record.getHydrogenQuantity())
.costPrice(costPrice)
.costAmount(costAmount)
.customerPrice(customerPrice)
.customerAmount(customerAmount)
.customerId(event.getCustomerId())
.costBearer(CostBearerEnum.CUSTOMER.getType())
.payMethod(PayMethodEnum.PREPAID.getType())
.auditStatus(AuditStatusEnum.PENDING.getStatus())
.deductionStatus(DeductionStatusEnum.NOT_DEDUCTED.getStatus())
.settlementStatus(0) // NOT_SETTLED
.build();
detailMapper.insert(detail);
// 5. Publish event (BEFORE_COMMIT listener will handle auto-deduction if configured)
eventPublisher.publishEvent(new DetailCreatedEvent(
detail.getId(), event.getStationId(), event.getCustomerId(),
detail.getContractId(), customerAmount));
}
@Override
public void updateDetail(HydrogenDetailSaveReqVO reqVO) {
validateDetailExists(reqVO.getId());
@@ -134,7 +84,7 @@ public class HydrogenDetailServiceImpl implements HydrogenDetailService {
// If approved, publish event for deduction
if (approved) {
eventPublisher.publishEvent(new DetailAuditedEvent(
eventPublisher.publishEvent(new DetailAuditPassedEvent(
id, detail.getStationId(), detail.getCustomerId(),
detail.getContractId(), detail.getCustomerAmount()));
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.energy.service.match;
import cn.iocoder.yudao.module.energy.dal.dataobject.record.EnergyHydrogenRecordDO;
import cn.iocoder.yudao.module.energy.service.match.dto.MatchResultDTO;
import cn.iocoder.yudao.module.energy.service.match.vo.MatchResultVO;
import java.util.List;
/**
* 加氢记录匹配服务
*
* @author 芋道源码
*/
public interface HydrogenMatchService {
/**
* 批量自动匹配记录
*
* @param recordIds 记录ID列表
* @return 匹配结果统计(成功数、失败数)
*/
MatchResultDTO batchMatch(List<Long> recordIds);
/**
* 单条记录匹配
*
* @param record 原始记录
* @return 匹配结果vehicleId, customerId, contractId
*/
MatchResultVO matchRecord(EnergyHydrogenRecordDO record);
}

View File

@@ -0,0 +1,150 @@
package cn.iocoder.yudao.module.energy.service.match;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.asset.dal.dataobject.contract.ContractDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.customer.CustomerDO;
import cn.iocoder.yudao.module.asset.dal.dataobject.vehicle.VehicleBaseDO;
import cn.iocoder.yudao.module.asset.dal.mysql.contract.ContractMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.customer.CustomerMapper;
import cn.iocoder.yudao.module.asset.dal.mysql.vehicle.VehicleBaseMapper;
import cn.iocoder.yudao.module.energy.dal.dataobject.record.EnergyHydrogenRecordDO;
import cn.iocoder.yudao.module.energy.dal.mysql.record.EnergyHydrogenRecordMapper;
import cn.iocoder.yudao.module.energy.service.match.dto.MatchResultDTO;
import cn.iocoder.yudao.module.energy.service.match.vo.MatchResultVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 加氢记录匹配服务实现
*
* @author 芋道源码
*/
@Service
@Slf4j
public class HydrogenMatchServiceImpl implements HydrogenMatchService {
@Resource
private EnergyHydrogenRecordMapper hydrogenRecordMapper;
@Resource
private VehicleBaseMapper vehicleBaseMapper;
@Resource
private CustomerMapper customerMapper;
@Resource
private ContractMapper contractMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public MatchResultDTO batchMatch(List<Long> recordIds) {
MatchResultDTO result = new MatchResultDTO();
for (Long recordId : recordIds) {
EnergyHydrogenRecordDO record = hydrogenRecordMapper.selectById(recordId);
if (record == null) {
log.warn("[batchMatch] 记录不存在recordId={}", recordId);
result.setFailCount(result.getFailCount() + 1);
result.getFailIds().add(recordId);
continue;
}
// 执行匹配
MatchResultVO matchResult = matchRecord(record);
// 更新记录
EnergyHydrogenRecordDO updateRecord = new EnergyHydrogenRecordDO();
updateRecord.setId(recordId);
updateRecord.setVehicleId(matchResult.getVehicleId());
updateRecord.setCustomerId(matchResult.getCustomerId());
updateRecord.setMatchStatus(matchResult.getMatchStatus());
updateRecord.setUpdateTime(LocalDateTime.now());
hydrogenRecordMapper.updateById(updateRecord);
// 统计结果
if (matchResult.getMatchStatus() == 0) {
result.setSuccessCount(result.getSuccessCount() + 1);
result.getSuccessIds().add(recordId);
log.info("[batchMatch] 匹配成功recordId={}, vehicleId={}, customerId={}, contractId={}",
recordId, matchResult.getVehicleId(), matchResult.getCustomerId(), matchResult.getContractId());
} else {
result.setFailCount(result.getFailCount() + 1);
result.getFailIds().add(recordId);
log.warn("[batchMatch] 匹配失败recordId={}, status={}, message={}",
recordId, matchResult.getMatchStatus(), matchResult.getMatchMessage());
}
}
return result;
}
@Override
public MatchResultVO matchRecord(EnergyHydrogenRecordDO record) {
MatchResultVO result = new MatchResultVO();
// 1. 匹配车辆(通过车牌号)
if (StrUtil.isNotBlank(record.getPlateNumber())) {
VehicleBaseDO vehicle = vehicleBaseMapper.selectOne(
VehicleBaseDO::getPlateNo, record.getPlateNumber()
);
if (vehicle != null) {
result.setVehicleId(vehicle.getId());
log.debug("[matchRecord] 车辆匹配成功plateNumber={}, vehicleId={}",
record.getPlateNumber(), vehicle.getId());
}
}
// 2. 匹配客户
// 2.1 如果车辆未匹配,尝试通过客户名称模糊匹配
if (result.getCustomerId() == null && StrUtil.isNotBlank(record.getPlateNumber())) {
// 注意:这里假设 plateNumber 字段可能包含客户信息,实际需要根据业务调整
// 如果 Record 有独立的 customerName 字段,应该使用那个字段
CustomerDO customer = customerMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CustomerDO>()
.like(CustomerDO::getCustomerName, record.getPlateNumber())
.last("LIMIT 1")
);
if (customer != null) {
result.setCustomerId(customer.getId());
log.debug("[matchRecord] 客户匹配成功通过名称customerId={}", customer.getId());
}
}
// 3. 匹配合同通过客户ID
if (result.getCustomerId() != null) {
ContractDO contract = contractMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ContractDO>()
.eq(ContractDO::getCustomerId, result.getCustomerId())
.eq(ContractDO::getContractStatus, 2) // 2=进行中
.gt(ContractDO::getEndDate, record.getHydrogenDate())
.orderByDesc(ContractDO::getStartDate)
.last("LIMIT 1")
);
if (contract != null) {
result.setContractId(contract.getId());
log.debug("[matchRecord] 合同匹配成功contractId={}", contract.getId());
}
}
// 4. 判断匹配状态
if (result.getVehicleId() != null && result.getCustomerId() != null && result.getContractId() != null) {
result.setMatchStatus(0); // 完全匹配
result.setMatchMessage("自动匹配成功");
} else if (result.getCustomerId() != null) {
result.setMatchStatus(1); // 部分匹配
result.setMatchMessage("客户匹配成功,但缺少车辆或合同信息");
} else {
result.setMatchStatus(2); // 未匹配
result.setMatchMessage("未找到匹配的客户");
}
return result;
}
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.energy.service.match.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 批量匹配结果 DTO
*
* @author 芋道源码
*/
@Data
public class MatchResultDTO {
/**
* 成功数量
*/
private Integer successCount = 0;
/**
* 失败数量
*/
private Integer failCount = 0;
/**
* 成功的记录ID列表
*/
private List<Long> successIds = new ArrayList<>();
/**
* 失败的记录ID列表
*/
private List<Long> failIds = new ArrayList<>();
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.energy.service.match.vo;
import lombok.Data;
/**
* 匹配结果 VO
*
* @author 芋道源码
*/
@Data
public class MatchResultVO {
/**
* 车辆ID
*/
private Long vehicleId;
/**
* 客户ID
*/
private Long customerId;
/**
* 合同ID
*/
private Long contractId;
/**
* 匹配状态0=完全匹配1=部分匹配2=未匹配)
*/
private Integer matchStatus;
/**
* 匹配说明
*/
private String matchMessage;
}

View File

@@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.energy.dal.mysql.record.EnergyHydrogenRecordMappe
import cn.iocoder.yudao.module.energy.enums.MatchStatusEnum;
import cn.iocoder.yudao.module.energy.enums.SourceTypeEnum;
import cn.iocoder.yudao.module.energy.event.RecordImportedEvent;
import cn.iocoder.yudao.module.energy.event.RecordMatchedEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
@@ -132,8 +131,7 @@ public class HydrogenRecordServiceImpl implements HydrogenRecordService {
.customerId(customerId)
.matchStatus(MatchStatusEnum.MATCHED.getStatus())
.build());
// Publish event
eventPublisher.publishEvent(new RecordMatchedEvent(id, record.getStationId(), vehicleId, customerId, record.getPlateNumber()));
// 手动匹配功能保留,批量匹配和明细生成由 RecordImportedEvent 统一处理
}
@Override

View File

@@ -0,0 +1,107 @@
server:
port: 48085
spring:
application:
name: energy-server
# 允许 Bean 覆盖
main:
allow-bean-definition-overriding: true
# 禁用 Nacos 配置中心
cloud:
nacos:
config:
enabled: false
import-check:
enabled: false
# 数据源配置
datasource:
druid:
url: jdbc:mysql://47.103.115.36:3306/oneos_energy?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: Passw0rd2026
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
test-while-idle: true
test-on-borrow: false
test-on-return: false
# Redis 配置
data:
redis:
host: 47.103.115.36
port: 6379
database: 0
password: Passw0rd2026
timeout: 10s
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
# MyBatis Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
type-aliases-package: cn.iocoder.yudao.module.energy.dal.dataobject
# 日志配置
logging:
level:
root: INFO
cn.iocoder.yudao.module.energy: DEBUG
cn.iocoder.yudao.framework: INFO
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n'
# 芋道配置
yudao:
info:
version: 1.0.0
base-package: cn.iocoder.yudao.module.energy
web:
admin-api:
prefix: /admin-api
controller: '**.controller.admin.**'
admin-ui:
url: http://localhost:3000
security:
permit-all_urls: []
xss:
enable: false
access-log:
enable: true
error-code:
enable: true
demo: false
tenant:
enable: false
# Swagger 配置
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
knife4j:
enable: true
setting:
language: zh_cn

View File

@@ -0,0 +1,27 @@
spring:
application:
name: energy-server
profiles:
active: dev
# 允许 Bean 覆盖
main:
allow-bean-definition-overriding: true
config:
import:
- optional:nacos:common-dev.yaml
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml
cloud:
nacos:
server-addr: ${NACOS_ADDR:localhost:8848}
namespace: ${NACOS_NAMESPACE:dev}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
discovery:
namespace: ${NACOS_NAMESPACE:dev}
config:
namespace: ${NACOS_NAMESPACE:dev}
file-extension: yaml