diff --git a/docs/Teld测试环境2.0参数.md b/docs/Teld测试环境2.0参数.md
new file mode 100644
index 0000000..76608ec
--- /dev/null
+++ b/docs/Teld测试环境2.0参数.md
@@ -0,0 +1,30 @@
+| 【特来电】互联互通对接信息登记表 | | | | |
+|------------------------|--------------------------------------------------|----------------------------------------------------------|---|---|
+| | | | | |
+| 类型 | 测试环境 | | | |
+| 组织机构代码(营业执照): | 395815801 | 平台对接的运营商ID(OperatorID) | | |
+| 企业名称: | 特来电 | | | |
+| 运营商秘钥(OperatorSecret): | 1234567890abcdef | 第三方调用特来电密钥 | | |
+| 签名秘钥(SigSecret): | 1234567890abcdef | | | |
+| 数据加密秘钥(DataSecret): | 1234567890abcdef | | | |
+| 初始化向量(DataSecretIV): | 1234567890abcdef | | | |
+| 测试环境互联互通url地址: | http://hlht.teld9.xyz/evcs/fv2016/Teld/商户组织机构代码/ | | | |
+| 其他: | | | | |
+| | | | | |
+| | | | | |
+| 【第三方】互联互通对接信息登记表 | | | | |
+| | | | | |
+| 类型 | 测试环境 | | | |
+| 组织机构代码(营业执照): | | 用于创建运营商ID,是9位组织机构代码(三证合一后18位的统一社会信用代码的第9~17位),与生产环境保持一致。 | | |
+| 企业名称: | | | | |
+| 运营商秘钥(OperatorSecret): | | 特来电调用第三方密钥 | | |
+| 签名秘钥(SigSecret): | | | | |
+| 数据加密秘钥(DataSecret): | | | | |
+| 初始化向量(DataSecretIV): | | | | |
+| 测试环境互联互通url地址: | | | | |
+| 其他: | | | | |
+| | | | | |
+| | | | | |
+| | | | | |
+| | | | | |
+| | | | | |
diff --git a/server/src/HuangyanParking.Api/Controllers/TldCallbackController.cs b/server/src/HuangyanParking.Api/Controllers/TldCallbackController.cs
index 31ce186..e878df2 100644
--- a/server/src/HuangyanParking.Api/Controllers/TldCallbackController.cs
+++ b/server/src/HuangyanParking.Api/Controllers/TldCallbackController.cs
@@ -7,6 +7,11 @@ namespace HuangyanParking.Api.Controllers;
///
/// 特来电回调控制器
/// 接收特来电推送的充电订单和设备状态
+///
+/// 密钥说明(互联互通标准双向密钥):
+/// - DataSecret/SigSecret:我方调特来电时用的密钥(特来电提供)
+/// - PeerDataSecret/PeerSigSecret:特来电调我方时用的密钥(我方提供)
+/// 本控制器处理的是特来电调我方,所以用Peer系列密钥
///
[ApiController]
[Route("api/callback/tld")]
@@ -31,7 +36,6 @@ public class TldCallbackController : ControllerBase
///
/// 充电订单推送回调
- /// 特来电接口名:notification_charge_order_info
/// POST /api/callback/tld/notification_charge_order_info
///
[HttpPost("notification_charge_order_info")]
@@ -45,7 +49,6 @@ public class TldCallbackController : ControllerBase
///
/// 设备状态变化推送回调
- /// 特来电接口名:notification_stationStatus
/// POST /api/callback/tld/notification_stationStatus
///
[HttpPost("notification_stationStatus")]
@@ -57,22 +60,35 @@ public class TldCallbackController : ControllerBase
///
/// 平台认证接口(特来电调用我方获取Token)
- /// 特来电接口名:query_token
/// POST /api/callback/tld/query_token
///
[HttpPost("query_token")]
public ActionResult QueryToken([FromBody] TldRequest request)
{
var tldConfig = _configuration.GetSection("Tld");
- var dataSecret = tldConfig["DataSecret"]!;
- var dataSecretIV = tldConfig["DataSecretIV"]!;
- var sigSecret = tldConfig["SigSecret"]!;
+ // 特来电调我方,用我方密钥解密
+ var peerDataSecret = tldConfig["PeerDataSecret"]!;
+ var peerDataSecretIV = tldConfig["PeerDataSecretIV"]!;
+ var peerSigSecret = tldConfig["PeerSigSecret"]!;
+
+ // 验签
+ if (!string.IsNullOrEmpty(request.Sig))
+ {
+ var sigValid = _tldCrypto.Verify(
+ request.OperatorID, request.Data, request.TimeStamp, request.Seq,
+ request.Sig, peerSigSecret);
+ if (!sigValid)
+ {
+ _logger.LogWarning("特来电Token请求验签失败");
+ return Ok(new TldResponse { Ret = 4001, Msg = "签名错误" });
+ }
+ }
// 解密请求
string decryptedData;
try
{
- decryptedData = _tldCrypto.Decrypt(request.Data, dataSecret, dataSecretIV);
+ decryptedData = _tldCrypto.Decrypt(request.Data, peerDataSecret, peerDataSecretIV);
}
catch (Exception ex)
{
@@ -85,7 +101,7 @@ public class TldCallbackController : ControllerBase
var tokenRequest = JsonSerializer.Deserialize(decryptedData);
var expectedSecret = tldConfig["PeerOperatorSecret"];
- // 验证对方的OperatorSecret(如果配置了的话)
+ // 验证对方的OperatorSecret
if (!string.IsNullOrEmpty(expectedSecret) &&
tokenRequest?.OperatorSecret != expectedSecret)
{
@@ -93,12 +109,12 @@ public class TldCallbackController : ControllerBase
{
OperatorID = request.OperatorID,
SuccStat = 1,
- FailReason = 2 // 密钥错误
+ FailReason = 2
};
return Ok(BuildEncryptedResponse(failResult));
}
- // 生成Token(使用简单的GUID,实际可用JWT)
+ // 生成Token
var token = request.OperatorID + DateTime.Now.ToString("yyyyMMddHHmmss") +
Guid.NewGuid().ToString("N")[..32];
var result = new TldTokenResult
@@ -113,19 +129,22 @@ public class TldCallbackController : ControllerBase
return Ok(BuildEncryptedResponse(result));
}
- /// 构建加密响应(加密Data + 响应签名)
+ ///
+ /// 构建加密响应
+ /// 响应给特来电时,用我方密钥加密和签名
+ ///
private TldResponse BuildEncryptedResponse(T data)
{
var tldConfig = _configuration.GetSection("Tld");
- var dataSecret = tldConfig["DataSecret"]!;
- var dataSecretIV = tldConfig["DataSecretIV"]!;
- var sigSecret = tldConfig["SigSecret"]!;
+ var peerDataSecret = tldConfig["PeerDataSecret"]!;
+ var peerDataSecretIV = tldConfig["PeerDataSecretIV"]!;
+ var peerSigSecret = tldConfig["PeerSigSecret"]!;
var responseJson = JsonSerializer.Serialize(data);
- var encryptedData = _tldCrypto.Encrypt(responseJson, dataSecret, dataSecretIV);
+ var encryptedData = _tldCrypto.Encrypt(responseJson, peerDataSecret, peerDataSecretIV);
// 响应签名拼接顺序:Ret + Msg + Data
- var sig = _tldCrypto.SignResponse(0, "", encryptedData, sigSecret);
+ var sig = _tldCrypto.SignResponse(0, "", encryptedData, peerSigSecret);
return new TldResponse
{
diff --git a/server/src/HuangyanParking.Api/appsettings.json b/server/src/HuangyanParking.Api/appsettings.json
index 41f376c..5492f14 100644
--- a/server/src/HuangyanParking.Api/appsettings.json
+++ b/server/src/HuangyanParking.Api/appsettings.json
@@ -14,12 +14,17 @@
"AppSecret": "c227aad12a3fbd0d5413759424124150"
},
"Tld": {
- "BaseUrl": "https://your-tld-api-url",
- "OperatorID": "your_operator_id",
- "OperatorSecret": "your_operator_secret",
- "DataSecret": "your_data_secret",
- "DataSecretIV": "your_data_secret_iv",
- "SigSecret": "your_sig_secret"
+ "BaseUrl": "http://hlht.teld9.xyz/evcs/fv2016/Teld/MA2DT09B8",
+ "OperatorID": "MA2DT09B8",
+ "OperatorSecret": "1234567890abcdef",
+ "DataSecret": "1234567890abcdef",
+ "DataSecretIV": "1234567890abcdef",
+ "SigSecret": "1234567890abcdef",
+ "TldOperatorID": "395815801",
+ "PeerOperatorSecret": "WUscAwtCHz5Uogqg",
+ "PeerDataSecret": "5uBBwJjf71uboUq8",
+ "PeerDataSecretIV": "5qCP6f5Jbx7J8Ird",
+ "PeerSigSecret": "UEVo9ruFt8aPSt73"
},
"Ygl": {
"BaseUrl": "https://api-test.1kmxc.com",
diff --git a/server/src/HuangyanParking.Infrastructure/Services/TldService.cs b/server/src/HuangyanParking.Infrastructure/Services/TldService.cs
index 85b6a26..1c3a620 100644
--- a/server/src/HuangyanParking.Infrastructure/Services/TldService.cs
+++ b/server/src/HuangyanParking.Infrastructure/Services/TldService.cs
@@ -29,6 +29,10 @@ public class TldService : ITldService
private readonly string _dataSecret;
private readonly string _dataSecretIV;
private readonly string _sigSecret;
+ // 我方密钥(特来电调用我方时使用)
+ private readonly string _peerDataSecret;
+ private readonly string _peerDataSecretIV;
+ private readonly string _peerSigSecret;
private const string TokenCacheKey = "tld:access_token";
@@ -55,6 +59,9 @@ public class TldService : ITldService
_dataSecret = tldConfig["DataSecret"]!;
_dataSecretIV = tldConfig["DataSecretIV"]!;
_sigSecret = tldConfig["SigSecret"]!;
+ _peerDataSecret = tldConfig["PeerDataSecret"]!;
+ _peerDataSecretIV = tldConfig["PeerDataSecretIV"]!;
+ _peerSigSecret = tldConfig["PeerSigSecret"]!;
}
/// 平台认证,获取Token(带Redis缓存)
@@ -122,12 +129,12 @@ public class TldService : ITldService
///
public async Task HandleChargeOrderAsync(TldRequest request)
{
- // 1. 验签
+ // 1. 验签(特来电用我方密钥签名,所以用PeerSigSecret验签)
if (!string.IsNullOrEmpty(request.Sig))
{
var isValid = _crypto.Verify(
request.OperatorID, request.Data, request.TimeStamp, request.Seq,
- request.Sig, _sigSecret);
+ request.Sig, _peerSigSecret);
if (!isValid)
{
@@ -136,8 +143,8 @@ public class TldService : ITldService
}
}
- // 2. 解密订单数据
- var decryptedData = _crypto.Decrypt(request.Data, _dataSecret, _dataSecretIV);
+ // 2. 解密订单数据(特来电用我方密钥加密,所以用PeerDataSecret解密)
+ var decryptedData = _crypto.Decrypt(request.Data, _peerDataSecret, _peerDataSecretIV);
_logger.LogInformation("特来电充电订单推送: {Data}", decryptedData);
var order = JsonSerializer.Deserialize(decryptedData);
@@ -228,13 +235,32 @@ public class TldService : ITldService
};
}
- // 6. 发放积分
- await _pointsService.GrantPointsAsync(
- userId.Value, points,
- $"充电积分:消费{order.TotalMoney:F2}元",
- tldOrderId: order.StartChargeSeq);
+ // 6. 发放积分(使用事务确保一致性)
+ await using var transaction = await _db.Database.BeginTransactionAsync();
+ try
+ {
+ await _pointsService.GrantPointsAsync(
+ userId.Value, points,
+ $"充电积分:消费{order.TotalMoney:F2}元",
+ tldOrderId: order.StartChargeSeq);
- await _db.SaveChangesAsync();
+ // 保存充电记录的更新(如果有)
+ if (chargeRecord is not null)
+ await _db.SaveChangesAsync();
+
+ await transaction.CommitAsync();
+ }
+ catch (Exception ex)
+ {
+ await transaction.RollbackAsync();
+ _logger.LogError(ex, "特来电订单积分发放失败: OrderId={OrderId}", order.StartChargeSeq);
+ return new TldChargeOrderResponse
+ {
+ StartChargeSeq = order.StartChargeSeq,
+ ConnectorID = order.ConnectorID,
+ ConfirmResult = 1
+ };
+ }
_logger.LogInformation("特来电订单积分发放完成: OrderId={OrderId}, UserId={UserId}, Points={Points}",
order.StartChargeSeq, userId.Value, points);
@@ -254,12 +280,12 @@ public class TldService : ITldService
///
public async Task HandleStationStatusAsync(TldRequest request)
{
- // 验签
+ // 验签(特来电用我方密钥签名)
if (!string.IsNullOrEmpty(request.Sig))
{
var isValid = _crypto.Verify(
request.OperatorID, request.Data, request.TimeStamp, request.Seq,
- request.Sig, _sigSecret);
+ request.Sig, _peerSigSecret);
if (!isValid)
{
@@ -268,8 +294,8 @@ public class TldService : ITldService
}
}
- // 解密并缓存到Redis(供前端查询设备状态)
- var decryptedData = _crypto.Decrypt(request.Data, _dataSecret, _dataSecretIV);
+ // 解密(特来电用我方密钥加密)并缓存到Redis
+ var decryptedData = _crypto.Decrypt(request.Data, _peerDataSecret, _peerDataSecretIV);
_logger.LogDebug("特来电设备状态变化: {Data}", decryptedData);
try