ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/vo/SiteInfoVO.java
@@ -64,6 +64,9 @@ @ApiModelProperty(value = "是否命中白名单") private Boolean whitelistMatched = false; @ApiModelProperty(value = "白名单账户可用余额(企业账户充电显示,共享模式=公司可用余额,分配模式=白名单用户余额)") private BigDecimal whitelistBalance; @ApiModelProperty(value = "白名单权益:1=全部免费,2=免电费,3=免服务费") private Integer whitelistBenefitType; ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
@@ -2,6 +2,7 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -33,6 +34,7 @@ private Integer fleetId; @ApiModelProperty("最大充电结束时间(授权截止时间,全天授权或未开启自动停充时为空)") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @TableField("max_end_time") private LocalDateTime maxEndTime; ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrder.java
@@ -233,6 +233,7 @@ private Integer fleetId; @ApiModelProperty(value = "最大充电结束时间(企业授权截止时间,全天授权或未开启自动停充时为空)") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @TableField("max_end_time") private LocalDateTime maxEndTime; ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TOrderEvaluate.java
@@ -31,7 +31,7 @@ @TableId(value = "id", type = IdType.NONE) private Long id; @ApiModelProperty(value = "订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单)") @ApiModelProperty(value = "订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单,5=VIN码充电订单)") @TableField("order_type") private Integer orderType; ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
@@ -85,6 +85,7 @@ @ApiModelProperty(value = "账户模式(1=共享企业账户,2=分配卡账户;VIN/刷卡订单建单快照)") private Integer accountMode; @ApiModelProperty(value = "授权结束时间(企业政策授权截止时间,全天授权为空)") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime maxEndTime; @ApiModelProperty(value = "锁定额度(申请时冻结金额,实扣冻结口径)") private BigDecimal lockedAmount; ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java
@@ -179,6 +179,10 @@ } AppUserWhitelistMatchVO whitelist = whitelistService.matchWhitelist(tokenService.getLoginUserApplet().getUserId(), chargingGun.getSiteId()); data.setWhitelistMatched(Boolean.TRUE.equals(whitelist.getMatched())); if (Boolean.TRUE.equals(whitelist.getMatched())) { // 充电页余额显示:按白名单所属公司账户模式下发账户可用余额(共享=公司余额,分配=白名单余额) data.setWhitelistBalance(whitelistService.resolveWhitelistAccountBalance(whitelist)); } // 白名单权益已下线(充电完全按企业优惠结算),不再下发权益类型与名称,仅保留命中标识 data.setWhitelistBenefitType(null); data.setWhitelistBenefitName(null); ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java
@@ -50,6 +50,13 @@ AppUserWhitelistMatchVO matchWhitelist(Long appUserId, Integer siteId); /** * 解析白名单账户可用余额(小程序充电页余额显示):按所属公司账户模式, * 共享模式=公司可用余额,分配模式=白名单用户余额,账户模式未知按共享处理; * 公司信息查询失败时降级返回 null(由调用方保持原余额显示) */ BigDecimal resolveWhitelistAccountBalance(AppUserWhitelistMatchVO match); /** * 按公司查询未删除的白名单用户列表(公司详情-充电用户信息聚合使用) */ List<TAppUserWhitelist> listByCompany(Integer companyId); ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -457,6 +457,26 @@ } @Override public BigDecimal resolveWhitelistAccountBalance(AppUserWhitelistMatchVO match) { if (match == null || !Boolean.TRUE.equals(match.getMatched()) || match.getCompanyId() == null) { return null; } try { TGroupCompany company = loadCompany(match.getCompanyId()); Integer accountMode = company == null ? null : company.getAccountMode(); if (accountMode == null) { // 历史数据账户模式为空,按共享模式处理 accountMode = ACCOUNT_MODE_SHARED; } return resolveAvailableBalance(accountMode, match.getBalance(), company == null ? null : company.getAvailableBalance()); } catch (Exception ex) { log.warn("解析白名单账户可用余额失败,companyId={}", match.getCompanyId(), ex); return null; } } @Override public List<TAppUserWhitelist> listByCompany(Integer companyId) { if (companyId == null) { return Collections.emptyList(); ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TCompanyAllocateRecordMapper.xml
@@ -4,6 +4,9 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.account.mapper.TCompanyAllocateRecordMapper"> <!-- 分配记录弹窗只统计「给子账号(VIN码/刷卡/白名单)分配余额」的流水: change_type = 1 仅取分配类(1=分配;排除 2=充电结算消费流水,其 after_balance 可为负数); user_type != 4 排除账户模式切换时企业账户自身的回退留痕(不属于子账号分配) --> <select id="selectRecordPage" resultType="com.ruoyi.account.api.model.TCompanyAllocateRecord"> SELECT id, company_id, @@ -21,6 +24,8 @@ create_time FROM t_company_allocate_record WHERE company_id = #{query.companyId} AND change_type = 1 AND user_type != 4 ORDER BY operate_time DESC, id DESC </select> </mapper> ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
@@ -50,6 +50,7 @@ import java.util.concurrent.atomic.AtomicLong; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -259,6 +260,83 @@ assertEquals(null, result.getFleetId()); } // ---------------- 白名单账户可用余额解析(充电页余额显示) ---------------- @Test void resolveWhitelistAccountBalanceReturnsCompanyBalanceForSharedMode() { TGroupCompany company = new TGroupCompany(); company.setAccountMode(1); company.setAvailableBalance(new BigDecimal("500.50")); when(groupCompanyClient.getCompanyById(12)).thenReturn(R.ok(company)); AppUserWhitelistMatchVO match = new AppUserWhitelistMatchVO(); match.setMatched(true); match.setCompanyId(12); match.setBalance(new BigDecimal("100.00")); BigDecimal result = service.resolveWhitelistAccountBalance(match); assertEquals(new BigDecimal("500.50"), result); } @Test void resolveWhitelistAccountBalanceReturnsWhitelistBalanceForAllocationMode() { TGroupCompany company = new TGroupCompany(); company.setAccountMode(2); company.setAvailableBalance(new BigDecimal("500.50")); when(groupCompanyClient.getCompanyById(12)).thenReturn(R.ok(company)); AppUserWhitelistMatchVO match = new AppUserWhitelistMatchVO(); match.setMatched(true); match.setCompanyId(12); match.setBalance(new BigDecimal("100.00")); BigDecimal result = service.resolveWhitelistAccountBalance(match); assertEquals(new BigDecimal("100.00"), result); } @Test void resolveWhitelistAccountBalanceTreatsNullAccountModeAsShared() { TGroupCompany company = new TGroupCompany(); company.setAccountMode(null); company.setAvailableBalance(new BigDecimal("500.50")); when(groupCompanyClient.getCompanyById(12)).thenReturn(R.ok(company)); AppUserWhitelistMatchVO match = new AppUserWhitelistMatchVO(); match.setMatched(true); match.setCompanyId(12); match.setBalance(new BigDecimal("100.00")); BigDecimal result = service.resolveWhitelistAccountBalance(match); assertEquals(new BigDecimal("500.50"), result); } @Test void resolveWhitelistAccountBalanceReturnsNullWhenCompanyQueryFails() { when(groupCompanyClient.getCompanyById(12)).thenReturn(R.fail("公司信息查询失败")); AppUserWhitelistMatchVO match = new AppUserWhitelistMatchVO(); match.setMatched(true); match.setCompanyId(12); match.setBalance(new BigDecimal("100.00")); BigDecimal result = service.resolveWhitelistAccountBalance(match); assertNull(result); } @Test void resolveWhitelistAccountBalanceReturnsNullWhenNotMatched() { AppUserWhitelistMatchVO match = new AppUserWhitelistMatchVO(); match.setMatched(false); BigDecimal result = service.resolveWhitelistAccountBalance(match); assertNull(result); } // ---------------- 额度分配 ---------------- @Test ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
@@ -17,6 +17,7 @@ import com.ruoyi.integration.iotda.model.*; import com.ruoyi.integration.iotda.utils.produce.IotMessageProduce; import com.ruoyi.integration.iotda.utils.tools.CP56Time2aConverter; import com.ruoyi.integration.iotda.utils.tools.IotCommandLog; import com.ruoyi.integration.iotda.utils.tools.MessageUtil; import com.ruoyi.integration.iotda.utils.tools.StrategyUtil; import com.ruoyi.integration.iotda.utils.tools.ChargingMessageUtil; @@ -84,11 +85,12 @@ content = body.getJSONObject("content"); }catch (Exception e){ String content1 = body.getString("content"); // 按对象解析失败时无法识别 service_id(无法判断是否心跳),记录原始报文兜底便于排查 IotCommandLog.uplinkUnparsed(deviceNoOf(jsonObject), content1); String substring = content1.substring(content1.length() - 1); if(!"}".equals(substring)){ content1 = content1.substring(0, content1.length() - 1) + "}"; } // log.info("content1:"+content1); content = JSON.parseObject(content1); } JSONObject header = jsonObject.getJSONObject("notify_data").getJSONObject("header"); @@ -96,10 +98,8 @@ String nodeId = header.getString("node_id"); // 设备编号 String productId = header.getString("product_id"); // 产品id String service_id = content.getString("service_id"); if(!SendTagConstant.PING.equals(service_id)){ log.info("收到IOT发送的消息:{}",jsonObject.toJSONString()); } // log.info("服务id:{}",service_id); // 记录上行指令日志(独立日志文件、含指令中文含义;心跳 ping 不记录) IotCommandLog.uplink(deviceNoOf(jsonObject), service_id, content.toJSONString()); ChargingMessage chargingMessage = new ChargingMessage(); chargingMessage.setServiceId(service_id); // 设备消息下发 @@ -127,7 +127,6 @@ log.warn("ONLINE消息处理:{} 毫秒", System.currentTimeMillis() - ONLINE); break; case SendTagConstant.PING: long PING = System.currentTimeMillis(); PingMessage pingMessage = JSON.parseObject(content.toJSONString(),PingMessage.class); String fullName = pingMessage.getCharging_pile_code() + pingMessage.getCharging_gun_code(); //主动丢弃一次心跳数据,用于降低调华为接口评率,防止被限流 @@ -174,8 +173,6 @@ vo1.setPile_code(pingMessage.getCharging_pile_code()); vo1.setStatus(pingMessage.getCharging_gun_status()); chargingPileClient.updateChargingPileStatus(vo1); log.warn("PING消息处理:{} 毫秒", System.currentTimeMillis() - PING); break; case SendTagConstant.END_CHARGE: long END_CHARGE = System.currentTimeMillis(); @@ -531,6 +528,22 @@ return AjaxResult.success(); } /** * 从IoTDA推送报文中提取设备编号(node_id 优先,缺失时回退 device_id) */ private String deviceNoOf(JSONObject msg) { try { JSONObject header = msg.getJSONObject("notify_data").getJSONObject("header"); if (header == null) { return null; } String nodeId = header.getString("node_id"); return (nodeId == null || nodeId.isEmpty()) ? header.getString("device_id") : nodeId; } catch (Exception e) { return null; } } @ApiOperation(value = "运营平台远程控制启机",tags = {"硬件接口"}) @GetMapping(value = "/platformStartCharging") ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/produce/IotMessageProduce.java
@@ -7,6 +7,7 @@ import com.huaweicloud.sdk.iotda.v5.model.DeviceMessageRequest; import com.ruoyi.common.core.utils.uuid.UUID; import com.ruoyi.integration.iotda.builder.IotBuilder; import com.ruoyi.integration.iotda.utils.tools.IotCommandLog; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; @@ -38,11 +39,15 @@ body.withName(name); body.withMessage(message); request.withBody(body); // 记录下行指令日志(独立日志文件、含指令中文含义;心跳应答 pong 不记录) String serviceId = message == null ? null : message.getString("service_id"); IotCommandLog.downlink(code, name, serviceId, message); try { CreateMessageResponse response = iotBuilder.buildIot().createMessage(request); return JSON.toJSONString(response); } catch (Exception e) { e.printStackTrace(); IotCommandLog.downlinkFail(code, name, serviceId, message, e.getMessage()); return e.getMessage(); } } ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/IotCommandLog.java
New file @@ -0,0 +1,157 @@ package com.ruoyi.integration.iotda.utils.tools; import com.ruoyi.integration.iotda.constant.SendTagConstant; import com.ruoyi.integration.iotda.enums.ServiceIdMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; /** * 充电桩指令收发日志 * <p> * 1、使用独立 logger(pile-command)输出到独立日志文件,与业务日志分离(见 logback.xml); * 2、日志记录统一打印指令含义,格式:指令=中文含义(service_id); * 3、心跳指令(上行 ping、下行 pong)不记录,避免日志量过大。 */ public class IotCommandLog { /** 独立日志 logger,输出到独立日志文件(logback.xml 中 pile-command 配置) */ private static final Logger COMMAND_LOG = LoggerFactory.getLogger("pile-command"); /** 上行指令 service_id -> 中文含义 */ private static final Map<String, String> UPLINK_MEANING = new HashMap<>(); static { UPLINK_MEANING.put(SendTagConstant.ONLINE, "充电桩登录认证"); UPLINK_MEANING.put(SendTagConstant.PING, "充电桩心跳包"); UPLINK_MEANING.put(SendTagConstant.END_CHARGE, "充电结束"); UPLINK_MEANING.put(SendTagConstant.ERROR_MESSAGE, "错误报文"); UPLINK_MEANING.put(SendTagConstant.BILLING_MODE_VERIFY, "计费模型验证请求"); UPLINK_MEANING.put(SendTagConstant.ACQUISITION_BILLING_MODE, "充电桩计费模型请求"); UPLINK_MEANING.put(SendTagConstant.UPLOAD_REAL_TIME_MONITORING_DATA, "上传实时监测数据"); UPLINK_MEANING.put(SendTagConstant.CHARGING_HANDSHAKE, "充电握手"); UPLINK_MEANING.put(SendTagConstant.PARAMETER_SETTING, "参数配置"); UPLINK_MEANING.put(SendTagConstant.BMS_ABORT, "充电阶段BMS中止"); UPLINK_MEANING.put(SendTagConstant.MOTOR_ABORT, "充电阶段充电机中止"); UPLINK_MEANING.put(SendTagConstant.BMS_DEMAND_AND_CHARGER_EXPORTATION, "充电过程BMS需求、充电机输出"); UPLINK_MEANING.put(SendTagConstant.BMS_INFORMATION, "充电过程BMS信息"); UPLINK_MEANING.put(SendTagConstant.CHARGING_PILE_STARTS_CHARGING, "充电桩主动申请启动充电"); UPLINK_MEANING.put(SendTagConstant.PLATFORM_START_CHARGING_REPLY, "远程启机命令回复"); UPLINK_MEANING.put(SendTagConstant.PLATFORM_STOP_CHARGING_REPLY, "远程停机命令回复"); UPLINK_MEANING.put(SendTagConstant.TRANSACTION_RECORD, "交易记录"); UPLINK_MEANING.put(SendTagConstant.UPDATE_BALANCE_REPLY, "余额更新应答"); UPLINK_MEANING.put(SendTagConstant.SYNCHRONIZE_OFFLINE_CARD_REPLY, "卡数据同步应答"); UPLINK_MEANING.put(SendTagConstant.CLEAR_OFFLINE_CARD_REPLY, "离线卡数据清除应答"); UPLINK_MEANING.put(SendTagConstant.QUERY_OFFLINE_CARD_REPLY, "离线卡数据查询应答"); UPLINK_MEANING.put(SendTagConstant.WORKING_PARAMETER_SETTING_REPLY, "充电桩工作参数设置应答"); UPLINK_MEANING.put(SendTagConstant.TIMING_SETTING, "对时设置"); UPLINK_MEANING.put(SendTagConstant.SETUP_BILLING_MODEL_REPLY, "计费模型应答"); UPLINK_MEANING.put(SendTagConstant.GROUND_LOCK_REAL_TIME_DATA, "地锁数据上送"); UPLINK_MEANING.put(SendTagConstant.CHARGING_PILE_RETURNS_GROUND_LOCK_DATA, "充电桩返回地锁数据"); UPLINK_MEANING.put(SendTagConstant.PLATFORM_RESTART_REPLY, "远程重启应答"); UPLINK_MEANING.put(SendTagConstant.PLATFORM_REMOTE_UPDATE_REPLY, "远程更新应答"); UPLINK_MEANING.put(SendTagConstant.QR_CODE_DELIVERY_REPLY, "二维码下发应答"); UPLINK_MEANING.put(SendTagConstant.SECURITY_DETECTION, "安全监测"); UPLINK_MEANING.put(SendTagConstant.GUN_STATUS, "推送充电设备接口状态信息"); UPLINK_MEANING.put(SendTagConstant.ORDER_STATUS, "推送充电状态信息"); UPLINK_MEANING.put(SendTagConstant.ORDER_INFO, "推送充电订单信息"); } private IotCommandLog() { } /** * 记录上行指令(设备 -> 平台) * 心跳(ping)不记录 * * @param deviceNo 设备编号 * @param serviceId 指令 service_id * @param payload 报文内容 */ public static void uplink(String deviceNo, String serviceId, Object payload) { if (SendTagConstant.PING.equals(serviceId)) { // 心跳不记录日志 return; } COMMAND_LOG.info("[上行] 设备={} 指令={} 报文={}", deviceNo, describeUplink(serviceId), payload); } /** * 记录下行指令(平台 -> 设备) * 心跳应答(pong)不记录 * * @param deviceNo 设备编号 * @param name 下发名称 * @param serviceId 指令 service_id * @param payload 报文内容 */ public static void downlink(String deviceNo, String name, String serviceId, Object payload) { if (isPong(serviceId)) { // 心跳应答不记录日志 return; } COMMAND_LOG.info("[下行] 设备={} 指令={} 报文={}", deviceNo, describeDownlink(serviceId, name), payload); } /** * 记录下行指令下发失败 * 心跳应答(pong)不记录 * * @param deviceNo 设备编号 * @param name 下发名称 * @param serviceId 指令 service_id * @param payload 报文内容 * @param error 失败原因 */ public static void downlinkFail(String deviceNo, String name, String serviceId, Object payload, String error) { if (isPong(serviceId)) { // 心跳应答不记录日志 return; } COMMAND_LOG.error("[下行失败] 设备={} 指令={} 报文={} 错误={}", deviceNo, describeDownlink(serviceId, name), payload, error); } /** * 记录无法解析的上行原始报文(兜底) * 报文解析失败时无法识别 service_id,不能判断是否为心跳,仍记录原始内容便于排查 * * @param deviceNo 设备编号 * @param rawPayload 原始报文内容 */ public static void uplinkUnparsed(String deviceNo, Object rawPayload) { COMMAND_LOG.info("[上行-原始报文] 设备={} 报文={}", deviceNo, rawPayload); } /** 是否心跳应答(pong) */ private static boolean isPong(String serviceId) { return ServiceIdMenu.PONG.getValue().equals(serviceId); } /** 上行指令含义:中文含义(service_id) */ private static String describeUplink(String serviceId) { String meaning = UPLINK_MEANING.get(serviceId); if (meaning == null || meaning.isEmpty()) { meaning = "未识别指令"; } return meaning + "(" + serviceId + ")"; } /** 下行指令含义:中文含义(service_id) */ private static String describeDownlink(String serviceId, String name) { String meaning = null; if (serviceId != null) { for (ServiceIdMenu menu : ServiceIdMenu.values()) { if (menu.getValue().equals(serviceId)) { meaning = menu.getKey(); break; } } } if (meaning == null || meaning.isEmpty()) { meaning = (name == null || name.isEmpty()) ? "未识别指令" : name; } return meaning + "(" + serviceId + ")"; } } ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
@@ -163,6 +163,32 @@ </filter> </appender> <!-- 2.6 充电桩指令收发独立日志,独立文件输出(心跳不记录,见 IotCommandLog) --> <appender name="PILE_COMMAND_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 正在记录的日志文档的路径及文档名 --> <file>${log.path}/pile-command.log</file> <!--日志文档输出格式--> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> <!-- 设置字符集 --> </encoder> <!-- 日志记录器的滚动策略,按日期,按大小记录 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志归档 --> <fileNamePattern>${log.path}/%d{yyyy-MM-dd}/pile-command-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!--日志文档保留天数--> <maxHistory>30</maxHistory> </rollingPolicy> </appender> <!-- 充电桩指令收发日志:独立文件输出,不向 root 传递(additivity=false),避免污染业务日志 --> <logger name="pile-command" level="INFO" additivity="false"> <appender-ref ref="PILE_COMMAND_FILE" /> </logger> <!-- <logger>用来设置某一个包或者具体的某一个类的日志打印级别、 以及指定<appender>。<logger>仅有一个name属性, ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -10,6 +10,7 @@ import java.math.RoundingMode; import java.text.DecimalFormat; import java.time.*; import java.util.concurrent.TimeUnit; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -1871,6 +1872,12 @@ /** * 安全检测等待窗口(秒):设备在此时长内未上报有效安全检测时,前端按「未插枪」提示用户, * 超时订单由启动超时兜底任务自动取消,避免订单永久停留在启动中状态。 */ private static final long STARTUP_WAIT_SECONDS = 90L; @ResponseBody @GetMapping(value = "/preChargeCheck/{id}") @ApiOperation(value = "获取安全检测数据", tags = {"小程序-扫一扫"}) @@ -1880,10 +1887,17 @@ public AjaxResult<PreChargeCheck> preChargeCheck(@PathVariable Integer id) { String key = "AQJC_" + id; PreChargeCheck cacheObject = redisService.getCacheObject(key); if(null != cacheObject){ if(LocalDateTime.now().minusMinutes(1).isAfter(cacheObject.getAddTime())){ redisService.deleteObject(key); cacheObject = null; if (null != cacheObject) { LocalDateTime addTime = cacheObject.getAddTime(); // 等待安全检测超出窗口且启动指令尚未下发时,按「未插枪」提示前端; // 缓存必须保留:设备安全检测上报依赖该缓存推进启动,删除会导致订单永久卡在启动中 if (Integer.valueOf(1).equals(cacheObject.getStartupSuccess()) && !Boolean.TRUE.equals(cacheObject.getStartCommandSent()) && null != addTime && LocalDateTime.now().isAfter(addTime.plusSeconds(STARTUP_WAIT_SECONDS))) { cacheObject.setStartupSuccess(3); cacheObject.setFailureCause(5); redisService.setCacheObject(key, cacheObject, 24L, TimeUnit.HOURS); } } return AjaxResult.success(cacheObject); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderEvaluateController.java
@@ -183,7 +183,7 @@ @ApiOperation(value = "获取订单评价详情", tags = {"小程序-充电记录"}) @ApiImplicitParams({ @ApiImplicitParam(value = "订单id", name = "orderId", required = true), @ApiImplicitParam(value = "订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单)", name = "orderType", required = true), @ApiImplicitParam(value = "订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单,5=VIN码充电订单)", name = "orderType", required = true), }) public AjaxResult<OrderEvaluateVo> getOrderEvaluate(String orderId, Integer orderType){ OrderEvaluateVo orderEvaluate = orderEvaluateService.getOrderEvaluate(orderId, orderType); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingDetails.java
@@ -36,7 +36,7 @@ private BigDecimal chargedDegrees; @ApiModelProperty("已充电时间") private String chargedTime; @ApiModelProperty("充电费用") @ApiModelProperty("已使用费用(设备上报已充金额,缺失时回退订单已使用金额)") private BigDecimal chargingCost; @ApiModelProperty("充电状态(0=未知,1=等待中,2=启动中,3=充电中,4=停止中,5=已结束)") private Integer status; ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -306,7 +306,9 @@ result.put("discountServiceCharge", order.getDiscountServiceCharge()); // 账户模式/授权截止时间/申请锁定额度(刷卡链路改造) result.put("accountMode", order.getAccountMode()); result.put("maxEndTime", order.getMaxEndTime()); // Map 值不走实体字段上的 @JsonFormat,显式格式化,避免前端展示 ISO 格式(含 T) result.put("maxEndTime", order.getMaxEndTime() == null ? null : order.getMaxEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); result.put("lockedAmount", order.getLockedAmount()); result.put("total", defaultDecimal(order.getElectrovalence()).add(defaultDecimal(order.getServiceCharge()))); result.put("realTotal", order.getPaymentAmount()); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -1936,13 +1936,7 @@ chargingOrder.setPayTime(callbackPayTime); //添加安全检测数据到缓存中,每步安全检测完成后需要更新缓存数据 PreChargeCheck preChargeCheck = new PreChargeCheck(); preChargeCheck.setElectronicLockLock(false); preChargeCheck.setInsulationTesting(false); preChargeCheck.setSecureConnectionDetection(false); preChargeCheck.setStartupSuccess(1); preChargeCheck.setStartCommandSent(false); preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now()); PreChargeCheck preChargeCheck = newPreChargeCheck(chargingOrder); String key = "AQJC_" + chargingOrder.getChargingGunId(); redisService.setCacheObject(key, preChargeCheck, 24L, TimeUnit.HOURS); @@ -2082,6 +2076,33 @@ && Boolean.TRUE.equals(check.getSecureConnectionDetection()); } /** * 查询充电枪进行中的待启动订单(支付完成、启动中) */ private TChargingOrder getPendingStartOrder(Integer chargingGunId) { return this.getOne(new LambdaQueryWrapper<TChargingOrder>() .eq(TChargingOrder::getChargingGunId, chargingGunId) .eq(TChargingOrder::getRechargePaymentStatus, 2) .eq(TChargingOrder::getStatus, 2) .eq(TChargingOrder::getDelFlag, 0) .orderByDesc(TChargingOrder::getCreateTime) .last("limit 1")); } /** * 初始化安全检测等待对象(与建单/支付完成时的初始状态保持一致) */ private PreChargeCheck newPreChargeCheck(TChargingOrder chargingOrder) { PreChargeCheck preChargeCheck = new PreChargeCheck(); preChargeCheck.setElectronicLockLock(false); preChargeCheck.setInsulationTesting(false); preChargeCheck.setSecureConnectionDetection(false); preChargeCheck.setStartupSuccess(1); preChargeCheck.setStartCommandSent(false); preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now()); return preChargeCheck; } private String normalizeVin(String vin) { return StringUtils.hasLength(vin) ? vin.replaceAll("\\s+", "").toUpperCase(Locale.ROOT) : null; } @@ -2111,6 +2132,44 @@ } } /** * 启动超时兜底:订单支付完成后长时间未真正开始充电(等待安全检测无进展、启动指令未生效), * 自动取消订单并释放锁定额度,避免订单永久停留在「启动中」导致该枪无法再次启动。 */ @Scheduled(fixedDelay = 30000L) public void cancelStartupTimeoutOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(3); List<TChargingOrder> timeoutOrders = this.list(new LambdaQueryWrapper<TChargingOrder>() .eq(TChargingOrder::getRechargePaymentStatus, 2) .eq(TChargingOrder::getStatus, 2) .eq(TChargingOrder::getDelFlag, 0) .isNull(TChargingOrder::getStartTime) .isNull(TChargingOrder::getReservationTime) .lt(TChargingOrder::getPayTime, deadline) .last("limit 100")); for (TChargingOrder order : timeoutOrders) { try { PreChargeCheck check = redisService.getCacheObject("AQJC_" + order.getChargingGunId()); if (null != check && Boolean.TRUE.equals(check.getStartCommandSent())) { // 已下发启动指令,交给既有启动结果检测(timingDetection)处理,避免误取消 continue; } log.warn("订单编号={}:等待安全检测超时,自动取消订单并释放额度,充电枪id={}", order.getCode(), order.getChargingGunId()); refund(order.getCode()); TChargingOrder finish = new TChargingOrder(); finish.setId(order.getId()); finish.setAppUserId(order.getAppUserId()); finish.setStatus(5); finish.setEndMode(0); finish.setEndTime(LocalDateTime.now()); this.updateById(finish); redisService.deleteObject("AQJC_" + order.getChargingGunId()); } catch (Exception e) { log.error("启动超时订单自动取消失败,orderCode={}", order.getCode(), e); } } } private void startChargingAfterSafety(TChargingOrder chargingOrder, PreChargeCheck check) { RLock lock = redissonClient.getLock("START_AFTER_SAFETY:" + chargingOrder.getId()); try { @@ -2133,12 +2192,18 @@ command.setCharging_gun_code(gun.getCode()); command.setCard_number(latest.getId().toString()); command.setAccount_balance(latest.getChargeAmount()); check.setStartCommandSent(true); latest.setActualVin(actualVin); this.updateById(latest); redisService.setCacheObject("AQJC_" + latest.getChargingGunId(), check, 24L, TimeUnit.HOURS); log.info(latest.getCode() + ":安全检测通过,下发启动充电命令:" + command); sendMessageClient.platformStartCharging(command); try { sendMessageClient.platformStartCharging(command); } catch (Exception e) { // 下发失败不置 startCommandSent:保留后续安全检测上报重试机会,超时订单由兜底任务取消 log.error(latest.getCode() + ":启动充电命令下发失败,等待下次安全检测上报重试", e); return; } check.setStartCommandSent(true); redisService.setCacheObject("AQJC_" + latest.getChargingGunId(), check, 24L, TimeUnit.HOURS); ScheduledExecutorService resultScheduler = Executors.newScheduledThreadPool(1); resultScheduler.scheduleAtFixedRate(() -> { if (timingDetection(latest.getId())) { @@ -2177,8 +2242,13 @@ Integer failure_cause = platformStartChargingReply.getFailure_cause(); Integer counter = counter_map.get(code); PreChargeCheck preChargeCheck1 = redisService.getCacheObject(key); //15分钟内还未插枪则取消充电,退回金额。 if (failure_cause == 5 && (null == counter || counter < 60 * 15)) { if (null == preChargeCheck1) { // 缓存缺失时用临时对象推进,避免启动结果补偿因空缓存中断 preChargeCheck1 = new PreChargeCheck(); preChargeCheck1.setStartupSuccess(1); } //15分钟内还未插枪则取消充电,退回金额。按10秒探测一次,90次约15分钟 if (failure_cause == 5 && (null == counter || counter < 90)) { counter = (null == counter ? 0 : counter) + 1; counter_map.put(code, counter); //启动失败 @@ -2229,6 +2299,11 @@ return true; } else { PreChargeCheck preChargeCheck1 = redisService.getCacheObject(key); if (null == preChargeCheck1) { // 缓存缺失时用临时对象推进,避免启动结果补偿因空缓存中断 preChargeCheck1 = new PreChargeCheck(); preChargeCheck1.setStartupSuccess(1); } UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getDataByOrderCode(code, "desc"); if (null != uploadRealTimeMonitoringData) { //启动成功 @@ -2260,8 +2335,8 @@ } else { Integer counter = boot_failed_map.get(code); log.info(code + ":-------------------未上传开启充电结果-------------------" + counter); //15分钟内未启动成功,退回金额。 if (null == counter || counter < 60 * 15) { //15分钟内未启动成功,退回金额。按10秒探测一次,90次约15分钟 if (null == counter || counter < 90) { counter = (null == counter ? 0 : counter) + 1; boot_failed_map.put(code, counter); //启动失败 @@ -2313,25 +2388,43 @@ GetChargingGunByCode code = new GetChargingGunByCode(); code.setCharging_pile_code(securityDetection.getCharging_pile_code()); code.setCharging_gun_code(securityDetection.getCharging_gun_code()); TChargingGun chargingGun = chargingGunClient.getChargingGunByCode(code).getData(); PreChargeCheck preChargeCheck1 = redisService.getCacheObject("AQJC_" + chargingGun.getId()); if (null != preChargeCheck1) { preChargeCheck1.setElectronicLockLock(Integer.valueOf(1).equals(securityDetection.getElectronic_lock_lock())); preChargeCheck1.setInsulationTesting(Integer.valueOf(0).equals(securityDetection.getNsulation_detection())); preChargeCheck1.setSecureConnectionDetection(Integer.valueOf(1).equals(securityDetection.getSecure_connection())); preChargeCheck1.setActualVin(normalizeVin(securityDetection.getVin())); redisService.setCacheObject("AQJC_" + chargingGun.getId(), preChargeCheck1, 24L, TimeUnit.HOURS); if (isSafetyCheckPassed(preChargeCheck1)) { TChargingOrder pendingOrder = this.getOne(new LambdaQueryWrapper<TChargingOrder>() .eq(TChargingOrder::getChargingGunId, chargingGun.getId()) .eq(TChargingOrder::getRechargePaymentStatus, 2) .eq(TChargingOrder::getStatus, 2) .eq(TChargingOrder::getDelFlag, 0) .orderByDesc(TChargingOrder::getCreateTime) .last("limit 1")); if (pendingOrder != null) { startChargingAfterSafety(pendingOrder, preChargeCheck1); } TChargingGun chargingGun = null; try { chargingGun = chargingGunClient.getChargingGunByCode(code).getData(); } catch (Exception e) { log.error("安全检测上报-查询充电枪异常,charging_pile_code={},charging_gun_code={}", securityDetection.getCharging_pile_code(), securityDetection.getCharging_gun_code(), e); } if (null == chargingGun) { log.warn("安全检测上报-未匹配到充电枪,忽略,charging_pile_code={},charging_gun_code={}", securityDetection.getCharging_pile_code(), securityDetection.getCharging_gun_code()); return; } String key = "AQJC_" + chargingGun.getId(); PreChargeCheck preChargeCheck1 = redisService.getCacheObject(key); if (null == preChargeCheck1) { // 缓存缺失(等待窗口内未收到上报、缓存被清理)时按该枪进行中的待启动订单重建, // 保证设备后续安全检测上报仍能推进启动,避免订单永久卡在启动中 TChargingOrder pendingOrder = getPendingStartOrder(chargingGun.getId()); if (null == pendingOrder) { log.info("安全检测上报-无待启动订单,忽略,充电枪id={},charging_pile_code={},charging_gun_code={}", chargingGun.getId(), securityDetection.getCharging_pile_code(), securityDetection.getCharging_gun_code()); return; } preChargeCheck1 = newPreChargeCheck(pendingOrder); log.warn("安全检测上报-AQJC缓存缺失,已按待启动订单重建,订单编号={},充电枪id={}", pendingOrder.getCode(), chargingGun.getId()); } preChargeCheck1.setElectronicLockLock(Integer.valueOf(1).equals(securityDetection.getElectronic_lock_lock())); preChargeCheck1.setInsulationTesting(Integer.valueOf(0).equals(securityDetection.getNsulation_detection())); preChargeCheck1.setSecureConnectionDetection(Integer.valueOf(1).equals(securityDetection.getSecure_connection())); preChargeCheck1.setActualVin(normalizeVin(securityDetection.getVin())); // 刷新等待基准时间,避免前端等待窗口/超时判断误伤正在推进的安全检测 preChargeCheck1.setAddTime(LocalDateTime.now()); redisService.setCacheObject(key, preChargeCheck1, 24L, TimeUnit.HOURS); if (isSafetyCheckPassed(preChargeCheck1)) { TChargingOrder pendingOrder = getPendingStartOrder(chargingGun.getId()); if (pendingOrder != null) { startChargingAfterSafety(pendingOrder, preChargeCheck1); } } } @@ -2356,6 +2449,11 @@ preChargeCheck1.setElectronicLockLock(true); preChargeCheck1.setInsulationTesting(true); preChargeCheck1.setStartupSuccess(1); } else { // 缓存缺失时用临时对象推进订单状态,避免设备已启动成功而订单因缓存为空无法流转 preChargeCheck1 = new PreChargeCheck(); preChargeCheck1.setElectronicLockLock(true); preChargeCheck1.setInsulationTesting(true); } if (0 == startup_result) { //启动失败 @@ -2750,19 +2848,22 @@ @Override public ChargingDetails getChargingDetails(Integer id) { Long userId = tokenService.getLoginUserApplet().getUserId(); // 状态包含已结束(5):订单结束后小程序需继续拿到该笔订单,用于关闭「请勿拔枪,正在结束充电」 // 并展示「充电完成,可拔枪」;仅取最近一笔,避免历史完成订单长期滞留结束态 TChargingOrder one = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getAppUserId, userId).eq(TChargingOrder::getChargingGunId, id) .eq(TChargingOrder::getRechargePaymentStatus, 2) .in(TChargingOrder::getStatus, Arrays.asList(2, 3, 4)) .in(TChargingOrder::getStatus, Arrays.asList(2, 3, 4, 5)) .eq(TChargingOrder::getDelFlag, 0) .orderByDesc(TChargingOrder::getCreateTime) .last(" limit 0, 1")); if (null == one || !one.getAppUserId().equals(userId)) { // VIN 码充电订单兜底:VD 订单存于 t_vin_charging_order,且后付模式无支付动作, // 不按 rechargePaymentStatus 过滤;TVinChargingOrder 继承 TChargingOrder,后续组装原样复用 // 状态同样包含已结束(5),保证 VIN 订单结束后前端仍能拿到完成态 TVinChargingOrder vinOrder = vinChargingOrderMapper.selectOne(new LambdaQueryWrapper<TVinChargingOrder>() .eq(TVinChargingOrder::getAppUserId, userId) .eq(TVinChargingOrder::getChargingGunId, id) .in(TVinChargingOrder::getStatus, Arrays.asList(2, 3, 4)) .in(TVinChargingOrder::getStatus, Arrays.asList(2, 3, 4, 5)) .eq(TVinChargingOrder::getDelFlag, 0) .orderByDesc(TVinChargingOrder::getCreateTime) .last(" limit 0, 1")); @@ -2782,7 +2883,8 @@ boolean reservationPending = ReservationOrderSupport.isPending( one.getStatus(), one.getReservationTime(), LocalDateTime.now()); chargingDetails.setStatus(reservationPending ? 1 : one.getStatus()); chargingDetails.setChargingCost(one.getResidualAmount()); // 已使用费用:默认取订单已使用金额,存在实时监测数据时以设备上报已充金额(电费+服务费)为准 chargingDetails.setChargingCost(money(one.getResidualAmount())); UploadRealTimeMonitoringData data = uploadRealTimeMonitoringDataService.getLastDataById(one.getCode()); if (null != data) { chargingDetails.setChargeCurrent(data.getOutput_current()); @@ -2795,6 +2897,10 @@ Integer h = Integer.valueOf(data.getCumulative_charging_time() / 60); Integer m = Integer.valueOf(data.getCumulative_charging_time() % 60); chargingDetails.setChargedTime(String.format("%02d", h) + ":" + String.format("%02d", m)); // 设备上报已充金额非空时覆盖,上报缺失则沿用订单已使用金额 if (null != data.getPaid_amount()) { chargingDetails.setChargingCost(money(data.getPaid_amount())); } if (null != one.getAppUserCarId()) { TAppUserCar appUserCar = appUserCarClient.getCarById(one.getAppUserCarId().toString()).getData(); // 续航 = 续航里程 * soc @@ -8216,13 +8322,7 @@ //添加安全检测数据到缓存中,每步安全检测完成后需要更新缓存数据 PreChargeCheck preChargeCheck = new PreChargeCheck(); preChargeCheck.setElectronicLockLock(false); preChargeCheck.setInsulationTesting(false); preChargeCheck.setSecureConnectionDetection(false); preChargeCheck.setStartupSuccess(1); preChargeCheck.setStartCommandSent(false); preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now()); PreChargeCheck preChargeCheck = newPreChargeCheck(chargingOrder); String key = "AQJC_" + chargingOrder.getChargingGunId(); redisService.setCacheObject(key, preChargeCheck, 24L, TimeUnit.HOURS); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImpl.java
@@ -24,6 +24,7 @@ import com.ruoyi.order.api.model.TEvaluationAutoReply; import com.ruoyi.order.api.model.TOrderEvaluate; import com.ruoyi.order.api.model.TOrderEvaluateTag; import com.ruoyi.order.api.model.TVinChargingOrder; import com.ruoyi.order.api.query.TOrderEvaluateQuery; import com.ruoyi.order.api.vo.SiteDetailEvaluateVO; import com.ruoyi.order.api.vo.TOrderEvaluateVO; @@ -36,6 +37,7 @@ import com.ruoyi.order.service.TChargingOrderService; import com.ruoyi.order.service.TEvaluationAutoReplyService; import com.ruoyi.order.service.TOrderEvaluateService; import com.ruoyi.order.service.TVinChargingOrderService; import com.ruoyi.order.service.WxSubscribeMessageService; import com.ruoyi.other.api.domain.TEvaluationTag; import com.ruoyi.other.api.domain.TIntegralRule; @@ -90,6 +92,9 @@ @Resource private TChargingOrderService chargingOrderService; @Resource private TVinChargingOrderService vinChargingOrderService; @Resource private IntegralRuleClient integralRuleClient; @@ -345,9 +350,25 @@ BeanUtils.copyProperties(query, orderEvaluate); Long userid = tokenService.getLoginUserApplet().getUserId(); orderEvaluate.setAppUserId(userid); if(query.getOrderType() == 1){ Integer orderType = query.getOrderType(); if(null == orderType){ return AjaxResult.error("订单类型不能为空"); } if(orderType == 1){ TChargingOrder chargingOrder = chargingOrderService.getById(query.getOrderId()); if(null == chargingOrder){ return AjaxResult.error("订单不存在"); } orderEvaluate.setAppUserCarId(chargingOrder.getAppUserCarId()); }else if(orderType == 5){ //VIN码充电订单,仅允许评价本人订单 TVinChargingOrder vinChargingOrder = vinChargingOrderService.getById(query.getOrderId()); if(null == vinChargingOrder || !userid.equals(vinChargingOrder.getAppUserId())){ return AjaxResult.error("订单不存在"); } orderEvaluate.setAppUserCarId(vinChargingOrder.getAppUserCarId()); }else{ return AjaxResult.error("订单类型不支持评价"); } TAppUser appUser = appUserClient.getUserById(userid).getData(); @@ -392,7 +413,6 @@ appUser.setPoints(appUser.getPoints() + num1); appUserClient.updateAppUser(appUser); TChargingOrder chargingOrder = chargingOrderService.getById(orderEvaluate.getOrderId()); //积分变动明细 TAppUserIntegralChange appUserIntegralChange = new TAppUserIntegralChange(); appUserIntegralChange.setAppUserId(appUser.getId()); @@ -400,7 +420,7 @@ appUserIntegralChange.setHistoricalIntegral(points); appUserIntegralChange.setCurrentIntegral(points + num1); appUserIntegralChange.setCreateTime(LocalDateTime.now()); appUserIntegralChange.setOrderCode(chargingOrder.getCode()); appUserIntegralChange.setOrderCode(getOrderCode(orderEvaluate.getOrderType(), orderEvaluate.getOrderId())); appUserIntegralChange.setExtension(orderEvaluate.getOrderId().toString()); appUserIntegralChangeClient.addAppUserIntegralChange(appUserIntegralChange); } @@ -408,6 +428,21 @@ return AjaxResult.success(); } /** * 根据评价的订单类型获取对应的订单编号 * @param orderType 订单类型(1=充电订单,5=VIN码充电订单) * @param orderId 订单id * @return 订单编号 */ private String getOrderCode(Integer orderType, Long orderId) { if(Integer.valueOf(5).equals(orderType)){ TVinChargingOrder vinChargingOrder = vinChargingOrderService.getById(orderId); return null == vinChargingOrder ? null : vinChargingOrder.getCode(); } TChargingOrder chargingOrder = chargingOrderService.getById(orderId); return null == chargingOrder ? null : chargingOrder.getCode(); } private void applyAutoReply(TOrderEvaluate orderEvaluate) { TEvaluationAutoReply rule = evaluationAutoReplyService.matchRule(orderEvaluate.getContent(), orderEvaluate.getMark()); if (rule == null || StringUtils.isEmpty(rule.getReplyContent())) { ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TCardChargingOrderMapper.xml
@@ -36,7 +36,7 @@ AND o.charging_gun_id = #{query.gunId} </if> <if test="startTime1 != null and startTime1 != '' and startTime2 != null and startTime2 != ''"> AND o.pay_time BETWEEN #{startTime1} AND #{startTime2} AND o.start_time BETWEEN #{startTime1} AND #{startTime2} </if> <if test="endTime1 != null and endTime1 != '' and endTime2 != null and endTime2 != ''"> AND o.end_time BETWEEN #{endTime1} AND #{endTime2} ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml
@@ -60,7 +60,7 @@ o.status, CASE WHEN EXISTS ( SELECT 1 FROM t_order_evaluate e WHERE e.order_id = o.id AND e.order_type = 1 AND e.del_flag = 0 WHERE e.order_id = o.id AND e.order_type = 5 AND e.del_flag = 0 ) THEN 2 ELSE 1 END AS evaluate FROM t_vin_charging_order o WHERE o.del_flag = 0 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplVinFallbackTest.java
@@ -18,6 +18,8 @@ import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService; import com.ruoyi.order.vo.EndOfChargePageInfo; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.AbstractWrapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.ruoyi.system.api.model.LoginUserApplet; import org.apache.ibatis.builder.MapperBuilderAssistant; @@ -25,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,13 +35,19 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -134,6 +143,46 @@ } @Test void getChargingDetailsUsesPaidAmountAsUsedCostWhenRealTimeDataPresent() { LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); when(mapper.selectOne(any())).thenReturn(null); TVinChargingOrder vinOrder = new TVinChargingOrder(); vinOrder.setId(1001L); vinOrder.setAppUserId(88L); vinOrder.setChargingGunId(5); vinOrder.setStatus(3); vinOrder.setCode("VD20261013001"); vinOrder.setResidualAmount(new BigDecimal("66.60")); when(vinChargingOrderMapper.selectOne(any())).thenReturn(vinOrder); TChargingGun gun = new TChargingGun(); gun.setChargingPileId(7); when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun)); TChargingPile pile = new TChargingPile(); pile.setName("1号桩"); pile.setSiteId(9); when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile)); Site site = new Site(); site.setName("城东站"); when(siteClient.getSiteByIds(Arrays.asList(9))).thenReturn(R.ok(Collections.singletonList(site))); UploadRealTimeMonitoringData data = new UploadRealTimeMonitoringData(); data.setOutput_current(new BigDecimal("10")); data.setOutput_voltage(new BigDecimal("220")); data.setSoc(50); data.setTime_remaining(30); data.setCharging_degree(new BigDecimal("5.5")); data.setCumulative_charging_time(75); // 设备上报已充金额为 4 位小数,展示口径统一保留 2 位 data.setPaid_amount(new BigDecimal("12.3456")); when(uploadRealTimeMonitoringDataService.getLastDataById("VD20261013001")).thenReturn(data); ChargingDetails details = service.getChargingDetails(5); assertEquals(0, new BigDecimal("12.35").compareTo(details.getChargingCost())); } @Test void getChargingDetailsReturnsNullWhenNeitherNormalNorVinOrderExists() { LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); @@ -191,4 +240,98 @@ assertNotNull(exception); assertEquals("充电订单不存在", exception.getMessage()); } @Test void getChargingDetailsReturnsFinishedNormalOrderAndQueriesStatus5() { // 小程序主动结束充电后订单已结束(5),充电中页面仍需拿到该笔订单, // 用于关闭「请勿拔枪,正在结束充电」并展示「充电完成,可拔枪」 LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); TChargingOrder order = new TChargingOrder(); order.setId(3003L); order.setAppUserId(88L); order.setChargingGunId(5); order.setStatus(5); order.setCode("CH20261014001"); order.setResidualAmount(new BigDecimal("20.00")); when(mapper.selectOne(any())).thenReturn(order); stubGunPileAndSite(); when(uploadRealTimeMonitoringDataService.getLastDataById("CH20261014001")).thenReturn(null); ChargingDetails details = service.getChargingDetails(5); assertNotNull(details, "订单已结束时应返回完成态而非 null"); assertEquals("3003", details.getId()); assertEquals(Integer.valueOf(5), details.getStatus()); assertEquals("CH20261014001", details.getCode()); assertEquals(0, new BigDecimal("20.00").compareTo(details.getChargingCost())); @SuppressWarnings("unchecked") ArgumentCaptor<Wrapper<TChargingOrder>> captor = ArgumentCaptor.forClass(Wrapper.class); verify(mapper).selectOne(captor.capture()); assertStatusFilterCoversFinished(captor.getValue()); } @Test void getChargingDetailsReturnsFinishedVinOrderAndQueriesStatus5() { // VIN 码订单结束后同样需要返回完成态,兜底查询的状态过滤必须与主查询一致 LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); when(mapper.selectOne(any())).thenReturn(null); TVinChargingOrder vinOrder = new TVinChargingOrder(); vinOrder.setId(1001L); vinOrder.setAppUserId(88L); vinOrder.setChargingGunId(5); vinOrder.setStatus(5); vinOrder.setCode("VD20261014001"); vinOrder.setResidualAmount(new BigDecimal("66.60")); when(vinChargingOrderMapper.selectOne(any())).thenReturn(vinOrder); stubGunPileAndSite(); when(uploadRealTimeMonitoringDataService.getLastDataById("VD20261014001")).thenReturn(null); ChargingDetails details = service.getChargingDetails(5); assertNotNull(details, "VIN 订单已结束时应返回完成态而非 null"); assertEquals(Integer.valueOf(5), details.getStatus()); assertEquals("VD20261014001", details.getCode()); @SuppressWarnings("unchecked") ArgumentCaptor<Wrapper<TVinChargingOrder>> captor = ArgumentCaptor.forClass(Wrapper.class); verify(vinChargingOrderMapper).selectOne(captor.capture()); assertStatusFilterCoversFinished(captor.getValue()); } /** getChargingDetails 组装链路依赖的充电枪/充电桩/站点远程调用 stub */ private void stubGunPileAndSite() { TChargingGun gun = new TChargingGun(); gun.setChargingPileId(7); when(chargingGunClient.getChargingGunById(5)).thenReturn(R.ok(gun)); TChargingPile pile = new TChargingPile(); pile.setName("1号桩"); pile.setSiteId(9); when(chargingPileClient.getChargingPileById(7)).thenReturn(R.ok(pile)); Site site = new Site(); site.setName("城东站"); when(siteClient.getSiteByIds(Arrays.asList(9))).thenReturn(R.ok(Collections.singletonList(site))); } /** * 断言订单查询条件中的 status 过滤覆盖 2、3、4、5。 * 已结束(5) 必须在过滤范围内,否则小程序结束充电后拿不到订单,会一直停留在「正在结束充电」提示。 */ private void assertStatusFilterCoversFinished(Wrapper<?> wrapper) { AbstractWrapper<?, ?, ?> queryWrapper = (AbstractWrapper<?, ?, ?>) wrapper; Matcher inMatcher = Pattern.compile("status\\s+in\\s*\\(([^)]*)\\)", Pattern.CASE_INSENSITIVE) .matcher(queryWrapper.getSqlSegment()); assertTrue(inMatcher.find(), "查询条件中应存在 status IN 过滤"); List<Object> statusValues = new ArrayList<>(); Matcher placeholderMatcher = Pattern.compile("MPGENVAL\\d+").matcher(inMatcher.group(1)); while (placeholderMatcher.find()) { statusValues.add(queryWrapper.getParamNameValuePairs().get(placeholderMatcher.group())); } assertTrue(statusValues.containsAll(Arrays.asList(2, 3, 4, 5)), "status 过滤应包含 2,3,4,5,实际为:" + statusValues); } } ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderStartupTimeoutTest.java
New file @@ -0,0 +1,187 @@ package com.ruoyi.order.service.impl; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient; import com.ruoyi.chargingPile.api.model.TChargingGun; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.redis.service.RedisService; import com.ruoyi.order.api.model.TChargingOrder; import com.ruoyi.order.api.vo.SecurityDetectionVO; import com.ruoyi.order.dto.PreChargeCheck; import com.ruoyi.order.mapper.TChargingOrderMapper; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; import java.util.Collections; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * 启动等待链路单测:安全检测上报缓存自愈重建、启动超时订单兜底取消 */ class TChargingOrderStartupTimeoutTest { private static final Integer GUN_ID = 1967; private static final String ORDER_CODE = "CD3720261014104949469"; /** * 纯单测无 Spring 容器,MyBatis-Plus 的 lambda 列缓存需手工初始化 */ @BeforeAll static void initMybatisPlusTableInfo() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, TChargingOrder.class); } private TChargingOrderServiceImpl service; private TChargingOrderMapper mapper; private RedisService redisService; private ChargingGunClient chargingGunClient; @BeforeEach void setUp() { service = spy(new TChargingOrderServiceImpl()); mapper = mock(TChargingOrderMapper.class); redisService = mock(RedisService.class); chargingGunClient = mock(ChargingGunClient.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "redisService", redisService); ReflectionTestUtils.setField(service, "chargingGunClient", chargingGunClient); } /** * 缓存缺失时按待启动订单重建,保证设备安全检测上报仍能推进启动 */ @Test void securityDetectionRebuildsCacheWhenCacheMissing() { when(chargingGunClient.getChargingGunByCode(any())).thenReturn(R.ok(chargingGun())); when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(null); when(mapper.selectOne(any())).thenReturn(pendingOrder()); // 绝缘检测未通过(1=不安全)时仅更新缓存,不进入启动下发 service.securityDetection(securityDetectionVO(1, 1, 1)); ArgumentCaptor<PreChargeCheck> captor = ArgumentCaptor.forClass(PreChargeCheck.class); verify(redisService).setCacheObject(eq("AQJC_" + GUN_ID), captor.capture(), eq(24L), eq(TimeUnit.HOURS)); PreChargeCheck saved = captor.getValue(); assertEquals(Integer.valueOf(1), saved.getStartupSuccess(), "重建后应回到等待启动状态"); assertEquals(Boolean.TRUE, saved.getElectronicLockLock()); assertEquals(Boolean.FALSE, saved.getInsulationTesting()); assertNotNull(saved.getAddTime(), "重建后应刷新等待基准时间"); } /** * 缓存缺失且无待启动订单时忽略上报,不产生脏缓存 */ @Test void securityDetectionIgnoresWhenNoPendingOrder() { when(chargingGunClient.getChargingGunByCode(any())).thenReturn(R.ok(chargingGun())); when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(null); when(mapper.selectOne(any())).thenReturn(null); service.securityDetection(securityDetectionVO(1, 0, 1)); verify(redisService, never()).setCacheObject(anyString(), any(), anyLong(), any(TimeUnit.class)); } /** * 等待安全检测超时的订单自动取消:退款、推进为已结束并清理缓存 */ @Test void startupTimeoutOrderIsCancelled() { when(mapper.selectList(any())).thenReturn(Collections.singletonList(pendingOrder())); when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(null); doNothing().when(service).refund(ORDER_CODE); service.cancelStartupTimeoutOrders(); ArgumentCaptor<TChargingOrder> captor = ArgumentCaptor.forClass(TChargingOrder.class); verify(mapper).updateById(captor.capture()); TChargingOrder finished = captor.getValue(); assertEquals(Integer.valueOf(5), finished.getStatus()); assertEquals(Integer.valueOf(0), finished.getEndMode()); assertNotNull(finished.getEndTime()); verify(service).refund(ORDER_CODE); verify(redisService).deleteObject("AQJC_" + GUN_ID); } /** * 已下发启动指令的订单交给启动结果检测处理,兜底任务不得误取消 */ @Test void startupTimeoutOrderWithCommandSentIsSkipped() { when(mapper.selectList(any())).thenReturn(Collections.singletonList(pendingOrder())); PreChargeCheck check = new PreChargeCheck(); check.setStartCommandSent(true); when(redisService.getCacheObject("AQJC_" + GUN_ID)).thenReturn(check); service.cancelStartupTimeoutOrders(); verify(service, never()).refund(anyString()); verify(mapper, never()).updateById(any()); verify(redisService, never()).deleteObject(anyString()); } /** * 初始化安全检测等待对象:预约订单以预约时间为等待基准 */ @Test void newPreChargeCheckUsesReservationTimeAsWaitBase() { LocalDateTime reservationTime = LocalDateTime.now().plusMinutes(5); TChargingOrder order = pendingOrder(); order.setReservationTime(reservationTime); PreChargeCheck check = ReflectionTestUtils.invokeMethod(service, "newPreChargeCheck", order); assertNotNull(check); assertEquals(reservationTime, check.getAddTime()); assertEquals(Integer.valueOf(1), check.getStartupSuccess()); assertEquals(Boolean.FALSE, check.getStartCommandSent()); } private static TChargingGun chargingGun() { TChargingGun gun = new TChargingGun(); gun.setId(GUN_ID); return gun; } private static TChargingOrder pendingOrder() { TChargingOrder order = new TChargingOrder(); order.setId(2110201381176229890L); order.setCode(ORDER_CODE); order.setChargingGunId(GUN_ID); order.setStatus(2); order.setRechargePaymentStatus(2); order.setDelFlag(false); order.setPayTime(LocalDateTime.now().minusMinutes(10)); return order; } private static SecurityDetectionVO securityDetectionVO(Integer electronicLock, Integer insulation, Integer secureConnection) { SecurityDetectionVO vo = new SecurityDetectionVO(); vo.setCharging_pile_code("8832010600495288"); vo.setCharging_gun_code("02"); vo.setElectronic_lock_lock(electronicLock); vo.setNsulation_detection(insulation); vo.setSecure_connection(secureConnection); return vo; } } ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TOrderEvaluateServiceImplVinTypeTest.java
New file @@ -0,0 +1,165 @@ package com.ruoyi.order.service.impl; import com.ruoyi.account.api.feignClient.AppUserClient; import com.ruoyi.account.api.model.TAppUser; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.common.security.service.TokenService; import com.ruoyi.order.api.model.TChargingOrder; import com.ruoyi.order.api.model.TOrderEvaluate; import com.ruoyi.order.api.model.TVinChargingOrder; import com.ruoyi.order.dto.OrderEvaluateVo; import com.ruoyi.order.mapper.TOrderEvaluateMapper; import com.ruoyi.order.service.TChargingOrderService; import com.ruoyi.order.service.TEvaluationAutoReplyService; import com.ruoyi.order.service.TVinChargingOrderService; import com.ruoyi.system.api.model.LoginUserApplet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * VIN 码充电订单评价(orderType=5)分支测试 * 覆盖:本人 VIN 订单可评价、他人订单/不存在订单被拒、普通充电订单不存在的兜底校验 */ @ExtendWith(MockitoExtension.class) class TOrderEvaluateServiceImplVinTypeTest { @InjectMocks private TOrderEvaluateServiceImpl service; @Mock private TOrderEvaluateMapper orderEvaluateMapper; @Mock private TokenService tokenService; @Mock private TChargingOrderService chargingOrderService; @Mock private TVinChargingOrderService vinChargingOrderService; @Mock private AppUserClient appUserClient; @Mock private TEvaluationAutoReplyService evaluationAutoReplyService; @BeforeEach void setUp() { ReflectionTestUtils.setField(service, "baseMapper", orderEvaluateMapper); ReflectionTestUtils.setField(service, "orderEvaluateMapper", orderEvaluateMapper); LoginUserApplet loginUser = new LoginUserApplet(); loginUser.setUserId(88L); when(tokenService.getLoginUserApplet()).thenReturn(loginUser); } private OrderEvaluateVo buildQuery(Long orderId, Integer orderType) { OrderEvaluateVo query = new OrderEvaluateVo(); query.setOrderId(orderId); query.setOrderType(orderType); query.setMark(5); return query; } @Test void addOrderEvaluateForOwnVinOrderStoresTypeFiveAndOwnerCar() { TVinChargingOrder vinOrder = new TVinChargingOrder(); vinOrder.setId(1001L); vinOrder.setAppUserId(88L); vinOrder.setAppUserCarId(66L); vinOrder.setCode("VD_1001"); when(vinChargingOrderService.getById(1001L)).thenReturn(vinOrder); TAppUser appUser = new TAppUser(); appUser.setId(88L); appUser.setWxOpenid("openid_88"); when(appUserClient.getUserById(88L)).thenReturn(R.ok(appUser)); when(orderEvaluateMapper.insert(any(TOrderEvaluate.class))).thenReturn(1); AjaxResult result = service.addOrderEvaluate(buildQuery(1001L, 5)); assertTrue(result.isSuccess()); ArgumentCaptor<TOrderEvaluate> captor = ArgumentCaptor.forClass(TOrderEvaluate.class); verify(orderEvaluateMapper).insert(captor.capture()); TOrderEvaluate saved = captor.getValue(); assertEquals(Integer.valueOf(5), saved.getOrderType()); assertEquals(Long.valueOf(88L), saved.getAppUserId()); assertEquals(Long.valueOf(66L), saved.getAppUserCarId()); // VIN 订单评价不应再走普通充电订单表 verify(chargingOrderService, never()).getById(anyLong()); } @Test void addOrderEvaluateRejectsVinOrderBelongingToOtherUser() { TVinChargingOrder vinOrder = new TVinChargingOrder(); vinOrder.setId(1001L); vinOrder.setAppUserId(99L); when(vinChargingOrderService.getById(1001L)).thenReturn(vinOrder); AjaxResult result = service.addOrderEvaluate(buildQuery(1001L, 5)); assertFalse(result.isSuccess()); assertEquals("订单不存在", result.get(AjaxResult.MSG_TAG)); verify(orderEvaluateMapper, never()).insert(any(TOrderEvaluate.class)); } @Test void addOrderEvaluateRejectsMissingVinOrder() { AjaxResult result = service.addOrderEvaluate(buildQuery(1001L, 5)); assertFalse(result.isSuccess()); assertEquals("订单不存在", result.get(AjaxResult.MSG_TAG)); verify(orderEvaluateMapper, never()).insert(any(TOrderEvaluate.class)); } @Test void addOrderEvaluateRejectsMissingChargingOrder() { AjaxResult result = service.addOrderEvaluate(buildQuery(2002L, 1)); assertFalse(result.isSuccess()); assertEquals("订单不存在", result.get(AjaxResult.MSG_TAG)); verify(orderEvaluateMapper, never()).insert(any(TOrderEvaluate.class)); } @Test void addOrderEvaluateRejectsUnsupportedOrderType() { AjaxResult result = service.addOrderEvaluate(buildQuery(3003L, 2)); assertFalse(result.isSuccess()); assertEquals("订单类型不支持评价", result.get(AjaxResult.MSG_TAG)); verify(orderEvaluateMapper, never()).insert(any(TOrderEvaluate.class)); } @Test void addOrderEvaluateStillWorksForNormalChargingOrder() { TChargingOrder chargingOrder = new TChargingOrder(); chargingOrder.setId(2002L); chargingOrder.setAppUserCarId(55L); chargingOrder.setCode("CD_2002"); when(chargingOrderService.getById(2002L)).thenReturn(chargingOrder); TAppUser appUser = new TAppUser(); appUser.setId(88L); appUser.setWxOpenid("openid_88"); when(appUserClient.getUserById(88L)).thenReturn(R.ok(appUser)); when(orderEvaluateMapper.insert(any(TOrderEvaluate.class))).thenReturn(1); AjaxResult result = service.addOrderEvaluate(buildQuery(2002L, 1)); assertTrue(result.isSuccess()); ArgumentCaptor<TOrderEvaluate> captor = ArgumentCaptor.forClass(TOrderEvaluate.class); verify(orderEvaluateMapper).insert(captor.capture()); TOrderEvaluate saved = captor.getValue(); assertEquals(Integer.valueOf(1), saved.getOrderType()); assertEquals(Long.valueOf(55L), saved.getAppUserCarId()); verify(vinChargingOrderService, never()).getById(anyLong()); } } sql/20261013_order_evaluate_vin_type.sql
New file @@ -0,0 +1,6 @@ -- VIN码充电订单评价:t_order_evaluate.order_type 新增取值 5=VIN码充电订单 -- 背景:小程序 VIN 码充电订单列表"评价/查看评价"原先按 order_type=1 提交,后端按普通充电订单表查询导致空指针; -- 现改为独立类型 5,与普通充电订单(1)区分,本脚本仅更新字段注释,无表结构调整。 -- 执行环境:测试库已执行,其他环境发版前同步执行 ALTER TABLE `t_order_evaluate` MODIFY COLUMN `order_type` int NULL DEFAULT NULL COMMENT '订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单,5=VIN码充电订单)'; sql/charging_pile_order.sql
@@ -4287,7 +4287,7 @@ DROP TABLE IF EXISTS `t_order_evaluate`; CREATE TABLE `t_order_evaluate` ( `id` bigint NOT NULL COMMENT '主键', `order_type` int NULL DEFAULT NULL COMMENT '订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单)', `order_type` int NULL DEFAULT NULL COMMENT '订单类型(1=充电订单,2=购物订单,3=兑换订单,4=会员订单,5=VIN码充电订单)', `order_id` bigint NULL DEFAULT NULL COMMENT '订单id', `app_user_id` bigint NULL DEFAULT NULL COMMENT '用户id', `mark` int NULL DEFAULT NULL COMMENT '评分',