浏览代码

feat(order): 支持渠道订单预付及失败订单定时修复

- 新增黑名单配置,充电站经纬度同步时跳过黑名单内站点,保留原有经纬度
- 充电订单新增结算时间字段,用于处理结算状态的订单
- 渠道启动充电接口新增参数校验及渠道用户自动注册功能
- 订单补偿逻辑调整,补偿成功订单状态设置为“已完成”,无充电数据订单设置为异常状态
- 实现失败订单(待启动和待结算)自动检测超过时限后调用修复
- 添加定时任务每3分钟执行失败订单修复处理
- 充电订单响应数据新增平台实际收取金额、服务费及实际电费字段
- 新增恶意请求拦截过滤器,对请求URI、参数进行安全检测并阻断恶意请求
- 新增安全事件日志相关接口、查询模型及页面视图封装
- 优化第三方接口充电启动及订单查询相关代码,合并相关服务类的依赖注入及业务逻辑
- 更新API文档,新增订单实收金额、服务费和电费字段说明及返回示例
wzq 4 天之前
父节点
当前提交
373672b5c3
共有 16 个文件被更改,包括 799 次插入28 次删除
  1. 12 0
      doc/第三方接入API文档.md
  2. 5 0
      src/main/java/com/zsElectric/boot/business/model/entity/ChargeOrderInfo.java
  3. 36 0
      src/main/java/com/zsElectric/boot/business/quartz/FailOrderDisposeJob.java
  4. 10 0
      src/main/java/com/zsElectric/boot/business/service/ChargeOrderInfoService.java
  5. 82 20
      src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java
  6. 17 6
      src/main/java/com/zsElectric/boot/business/service/impl/ThirdPartyChargingServiceImpl.java
  7. 1 0
      src/main/java/com/zsElectric/boot/charging/service/impl/ChargingReceptionServiceImpl.java
  8. 49 0
      src/main/java/com/zsElectric/boot/config/property/BlackChargingStationsProperties.java
  9. 266 0
      src/main/java/com/zsElectric/boot/core/filter/MaliciousRequestBlockFilter.java
  10. 71 0
      src/main/java/com/zsElectric/boot/core/security/SecurityThreatDetector.java
  11. 34 0
      src/main/java/com/zsElectric/boot/system/controller/SecurityEventLogController.java
  12. 61 0
      src/main/java/com/zsElectric/boot/system/model/query/SecurityEventLogPageQuery.java
  13. 105 0
      src/main/java/com/zsElectric/boot/system/model/vo/SecurityEventLogPageVO.java
  14. 18 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeOrderDetailResponseData.java
  15. 18 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeOrderListResponseData.java
  16. 14 2
      src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java

+ 12 - 0
doc/第三方接入API文档.md

@@ -915,6 +915,9 @@
 | plateNum | 车牌号 | String | 车牌号 |
 | stopType | 停止类型 | Integer | 1-主动停止,2-充满停止,3-余额不足停止,4-电桩按钮停止 |
 | stopReason | 停止原因 | String | 停止原因描述 |
+| realCost | 平台实际收取金额 | BigDecimal | 平台实际收取金额(元) |
+| realServiceCost | 平台总服务费 | BigDecimal | 平台总服务费(元) |
+| thirdPartyElecfee | 实际电费 | BigDecimal | 实际电费(元) |
 | createTime | 创建时间 | String | 订单创建时间 |
 
 ### 12.5 返回示例
@@ -946,6 +949,9 @@
       "plateNum": "京A12345",
       "stopType": 2,
       "stopReason": "充满停止",
+      "realCost": 25.60,
+      "realServiceCost": 5.20,
+      "thirdPartyElecfee": 20.40,
       "createTime": "2024-03-16 10:00:00"
     }
   ]
@@ -1004,6 +1010,9 @@
 | plateNum | 车牌号 | String | 车牌号 |
 | stopType | 停止类型 | Integer | 1-主动停止,2-充满停止,3-余额不足停止,4-电桩按钮停止 |
 | stopReason | 停止原因 | String | 停止原因描述 |
+| realCost | 平台实际收取金额 | BigDecimal | 平台实际收取金额(元) |
+| realServiceCost | 平台总服务费 | BigDecimal | 平台总服务费(元) |
+| thirdPartyElecfee | 实际电费 | BigDecimal | 实际电费(元) |
 | createTime | 创建时间 | String | 订单创建时间 |
 
 ### 13.5 返回示例
@@ -1031,6 +1040,9 @@
   "plateNum": "京A12345",
   "stopType": 2,
   "stopReason": "充满停止",
+  "realCost": 25.60,
+  "realServiceCost": 5.20,
+  "thirdPartyElecfee": 20.40,
   "createTime": "2024-03-16 10:00:00"
 }
 ```

+ 5 - 0
src/main/java/com/zsElectric/boot/business/model/entity/ChargeOrderInfo.java

@@ -9,6 +9,7 @@ import lombok.Setter;
 import com.baomidou.mybatisplus.annotation.TableName;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 
 /**
  * 充电订单信息实体对象
@@ -67,6 +68,10 @@ public class ChargeOrderInfo extends BaseEntity {
      * 充电时间:秒
      */
     private Integer chargeTime;
+    /**
+     * 结算时间
+     */
+    private LocalDateTime settleTime;
     /**
      * 状态0待启动 1 充电中 2 结算中 3 已完成, 5未成功充电
      */

+ 36 - 0
src/main/java/com/zsElectric/boot/business/quartz/FailOrderDisposeJob.java

@@ -0,0 +1,36 @@
+package com.zsElectric.boot.business.quartz;
+
+import com.zsElectric.boot.business.service.ChargeOrderInfoService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 失败订单处理-定时任务
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class FailOrderDisposeJob {
+
+    private final ChargeOrderInfoService chargeOrderInfoService;
+    /**
+     * 每3分钟执行一次
+     */
+    @Scheduled(cron = "0 */3 * * * ?")
+    public void failOrderDispose(){
+        log.info("开始执行失败订单处理定时任务");
+
+        try {
+            //查询所有待充电的订单
+            chargeOrderInfoService.failOrderDispose();
+            //查询所有待结算的订单
+            chargeOrderInfoService.settleOrderFailDispose();
+        } catch (Exception e) {
+            log.error("失败订单处理失败 - 系统异常", e);
+        }
+
+        log.info("失败订单处理定时任务执行结束");
+    }
+}

+ 10 - 0
src/main/java/com/zsElectric/boot/business/service/ChargeOrderInfoService.java

@@ -123,4 +123,14 @@ public interface ChargeOrderInfoService extends IService<ChargeOrderInfo> {
      */
     String compensateChannelOrderPush();
 
+    /**
+     * 失败订单处理(待充电)
+     */
+    void failOrderDispose();
+
+    /**
+     * 失败订单处理(待结算)
+     */
+    void settleOrderFailDispose();
+
 }

+ 82 - 20
src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java

@@ -94,6 +94,8 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
 
     private final UserInfoMapper userInfoMapper;
 
+    private final UserInfoService userInfoService;
+
     private final FirmInfoMapper firmInfoMapper;
 
     private final CouponMapper couponMapper;
@@ -384,6 +386,40 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
      * @return
      */
     public AppChargeVO channelInvokeCharge(AppInvokeChargeForm formData) throws JsonProcessingException {
+        if (StrUtil.isBlank(formData.getOperatorId())) {
+            throw new BusinessException("运营商ID不能为空");
+        }
+        if (StrUtil.isBlank(formData.getChannelOrderNo())) {
+            throw new BusinessException("渠道订单号不能为空");
+        }
+        if (StrUtil.isBlank(formData.getChannelUserPhone())) {
+            throw new BusinessException("渠道用户手机号不能为空");
+        }
+        if (formData.getChannelPreAmt() == null || formData.getChannelPreAmt().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BusinessException("渠道预支付金额必须大于0");
+        }
+
+        ThirdPartyInfo thirdPartyInfo = thirdPartyInfoMapper.selectOne(
+                Wrappers.lambdaQuery(ThirdPartyInfo.class)
+                        .eq(ThirdPartyInfo::getOperatorId, formData.getOperatorId())
+                        .last("limit 1"));
+        if (thirdPartyInfo == null) {
+            throw new BusinessException("渠道运营商不存在");
+        }
+
+        UserInfo channelUser = userInfoService.getUserInfoByPhoneAndOperatorId(
+                formData.getChannelUserPhone(),
+                thirdPartyInfo.getId()
+        );
+        if (channelUser == null) {
+            channelUser = userInfoService.registerThirdPartyUserByPhone(
+                    formData.getChannelUserPhone(),
+                    thirdPartyInfo.getId()
+            );
+        }
+        if (channelUser == null) {
+            throw new BusinessException("渠道用户不存在且自动注册失败");
+        }
 
         String seq = ConnectivityConstants.OPERATOR_ID + formData.getChannelOrderNo();
 
@@ -399,8 +435,9 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
 
         //创建订单
         ChargeOrderInfo chargeOrderInfo = new ChargeOrderInfo();
-        Long userId = SecurityUtils.getUserId();
-        User user = userMapper.selectById(userId);
+        Long currentUserId = SecurityUtils.getUserId();
+        chargeOrderInfo.setUserId(channelUser.getId());
+        User user = currentUserId == null ? null : userMapper.selectById(currentUserId);
         if(ObjectUtil.isNotEmpty(user)){
             FirmInfo firmInfo = firmInfoMapper.selectOne(Wrappers.lambdaQuery(FirmInfo.class).eq(FirmInfo::getDeptId, user.getDeptId()).last("limit 1"));
             if(firmInfo != null) {
@@ -408,23 +445,6 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             }
         }
 
-        if (ObjectUtil.isNotEmpty(formData.getOperatorId())){
-            ThirdPartyInfo thirdPartyInfo = thirdPartyInfoMapper.selectOne(
-                    Wrappers.lambdaQuery(ThirdPartyInfo.class)
-                            .eq(ThirdPartyInfo::getOperatorId, formData.getOperatorId())
-                            .last("limit 1"));
-            if (thirdPartyInfo != null) {
-                UserInfo userInfo = userInfoMapper.selectOne(
-                        Wrappers.lambdaQuery(UserInfo.class)
-                                .eq(UserInfo::getPhone, formData.getChannelUserPhone())
-                                .eq(UserInfo::getThirdPartId, thirdPartyInfo.getId())
-                                .last("limit 1"));
-                if (userInfo != null) {
-                    chargeOrderInfo.setUserId(userInfo.getId());
-                }
-            }
-        }
-
         chargeOrderInfo.setOrderType(SystemConstants.CHARGE_ORDER_TYPE_CHANNEL);
         chargeOrderInfo.setConnectorId(formData.getConnectorId());
         chargeOrderInfo.setEquipmentId(formData.getEquipmentId());
@@ -992,6 +1012,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
                 if (success) {
                     // 设置补偿状态为已补偿
                     order.setCompensateStatus(1);
+                    order.setStatus(3);
                     this.updateById(order);
                     log.info("订单{}通过API日志修复成功", chargeOrderNo);
                     return "订单" + chargeOrderNo + "通过API日志修复成功,实际费用: " + order.getRealCost();
@@ -1007,6 +1028,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
                     || chargeStatus.getTotalPower().compareTo(BigDecimal.ZERO) <= 0) {
                 // 设置补偿状态为异常无须补偿
                 order.setCompensateStatus(2);
+                order.setStatus(5);
                 this.updateById(order);
                 return "订单" + chargeOrderNo + "无有效充电数据,设置为异常无须补偿";
             }
@@ -1058,7 +1080,8 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             order.setRemark("通过补偿修复处理");
             // 设置补偿状态为已补偿
             order.setCompensateStatus(1);
-            
+            order.setStatus(3);
+
             this.updateById(order);
             
             // 9. 执行账户余额扣减(仅平台订单和企业订单)
@@ -1732,4 +1755,43 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
         return result;
     }
 
+    @Override
+    public void failOrderDispose() {
+        //1.查询所有待启动的订单
+        List<ChargeOrderInfo> orderInfoList = this.list(Wrappers.<ChargeOrderInfo>lambdaQuery().eq(ChargeOrderInfo::getStatus, 0).last("LIMIT 1000"));
+        //2.遍历判断订单创建时间是否已超过3分钟
+        for (ChargeOrderInfo chargeOrderInfo : orderInfoList) {
+            LocalDateTime createTime = chargeOrderInfo.getCreateTime();
+            if (createTime != null && createTime.plusMinutes(3).isBefore(LocalDateTime.now())) {
+                String chargeOrderNo = chargeOrderInfo.getChargeOrderNo();
+                try {
+                    log.info("失败订单处理: 订单{}创建时间已超过3分钟,调用订单修复", chargeOrderNo);
+                    this.repairOrderByOrderNo(chargeOrderNo);
+                } catch (Exception e) {
+                    log.error("失败订单处理: 订单{}修复失败: {}", chargeOrderNo, e.getMessage(), e);
+                }
+            }
+        }
+
+    }
+
+    @Override
+    public void settleOrderFailDispose() {
+        //1.查询所有结算中的订单
+        List<ChargeOrderInfo> orderInfoList = this.list(Wrappers.<ChargeOrderInfo>lambdaQuery().eq(ChargeOrderInfo::getStatus, 2).last("LIMIT 1000"));
+        //2.遍历判断订单结算时间是否已超过5分钟
+        for (ChargeOrderInfo chargeOrderInfo : orderInfoList) {
+            LocalDateTime settleTime = chargeOrderInfo.getSettleTime();
+            if (settleTime != null && settleTime.plusMinutes(5).isBefore(LocalDateTime.now())) {
+                String chargeOrderNo = chargeOrderInfo.getChargeOrderNo();
+                try {
+                    log.info("失败订单处理: 订单{}结算时间已超过5分钟,调用订单修复", chargeOrderNo);
+                    this.repairOrderByOrderNo(chargeOrderNo);
+                } catch (Exception e) {
+                    log.error("失败订单处理: 订单{}修复失败: {}", chargeOrderNo, e.getMessage(), e);
+                }
+            }
+        }
+    }
+
 }

+ 17 - 6
src/main/java/com/zsElectric/boot/business/service/impl/ThirdPartyChargingServiceImpl.java

@@ -31,6 +31,7 @@ import com.zsElectric.boot.business.model.dto.ConnectorTipsUpdateDTO;
 import com.zsElectric.boot.business.service.ThirdPartyChargingService;
 import com.zsElectric.boot.charging.vo.ChargingPricePolicyVO;
 import com.zsElectric.boot.charging.vo.QueryStationsInfoVO;
+import com.zsElectric.boot.config.property.BlackChargingStationsProperties;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -66,6 +67,7 @@ public class ThirdPartyChargingServiceImpl implements ThirdPartyChargingService
     private final ThirdPartyInfoMapper thirdPartyInfoMapper;
     private final PolicyFeeMapper policyFeeMapper;
     private final ObjectMapper objectMapper;
+    private final BlackChargingStationsProperties blackChargingStationsProperties;
 
     // ==================== 充电站信息查询 ====================
 
@@ -215,12 +217,21 @@ public class ThirdPartyChargingServiceImpl implements ThirdPartyChargingService
         entity.setStationStatus(stationInfo.getStationStatus());
         entity.setParkNums(stationInfo.getParkNums());
 
-        // 处理经纬度
-        if (stationInfo.getStationLng() != null) {
-            entity.setStationLng(BigDecimal.valueOf(stationInfo.getStationLng()));
-        }
-        if (stationInfo.getStationLat() != null) {
-            entity.setStationLat(BigDecimal.valueOf(stationInfo.getStationLat()));
+        // 处理经纬度(黑名单中的站点不同步经纬度,保留数据库原有值)
+        if (blackChargingStationsProperties.isBlacklisted(stationInfo.getStationID())) {
+            // 黑名单站点:保留原有经纬度
+            if (existingStation != null) {
+                entity.setStationLng(existingStation.getStationLng());
+                entity.setStationLat(existingStation.getStationLat());
+            }
+            log.debug("充电站[{}]在经纬度黑名单中,跳过经纬度同步", stationInfo.getStationID());
+        } else {
+            if (stationInfo.getStationLng() != null) {
+                entity.setStationLng(BigDecimal.valueOf(stationInfo.getStationLng()));
+            }
+            if (stationInfo.getStationLat() != null) {
+                entity.setStationLat(BigDecimal.valueOf(stationInfo.getStationLat()));
+            }
         }
 
         entity.setSiteGuide(stationInfo.getSiteGuide());

+ 1 - 0
src/main/java/com/zsElectric/boot/charging/service/impl/ChargingReceptionServiceImpl.java

@@ -577,6 +577,7 @@ public class ChargingReceptionServiceImpl implements ChargingReceptionService {
                             if (Objects.equals(chargeOrderInfo.getStatus(), 0) ||
                                 Objects.equals(chargeOrderInfo.getStatus(), 1)) {
                                 chargeOrderInfo.setStatus(SystemConstants.STATUS_TWO);
+                                chargeOrderInfo.setSettleTime(LocalDateTime.now());
                                 chargeOrderInfoService.updateById(chargeOrderInfo);
                                 log.info("更新订单状态为结算中 - orderId: {}", chargeOrderInfo.getId());
 

+ 49 - 0
src/main/java/com/zsElectric/boot/config/property/BlackChargingStationsProperties.java

@@ -0,0 +1,49 @@
+package com.zsElectric.boot.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 充电站经纬度同步黑名单配置
+ *
+ * <p>配置在黑名单中的充电站,同步时不会覆盖其经纬度信息</p>
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "black-charging-stations")
+public class BlackChargingStationsProperties {
+
+    /**
+     * 不同步经纬度的充电站ID列表(逗号分隔)
+     */
+    private String stationList = "";
+
+    /**
+     * 判断指定stationId是否在黑名单中
+     */
+    public boolean isBlacklisted(String stationId) {
+        if (stationId == null || stationList == null || stationList.isBlank()) {
+            return false;
+        }
+        return getStationSet().contains(stationId);
+    }
+
+    /**
+     * 获取黑名单Station ID集合
+     */
+    public Set<String> getStationSet() {
+        if (stationList == null || stationList.isBlank()) {
+            return Collections.emptySet();
+        }
+        return Arrays.stream(stationList.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .collect(Collectors.toSet());
+    }
+}

+ 266 - 0
src/main/java/com/zsElectric/boot/core/filter/MaliciousRequestBlockFilter.java

@@ -0,0 +1,266 @@
+package com.zsElectric.boot.core.filter;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zsElectric.boot.common.util.IPUtils;
+import com.zsElectric.boot.config.property.SecurityProperties;
+import com.zsElectric.boot.core.security.SecurityThreatDetector;
+import com.zsElectric.boot.core.web.Result;
+import com.zsElectric.boot.core.web.ResultCode;
+import com.zsElectric.boot.security.util.SecurityUtils;
+import com.zsElectric.boot.system.model.entity.SecurityEventLog;
+import com.zsElectric.boot.system.service.SecurityEventLogService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+
+/**
+ * Blocks high-confidence malicious requests before they reach controllers.
+ *
+ * <p>The filter intentionally checks only URI, query string and decoded query/form parameters. It does
+ * not read JSON bodies, so normal request body consumption is not affected.</p>
+ *
+ * @author zsElectric
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@Order(Ordered.HIGHEST_PRECEDENCE + 20)
+public class MaliciousRequestBlockFilter extends OncePerRequestFilter {
+
+    private static final int SHORT_TEXT_LIMIT = 128;
+    private static final int MIDDLE_TEXT_LIMIT = 512;
+    private static final int LONG_TEXT_LIMIT = 2048;
+
+    private final SecurityEventLogService securityEventLogService;
+    private final ObjectMapper objectMapper;
+    private final SecurityProperties securityProperties;
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        if (!isEnabled()) {
+            return true;
+        }
+        String uri = request.getRequestURI();
+        return StrUtil.startWith(uri, "/api/v1/security-event-logs");
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        RiskMatch riskMatch = findRiskMatch(request);
+        if (riskMatch == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        recordBlockedRequest(request, riskMatch);
+        log.warn("非法请求已拦截: ip={}, uri={}, matchedField={}, eventType={}",
+                IPUtils.getIpAddr(request), request.getRequestURI(), riskMatch.matchedField(), riskMatch.eventType());
+        writeBlockedResponse(response);
+    }
+
+    private RiskMatch findRiskMatch(HttpServletRequest request) {
+        RiskMatch uriRisk = detect("requestUri", request.getRequestURI());
+        if (uriRisk != null) {
+            return uriRisk;
+        }
+
+        String queryString = request.getQueryString();
+        RiskMatch queryRisk = detect("queryString", firstNotBlank(decode(queryString), queryString));
+        if (queryRisk != null) {
+            return queryRisk;
+        }
+
+        if (!shouldInspectParameterMap(request)) {
+            return null;
+        }
+
+        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
+            RiskMatch nameRisk = detect(entry.getKey(), entry.getKey());
+            if (nameRisk != null) {
+                return nameRisk;
+            }
+
+            String[] values = entry.getValue();
+            if (values == null) {
+                continue;
+            }
+            for (String value : values) {
+                RiskMatch valueRisk = detect(entry.getKey(), value);
+                if (valueRisk != null) {
+                    return valueRisk;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private boolean shouldInspectParameterMap(HttpServletRequest request) {
+        String method = request.getMethod();
+        if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
+            return true;
+        }
+
+        String contentType = request.getContentType();
+        return StrUtil.isNotBlank(contentType)
+                && contentType.toLowerCase().startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
+    }
+
+    private RiskMatch detect(String matchedField, String content) {
+        if (StrUtil.isBlank(content) || !SecurityThreatDetector.containsMaliciousFeature(content)) {
+            return null;
+        }
+        return new RiskMatch(
+                matchedField,
+                content,
+                SecurityThreatDetector.resolveEventType("INVALID_PARAMETER", content),
+                SecurityThreatDetector.resolveRiskLevel(content)
+        );
+    }
+
+    private void recordBlockedRequest(HttpServletRequest request, RiskMatch riskMatch) {
+        try {
+            SecurityEventLog eventLog = new SecurityEventLog();
+            eventLog.setEventType(riskMatch.eventType());
+            eventLog.setRiskLevel(riskMatch.riskLevel());
+            eventLog.setDetector("APP_RULE");
+            eventLog.setRuleId("malicious-request-block-filter");
+            eventLog.setEventDesc("非法请求已被应用过滤器拦截");
+            eventLog.setRequestUri(truncate(request.getRequestURI(), MIDDLE_TEXT_LIMIT));
+            eventLog.setRequestMethod(truncate(request.getMethod(), 16));
+            eventLog.setQueryString(truncate(request.getQueryString(), LONG_TEXT_LIMIT));
+            eventLog.setClientIp(truncate(IPUtils.getIpAddr(request), 45));
+            eventLog.setXForwardedFor(truncate(request.getHeader("X-Forwarded-For"), MIDDLE_TEXT_LIMIT));
+            eventLog.setUserAgent(truncate(request.getHeader(HttpHeaders.USER_AGENT), MIDDLE_TEXT_LIMIT));
+            eventLog.setReferer(truncate(request.getHeader(HttpHeaders.REFERER), MIDDLE_TEXT_LIMIT));
+            eventLog.setUserId(getCurrentUserId());
+            eventLog.setOperatorId(truncate(resolveOperatorId(request), 64));
+            eventLog.setRequestId(truncate(resolveRequestId(request), SHORT_TEXT_LIMIT));
+            eventLog.setMatchedField(truncate(riskMatch.matchedField(), SHORT_TEXT_LIMIT));
+            eventLog.setPayloadExcerpt(truncate(riskMatch.payloadExcerpt(), LONG_TEXT_LIMIT));
+            eventLog.setPayloadHash(sha256(riskMatch.payloadExcerpt()));
+            eventLog.setAction("BLOCK");
+            eventLog.setHttpStatus(HttpStatus.BAD_REQUEST.value());
+            eventLog.setHandleResult("blocked_by_app_filter");
+            eventLog.setIsDeleted(0);
+            securityEventLogService.record(eventLog);
+        } catch (Exception e) {
+            log.warn("非法请求拦截日志写入失败: {}", e.getMessage(), e);
+        }
+    }
+
+    private void writeBlockedResponse(HttpServletResponse response) throws IOException {
+        response.setStatus(HttpStatus.BAD_REQUEST.value());
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getWriter().write(objectMapper.writeValueAsString(
+                Result.failed(ResultCode.USER_INPUT_CONTENT_ILLEGAL, ResultCode.USER_INPUT_CONTENT_ILLEGAL.getMsg())
+        ));
+    }
+
+    private Long getCurrentUserId() {
+        try {
+            return SecurityUtils.getUserId();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private String resolveRequestId(HttpServletRequest request) {
+        return firstNotBlank(
+                request.getHeader("X-Request-Id"),
+                request.getHeader("X-Request-ID"),
+                request.getHeader("Request-ID"),
+                request.getHeader("Trace-Id"),
+                request.getHeader("traceId"),
+                MDC.get("traceId"),
+                MDC.get("requestId")
+        );
+    }
+
+    private String resolveOperatorId(HttpServletRequest request) {
+        return firstNotBlank(
+                request.getHeader("OperatorID"),
+                request.getHeader("OperatorId"),
+                request.getHeader("operatorID"),
+                request.getHeader("operatorId"),
+                request.getHeader("Operator-Id")
+        );
+    }
+
+    private String firstNotBlank(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StrUtil.isNotBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private String decode(String value) {
+        if (StrUtil.isBlank(value)) {
+            return value;
+        }
+        try {
+            return URLDecoder.decode(value, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            return value;
+        }
+    }
+
+    private String truncate(String value, int maxLength) {
+        if (value == null || value.length() <= maxLength) {
+            return value;
+        }
+        return value.substring(0, maxLength);
+    }
+
+    private String sha256(String value) {
+        if (StrUtil.isBlank(value)) {
+            return null;
+        }
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
+            StringBuilder builder = new StringBuilder(bytes.length * 2);
+            for (byte b : bytes) {
+                builder.append(String.format("%02x", b));
+            }
+            return builder.toString();
+        } catch (NoSuchAlgorithmException e) {
+            log.warn("SHA-256算法不可用: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private boolean isEnabled() {
+        return securityProperties.getMaliciousRequestBlock() == null
+                || !Boolean.FALSE.equals(securityProperties.getMaliciousRequestBlock().getEnabled());
+    }
+
+    private record RiskMatch(String matchedField, String payloadExcerpt, String eventType, String riskLevel) {
+    }
+}

+ 71 - 0
src/main/java/com/zsElectric/boot/core/security/SecurityThreatDetector.java

@@ -0,0 +1,71 @@
+package com.zsElectric.boot.core.security;
+
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Locale;
+
+/**
+ * High-confidence malicious request feature detector.
+ *
+ * <p>This detector is intentionally conservative. It is used for application-side blocking and event
+ * classification, so rules should prefer clear attack features over broad keyword matching.</p>
+ *
+ * @author zsElectric
+ */
+public final class SecurityThreatDetector {
+
+    private SecurityThreatDetector() {
+    }
+
+    public static boolean containsMaliciousFeature(String content) {
+        return containsSqlInjectionFeature(content) || containsXssFeature(content);
+    }
+
+    public static String resolveEventType(String defaultEventType, String content) {
+        if (containsSqlInjectionFeature(content)) {
+            return "SQL_INJECTION";
+        }
+        if (containsXssFeature(content)) {
+            return "XSS";
+        }
+        return defaultEventType;
+    }
+
+    public static String resolveRiskLevel(String content) {
+        return containsMaliciousFeature(content) ? "HIGH" : "LOW";
+    }
+
+    public static boolean containsSqlInjectionFeature(String content) {
+        if (StrUtil.isBlank(content)) {
+            return false;
+        }
+        String normalized = content.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ");
+        return normalized.contains(" union select ")
+                || normalized.contains("' union select")
+                || normalized.contains("\" union select")
+                || normalized.contains(" or 1=1")
+                || normalized.contains("' or '1'='1")
+                || normalized.contains("\" or \"1\"=\"1")
+                || normalized.contains("information_schema")
+                || normalized.contains(" sleep(")
+                || normalized.contains(" benchmark(")
+                || normalized.contains(" extractvalue(")
+                || normalized.contains(" updatexml(")
+                || normalized.contains(" load_file(")
+                || normalized.contains(" into outfile")
+                || normalized.matches(".*;\\s*(drop|truncate|alter)\\s+.*");
+    }
+
+    public static boolean containsXssFeature(String content) {
+        if (StrUtil.isBlank(content)) {
+            return false;
+        }
+        String normalized = content.toLowerCase(Locale.ROOT);
+        return normalized.contains("<script")
+                || normalized.contains("</script")
+                || normalized.contains("javascript:")
+                || normalized.contains(" onerror=")
+                || normalized.contains(" onload=")
+                || normalized.contains("alert(");
+    }
+}

+ 34 - 0
src/main/java/com/zsElectric/boot/system/controller/SecurityEventLogController.java

@@ -0,0 +1,34 @@
+package com.zsElectric.boot.system.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zsElectric.boot.core.web.PageResult;
+import com.zsElectric.boot.system.model.query.SecurityEventLogPageQuery;
+import com.zsElectric.boot.system.model.vo.SecurityEventLogPageVO;
+import com.zsElectric.boot.system.service.SecurityEventLogService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Security event log controller.
+ *
+ * @author zsElectric
+ */
+@Tag(name = "Security event log API")
+@RestController
+@RequestMapping("/api/v1/security-event-logs")
+@RequiredArgsConstructor
+public class SecurityEventLogController {
+
+    private final SecurityEventLogService securityEventLogService;
+
+    @Operation(summary = "Security event log page")
+    @GetMapping("/page")
+    public PageResult<SecurityEventLogPageVO> getSecurityEventLogPage(SecurityEventLogPageQuery queryParams) {
+        Page<SecurityEventLogPageVO> result = securityEventLogService.getSecurityEventLogPage(queryParams);
+        return PageResult.success(result);
+    }
+}

+ 61 - 0
src/main/java/com/zsElectric/boot/system/model/query/SecurityEventLogPageQuery.java

@@ -0,0 +1,61 @@
+package com.zsElectric.boot.system.model.query;
+
+import com.zsElectric.boot.common.base.BasePageQuery;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * Security event log page query.
+ *
+ * @author zsElectric
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Schema(description = "Security event log page query")
+public class SecurityEventLogPageQuery extends BasePageQuery {
+
+    @Schema(description = "Keyword for event description, uri, ip, matched field, request id or payload excerpt")
+    private String keywords;
+
+    @Schema(description = "Event type, such as SQL_INJECTION, XSS, INVALID_JSON, INVALID_PARAMETER")
+    private String eventType;
+
+    @Schema(description = "Risk level: LOW, MEDIUM, HIGH, CRITICAL")
+    private String riskLevel;
+
+    @Schema(description = "Detector source: APP_VALIDATION, APP_RULE, WAF, GATEWAY, RATE_LIMIT, MANUAL")
+    private String detector;
+
+    @Schema(description = "Action: OBSERVE, REJECT, BLOCK, RATE_LIMIT, ALLOW")
+    private String action;
+
+    @Schema(description = "Client IP")
+    private String clientIp;
+
+    @Schema(description = "Request URI")
+    private String requestUri;
+
+    @Schema(description = "Matched request field or parameter name")
+    private String matchedField;
+
+    @Schema(description = "Request trace id")
+    private String requestId;
+
+    @Schema(description = "Payload SHA-256 hash")
+    private String payloadHash;
+
+    @Schema(description = "HTTP response status")
+    private Integer httpStatus;
+
+    @Schema(description = "Platform user id")
+    private Long userId;
+
+    @Schema(description = "Third-party operator id")
+    private String operatorId;
+
+    @Schema(description = "Create time range")
+    private List<String> createTime;
+}

+ 105 - 0
src/main/java/com/zsElectric/boot/system/model/vo/SecurityEventLogPageVO.java

@@ -0,0 +1,105 @@
+package com.zsElectric.boot.system.model.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * Security event log page view object.
+ *
+ * @author zsElectric
+ */
+@Data
+@Schema(description = "Security event log page view object")
+public class SecurityEventLogPageVO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "Primary key")
+    private Long id;
+
+    @Schema(description = "Event type")
+    private String eventType;
+
+    @Schema(description = "Risk level")
+    private String riskLevel;
+
+    @Schema(description = "Detector source")
+    private String detector;
+
+    @Schema(description = "Rule id")
+    private String ruleId;
+
+    @Schema(description = "Event description")
+    private String eventDesc;
+
+    @Schema(description = "Request URI")
+    private String requestUri;
+
+    @Schema(description = "Request method")
+    private String requestMethod;
+
+    @Schema(description = "Query string excerpt")
+    private String queryString;
+
+    @Schema(description = "Client IP")
+    private String clientIp;
+
+    @Schema(description = "X-Forwarded-For header")
+    private String xForwardedFor;
+
+    @Schema(description = "User-Agent header")
+    private String userAgent;
+
+    @Schema(description = "Referer header")
+    private String referer;
+
+    @Schema(description = "Platform user id")
+    private Long userId;
+
+    @Schema(description = "Third-party operator id")
+    private String operatorId;
+
+    @Schema(description = "Request trace id")
+    private String requestId;
+
+    @Schema(description = "Matched field or parameter")
+    private String matchedField;
+
+    @Schema(description = "Payload excerpt")
+    private String payloadExcerpt;
+
+    @Schema(description = "Payload SHA-256 hash")
+    private String payloadHash;
+
+    @Schema(description = "Action")
+    private String action;
+
+    @Schema(description = "HTTP status")
+    private Integer httpStatus;
+
+    @Schema(description = "Handle result")
+    private String handleResult;
+
+    @Schema(description = "Remark")
+    private String remark;
+
+    @Schema(description = "Create user id")
+    private Long createBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "Create time")
+    private LocalDateTime createTime;
+
+    @Schema(description = "Update user id")
+    private Long updateBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "Update time")
+    private LocalDateTime updateTime;
+}

+ 18 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeOrderDetailResponseData.java

@@ -132,6 +132,24 @@ public class ChargeOrderDetailResponseData implements Serializable {
     @JsonProperty("stopReason")
     private String stopReason;
 
+    /**
+     * 平台实际收取金额
+     */
+    @JsonProperty("realCost")
+    private BigDecimal realCost;
+
+    /**
+     * 平台总服务费
+     */
+    @JsonProperty("realServiceCost")
+    private BigDecimal realServiceCost;
+
+    /**
+     * 实际电费
+     */
+    @JsonProperty("thirdPartyElecfee")
+    private BigDecimal thirdPartyElecfee;
+
     /**
      * 创建时间
      */

+ 18 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeOrderListResponseData.java

@@ -160,6 +160,24 @@ public class ChargeOrderListResponseData implements Serializable {
         @JsonProperty("stopReason")
         private String stopReason;
 
+        /**
+         * 平台实际收取金额
+         */
+        @JsonProperty("realCost")
+        private BigDecimal realCost;
+
+        /**
+         * 平台总服务费
+         */
+        @JsonProperty("realServiceCost")
+        private BigDecimal realServiceCost;
+
+        /**
+         * 实际电费
+         */
+        @JsonProperty("thirdPartyElecfee")
+        private BigDecimal thirdPartyElecfee;
+
         /**
          * 创建时间
          */

+ 14 - 2
src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java

@@ -1166,8 +1166,13 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             log.info("启动充电-解密后的请求数据: {}", decryptedData);
 
             InvokeChargeRequestData invokeRequest = objectMapper.readValue(decryptedData, InvokeChargeRequestData.class);
-            if (invokeRequest == null || StrUtil.isBlank(invokeRequest.getEquipmentId()) 
-                    || StrUtil.isBlank(invokeRequest.getStationId()) || StrUtil.isBlank(invokeRequest.getConnectorId())) {
+            if (invokeRequest == null || StrUtil.isBlank(invokeRequest.getEquipmentId())
+                    || StrUtil.isBlank(invokeRequest.getStationId())
+                    || StrUtil.isBlank(invokeRequest.getConnectorId())
+                    || StrUtil.isBlank(invokeRequest.getChannelOrderNo())
+                    || StrUtil.isBlank(invokeRequest.getChannelUserPhone())
+                    || invokeRequest.getChannelPreAmt() == null
+                    || invokeRequest.getChannelPreAmt().compareTo(BigDecimal.ZERO) <= 0) {
                 return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
             }
 
@@ -1341,6 +1346,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 8. 转换为响应数据
             List<ChargeOrderListResponseData.ChargeOrderItem> records = pageResult.getRecords().stream()
+
                     .map(this::convertToChargeOrderItem)
                     .collect(Collectors.toList());
 
@@ -1381,6 +1387,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
         item.setPlateNum(order.getPlateNum());
         item.setStopType(order.getStopType());
         item.setStopReason(order.getStopReason());
+        item.setRealCost(order.getRealCost());
+        item.setRealServiceCost(order.getRealServiceCost() != null ? order.getRealServiceCost().add(order.getThirdPartyServerfee() != null ? order.getThirdPartyServerfee() : BigDecimal.ZERO) : null);
+        item.setThirdPartyElecfee(order.getThirdPartyElecfee());
         item.setCreateTime(order.getCreateTime() != null ? order.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null);
 
         // 查询充电站名称
@@ -1474,6 +1483,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             responseData.setPlateNum(order.getPlateNum());
             responseData.setStopType(order.getStopType());
             responseData.setStopReason(order.getStopReason());
+            responseData.setRealCost(order.getRealCost());
+            responseData.setRealServiceCost(order.getRealServiceCost().add(order.getThirdPartyServerfee()));
+            responseData.setThirdPartyElecfee(order.getThirdPartyElecfee());
             responseData.setCreateTime(order.getCreateTime() != null ? order.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null);
 
             // 查询充电站名称