|
|
@@ -12,9 +12,12 @@ import com.usky.vpp.domain.VppResponseDeviationPrice;
|
|
|
import com.usky.vpp.domain.VppSite;
|
|
|
import com.usky.vpp.mapper.*;
|
|
|
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;
|
|
|
@@ -29,6 +32,7 @@ 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;
|
|
|
@@ -44,6 +48,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
|
|
|
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;
|
|
|
@@ -55,6 +60,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
private VppSiteMapper siteMapper;
|
|
|
@Autowired
|
|
|
private VppResponseDeviationPriceMapper priceAdjustmentMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppBaselineService vppBaselineService;
|
|
|
|
|
|
@Override
|
|
|
public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
|
|
|
@@ -87,25 +94,62 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
- public Boolean create(DrSubsidyPredictionRequest request) {
|
|
|
+ public DrSubsidyPredictionVO calculate(DrSubsidyPredictionCalculateRequest request) {
|
|
|
if (request == null) {
|
|
|
throw new BusinessException("请求参数不能为空");
|
|
|
}
|
|
|
- if (request.getSiteId() == null) {
|
|
|
- throw new BusinessException("站点不能为空");
|
|
|
+ if (!StringUtils.hasText(request.getInvitationNo())) {
|
|
|
+ throw new BusinessException("邀约编号不能为空");
|
|
|
}
|
|
|
- if (request.getEventStartTime() == null || request.getEventEndTime() == null) {
|
|
|
- throw new BusinessException("响应起止时间不能为空");
|
|
|
+ if (request.getActualResponseGridKw() == null) {
|
|
|
+ throw new BusinessException("实际响应容量(电网)不能为空");
|
|
|
}
|
|
|
- if (!request.getEventEndTime().isAfter(request.getEventStartTime())) {
|
|
|
- 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.setEventStartTime(event.getStartTime());
|
|
|
+ entity.setEventEndTime(event.getEndTime());
|
|
|
+ entity.setSubsidyPrice(event.getSubsidyPrice());
|
|
|
+
|
|
|
+ BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
|
|
|
+ ? invitation.getDeclaredCapacityKw()
|
|
|
+ : invitation.getActualClearedCapacityKw();
|
|
|
+ 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(declaredKw);
|
|
|
+ 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("请求参数不能为空");
|
|
|
+ }
|
|
|
VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
|
|
|
applyRequest(entity, request);
|
|
|
fillSiteName(entity);
|
|
|
- recalculate(entity, loadActivePriceAdjustments(SecurityUtils.getTenantId()));
|
|
|
entity.setTenantId(SecurityUtils.getTenantId());
|
|
|
entity.setCreateTime(LocalDateTime.now());
|
|
|
entity.setUpdateTime(LocalDateTime.now());
|
|
|
@@ -131,7 +175,6 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
if (request.getSiteId() != null) {
|
|
|
fillSiteName(existing);
|
|
|
}
|
|
|
- recalculate(existing, loadActivePriceAdjustments(tenantId != null ? tenantId : existing.getTenantId()));
|
|
|
existing.setUpdateTime(LocalDateTime.now());
|
|
|
return subsidyPredictionMapper.updateById(existing) > 0;
|
|
|
}
|
|
|
@@ -220,7 +263,9 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
p.setEventEndTime(event.getEndTime());
|
|
|
p.setSubsidyPrice(event.getSubsidyPrice());
|
|
|
|
|
|
- BigDecimal platformKw = invitation.getEstimatedResponseCapacityKw();
|
|
|
+ BigDecimal platformKw = resolvePlatformActualResponseKw(
|
|
|
+ invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
|
|
|
+ invitation.getEstimatedResponseCapacityKw());
|
|
|
BigDecimal gridKw = invitation.getActualResponseCapacityKw();
|
|
|
BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
|
|
|
? invitation.getDeclaredCapacityKw()
|
|
|
@@ -236,6 +281,46 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
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;
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
private void applyRequest(VppDrSubsidyPrediction entity, DrSubsidyPredictionRequest request) {
|
|
|
if (request.getInvitationId() != null) {
|
|
|
entity.setInvitationId(request.getInvitationId());
|
|
|
@@ -269,6 +354,21 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
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());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private void fillSiteName(VppDrSubsidyPrediction entity) {
|
|
|
@@ -292,13 +392,17 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
List<VppResponseDeviationPrice> adjustments) {
|
|
|
if (adjustments != null) {
|
|
|
for (VppResponseDeviationPrice adjustment : adjustments) {
|
|
|
- if (VppResponseCoefficientHelper.matches(adjustment.getResponseCoefficient(), completionRate)) {
|
|
|
+ if (VppResponseCoefficientHelper.matches(
|
|
|
+ adjustment.getCoeffLower(),
|
|
|
+ adjustment.getCoeffUpper(),
|
|
|
+ adjustment.getLowerInclusive(),
|
|
|
+ adjustment.getUpperInclusive(),
|
|
|
+ completionRate)) {
|
|
|
return adjustment.getPriceAdjustmentCoefficient();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- throw new BusinessException("完成率 " + completionRate.stripTrailingZeros().toPlainString()
|
|
|
- + " 未匹配到响应量偏差价格调整配置");
|
|
|
+ throw new BusinessException("未匹配到响应量偏差价格调整配置!");
|
|
|
}
|
|
|
|
|
|
private Map<Long, String> loadInvitationNoMap(List<VppDrSubsidyPrediction> predictions) {
|
|
|
@@ -317,9 +421,9 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
/**
|
|
|
* 自动计算:响应时长、偏差率、完成率、补贴金额
|
|
|
* <ul>
|
|
|
- * <li>偏差率 = (电网实际响应量 − 平台实际响应量) / 申报出清量</li>
|
|
|
- * <li>完成率 = 1 − 偏差率</li>
|
|
|
- * <li>补贴金额 = 实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
|
|
|
+ * <li>完成率(%)= 平台实际响应量 / 申报出清量 × 100(≥0,正常 0~100,超发可 >100)</li>
|
|
|
+ * <li>偏差率(%)= (电网实际响应量 − 平台实际响应量) / 申报出清量 × 100(可正可负)</li>
|
|
|
+ * <li>补贴金额 = 平台实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
|
|
|
* </ul>
|
|
|
*/
|
|
|
private void recalculate(VppDrSubsidyPrediction entity,
|
|
|
@@ -340,7 +444,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
: entity.getClearedCapacityKw();
|
|
|
|
|
|
if (declaredKw == null || declaredKw.compareTo(BigDecimal.ZERO) <= 0
|
|
|
- || gridKw == null || platformKw == null) {
|
|
|
+ || platformKw == null || gridKw == null) {
|
|
|
entity.setDeviationRate(null);
|
|
|
entity.setCompletionRate(null);
|
|
|
entity.setPriceAdjustmentCoefficient(null);
|
|
|
@@ -348,20 +452,24 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- BigDecimal deviationRate = gridKw.subtract(platformKw)
|
|
|
- .divide(declaredKw, 4, RoundingMode.HALF_UP);
|
|
|
- BigDecimal completionRate = BigDecimal.ONE.subtract(deviationRate)
|
|
|
- .setScale(4, RoundingMode.HALF_UP);
|
|
|
- BigDecimal priceAdjustmentCoefficient = resolvePriceAdjustmentCoefficient(completionRate, adjustments);
|
|
|
- entity.setDeviationRate(deviationRate);
|
|
|
- entity.setCompletionRate(completionRate);
|
|
|
+ // 比例:完成率用于匹配响应量偏差价格调整配置(区间按 0~1+ 配置)
|
|
|
+ BigDecimal completionRatio = platformKw.divide(declaredKw, 6, RoundingMode.HALF_UP);
|
|
|
+ if (completionRatio.compareTo(BigDecimal.ZERO) < 0) {
|
|
|
+ completionRatio = BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+ BigDecimal deviationRatio = gridKw.subtract(platformKw)
|
|
|
+ .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 actualResponseKw = gridKw;
|
|
|
BigDecimal subsidyAmount = null;
|
|
|
if (entity.getSubsidyPrice() != null && durationMin > 0) {
|
|
|
BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
|
|
|
- subsidyAmount = actualResponseKw
|
|
|
+ subsidyAmount = platformKw
|
|
|
.multiply(hours)
|
|
|
.multiply(priceAdjustmentCoefficient)
|
|
|
.multiply(entity.getSubsidyPrice())
|