|
|
@@ -4,15 +4,19 @@ 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.VppContract;
|
|
|
import com.usky.vpp.domain.VppDrEvent;
|
|
|
import com.usky.vpp.domain.VppDrInvitation;
|
|
|
import com.usky.vpp.domain.VppDrSubsidyPrediction;
|
|
|
import com.usky.vpp.domain.VppSite;
|
|
|
+import com.usky.vpp.mapper.VppContractMapper;
|
|
|
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.service.VppDrSubsidyPredictionService;
|
|
|
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
|
|
|
import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
|
|
|
import com.usky.vpp.util.VppAuditHelper;
|
|
|
import org.slf4j.Logger;
|
|
|
@@ -32,7 +36,6 @@ 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 +45,7 @@ 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);
|
|
|
|
|
|
@Autowired
|
|
|
private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
|
|
|
@@ -51,6 +55,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
private VppDrEventMapper eventMapper;
|
|
|
@Autowired
|
|
|
private VppSiteMapper siteMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppContractMapper contractMapper;
|
|
|
|
|
|
@Override
|
|
|
public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
|
|
|
@@ -60,6 +66,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
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);
|
|
|
}
|
|
|
@@ -72,7 +80,6 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
wrapper.orderByDesc(VppDrSubsidyPrediction::getCreateTime);
|
|
|
|
|
|
Page<VppDrSubsidyPrediction> result = subsidyPredictionMapper.selectPage(page, wrapper);
|
|
|
-
|
|
|
List<DrSubsidyPredictionVO> list = result.getRecords().stream()
|
|
|
.map(this::toVo)
|
|
|
.collect(Collectors.toList());
|
|
|
@@ -80,6 +87,73 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
return new CommonPage<>(list, result.getTotal(), size != null ? size : 20, current != null ? current : 1);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public Boolean create(DrSubsidyPredictionRequest request) {
|
|
|
+ if (request == null) {
|
|
|
+ throw new BusinessException("请求参数不能为空");
|
|
|
+ }
|
|
|
+ if (request.getSiteId() == null) {
|
|
|
+ throw new BusinessException("站点不能为空");
|
|
|
+ }
|
|
|
+ if (request.getEventStartTime() == null || request.getEventEndTime() == null) {
|
|
|
+ throw new BusinessException("响应起止时间不能为空");
|
|
|
+ }
|
|
|
+ if (!request.getEventEndTime().isAfter(request.getEventStartTime())) {
|
|
|
+ throw new BusinessException("响应结束时间必须晚于开始时间");
|
|
|
+ }
|
|
|
+
|
|
|
+ VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
|
|
|
+ applyRequest(entity, request);
|
|
|
+ fillSiteName(entity);
|
|
|
+ recalculate(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("补贴预测记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ applyRequest(existing, request);
|
|
|
+ if (request.getSiteId() != null) {
|
|
|
+ fillSiteName(existing);
|
|
|
+ }
|
|
|
+ recalculate(existing);
|
|
|
+ existing.setUpdateTime(LocalDateTime.now());
|
|
|
+ return subsidyPredictionMapper.updateById(existing) > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @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 +161,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 +185,22 @@ 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, BigDecimal> shareRatioMap = loadCustomerShareRatio(customerIds);
|
|
|
+
|
|
|
for (VppDrInvitation invitation : invitations) {
|
|
|
- VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap);
|
|
|
+ VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap, shareRatioMap);
|
|
|
|
|
|
- // 按 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,56 +213,158 @@ 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, BigDecimal> shareRatioMap) {
|
|
|
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.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 = invitation.getEstimatedResponseCapacityKw();
|
|
|
+ BigDecimal gridKw = invitation.getActualResponseCapacityKw();
|
|
|
+ BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
|
|
|
+ ? invitation.getDeclaredCapacityKw()
|
|
|
+ : invitation.getActualClearedCapacityKw();
|
|
|
+
|
|
|
+ p.setActualResponsePlatformKw(platformKw);
|
|
|
+ p.setActualResponseGridKw(gridKw);
|
|
|
+ p.setDeclaredClearedKw(declaredKw);
|
|
|
+ p.setEstimatedCapacityKw(platformKw);
|
|
|
+ p.setClearedCapacityKw(declaredKw);
|
|
|
+ p.setShareRatio(normalizeShareRatio(shareRatioMap.get(invitation.getCustomerId())));
|
|
|
+ p.setTenantId(event.getTenantId());
|
|
|
+ recalculate(p);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, BigDecimal> loadCustomerShareRatio(Set<Long> customerIds) {
|
|
|
+ if (customerIds == null || customerIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ List<VppContract> contracts = contractMapper.selectList(new LambdaQueryWrapper<VppContract>()
|
|
|
+ .in(VppContract::getCustomerId, customerIds)
|
|
|
+ .eq(VppContract::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .orderByDesc(VppContract::getCreateTime));
|
|
|
+ Map<Long, BigDecimal> map = new java.util.HashMap<>();
|
|
|
+ for (VppContract contract : contracts) {
|
|
|
+ if (contract.getCustomerId() == null || map.containsKey(contract.getCustomerId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (contract.getCustomerRatio() != null) {
|
|
|
+ map.put(contract.getCustomerId(), contract.getCustomerRatio());
|
|
|
+ }
|
|
|
}
|
|
|
- p.setResponseDurationMin(durationMin);
|
|
|
+ return map;
|
|
|
+ }
|
|
|
|
|
|
- // 补贴标准
|
|
|
- p.setSubsidyPrice(event.getSubsidyPrice());
|
|
|
+ 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.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()));
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 预估响应容量
|
|
|
- p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
|
|
|
+ private void fillSiteName(VppDrSubsidyPrediction entity) {
|
|
|
+ if (entity.getSiteId() == null) {
|
|
|
+ entity.setSiteName(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ VppSite site = siteMapper.selectById(entity.getSiteId());
|
|
|
+ entity.setSiteName(site != null ? site.getSiteName() : null);
|
|
|
+ }
|
|
|
|
|
|
- // 实际出清
|
|
|
- p.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
|
|
|
+ /**
|
|
|
+ * 自动计算:响应时长、偏差率、完成率、补贴金额
|
|
|
+ * <ul>
|
|
|
+ * <li>偏差率 = (电网实际响应量 − 平台实际响应量) / 申报出清量</li>
|
|
|
+ * <li>完成率 = 平台实际响应量 / 申报出清量</li>
|
|
|
+ * <li>补贴金额 = 平台实际响应量 × 响应时长(h) × 补贴单价 × 分成比例</li>
|
|
|
+ * </ul>
|
|
|
+ */
|
|
|
+ private void recalculate(VppDrSubsidyPrediction entity) {
|
|
|
+ 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 platformKw = entity.getActualResponsePlatformKw() != null
|
|
|
+ ? entity.getActualResponsePlatformKw()
|
|
|
+ : entity.getEstimatedCapacityKw();
|
|
|
+ BigDecimal gridKw = entity.getActualResponseGridKw();
|
|
|
+ BigDecimal declaredKw = entity.getDeclaredClearedKw() != null
|
|
|
+ ? entity.getDeclaredClearedKw()
|
|
|
+ : entity.getClearedCapacityKw();
|
|
|
|
|
|
- // 预估完成率 = 预估容量 / 实际出清;实际出清为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);
|
|
|
+ BigDecimal deviationRate = BigDecimal.ZERO;
|
|
|
+ if (declaredKw != null && declaredKw.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ if (platformKw != null) {
|
|
|
+ completionRate = platformKw.divide(declaredKw, 4, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ if (gridKw != null && platformKw != null) {
|
|
|
+ deviationRate = gridKw.subtract(platformKw).divide(declaredKw, 4, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
}
|
|
|
- p.setCompletionRate(completionRate);
|
|
|
-
|
|
|
- // 预估偏差率 = 1 - 预估完成率
|
|
|
- BigDecimal deviationRate = BigDecimal.ONE.subtract(completionRate);
|
|
|
- p.setDeviationRate(deviationRate);
|
|
|
+ entity.setCompletionRate(completionRate);
|
|
|
+ entity.setDeviationRate(deviationRate);
|
|
|
|
|
|
- // 预估补贴金额 = 补贴标准(为NULL时记为NULL)
|
|
|
- p.setEstimatedSubsidyAmount(event.getSubsidyPrice());
|
|
|
+ BigDecimal subsidyAmount = null;
|
|
|
+ BigDecimal shareRatio = normalizeShareRatio(entity.getShareRatio());
|
|
|
+ if (platformKw != null && entity.getSubsidyPrice() != null && durationMin > 0 && shareRatio != null) {
|
|
|
+ BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
|
|
|
+ subsidyAmount = platformKw
|
|
|
+ .multiply(hours)
|
|
|
+ .multiply(entity.getSubsidyPrice())
|
|
|
+ .multiply(shareRatio)
|
|
|
+ .setScale(4, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ entity.setEstimatedSubsidyAmount(subsidyAmount);
|
|
|
+ }
|
|
|
|
|
|
- p.setTenantId(event.getTenantId());
|
|
|
- return p;
|
|
|
+ /** 分成比例统一为 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) {
|
|
|
@@ -204,6 +380,10 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
|
|
|
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.setCompletionRate(entity.getCompletionRate());
|
|
|
vo.setDeviationRate(entity.getDeviationRate());
|
|
|
vo.setEstimatedSubsidyAmount(entity.getEstimatedSubsidyAmount());
|