|
|
@@ -4,17 +4,24 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
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.VppDrEventMapper;
|
|
|
-import com.usky.vpp.mapper.VppDrInvitationMapper;
|
|
|
-import com.usky.vpp.mapper.VppDrSubsidyPredictionMapper;
|
|
|
-import com.usky.vpp.mapper.VppSiteMapper;
|
|
|
+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;
|
|
|
+import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
|
|
|
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
|
|
|
import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
|
|
|
+import com.usky.vpp.service.vo.SiteDeclaredCapacityVO;
|
|
|
import com.usky.vpp.util.VppAuditHelper;
|
|
|
+import com.usky.vpp.util.VppResponseCoefficientHelper;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
@@ -27,12 +34,12 @@ import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
import java.time.Duration;
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
import java.util.Collections;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Objects;
|
|
|
import java.util.Set;
|
|
|
-import java.util.function.Function;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
@@ -42,6 +49,8 @@ import java.util.stream.Collectors;
|
|
|
public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPredictionService {
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(VppDrSubsidyPredictionServiceImpl.class);
|
|
|
+ private static final BigDecimal MINUTES_PER_HOUR = BigDecimal.valueOf(60);
|
|
|
+ private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
|
@Autowired
|
|
|
private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
|
|
|
@@ -51,18 +60,30 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
private VppDrEventMapper eventMapper;
|
|
|
@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,
|
|
|
size != null && size <= 100 ? size : 20);
|
|
|
|
|
|
LambdaQueryWrapper<VppDrSubsidyPrediction> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ wrapper.eq(tenantId != null, VppDrSubsidyPrediction::getTenantId, tenantId);
|
|
|
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);
|
|
|
}
|
|
|
@@ -72,14 +93,151 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
wrapper.orderByDesc(VppDrSubsidyPrediction::getCreateTime);
|
|
|
|
|
|
Page<VppDrSubsidyPrediction> result = subsidyPredictionMapper.selectPage(page, wrapper);
|
|
|
-
|
|
|
+ Map<Long, String> invitationNoMap = loadInvitationNoMap(result.getRecords());
|
|
|
List<DrSubsidyPredictionVO> list = result.getRecords().stream()
|
|
|
- .map(this::toVo)
|
|
|
+ .map(item -> toVo(item, invitationNoMap))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
return new CommonPage<>(list, result.getTotal(), size != null ? size : 20, current != null ? current : 1);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public DrSubsidyPredictionVO calculate(DrSubsidyPredictionCalculateRequest request) {
|
|
|
+ if (request == null) {
|
|
|
+ throw new BusinessException("请求参数不能为空");
|
|
|
+ }
|
|
|
+ if (!StringUtils.hasText(request.getInvitationNo())) {
|
|
|
+ throw new BusinessException("邀约编号不能为空");
|
|
|
+ }
|
|
|
+ if (request.getActualResponseGridKw() == null) {
|
|
|
+ throw new BusinessException("实际响应容量(电网)不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ VppDrInvitation invitation = findInvitationByNo(request.getInvitationNo().trim());
|
|
|
+ VppDrEvent event = invitation.getDrEventId() == null ? null : eventMapper.selectById(invitation.getDrEventId());
|
|
|
+ if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
|
|
|
+ throw new BusinessException("邀约关联的需求响应事件不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
|
|
|
+ entity.setInvitationId(invitation.getId());
|
|
|
+ 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());
|
|
|
+
|
|
|
+ BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
|
|
|
+ ? invitation.getDeclaredCapacityKw()
|
|
|
+ : invitation.getActualClearedCapacityKw();
|
|
|
+ BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
|
|
|
+ ? invitation.getActualClearedCapacityKw()
|
|
|
+ : declaredKw;
|
|
|
+ BigDecimal platformKw = resolvePlatformActualResponseKw(
|
|
|
+ invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
|
|
|
+ invitation.getEstimatedResponseCapacityKw());
|
|
|
+
|
|
|
+ entity.setActualResponseGridKw(request.getActualResponseGridKw());
|
|
|
+ entity.setActualResponsePlatformKw(platformKw);
|
|
|
+ entity.setDeclaredClearedKw(declaredKw);
|
|
|
+ entity.setEstimatedCapacityKw(platformKw);
|
|
|
+ entity.setClearedCapacityKw(actualClearedKw);
|
|
|
+ entity.setTenantId(event.getTenantId() != null ? event.getTenantId() : invitation.getTenantId());
|
|
|
+
|
|
|
+ recalculate(entity, loadActivePriceAdjustments(
|
|
|
+ entity.getTenantId() != null ? entity.getTenantId() : SecurityUtils.getTenantId()));
|
|
|
+
|
|
|
+ Map<Long, String> invitationNoMap = Collections.singletonMap(invitation.getId(), invitation.getInvitationNo());
|
|
|
+ return toVo(entity, invitationNoMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public Boolean create(DrSubsidyPredictionRequest request) {
|
|
|
+ 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());
|
|
|
+ return subsidyPredictionMapper.insert(entity) > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public Boolean update(DrSubsidyPredictionRequest request) {
|
|
|
+ if (request == null || request.getId() == null) {
|
|
|
+ throw new BusinessException("主键ID不能为空");
|
|
|
+ }
|
|
|
+ VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectById(request.getId());
|
|
|
+ if (existing == null) {
|
|
|
+ throw new BusinessException("补贴预测记录不存在");
|
|
|
+ }
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ 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) {
|
|
|
+ if (id == null) {
|
|
|
+ throw new BusinessException("主键ID不能为空");
|
|
|
+ }
|
|
|
+ VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectById(id);
|
|
|
+ if (existing == null) {
|
|
|
+ throw new BusinessException("补贴预测记录不存在");
|
|
|
+ }
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ if (tenantId != null && existing.getTenantId() != null && !tenantId.equals(existing.getTenantId())) {
|
|
|
+ throw new BusinessException("补贴预测记录不存在");
|
|
|
+ }
|
|
|
+ return subsidyPredictionMapper.deleteById(id) > 0;
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
|
|
public void generateByEventId(Long drEventId) {
|
|
|
@@ -87,26 +245,22 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
throw new BusinessException("事件ID不能为空");
|
|
|
}
|
|
|
|
|
|
- // 查询事件
|
|
|
VppDrEvent event = eventMapper.selectById(drEventId);
|
|
|
if (event == null) {
|
|
|
log.warn("补贴预测生成跳过:事件不存在 drEventId={}", drEventId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 查询事件下所有邀约
|
|
|
List<VppDrInvitation> invitations = invitationMapper.selectList(
|
|
|
new LambdaQueryWrapper<VppDrInvitation>()
|
|
|
.eq(VppDrInvitation::getDrEventId, drEventId)
|
|
|
.eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
);
|
|
|
-
|
|
|
if (invitations.isEmpty()) {
|
|
|
log.info("补贴预测生成跳过:事件下无邀约 drEventId={}", drEventId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 批量查询站点名称
|
|
|
Set<Long> siteIds = invitations.stream()
|
|
|
.map(VppDrInvitation::getSiteId)
|
|
|
.filter(Objects::nonNull)
|
|
|
@@ -115,18 +269,25 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
: siteMapper.selectBatchIds(siteIds).stream()
|
|
|
.collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
|
|
|
|
|
|
- // 逐条计算并 upsert
|
|
|
+ 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);
|
|
|
+ VppDrSubsidyPrediction prediction = buildPrediction(
|
|
|
+ event, invitation, siteNameMap, customerNameMap, adjustments);
|
|
|
|
|
|
- // 按 invitation_id 唯一键,存在则更新,不存在则新增
|
|
|
VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectOne(
|
|
|
new LambdaQueryWrapper<VppDrSubsidyPrediction>()
|
|
|
.eq(VppDrSubsidyPrediction::getInvitationId, invitation.getId())
|
|
|
);
|
|
|
-
|
|
|
if (existing != null) {
|
|
|
prediction.setId(existing.getId());
|
|
|
+ prediction.setCreateTime(existing.getCreateTime());
|
|
|
prediction.setUpdateTime(LocalDateTime.now());
|
|
|
subsidyPredictionMapper.updateById(prediction);
|
|
|
} else {
|
|
|
@@ -139,71 +300,301 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
log.info("补贴预测生成完成 drEventId={}, invitationCount={}", drEventId, invitations.size());
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 构建单条补贴预测记录
|
|
|
- */
|
|
|
private VppDrSubsidyPrediction buildPrediction(VppDrEvent event, VppDrInvitation invitation,
|
|
|
- Map<Long, String> siteNameMap) {
|
|
|
+ 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.getOrDefault(invitation.getSiteId(), null));
|
|
|
+ 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());
|
|
|
|
|
|
- // 响应时长(分钟)
|
|
|
- int durationMin = 0;
|
|
|
- if (event.getStartTime() != null && event.getEndTime() != null
|
|
|
- && event.getEndTime().isAfter(event.getStartTime())) {
|
|
|
- durationMin = (int) Duration.between(event.getStartTime(), event.getEndTime()).toMinutes();
|
|
|
+ BigDecimal platformKw = resolvePlatformActualResponseKw(
|
|
|
+ invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
|
|
|
+ invitation.getEstimatedResponseCapacityKw());
|
|
|
+ BigDecimal gridKw = invitation.getActualResponseCapacityKw();
|
|
|
+ BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
|
|
|
+ ? invitation.getDeclaredCapacityKw()
|
|
|
+ : invitation.getActualClearedCapacityKw();
|
|
|
+ BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
|
|
|
+ ? invitation.getActualClearedCapacityKw()
|
|
|
+ : declaredKw;
|
|
|
+
|
|
|
+ p.setActualResponsePlatformKw(platformKw);
|
|
|
+ p.setActualResponseGridKw(gridKw);
|
|
|
+ p.setDeclaredClearedKw(declaredKw);
|
|
|
+ p.setEstimatedCapacityKw(platformKw);
|
|
|
+ p.setClearedCapacityKw(actualClearedKw);
|
|
|
+ p.setTenantId(event.getTenantId());
|
|
|
+ recalculate(p, adjustments);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 平台实际响应量:复用申报容量链路(可调峰资源点→设备→TSDB 响应时段有功功率均值)。
|
|
|
+ * TSDB 无数据时回退邀约上的预估响应容量。
|
|
|
+ */
|
|
|
+ private BigDecimal resolvePlatformActualResponseKw(Long siteId,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime,
|
|
|
+ BigDecimal fallbackKw) {
|
|
|
+ if (siteId == null || startTime == null || endTime == null || !endTime.isAfter(startTime)) {
|
|
|
+ return fallbackKw;
|
|
|
}
|
|
|
- p.setResponseDurationMin(durationMin);
|
|
|
+ try {
|
|
|
+ SiteDeclaredCapacityVO capacity = vppBaselineService.getSiteDeclaredCapacity(
|
|
|
+ siteId,
|
|
|
+ startTime.format(DATE_TIME_FMT),
|
|
|
+ endTime.format(DATE_TIME_FMT));
|
|
|
+ if (capacity != null && capacity.getEstimatedResponseCapacityKw() != null
|
|
|
+ && capacity.getEstimatedResponseCapacityKw().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ return capacity.getEstimatedResponseCapacityKw();
|
|
|
+ }
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.warn("查询平台实际响应量失败,回退邀约预估容量 siteId={}, err={}", siteId, ex.getMessage());
|
|
|
+ }
|
|
|
+ return fallbackKw;
|
|
|
+ }
|
|
|
|
|
|
- // 补贴标准
|
|
|
- p.setSubsidyPrice(event.getSubsidyPrice());
|
|
|
+ private VppDrInvitation findInvitationByNo(String invitationNo) {
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ VppDrInvitation invitation = invitationMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<VppDrInvitation>()
|
|
|
+ .eq(VppDrInvitation::getInvitationNo, invitationNo)
|
|
|
+ .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (invitation == null) {
|
|
|
+ throw new BusinessException("邀约不存在:" + invitationNo);
|
|
|
+ }
|
|
|
+ return invitation;
|
|
|
+ }
|
|
|
|
|
|
- // 预估响应容量
|
|
|
- p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
|
|
|
+ private void applyRequest(VppDrSubsidyPrediction entity, DrSubsidyPredictionRequest request) {
|
|
|
+ if (request.getInvitationId() != null) {
|
|
|
+ entity.setInvitationId(request.getInvitationId());
|
|
|
+ }
|
|
|
+ if (request.getDrEventId() != null) {
|
|
|
+ entity.setDrEventId(request.getDrEventId());
|
|
|
+ }
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+ if (request.getEventEndTime() != null) {
|
|
|
+ entity.setEventEndTime(request.getEventEndTime());
|
|
|
+ }
|
|
|
+ if (request.getSubsidyPrice() != null) {
|
|
|
+ entity.setSubsidyPrice(request.getSubsidyPrice());
|
|
|
+ }
|
|
|
+ if (request.getActualResponseGridKw() != null) {
|
|
|
+ entity.setActualResponseGridKw(request.getActualResponseGridKw());
|
|
|
+ }
|
|
|
+ if (request.getActualResponsePlatformKw() != null) {
|
|
|
+ entity.setActualResponsePlatformKw(request.getActualResponsePlatformKw());
|
|
|
+ entity.setEstimatedCapacityKw(request.getActualResponsePlatformKw());
|
|
|
+ }
|
|
|
+ if (request.getDeclaredClearedKw() != null) {
|
|
|
+ entity.setDeclaredClearedKw(request.getDeclaredClearedKw());
|
|
|
+ entity.setClearedCapacityKw(request.getDeclaredClearedKw());
|
|
|
+ }
|
|
|
+ if (request.getShareRatio() != null) {
|
|
|
+ entity.setShareRatio(normalizeShareRatio(request.getShareRatio()));
|
|
|
+ }
|
|
|
+ if (request.getResponseDurationMin() != null) {
|
|
|
+ entity.setResponseDurationMin(request.getResponseDurationMin());
|
|
|
+ }
|
|
|
+ if (request.getDeviationRate() != null) {
|
|
|
+ entity.setDeviationRate(request.getDeviationRate());
|
|
|
+ }
|
|
|
+ if (request.getCompletionRate() != null) {
|
|
|
+ entity.setCompletionRate(request.getCompletionRate());
|
|
|
+ }
|
|
|
+ if (request.getPriceAdjustmentCoefficient() != null) {
|
|
|
+ entity.setPriceAdjustmentCoefficient(request.getPriceAdjustmentCoefficient());
|
|
|
+ }
|
|
|
+ if (request.getEstimatedSubsidyAmount() != null) {
|
|
|
+ entity.setEstimatedSubsidyAmount(request.getEstimatedSubsidyAmount());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 实际出清
|
|
|
- p.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 预估完成率 = 预估容量 / 实际出清;实际出清为NULL或0时记为0
|
|
|
- BigDecimal completionRate = BigDecimal.ZERO;
|
|
|
- BigDecimal estimatedKw = invitation.getEstimatedResponseCapacityKw();
|
|
|
- BigDecimal actualKw = invitation.getActualClearedCapacityKw();
|
|
|
- if (estimatedKw != null && actualKw != null && actualKw.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
- completionRate = estimatedKw.divide(actualKw, 4, RoundingMode.HALF_UP);
|
|
|
+ private void fillSiteName(VppDrSubsidyPrediction entity) {
|
|
|
+ if (entity.getSiteId() == null) {
|
|
|
+ entity.setSiteName(null);
|
|
|
+ return;
|
|
|
}
|
|
|
- p.setCompletionRate(completionRate);
|
|
|
+ VppSite site = siteMapper.selectById(entity.getSiteId());
|
|
|
+ entity.setSiteName(site != null ? site.getSiteName() : null);
|
|
|
+ }
|
|
|
|
|
|
- // 预估偏差率 = 1 - 预估完成率
|
|
|
- BigDecimal deviationRate = BigDecimal.ONE.subtract(completionRate);
|
|
|
- p.setDeviationRate(deviationRate);
|
|
|
+ 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);
|
|
|
+ }
|
|
|
|
|
|
- // 预估补贴金额 = 补贴标准(为NULL时记为NULL)
|
|
|
- p.setEstimatedSubsidyAmount(event.getSubsidyPrice());
|
|
|
+ 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));
|
|
|
+ }
|
|
|
|
|
|
- p.setTenantId(event.getTenantId());
|
|
|
- return p;
|
|
|
+ private List<VppResponseDeviationPrice> loadActivePriceAdjustments(Integer tenantId) {
|
|
|
+ return priceAdjustmentMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<VppResponseDeviationPrice>()
|
|
|
+ .eq(VppResponseDeviationPrice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppResponseDeviationPrice::getTenantId, tenantId)
|
|
|
+ .orderByAsc(VppResponseDeviationPrice::getId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal resolvePriceAdjustmentCoefficient(BigDecimal completionRate,
|
|
|
+ List<VppResponseDeviationPrice> adjustments) {
|
|
|
+ if (adjustments != null) {
|
|
|
+ for (VppResponseDeviationPrice adjustment : adjustments) {
|
|
|
+ if (VppResponseCoefficientHelper.matches(
|
|
|
+ adjustment.getCoeffLower(),
|
|
|
+ adjustment.getCoeffUpper(),
|
|
|
+ adjustment.getLowerInclusive(),
|
|
|
+ adjustment.getUpperInclusive(),
|
|
|
+ completionRate)) {
|
|
|
+ return adjustment.getPriceAdjustmentCoefficient();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new BusinessException("未匹配到响应量偏差价格调整配置!");
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, String> loadInvitationNoMap(List<VppDrSubsidyPrediction> predictions) {
|
|
|
+ Set<Long> invitationIds = predictions.stream()
|
|
|
+ .map(VppDrSubsidyPrediction::getInvitationId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (invitationIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ return invitationMapper.selectBatchIds(invitationIds).stream()
|
|
|
+ .collect(Collectors.toMap(VppDrInvitation::getId, VppDrInvitation::getInvitationNo,
|
|
|
+ (left, right) -> left));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自动计算:响应时长、偏差率、完成率、补贴金额
|
|
|
+ * <ul>
|
|
|
+ * <li>完成率(%)= 实际响应容量(电网) / 实际出清 × 100(实际出清为 null 或 ≤0 时不计算)</li>
|
|
|
+ * <li>偏差率(%)= (1 − 完成率) × 100</li>
|
|
|
+ * <li>价格调整系数:按完成率比例匹配响应量偏差价格调整配置</li>
|
|
|
+ * <li>补贴金额 = 电网实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
|
|
|
+ * </ul>
|
|
|
+ */
|
|
|
+ private void recalculate(VppDrSubsidyPrediction entity,
|
|
|
+ List<VppResponseDeviationPrice> adjustments) {
|
|
|
+ int durationMin = 0;
|
|
|
+ if (entity.getEventStartTime() != null && entity.getEventEndTime() != null
|
|
|
+ && entity.getEventEndTime().isAfter(entity.getEventStartTime())) {
|
|
|
+ durationMin = (int) Duration.between(entity.getEventStartTime(), entity.getEventEndTime()).toMinutes();
|
|
|
+ }
|
|
|
+ entity.setResponseDurationMin(durationMin);
|
|
|
+
|
|
|
+ BigDecimal gridKw = entity.getActualResponseGridKw();
|
|
|
+ BigDecimal clearedKw = entity.getClearedCapacityKw() != null
|
|
|
+ ? entity.getClearedCapacityKw()
|
|
|
+ : entity.getDeclaredClearedKw();
|
|
|
+
|
|
|
+ if (clearedKw == null || clearedKw.compareTo(BigDecimal.ZERO) <= 0 || gridKw == null) {
|
|
|
+ entity.setDeviationRate(null);
|
|
|
+ entity.setCompletionRate(null);
|
|
|
+ entity.setPriceAdjustmentCoefficient(null);
|
|
|
+ entity.setEstimatedSubsidyAmount(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal completionRatio = gridKw.divide(clearedKw, 6, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal deviationRatio = BigDecimal.ONE.subtract(completionRatio);
|
|
|
+ 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 = gridKw
|
|
|
+ .multiply(hours)
|
|
|
+ .multiply(priceAdjustmentCoefficient)
|
|
|
+ .multiply(entity.getSubsidyPrice())
|
|
|
+ .setScale(4, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ entity.setEstimatedSubsidyAmount(subsidyAmount);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 分成比例统一为 0~1;若传入值 > 1 则按百分数换算 */
|
|
|
+ private BigDecimal normalizeShareRatio(BigDecimal raw) {
|
|
|
+ if (raw == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (raw.compareTo(BigDecimal.ONE) > 0) {
|
|
|
+ return raw.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ return raw;
|
|
|
}
|
|
|
|
|
|
- private DrSubsidyPredictionVO toVo(VppDrSubsidyPrediction entity) {
|
|
|
+ private DrSubsidyPredictionVO toVo(VppDrSubsidyPrediction entity, Map<Long, String> invitationNoMap) {
|
|
|
DrSubsidyPredictionVO vo = new DrSubsidyPredictionVO();
|
|
|
vo.setId(entity.getId());
|
|
|
vo.setInvitationId(entity.getInvitationId());
|
|
|
+ vo.setInvitationNo(invitationNoMap.get(entity.getInvitationId()));
|
|
|
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());
|
|
|
vo.setSubsidyPrice(entity.getSubsidyPrice());
|
|
|
vo.setEstimatedCapacityKw(entity.getEstimatedCapacityKw());
|
|
|
vo.setClearedCapacityKw(entity.getClearedCapacityKw());
|
|
|
+ vo.setActualResponseGridKw(entity.getActualResponseGridKw());
|
|
|
+ vo.setActualResponsePlatformKw(entity.getActualResponsePlatformKw());
|
|
|
+ vo.setDeclaredClearedKw(entity.getDeclaredClearedKw());
|
|
|
+ vo.setShareRatio(entity.getShareRatio());
|
|
|
+ vo.setPriceAdjustmentCoefficient(entity.getPriceAdjustmentCoefficient());
|
|
|
vo.setCompletionRate(entity.getCompletionRate());
|
|
|
vo.setDeviationRate(entity.getDeviationRate());
|
|
|
vo.setEstimatedSubsidyAmount(entity.getEstimatedSubsidyAmount());
|