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