Explorar el Código

补贴预测新增客户ID与名称

fuyuchuan hace 6 días
padre
commit
57c2f03dd8

+ 9 - 6
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java

@@ -190,20 +190,23 @@ public class DrController {
 
     /**
      * 分页查询补贴金额预测
-     * siteName  站点名称(模糊匹配)
-     * startTime 响应开始时间起始
-     * endTime   响应开始时间截止
-     * current   页码
-     * size      页大小
+     * siteName     站点名称(模糊匹配)
+     * customerName 企业名称(模糊匹配)
+     * startTime    响应开始时间起始
+     * endTime      响应开始时间截止
+     * current      页码
+     * size         页大小
      */
     @GetMapping(value = "/subsidyPrediction")
     public ApiResult<CommonPage<DrSubsidyPredictionVO>> pageSubsidyPrediction(
             @RequestParam(value = "siteName", required = false) String siteName,
+            @RequestParam(value = "customerName", required = false) String customerName,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
             @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
-        return ApiResult.success(vppDrSubsidyPredictionService.page(siteName, startTime, endTime, current, size));
+        return ApiResult.success(vppDrSubsidyPredictionService.page(
+                siteName, customerName, startTime, endTime, current, size));
     }
 
     /**

+ 8 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java

@@ -37,6 +37,14 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("site_name")
     private String siteName;
 
+    /** 企业/客户ID */
+    @TableField("customer_id")
+    private Long customerId;
+
+    /** 企业名称 */
+    @TableField("customer_name")
+    private String customerName;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @TableField("event_start_time")
     private LocalDateTime eventStartTime;

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java

@@ -13,7 +13,8 @@ public interface VppDrSubsidyPredictionService {
     /**
      * 分页查询补贴金额预测
      */
-    CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
+    CommonPage<DrSubsidyPredictionVO> page(String siteName, String customerName,
+                                           String startTime, String endTime,
                                            Integer current, Integer size);
 
     /**

+ 56 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java

@@ -10,10 +10,18 @@ import com.usky.vpp.domain.VppContract;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppSettlementBill;
+import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.mapper.VppContractMapper;
 import com.usky.vpp.mapper.VppCustomerAccessMapper;
 import com.usky.vpp.mapper.VppCustomerContactMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppSettlementBillMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppCustomerService;
 import com.usky.vpp.service.VppSiteService;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
@@ -73,6 +81,14 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     private VppCustomerContactMapper contactMapper;
     @Autowired
     private VppContractMapper contractMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppSettlementBillMapper settlementBillMapper;
 
     // ==================== 准入管理 ====================
 
@@ -693,19 +709,51 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     @Transactional(rollbackFor = Exception.class)
     public Boolean deleteCustomer(Long id) {
         VppCustomer customer = findCustomerById(id);
+        assertCustomerDeletable(id);
 
-        // 客户下存在合同(不论状态)则不允许删除
-        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
-        contractWrapper.eq(VppContract::getCustomerId, id)
-                .eq(VppContract::getDeleteFlag, 0);
-        Integer contractCount = contractMapper.selectCount(contractWrapper);
+        VppAuditHelper.fillSoftDelete(customer);
+        int result = customerMapper.updateById(customer);
+        return result > 0;
+    }
+
+    /**
+     * 客户下存在合同、站点、邀约、参与记录等业务数据时不允许删除。
+     */
+    private void assertCustomerDeletable(Long customerId) {
+        Integer contractCount = contractMapper.selectCount(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getCustomerId, customerId)
+                .eq(VppContract::getDeleteFlag, VppAuditHelper.NOT_DELETED));
         if (contractCount != null && contractCount > 0) {
             throw new BusinessException("该客户下存在合同,无法删除!");
         }
 
-        VppAuditHelper.fillSoftDelete(customer);
-        int result = customerMapper.updateById(customer);
-        return result > 0;
+        Integer siteCount = siteMapper.selectCount(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getCustomerId, customerId)
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (siteCount != null && siteCount > 0) {
+            throw new BusinessException("该客户下存在站点,无法删除!");
+        }
+
+        Integer invitationCount = invitationMapper.selectCount(new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getCustomerId, customerId)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (invitationCount != null && invitationCount > 0) {
+            throw new BusinessException("该客户下存在需求响应邀约,无法删除!");
+        }
+
+        Integer participationCount = participationMapper.selectCount(new LambdaQueryWrapper<VppDrParticipation>()
+                .eq(VppDrParticipation::getCustomerId, customerId)
+                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (participationCount != null && participationCount > 0) {
+            throw new BusinessException("该客户下存在需求响应参与记录,无法删除!");
+        }
+
+        Integer billCount = settlementBillMapper.selectCount(new LambdaQueryWrapper<VppSettlementBill>()
+                .eq(VppSettlementBill::getCustomerId, customerId)
+                .eq(VppSettlementBill::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (billCount != null && billCount > 0) {
+            throw new BusinessException("该客户下存在结算账单,无法删除!");
+        }
     }
 
     @Override

+ 96 - 6
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -5,12 +5,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrSubsidyPrediction;
 import com.usky.vpp.domain.VppResponseDeviationPrice;
 import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.mapper.*;
+import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.mapper.VppResponseDeviationPriceMapper;
 import com.usky.vpp.service.VppBaselineService;
 import com.usky.vpp.service.VppDrSubsidyPredictionService;
@@ -59,12 +61,15 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     @Autowired
     private VppSiteMapper siteMapper;
     @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
     private VppResponseDeviationPriceMapper priceAdjustmentMapper;
     @Autowired
     private VppBaselineService vppBaselineService;
 
     @Override
-    public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
+    public CommonPage<DrSubsidyPredictionVO> page(String siteName, String customerName,
+                                                   String startTime, String endTime,
                                                    Integer current, Integer size) {
         Page<VppDrSubsidyPrediction> page = new Page<>(
                 current != null ? current : 1,
@@ -76,6 +81,9 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         if (StringUtils.hasText(siteName)) {
             wrapper.like(VppDrSubsidyPrediction::getSiteName, siteName);
         }
+        if (StringUtils.hasText(customerName)) {
+            wrapper.like(VppDrSubsidyPrediction::getCustomerName, customerName);
+        }
         if (StringUtils.hasText(startTime)) {
             wrapper.ge(VppDrSubsidyPrediction::getEventStartTime, startTime);
         }
@@ -116,6 +124,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         entity.setDrEventId(event.getId());
         entity.setSiteId(invitation.getSiteId());
         fillSiteName(entity);
+        entity.setCustomerId(invitation.getCustomerId());
+        fillCustomerName(entity);
         entity.setEventStartTime(event.getStartTime());
         entity.setEventEndTime(event.getEndTime());
         entity.setSubsidyPrice(event.getSubsidyPrice());
@@ -147,9 +157,13 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         if (request == null) {
             throw new BusinessException("请求参数不能为空");
         }
+        assertInvitationNotDuplicated(request.getInvitationId(), null);
+
         VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
         applyRequest(entity, request);
+        fillCustomerFromInvitationIfAbsent(entity);
         fillSiteName(entity);
+        fillCustomerName(entity);
         entity.setTenantId(SecurityUtils.getTenantId());
         entity.setCreateTime(LocalDateTime.now());
         entity.setUpdateTime(LocalDateTime.now());
@@ -170,15 +184,40 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         if (tenantId != null && existing.getTenantId() != null && !tenantId.equals(existing.getTenantId())) {
             throw new BusinessException("补贴预测记录不存在");
         }
+        Long invitationId = request.getInvitationId() != null
+                ? request.getInvitationId()
+                : existing.getInvitationId();
+        assertInvitationNotDuplicated(invitationId, existing.getId());
 
         applyRequest(existing, request);
         if (request.getSiteId() != null) {
             fillSiteName(existing);
         }
+        if (request.getCustomerId() != null) {
+            fillCustomerName(existing);
+        }
         existing.setUpdateTime(LocalDateTime.now());
         return subsidyPredictionMapper.updateById(existing) > 0;
     }
 
+    /**
+     * 同一邀约不可重复新增补贴预测。
+     */
+    private void assertInvitationNotDuplicated(Long invitationId, Long excludeId) {
+        if (invitationId == null) {
+            return;
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        Integer count = subsidyPredictionMapper.selectCount(
+                new LambdaQueryWrapper<VppDrSubsidyPrediction>()
+                        .eq(VppDrSubsidyPrediction::getInvitationId, invitationId)
+                        .eq(tenantId != null, VppDrSubsidyPrediction::getTenantId, tenantId)
+                        .ne(excludeId != null, VppDrSubsidyPrediction::getId, excludeId));
+        if (count != null && count > 0) {
+            throw new BusinessException("该邀约事件已添加补贴金额预测,不能重复添加!");
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean delete(Long id) {
@@ -227,10 +266,17 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 : siteMapper.selectBatchIds(siteIds).stream()
                         .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
 
+        Set<Long> customerIds = invitations.stream()
+                .map(VppDrInvitation::getCustomerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, String> customerNameMap = loadCustomerNameMap(customerIds);
+
         List<VppResponseDeviationPrice> adjustments = loadActivePriceAdjustments(event.getTenantId());
 
         for (VppDrInvitation invitation : invitations) {
-            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap, adjustments);
+            VppDrSubsidyPrediction prediction = buildPrediction(
+                    event, invitation, siteNameMap, customerNameMap, adjustments);
 
             VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectOne(
                     new LambdaQueryWrapper<VppDrSubsidyPrediction>()
@@ -253,12 +299,15 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
 
     private VppDrSubsidyPrediction buildPrediction(VppDrEvent event, VppDrInvitation invitation,
                                                     Map<Long, String> siteNameMap,
+                                                    Map<Long, String> customerNameMap,
                                                     List<VppResponseDeviationPrice> adjustments) {
         VppDrSubsidyPrediction p = new VppDrSubsidyPrediction();
         p.setInvitationId(invitation.getId());
         p.setDrEventId(event.getId());
         p.setSiteId(invitation.getSiteId());
         p.setSiteName(invitation.getSiteId() == null ? null : siteNameMap.get(invitation.getSiteId()));
+        p.setCustomerId(invitation.getCustomerId());
+        p.setCustomerName(invitation.getCustomerId() == null ? null : customerNameMap.get(invitation.getCustomerId()));
         p.setEventStartTime(event.getStartTime());
         p.setEventEndTime(event.getEndTime());
         p.setSubsidyPrice(event.getSubsidyPrice());
@@ -331,6 +380,12 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         if (request.getSiteId() != null) {
             entity.setSiteId(request.getSiteId());
         }
+        if (request.getCustomerId() != null) {
+            entity.setCustomerId(request.getCustomerId());
+        }
+        if (StringUtils.hasText(request.getCustomerName())) {
+            entity.setCustomerName(request.getCustomerName().trim());
+        }
         if (request.getEventStartTime() != null) {
             entity.setEventStartTime(request.getEventStartTime());
         }
@@ -371,6 +426,16 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         }
     }
 
+    private void fillCustomerFromInvitationIfAbsent(VppDrSubsidyPrediction entity) {
+        if (entity.getCustomerId() != null || entity.getInvitationId() == null) {
+            return;
+        }
+        VppDrInvitation invitation = invitationMapper.selectById(entity.getInvitationId());
+        if (invitation != null) {
+            entity.setCustomerId(invitation.getCustomerId());
+        }
+    }
+
     private void fillSiteName(VppDrSubsidyPrediction entity) {
         if (entity.getSiteId() == null) {
             entity.setSiteName(null);
@@ -380,6 +445,28 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         entity.setSiteName(site != null ? site.getSiteName() : null);
     }
 
+    private void fillCustomerName(VppDrSubsidyPrediction entity) {
+        if (entity.getCustomerId() == null) {
+            if (!StringUtils.hasText(entity.getCustomerName())) {
+                entity.setCustomerName(null);
+            }
+            return;
+        }
+        if (StringUtils.hasText(entity.getCustomerName())) {
+            return;
+        }
+        VppCustomer customer = customerMapper.selectById(entity.getCustomerId());
+        entity.setCustomerName(customer != null ? customer.getCustomerName() : null);
+    }
+
+    private Map<Long, String> loadCustomerNameMap(Set<Long> customerIds) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return customerMapper.selectBatchIds(customerIds).stream()
+                .collect(Collectors.toMap(VppCustomer::getId, VppCustomer::getCustomerName, (a, b) -> a));
+    }
+
     private List<VppResponseDeviationPrice> loadActivePriceAdjustments(Integer tenantId) {
         return priceAdjustmentMapper.selectList(
                 new LambdaQueryWrapper<VppResponseDeviationPrice>()
@@ -423,7 +510,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
      * <ul>
      *   <li>完成率(%)= 平台实际响应量 / 申报出清量 × 100(≥0,正常 0~100,超发可 &gt;100)</li>
      *   <li>偏差率(%)= (电网实际响应量 − 平台实际响应量) / 申报出清量 × 100(可正可负)</li>
-     *   <li>补贴金额 = 平台实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
+     *   <li>价格调整系数:按完成率比例匹配响应量偏差价格调整配置</li>
+     *   <li>补贴金额 = 电网实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
      * </ul>
      */
     private void recalculate(VppDrSubsidyPrediction entity,
@@ -452,7 +540,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
             return;
         }
 
-        // 比例:完成率用于匹配响应量偏差价格调整配置(区间按 0~1+ 配置)
+        // 完成率用平台量;价格档按完成率比例匹配
         BigDecimal completionRatio = platformKw.divide(declaredKw, 6, RoundingMode.HALF_UP);
         if (completionRatio.compareTo(BigDecimal.ZERO) < 0) {
             completionRatio = BigDecimal.ZERO;
@@ -461,15 +549,15 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 .divide(declaredKw, 6, RoundingMode.HALF_UP);
         BigDecimal priceAdjustmentCoefficient = resolvePriceAdjustmentCoefficient(completionRatio, adjustments);
 
-        // 对外/入库为百分比
         entity.setCompletionRate(completionRatio.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP));
         entity.setDeviationRate(deviationRatio.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP));
         entity.setPriceAdjustmentCoefficient(priceAdjustmentCoefficient);
 
+        // 补贴按电网核定实际响应量结算
         BigDecimal subsidyAmount = null;
         if (entity.getSubsidyPrice() != null && durationMin > 0) {
             BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
-            subsidyAmount = platformKw
+            subsidyAmount = gridKw
                     .multiply(hours)
                     .multiply(priceAdjustmentCoefficient)
                     .multiply(entity.getSubsidyPrice())
@@ -497,6 +585,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         vo.setDrEventId(entity.getDrEventId());
         vo.setSiteId(entity.getSiteId());
         vo.setSiteName(entity.getSiteName());
+        vo.setCustomerId(entity.getCustomerId());
+        vo.setCustomerName(entity.getCustomerName());
         vo.setEventStartTime(entity.getEventStartTime());
         vo.setEventEndTime(entity.getEventEndTime());
         vo.setResponseDurationMin(entity.getResponseDurationMin());

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionRequest.java

@@ -24,6 +24,12 @@ public class DrSubsidyPredictionRequest {
     /** 站点ID(手动选择) */
     private Long siteId;
 
+    /** 企业/客户ID */
+    private Long customerId;
+
+    /** 企业名称 */
+    private String customerName;
+
     /** 响应开始时间(手动选择) */
     private LocalDateTime eventStartTime;
 

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java

@@ -24,6 +24,12 @@ public class DrSubsidyPredictionVO {
 
     private String siteName;
 
+    /** 企业/客户ID */
+    private Long customerId;
+
+    /** 企业名称 */
+    private String customerName;
+
     private LocalDateTime eventStartTime;
 
     private LocalDateTime eventEndTime;

+ 4 - 1
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -673,6 +673,8 @@ CREATE TABLE `vpp_dr_subsidy_prediction` (
     `dr_event_id` BIGINT NULL COMMENT '需求响应事件ID',
     `site_id` BIGINT NULL COMMENT '站点ID',
     `site_name` VARCHAR(200) NULL COMMENT '站点名称',
+    `customer_id` BIGINT NULL COMMENT '企业/客户ID',
+    `customer_name` VARCHAR(200) NULL COMMENT '企业名称',
     `event_start_time` DATETIME(3) NULL COMMENT '事件开始时间',
     `event_end_time` DATETIME(3) NULL COMMENT '事件结束时间',
     `response_duration_min` INT NULL COMMENT '响应时长 分钟',
@@ -692,7 +694,8 @@ CREATE TABLE `vpp_dr_subsidy_prediction` (
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
     PRIMARY KEY (`id`),
     KEY `idx_subsidy_prediction_invitation` (`invitation_id`),
-    KEY `idx_subsidy_prediction_event_site` (`dr_event_id`, `site_id`)
+    KEY `idx_subsidy_prediction_event_site` (`dr_event_id`, `site_id`),
+    KEY `idx_subsidy_prediction_customer` (`customer_id`, `customer_name`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='需求响应补贴预测';
 
 CREATE TABLE `vpp_response_deviation_price` (