Pārlūkot izejas kodu

feat(service): 新增计算用户可用充电金额功能

- 在AppletHomeService接口添加calculateAvailableChargingAmount方法声明
- 实现跨时段电费、服务费、运营费及增值费用的复杂计算逻辑
- 引入安全价扣除及企业用户优惠区别处理
- 查询并利用字典表及价格策略明细,支持不同时段收费规则
- 实现用户登录校验和余额校验,保证计算安全性
- 在AppletConnectorDetailVO中新增isFirmUser字段及相关描述
- 更新数据库映射及SQL,支持企业价格及常规价格计算展示
- 优化日志记录,便于调试和跟踪计算过程
SheepHy 2 dienas atpakaļ
vecāks
revīzija
094d8afb13

+ 5 - 2
src/main/java/com/zsElectric/boot/business/model/vo/applet/AppletConnectorDetailVO.java

@@ -88,12 +88,15 @@ public class AppletConnectorDetailVO implements Serializable {
     @Schema(description = "国家标准名称")
     private String nationalStandardName;
 
-    @Schema(description = "当前价格(元/度)")
+    @Schema(description = "常规价格(元/度),电价+服务费+常规运营费+增值费用")
     private BigDecimal currentPrice;
 
-    @Schema(description = "企业价格(元/度),仅企业用户且有企业价时返回")
+    @Schema(description = "企业价格(元/度),电价+服务费+企业运营费+增值费用,仅企业用户有值")
     private BigDecimal enterprisePrice;
 
+    @Schema(description = "是否企业用户")
+    private Boolean isFirmUser;
+
     @Schema(description = "当前时段描述")
     private String currentPeriodDesc;
 

+ 15 - 0
src/main/java/com/zsElectric/boot/business/service/AppletHomeService.java

@@ -81,4 +81,19 @@ public interface AppletHomeService {
      */
     AppletConnectorDetailVO getConnectorDetail(Long connectorId);
 
+    /**
+     * 计算当前用户可用充电金额
+     * 公式:可用余额 = 当前用户余额 - 安全价 - [((当前用户余额 - 安全价) / (电费 + 服务费)) × (运营费 + 增值费用)]
+     * 说明:
+     * 1. 先计算可充电度数 = (余额 - 安全价) / (电费 + 服务费)
+     * 2. 根据度数计算运营费总额 = 度数 × 运营费
+     * 3. 根据度数计算增值费用总额 = 度数 × 增值费用
+     * 4. 可用余额 = 余额 - 安全价 - 运营费总额 - 增值费用总额
+     * 注意:需要处理跨时段情况,不同时段价格不同,需要分段计算
+     * 
+     * @param connectorId 充电设备接口ID
+     * @return 可用充电金额(元)
+     */
+    BigDecimal calculateAvailableChargingAmount(Long connectorId);
+
 }

+ 224 - 0
src/main/java/com/zsElectric/boot/business/service/impl/AppletHomeServiceImpl.java

@@ -10,6 +10,7 @@ import com.zsElectric.boot.business.mapper.PolicyFeeMapper;
 import com.zsElectric.boot.business.mapper.ThirdPartyStationInfoMapper;
 import com.zsElectric.boot.business.mapper.UserAccountMapper;
 import com.zsElectric.boot.business.mapper.UserFirmMapper;
+import com.zsElectric.boot.system.mapper.DictItemMapper;
 import com.zsElectric.boot.charging.entity.ThirdPartyChargeStatus;
 import com.zsElectric.boot.charging.entity.ThirdPartyConnectorInfo;
 import com.zsElectric.boot.charging.entity.ThirdPartyEquipmentInfo;
@@ -27,6 +28,7 @@ import com.zsElectric.boot.business.model.entity.DiscountsActivity;
 import com.zsElectric.boot.business.model.entity.PolicyFee;
 import com.zsElectric.boot.business.model.entity.UserAccount;
 import com.zsElectric.boot.business.model.entity.UserFirm;
+import com.zsElectric.boot.system.model.entity.DictItem;
 import com.zsElectric.boot.business.model.query.StationInfoQuery;
 import com.zsElectric.boot.business.model.vo.AppletConnectorListVO;
 import com.zsElectric.boot.business.model.vo.AppletStationDetailVO;
@@ -45,6 +47,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
@@ -71,6 +74,7 @@ public class AppletHomeServiceImpl implements AppletHomeService {
     private final ThirdPartyChargeStatusMapper thirdPartyChargeStatusMapper;
     private final ChargeOrderInfoMapper chargeOrderInfoMapper;
     private final DiscountsActivityMapper discountsActivityMapper;
+    private final DictItemMapper dictItemMapper;
 
     /**
      * 时间格式化器 HHmmss
@@ -750,5 +754,225 @@ public class AppletHomeServiceImpl implements AppletHomeService {
         return result;
     }
 
+    @Override
+    public BigDecimal calculateAvailableChargingAmount(Long connectorId) {
+        // 获取当前登录用户ID
+        Long userId = SecurityUtils.getUserId();
+        if (userId == null) {
+            log.warn("未登录用户,无法计算可用充电金额");
+            return BigDecimal.ZERO;
+        }
+        
+        // 1. 查询当前用户余额
+        UserAccount userAccount = userAccountMapper.selectOne(
+                new LambdaQueryWrapper<UserAccount>()
+                        .eq(UserAccount::getUserId, userId)
+                        .eq(UserAccount::getIsDeleted, 0)
+        );
+        if (userAccount == null || userAccount.getBalance() == null) {
+            log.warn("用户账户不存在或余额为空 - userId: {}", userId);
+            return BigDecimal.ZERO;
+        }
+        BigDecimal userBalance = userAccount.getBalance();
+        
+        // 2. 从字典表查询安全价(safety_fee)
+        DictItem safetyFeeItem = dictItemMapper.selectOne(
+                new LambdaQueryWrapper<DictItem>()
+                        .eq(DictItem::getDictCode, "safety_fee")
+                        .eq(DictItem::getStatus, 1)
+                        .last("LIMIT 1")
+        );
+        BigDecimal safetyFee = safetyFeeItem != null && safetyFeeItem.getValue() != null 
+                ? new BigDecimal(safetyFeeItem.getValue()) 
+                : BigDecimal.ZERO;
+        
+        // 计算扣除安全价后的余额
+        BigDecimal balanceAfterSafety = userBalance.subtract(safetyFee);
+        if (balanceAfterSafety.compareTo(BigDecimal.ZERO) <= 0) {
+            log.info("用户余额不足 - userId: {}, balance: {}, safetyFee: {}", userId, userBalance, safetyFee);
+            return BigDecimal.ZERO;
+        }
+        
+        // 3. 查询充电接口、设备、站点信息
+        ThirdPartyConnectorInfo connectorInfo = thirdPartyConnectorInfoMapper.selectById(connectorId);
+        if (connectorInfo == null) {
+            log.warn("充电接口不存在 - connectorId: {}", connectorId);
+            return BigDecimal.ZERO;
+        }
+        
+        ThirdPartyEquipmentInfo equipmentInfo = thirdPartyEquipmentInfoMapper.selectOne(
+                new LambdaQueryWrapper<ThirdPartyEquipmentInfo>()
+                        .eq(ThirdPartyEquipmentInfo::getEquipmentId, connectorInfo.getEquipmentId())
+                        .eq(ThirdPartyEquipmentInfo::getIsDeleted, 0)
+                        .last("LIMIT 1")
+        );
+        if (equipmentInfo == null) {
+            log.warn("设备信息不存在 - equipmentId: {}", connectorInfo.getEquipmentId());
+            return BigDecimal.ZERO;
+        }
+        
+        ThirdPartyStationInfo stationInfo = thirdPartyStationInfoMapper.selectOne(
+                new LambdaQueryWrapper<ThirdPartyStationInfo>()
+                        .eq(ThirdPartyStationInfo::getStationId, equipmentInfo.getStationId())
+                        .eq(ThirdPartyStationInfo::getIsDeleted, 0)
+                        .last("LIMIT 1")
+        );
+        if (stationInfo == null) {
+            log.warn("站点信息不存在 - stationId: {}", equipmentInfo.getStationId());
+            return BigDecimal.ZERO;
+        }
+        
+        // 4. 查询价格策略
+        ThirdPartyEquipmentPricePolicy pricePolicy = thirdPartyEquipmentPricePolicyMapper.selectOne(
+                new LambdaQueryWrapper<ThirdPartyEquipmentPricePolicy>()
+                        .eq(ThirdPartyEquipmentPricePolicy::getConnectorId, connectorInfo.getConnectorId())
+                        .eq(ThirdPartyEquipmentPricePolicy::getIsDeleted, 0)
+                        .last("LIMIT 1")
+        );
+        if (pricePolicy == null) {
+            log.warn("价格策略不存在 - connectorId: {}", connectorInfo.getConnectorId());
+            return BigDecimal.ZERO;
+        }
+        
+        // 5. 查询所有时段的价格信息(按开始时间排序)
+        List<ThirdPartyPolicyInfo> policyInfoList = thirdPartyPolicyInfoMapper.selectList(
+                new LambdaQueryWrapper<ThirdPartyPolicyInfo>()
+                        .eq(ThirdPartyPolicyInfo::getPricePolicyId, pricePolicy.getId())
+                        .eq(ThirdPartyPolicyInfo::getIsDeleted, 0)
+                        .orderByAsc(ThirdPartyPolicyInfo::getStartTime)
+        );
+        if (policyInfoList.isEmpty()) {
+            log.warn("价格策略明细不存在 - pricePolicyId: {}", pricePolicy.getId());
+            return BigDecimal.ZERO;
+        }
+        
+        // 6. 查询用户是否为企业用户
+        UserFirm userFirm = userFirmMapper.selectOne(
+                new LambdaQueryWrapper<UserFirm>()
+                        .eq(UserFirm::getUserId, userId)
+                        .eq(UserFirm::getIsDeleted, 0)
+                        .last("LIMIT 1")
+        );
+        
+        // 7. 跨时段计算可用充电金额
+        // 公式:可用余额 = 当前用户余额 - 安全价 - [(度数 × 运营费) + (度数 × 增值费用)]
+        // 其中:度数 = (余额 - 安全价) / (电费 + 服务费)
+        
+        BigDecimal remainingBalance = balanceAfterSafety; // 剩余可用余额(用于计算度数)
+        BigDecimal totalOpFeeCost = BigDecimal.ZERO; // 总运营费
+        BigDecimal totalValueAddedCost = BigDecimal.ZERO; // 总增值费用
+        
+        // 遍历每个时段,计算在该时段能充多少度电及对应的费用
+        for (ThirdPartyPolicyInfo policyInfo : policyInfoList) {
+            // 获取电费、服务费
+            BigDecimal elecPrice = policyInfo.getElecPrice() != null ? policyInfo.getElecPrice() : BigDecimal.ZERO;
+            BigDecimal servicePrice = policyInfo.getServicePrice() != null ? policyInfo.getServicePrice() : BigDecimal.ZERO;
+            BigDecimal basePrice = elecPrice.add(servicePrice); // 基础价格 = 电费 + 服务费
+            
+            if (basePrice.compareTo(BigDecimal.ZERO) == 0) {
+                continue; // 跳过基础价格为0的时段
+            }
+            
+            // 查询该时段的运营费
+            PolicyFee policyFee;
+            if (userFirm != null) {
+                policyFee = policyFeeMapper.selectOne(
+                        new LambdaQueryWrapper<PolicyFee>()
+                                .eq(PolicyFee::getStationInfoId, stationInfo.getId())
+                                .eq(PolicyFee::getStartTime, policyInfo.getStartTime())
+                                .eq(PolicyFee::getSalesType, 1)
+                                .eq(PolicyFee::getFirmId, userFirm.getFirmId())
+                                .eq(PolicyFee::getIsDeleted, 0)
+                                .last("LIMIT 1")
+                );
+            } else {
+                policyFee = policyFeeMapper.selectOne(
+                        new LambdaQueryWrapper<PolicyFee>()
+                                .eq(PolicyFee::getStationInfoId, stationInfo.getId())
+                                .eq(PolicyFee::getStartTime, policyInfo.getStartTime())
+                                .eq(PolicyFee::getSalesType, 0)
+                                .eq(PolicyFee::getIsDeleted, 0)
+                                .last("LIMIT 1")
+                );
+            }
+            
+            BigDecimal opFee = (policyFee != null && policyFee.getOpFee() != null) 
+                    ? policyFee.getOpFee() 
+                    : BigDecimal.ZERO;
+            
+            // 查询增值费用
+            BigDecimal valueAddedFee = BigDecimal.ZERO;
+            if (policyInfo.getPeriodFlag() != null) {
+                String periodLabel = "";
+                switch (policyInfo.getPeriodFlag()) {
+                    case 1: periodLabel = "尖"; break;
+                    case 2: periodLabel = "峰"; break;
+                    case 3: periodLabel = "平"; break;
+                    case 4: periodLabel = "谷"; break;
+                }
+                
+                if (!periodLabel.isEmpty()) {
+                    DictItem valueAddedItem = dictItemMapper.selectOne(
+                            new LambdaQueryWrapper<DictItem>()
+                                    .eq(DictItem::getDictCode, "time_period_flag")
+                                    .eq(DictItem::getLabel, periodLabel)
+                                    .eq(DictItem::getStatus, 1)
+                                    .last("LIMIT 1")
+                    );
+                    if (valueAddedItem != null && valueAddedItem.getValue() != null) {
+                        valueAddedFee = new BigDecimal(valueAddedItem.getValue());
+                    }
+                }
+            }
+            
+            // 计算在当前时段能充多少度电(使用剩余余额)
+            // 度数 = 剩余余额 / (电费 + 服务费)
+            BigDecimal kwh = remainingBalance.divide(basePrice, 4, RoundingMode.HALF_UP);
+            
+            // 如果剩余余额不足以充电,结束计算
+            if (kwh.compareTo(BigDecimal.ZERO) <= 0) {
+                break;
+            }
+            
+            // 计算该时段的运营费和增值费用
+            BigDecimal periodOpFeeCost = kwh.multiply(opFee);
+            BigDecimal periodValueAddedCost = kwh.multiply(valueAddedFee);
+            
+            totalOpFeeCost = totalOpFeeCost.add(periodOpFeeCost);
+            totalValueAddedCost = totalValueAddedCost.add(periodValueAddedCost);
+            
+            // 更新剩余余额(减去当前时段的所有费用)
+            // 当前时段总费用 = (电费 + 服务费) * 度数 + 运营费 + 增值费用
+            BigDecimal periodBaseCost = kwh.multiply(basePrice);
+            BigDecimal periodTotalCost = periodBaseCost.add(periodOpFeeCost).add(periodValueAddedCost);
+            remainingBalance = remainingBalance.subtract(periodTotalCost);
+            
+            log.debug("时段计算 - startTime: {}, 电费: {}, 服务费: {}, 运营费: {}, 增值费: {}, 度数: {}, 该时段费用: {}, 剩余余额: {}",
+                    policyInfo.getStartTime(), elecPrice, servicePrice, opFee, valueAddedFee, kwh, periodTotalCost, remainingBalance);
+            
+            // 如果剩余余额小于等于0,结束计算
+            if (remainingBalance.compareTo(BigDecimal.ZERO) <= 0) {
+                break;
+            }
+        }
+        
+        // 8. 计算最终可用余额
+        BigDecimal availableAmount = userBalance.subtract(safetyFee).subtract(totalOpFeeCost).subtract(totalValueAddedCost);
+        
+        // 确保不为负数
+        if (availableAmount.compareTo(BigDecimal.ZERO) < 0) {
+            availableAmount = BigDecimal.ZERO;
+        }
+        
+        // 保留 2 位小数
+        availableAmount = availableAmount.setScale(2, RoundingMode.HALF_UP);
+        
+        log.info("可用充电金额计算完成 - userId: {}, connectorId: {}, userBalance: {}, safetyFee: {}, " +
+                        "totalOpFeeCost: {}, totalValueAddedCost: {}, availableAmount: {}",
+                userId, connectorId, userBalance, safetyFee, totalOpFeeCost, totalValueAddedCost, availableAmount);
+        
+        return availableAmount;
+    }
+
 
 }

+ 53 - 6
src/main/resources/mapper/charging/ThirdPartyConnectorInfoMapper.xml

@@ -28,6 +28,7 @@
         <result property="nationalStandardName" column="national_standard_name"/>
         <result property="currentPrice" column="current_price"/>
         <result property="enterprisePrice" column="enterprise_price"/>
+        <result property="isFirmUser" column="is_firm_user"/>
         <result property="currentPeriodDesc" column="current_period_desc"/>
         <result property="parkingTips" column="parking_tips"/>
         <result property="availableBalance" column="available_balance"/>
@@ -95,8 +96,52 @@
             tpsi.address AS station_address,
             tpsi.station_tips AS parking_tips,
             
-            -- 当前价格(电价 + 服务费)
-            (IFNULL(tppi.elec_price, 0) + IFNULL(tppi.service_price, 0)) AS current_price,
+            -- 常规价格(电价 + 服务费 + 常规运营费 + 增值费用)
+            ROUND(
+                IFNULL(tppi.elec_price, 0) + IFNULL(tppi.service_price, 0) + 
+                IFNULL(pf_common.op_fee, 0) + 
+                IFNULL(
+                    (SELECT CAST(di.value AS DECIMAL(10,4))
+                     FROM sys_dict_item di
+                     WHERE di.dict_code = 'time_period_flag'
+                       AND di.status = 1
+                       AND di.label = CASE tppi.period_flag
+                           WHEN 1 THEN '尖'
+                           WHEN 2 THEN '峰'
+                           WHEN 3 THEN '平'
+                           WHEN 4 THEN '谷'
+                           ELSE ''
+                       END
+                     LIMIT 1),
+                    0
+                ),
+                4
+            ) AS current_price,
+            
+            -- 企业价格(电价 + 服务费 + 企业运营费 + 增值费用)
+            ROUND(
+                IFNULL(tppi.elec_price, 0) + IFNULL(tppi.service_price, 0) + 
+                IFNULL(pf.op_fee, 0) + 
+                IFNULL(
+                    (SELECT CAST(di.value AS DECIMAL(10,4))
+                     FROM sys_dict_item di
+                     WHERE di.dict_code = 'time_period_flag'
+                       AND di.status = 1
+                       AND di.label = CASE tppi.period_flag
+                           WHEN 1 THEN '尖'
+                           WHEN 2 THEN '峰'
+                           WHEN 3 THEN '平'
+                           WHEN 4 THEN '谷'
+                           ELSE ''
+                       END
+                     LIMIT 1),
+                    0
+                ),
+                4
+            ) AS enterprise_price,
+            
+            -- 是否企业用户(如果查询到企业价格则为true)
+            CASE WHEN pf.id IS NOT NULL THEN 1 ELSE 0 END AS is_firm_user,
             
             -- 当前时段描述(格式:峰 HH:mm-HH:mm)
             (
@@ -130,10 +175,6 @@
                 WHERE t1.id = tppi.id
             ) AS current_period_desc,
             
-            -- 企业价格(电价 + 服务费 + 运营费 + 综合销售费)
-            (IFNULL(tppi.elec_price, 0) + IFNULL(tppi.service_price, 0) + 
-             IFNULL(pf.op_fee, 0) + IFNULL(pf.comp_sales_fee, 0)) AS enterprise_price,
-            
             -- 用户余额(从参数传入)
             #{userBalance} AS available_balance,
             
@@ -168,6 +209,12 @@
             LIMIT 1
         )
         
+        -- 关联常规价格(sales_type = 0,不限制企业)
+        LEFT JOIN c_policy_fee pf_common ON pf_common.station_info_id = tpsi.id 
+            AND pf_common.start_time = tppi.start_time
+            AND pf_common.is_deleted = 0
+            AND pf_common.sales_type = 0
+        
         -- 关联企业价格(仅当用户ID不为空时关联)
         LEFT JOIN (
             SELECT pf1.*