对接科拓停车优惠

This commit is contained in:
Guoqs
2026-07-03 16:08:17 +08:00
parent 2fa0180a26
commit b0c138f704
7 changed files with 461 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
# 科拓停车充电桩抵扣配置指南
本文用于配置科拓停车系统 2.5.4 充电桩抵扣接口。配置完成后,订单满足站点停车优惠条件时,系统会向科拓下发 `syncChargePilePay` 抵扣请求。
## 一、停车场平台配置
数据表:`thirdparty_parking_config`
| 字段 | 是否必填 | 科拓配置说明 |
| --- | --- | --- |
| `parking_name` | 建议填 | 停车场/商户名称,便于后台识别。 |
| `platform_type` | 是 | 科拓固定填 `4`。 |
| `app_id` | 是 | 科拓分配的 `appId`。 |
| `secret_key` | 是 | 科拓分配的 `appSercert`,用于生成请求体 `key`。 |
| `park_id` | 是 | 科拓分配的车场 ID。 |
| `api_url` | 是 | 科拓接口地址。可填接口基址,如 `https://IP:端口`;也可填完整地址 `https://IP:端口/api/wec/SyncChargePilePay`。 |
| `org_id` | 否 | qcyun 使用,科拓不用。 |
| `del_flag` | 是 | 正常数据填 `0`。 |
示例:
```sql
insert into thirdparty_parking_config (
parking_name,
platform_type,
app_id,
secret_key,
park_id,
api_url,
create_time,
create_by,
del_flag
) values (
'示例科拓停车场',
'4',
'10038',
'KETUO_APP_SECRET',
'592011253',
'https://IP:端口',
now(),
'admin',
'0'
);
```
## 二、站点绑定停车场配置
数据表:`pile_station_info`
```sql
update pile_station_info
set parking_id = 'thirdparty_parking_config.id'
where id = '站点ID';
```
后台接口同样可用:
```http
POST /pile/station/bindParkingPlatform
```
请求体:
```json
{
"stationId": "站点ID",
"parkingId": "thirdparty_parking_config.id"
}
```
## 三、站点停车优惠规则
数据表:`charge_parking_discount`
| 字段 | 是否必填 | 说明 |
| --- | --- | --- |
| `station_id` | 是 | 站点 ID。 |
| `parking_platform_id` | 是 | 科拓固定填 `4`。 |
| `condition_type` | 是 | 发券门槛类型:`1` 固定电量;`2` 固定时长。 |
| `condition_value` | 是 | 门槛值。电量单位度,时长单位分钟。 |
| `discount_type` | 是 | 优惠类型:`1` 减时间;`2` 减金额。 |
| `discount_value` | 是 | 优惠值。减时间单位分钟,系统下发科拓时会转成秒;减金额单位元,系统下发科拓时会转成分。 |
| `start_time` | 是 | 优惠生效开始时间。 |
| `end_time` | 是 | 优惠生效结束时间。 |
| `del_flag` | 是 | 正常数据填 `0`。 |
示例:充电满 10 度,抵扣 120 分钟停车时长。
```sql
insert into charge_parking_discount (
station_id,
parking_platform_id,
condition_type,
condition_value,
discount_type,
discount_value,
start_time,
end_time,
create_time,
create_by,
del_flag
) values (
'站点ID',
4,
'1',
'10',
'1',
'120',
'00:00:00',
'23:59:00',
now(),
'admin',
'0'
);
```
## 四、下发给科拓的数据来源
| 科拓字段 | 当前系统取值 |
| --- | --- |
| `orderNo` | `order_basic_info.order_code` |
| `plateNo` | `order_basic_info.plate_number` |
| `startTime` | `order_basic_info.charge_start_time`,为空时用订单创建时间 |
| `endTime` | `order_basic_info.charge_end_time`,为空时用当前时间 |
| `stationId` | `order_basic_info.station_id` |
| `stationName` | `pile_station_info.station_name` |
| `deviceId` | `order_basic_info.pile_sn` |
| `deviceName` | `pile_basic_info.name`,为空时用桩 SN |
| `spaceNo` | `pile_connector_info.park_no`,为空时用 `0` |
| `power` | 实时数据 `chargingDegree`,保留 2 位小数 |
| `totalMoney` | 实时数据 `chargingAmount`,元转分 |
| `elecMoney` | 当前填 `0` |
| `seviceMoney` | 当前填 `totalMoney` |
| `freeType` | 固定填 `0`,按 `freeMoney/freeTime` 减免 |
| `freeMoney` | 金额优惠时取优惠金额并元转分;时长优惠时为 `0` |
| `freeTime` | 时长优惠时取优惠分钟并转秒;金额优惠时为 `0` |
## 五、生效条件
沿用现有充电停车优惠链路:
1. 订单有车牌号。
2. 站点存在 `charge_parking_discount` 有效配置。
3. 订单创建时间在优惠时间段内。
4. 订单实时充电电量或充电时长满足门槛。
5. 同一订单未在 `car_coupon_record` 写入过发券记录。
6. 优惠规则 `parking_platform_id = 4`
7. 站点绑定的 `thirdparty_parking_config.platform_type = '4'`
8. 科拓配置 `app_id``secret_key``park_id``api_url` 不为空。

View File

@@ -10,6 +10,7 @@ public enum ParkingPlatformEnum {
LU_TONG_YUN_TING_PLATFORM("1", "路通云停停车平台"), LU_TONG_YUN_TING_PLATFORM("1", "路通云停停车平台"),
RUAN_JIE_PLATFORM("2", "软杰停车平台"), RUAN_JIE_PLATFORM("2", "软杰停车平台"),
SHEN_ZHEN_PLATFORM("3", "深圳停车道闸平台"), SHEN_ZHEN_PLATFORM("3", "深圳停车道闸平台"),
KE_TUO_PLATFORM("4", "科拓停车平台"),
; ;
private String code; private String code;

View File

@@ -27,7 +27,7 @@ public class ThirdpartyParkingConfig {
private String parkingName; private String parkingName;
/** /**
* 停车平台类型(1-路通云停;2-软杰;3-qcyun) * 停车平台类型(1-路通云停;2-软杰;3-qcyun;4-科拓)
*/ */
private String platformType; private String platformType;

View File

@@ -17,6 +17,8 @@ import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.CarCouponRecord; import com.jsowell.pile.domain.CarCouponRecord;
import com.jsowell.pile.domain.OrderBasicInfo; import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.domain.PileBasicInfo; import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.domain.PileConnectorInfo;
import com.jsowell.pile.domain.PileStationInfo;
import com.jsowell.pile.domain.ThirdPartyStationRelation; import com.jsowell.pile.domain.ThirdPartyStationRelation;
import com.jsowell.pile.dto.PushRealTimeInfoDTO; import com.jsowell.pile.dto.PushRealTimeInfoDTO;
import com.jsowell.pile.dto.PushStationInfoDTO; import com.jsowell.pile.dto.PushStationInfoDTO;
@@ -34,6 +36,8 @@ import com.jsowell.thirdparty.huawei.HuaweiServiceV2;
import com.jsowell.thirdparty.lianlian.service.LianLianService; import com.jsowell.thirdparty.lianlian.service.LianLianService;
import com.jsowell.thirdparty.nanrui.service.NRService; import com.jsowell.thirdparty.nanrui.service.NRService;
import com.jsowell.thirdparty.parking.common.bean.QcyunParkCouponDTO; import com.jsowell.thirdparty.parking.common.bean.QcyunParkCouponDTO;
import com.jsowell.thirdparty.parking.common.bean.KetuoChargePileDeductionDTO;
import com.jsowell.thirdparty.parking.service.KetuoService;
import com.jsowell.thirdparty.parking.service.LTYTService; import com.jsowell.thirdparty.parking.service.LTYTService;
import com.jsowell.thirdparty.parking.service.QcyunsService; import com.jsowell.thirdparty.parking.service.QcyunsService;
import com.jsowell.thirdparty.parking.service.RJService; import com.jsowell.thirdparty.parking.service.RJService;
@@ -54,6 +58,7 @@ import org.springframework.stereotype.Service;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
@@ -112,6 +117,12 @@ public class CommonService {
@Autowired @Autowired
private QcyunsService qcyunsService; private QcyunsService qcyunsService;
@Autowired
private KetuoService ketuoService;
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
@Autowired @Autowired
private HuaweiServiceV2 huaweiServiceV2; private HuaweiServiceV2 huaweiServiceV2;
@@ -827,6 +838,11 @@ public class CommonService {
.discountValue(chargeParkingDiscount.getDiscountValue()) .discountValue(chargeParkingDiscount.getDiscountValue())
.build(); .build();
discountFlag = qcyunsService.issuanceOfParkingTickets(dto); discountFlag = qcyunsService.issuanceOfParkingTickets(dto);
} else if (StringUtils.equals(ParkingPlatformEnum.KE_TUO_PLATFORM.getCode(), parkingPlatformId)) {
// 科拓--充电桩抵扣
KetuoChargePileDeductionDTO dto = buildKetuoChargePileDeductionDTO(
realTimeMonitorData, orderBasicInfo, chargeParkingDiscount);
discountFlag = ketuoService.syncChargePilePay(dto);
} }
if (discountFlag) { if (discountFlag) {
@@ -842,6 +858,81 @@ public class CommonService {
} }
} }
private KetuoChargePileDeductionDTO buildKetuoChargePileDeductionDTO(RealTimeMonitorData realTimeMonitorData,
OrderBasicInfo orderBasicInfo,
ChargeParkingDiscountVO chargeParkingDiscount) {
PileStationInfo stationInfo = pileStationInfoService.selectPileStationInfoById(Long.parseLong(orderBasicInfo.getStationId()));
PileBasicInfo pileBasicInfo = pileBasicInfoService.selectPileBasicInfoBySN(orderBasicInfo.getPileSn());
PileConnectorInfo connectorInfo = getPileConnectorInfo(orderBasicInfo);
Integer totalMoney = yuanToFen(realTimeMonitorData.getChargingAmount());
Integer freeMoney = Constants.TWO.equals(String.valueOf(chargeParkingDiscount.getDiscountType()))
? yuanToFen(chargeParkingDiscount.getDiscountValue())
: 0;
Integer freeTime = Constants.ONE.equals(String.valueOf(chargeParkingDiscount.getDiscountType()))
? minutesToSeconds(chargeParkingDiscount.getDiscountValue())
: 0;
return KetuoChargePileDeductionDTO.builder()
.stationId(orderBasicInfo.getStationId())
.orderNo(orderBasicInfo.getOrderCode())
.plateNo(orderBasicInfo.getPlateNumber())
.startTime(formatDateTime(orderBasicInfo.getChargeStartTime(), orderBasicInfo.getCreateTime()))
.endTime(formatDateTime(orderBasicInfo.getChargeEndTime(), new java.util.Date()))
.stationName(stationInfo == null ? orderBasicInfo.getStationId() : stationInfo.getStationName())
.deviceId(orderBasicInfo.getPileSn())
.deviceName(pileBasicInfo == null || StringUtils.isBlank(pileBasicInfo.getName())
? orderBasicInfo.getPileSn() : pileBasicInfo.getName())
.spaceNo(connectorInfo == null || StringUtils.isBlank(connectorInfo.getParkNo())
? "0" : connectorInfo.getParkNo())
.power(formatPower(realTimeMonitorData.getChargingDegree()))
.elecMoney(0)
.seviceMoney(totalMoney)
.totalMoney(totalMoney)
.freeType(0)
.freeMoney(freeMoney)
.freeTime(freeTime)
.build();
}
private PileConnectorInfo getPileConnectorInfo(OrderBasicInfo orderBasicInfo) {
if (StringUtils.isBlank(orderBasicInfo.getPileConnectorCode())) {
return null;
}
PileConnectorInfo query = new PileConnectorInfo();
query.setPileConnectorCode(orderBasicInfo.getPileConnectorCode());
List<PileConnectorInfo> connectorInfoList = pileConnectorInfoService.selectPileConnectorInfoList(query);
if (CollectionUtils.isNotEmpty(connectorInfoList)) {
return connectorInfoList.get(0);
}
return null;
}
private String formatDateTime(java.util.Date date, java.util.Date defaultDate) {
return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date == null ? defaultDate : date);
}
private String formatPower(String power) {
if (StringUtils.isBlank(power)) {
return "0.00";
}
return new BigDecimal(power).setScale(2, RoundingMode.HALF_UP).toPlainString();
}
private Integer yuanToFen(String amount) {
if (StringUtils.isBlank(amount)) {
return 0;
}
return new BigDecimal(amount).multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).intValue();
}
private Integer minutesToSeconds(String minutes) {
if (StringUtils.isBlank(minutes)) {
return 0;
}
return new BigDecimal(minutes).multiply(new BigDecimal("60")).setScale(0, RoundingMode.HALF_UP).intValue();
}
/** /**
* 统一请求启动充电,目前给华为平台用 * 统一请求启动充电,目前给华为平台用
* @param dto * @param dto

View File

@@ -0,0 +1,68 @@
package com.jsowell.thirdparty.parking.common.bean;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 科拓充电桩抵扣 DTO.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KetuoChargePileDeductionDTO {
private String stationId;
private String orderNo;
private String plateNo;
private String startTime;
private String endTime;
private String stationName;
private String deviceId;
private String deviceName;
private String spaceNo;
/**
* 充电量,单位度,小数点后 2 位。
*/
private String power;
/**
* 电费,单位分。
*/
private Integer elecMoney;
/**
* 服务费/附加费,单位分。科拓接口字段名为 seviceMoney。
*/
private Integer seviceMoney;
/**
* 总费用,单位分。
*/
private Integer totalMoney;
/**
* 0: 根据 freeMoney/freeTime 减免; 1: 本地车场配置减免规则。
*/
private Integer freeType;
/**
* 减免停车金额,单位分。
*/
private Integer freeMoney;
/**
* 减免停车时长,单位秒。
*/
private Integer freeTime;
}

View File

@@ -0,0 +1,11 @@
package com.jsowell.thirdparty.parking.service;
import com.jsowell.thirdparty.parking.common.bean.KetuoChargePileDeductionDTO;
public interface KetuoService {
/**
* 科拓--充电桩抵扣。
*/
boolean syncChargePilePay(KetuoChargePileDeductionDTO dto);
}

View File

@@ -0,0 +1,139 @@
package com.jsowell.thirdparty.parking.service.impl;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.enums.parkplatform.ParkingPlatformEnum;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.id.UUID;
import com.jsowell.common.util.sign.MD5Util;
import com.jsowell.pile.domain.ThirdpartyParkingConfig;
import com.jsowell.pile.service.ThirdPartyParkingConfigService;
import com.jsowell.thirdparty.parking.common.bean.KetuoChargePileDeductionDTO;
import com.jsowell.thirdparty.parking.service.KetuoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.TreeMap;
@Slf4j
@Service
public class KetuoServiceImpl implements KetuoService {
private static final String SERVICE_CODE_SYNC_CHARGE_PILE_PAY = "syncChargePilePay";
private static final String API_PATH_SYNC_CHARGE_PILE_PAY = "/api/wec/SyncChargePilePay";
@Autowired
private ThirdPartyParkingConfigService thirdPartyParkingConfigService;
@Override
public boolean syncChargePilePay(KetuoChargePileDeductionDTO dto) {
ThirdpartyParkingConfig parkingConfig = resolveParkingConfig(dto);
if (parkingConfig == null) {
return false;
}
JSONObject requestBody = buildRequestBody(dto, parkingConfig);
requestBody.put("key", generateKey(requestBody, parkingConfig.getSecretKey()));
String requestUrl = getRequestUrl(parkingConfig);
log.info("科拓充电桩抵扣 requestUrl:{}, requestBody:{}", requestUrl, requestBody.toJSONString());
String result = HttpUtil.post(requestUrl, requestBody.toJSONString());
if (StringUtils.isBlank(result)) {
log.warn("科拓充电桩抵扣返回为空, requestBody:{}", requestBody.toJSONString());
return false;
}
JSONObject response = JSON.parseObject(result);
log.info("科拓充电桩抵扣 response:{}", JSON.toJSONString(response));
return StringUtils.equals("0", response.getString("resCode"));
}
private ThirdpartyParkingConfig resolveParkingConfig(KetuoChargePileDeductionDTO dto) {
if (dto == null || StringUtils.isBlank(dto.getStationId())) {
log.warn("科拓充电桩抵扣失败, stationId为空, dto:{}", JSON.toJSONString(dto));
return null;
}
ThirdpartyParkingConfig parkingConfig = thirdPartyParkingConfigService.selectByStationId(dto.getStationId());
if (parkingConfig == null) {
log.warn("科拓充电桩抵扣失败, 站点未绑定停车场配置, stationId:{}", dto.getStationId());
return null;
}
if (!StringUtils.equals(ParkingPlatformEnum.KE_TUO_PLATFORM.getCode(), parkingConfig.getPlatformType())) {
log.warn("科拓充电桩抵扣失败, 站点绑定的停车平台类型不是科拓, stationId:{}, parkingConfigId:{}, platformType:{}",
dto.getStationId(), parkingConfig.getId(), parkingConfig.getPlatformType());
return null;
}
if (StringUtils.isBlank(parkingConfig.getAppId())
|| StringUtils.isBlank(parkingConfig.getSecretKey())
|| StringUtils.isBlank(parkingConfig.getParkId())) {
log.warn("科拓充电桩抵扣失败, 停车场配置缺少appId/secretKey/parkId, stationId:{}, parkingConfigId:{}",
dto.getStationId(), parkingConfig.getId());
return null;
}
if (StringUtils.isBlank(parkingConfig.getApiUrl())) {
log.warn("科拓充电桩抵扣失败, 停车场配置缺少apiUrl, stationId:{}, parkingConfigId:{}",
dto.getStationId(), parkingConfig.getId());
return null;
}
return parkingConfig;
}
private JSONObject buildRequestBody(KetuoChargePileDeductionDTO dto, ThirdpartyParkingConfig parkingConfig) {
JSONObject requestBody = new JSONObject();
requestBody.put("appId", parkingConfig.getAppId());
requestBody.put("parkId", parkingConfig.getParkId());
requestBody.put("serviceCode", SERVICE_CODE_SYNC_CHARGE_PILE_PAY);
requestBody.put("ts", System.currentTimeMillis());
requestBody.put("reqId", UUID.randomUUID().toString().replace("-", ""));
requestBody.put("orderNo", dto.getOrderNo());
requestBody.put("plateNo", dto.getPlateNo());
requestBody.put("startTime", dto.getStartTime());
requestBody.put("endTime", dto.getEndTime());
requestBody.put("stationId", dto.getStationId());
requestBody.put("stationName", dto.getStationName());
requestBody.put("deviceId", dto.getDeviceId());
requestBody.put("deviceName", dto.getDeviceName());
requestBody.put("spaceNo", dto.getSpaceNo());
requestBody.put("power", dto.getPower());
requestBody.put("elecMoney", dto.getElecMoney());
requestBody.put("seviceMoney", dto.getSeviceMoney());
requestBody.put("totalMoney", dto.getTotalMoney());
requestBody.put("freeType", dto.getFreeType());
requestBody.put("freeMoney", dto.getFreeMoney());
requestBody.put("freeTime", dto.getFreeTime());
return requestBody;
}
private String generateKey(JSONObject requestBody, String appSecret) {
TreeMap<String, String> params = new TreeMap<>();
requestBody.entrySet().stream()
.filter(entry -> !"key".equals(entry.getKey()) && !"appId".equals(entry.getKey()))
.filter(entry -> entry.getValue() != null)
.filter(entry -> !(entry.getValue() instanceof Map) && !(entry.getValue() instanceof Iterable))
.filter(entry -> !StringUtils.equals("", entry.getValue().toString()))
.forEach(entry -> params.put(entry.getKey(), entry.getValue().toString()));
StringBuilder signContent = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (signContent.length() > 0) {
signContent.append("&");
}
signContent.append(entry.getKey()).append("=").append(entry.getValue());
}
signContent.append("&").append(appSecret);
return MD5Util.MD5Encode(signContent.toString()).toUpperCase();
}
private String getRequestUrl(ThirdpartyParkingConfig parkingConfig) {
String apiUrl = parkingConfig.getApiUrl();
if (apiUrl.endsWith(API_PATH_SYNC_CHARGE_PILE_PAY)) {
return apiUrl;
}
while (apiUrl.endsWith("/")) {
apiUrl = apiUrl.substring(0, apiUrl.length() - 1);
}
return apiUrl + API_PATH_SYNC_CHARGE_PILE_PAY;
}
}