feat(third_party/heepay): 添加Heepay支付实现及相关测试

- 新增HeepayImpl结构体,包含支付、签名生成、加密等功能
- 实现SendCard方法用于发送卡密
- 添加PayNotify方法处理支付回调
- 新增单元测试以验证Heepay相关功能的正确性
- 更新go.mod以引入testify库进行测试
This commit is contained in:
danial
2025-04-09 23:51:25 +08:00
parent 9eedfe800b
commit 4c099fcb9a
5 changed files with 696 additions and 52 deletions

1
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/rs/xid v1.6.0
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.10.0
github.com/widuu/gojson v0.0.0-20170212122013-7da9d2cd949b
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0

47
go.sum
View File

@@ -8,13 +8,9 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/gopkg v0.1.2-0.20240828084325-780ca9ee70fb h1:glte+Ka6C5efXn/QlEAE/wwNrvE+3mYo/ce69fpvtrE=
github.com/bytedance/gopkg v0.1.2-0.20240828084325-780ca9ee70fb/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/carlmjohnson/requests v0.24.3 h1:LYcM/jVIVPkioigMjEAnBACXl2vb42TVqiC8EYNoaXQ=
@@ -30,8 +26,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/duke-git/lancet/v2 v2.3.5 h1:vb49UWkkdyu2eewilZbl0L3X3T133znSQG0FaeJIBMg=
github.com/duke-git/lancet/v2 v2.3.5/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
@@ -43,8 +37,6 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stomp/stomp/v3 v3.1.3 h1:5/wi+bI38O1Qkf2cc7Gjlw7N5beHMWB/BxpX+4p/MGI=
@@ -53,13 +45,10 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
@@ -126,56 +115,30 @@ github.com/widuu/gojson v0.0.0-20170212122013-7da9d2cd949b/go.mod h1:9W1pyetRkwX
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otelzap v0.9.0 h1:f+xpAfhQTjR8beiSMe1bnT/25PkeyWmOcI+SjXWguNw=
go.opentelemetry.io/contrib/bridges/otelzap v0.9.0/go.mod h1:T1Z1jyS5FttgQoF6UcGhnM+gF9wU32B4lHO69nXw4FE=
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU=
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 h1:opwv08VbCZ8iecIWs+McMdHRcAXzjAeda3uG2kI/hcA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0/go.mod h1:oOP3ABpW7vFHulLpE8aYtNBodrHhMTrvfxUXGvqm7Ac=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/log v0.10.0 h1:1CXmspaRITvFcjA4kyVszuG4HjA61fPDxMb7q3BuyF0=
go.opentelemetry.io/otel/log v0.10.0/go.mod h1:PbVdm9bXKku/gL0oFfUF4wwsQsOPlpo4VEqjvxih+FM=
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw=
go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo=
go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
@@ -220,20 +183,12 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -0,0 +1,558 @@
package third_party
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/des"
"encoding/base64"
"encoding/json"
"fmt"
"gateway/internal/config"
"gateway/internal/otelTrace"
"gateway/internal/service/supplier"
"net/http"
"net/url"
"gateway/internal/models/merchant"
"gateway/internal/models/order"
"gateway/internal/models/payfor"
"gateway/internal/models/road"
"gateway/internal/models/supply_model"
"gateway/internal/service"
"gateway/internal/utils"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/client/httplib"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/cryptor"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"github.com/beego/beego/v2/server/web"
"github.com/bytedance/sonic"
"github.com/widuu/gojson"
)
type HeepayImpl struct {
web.Controller
}
// HasDependencyHTML 是否有单独的支付页面
func (c *HeepayImpl) HasDependencyHTML() bool {
return false
}
// 生成tripleDes算法加密
func TripleDesEncrypt(data, key string) string {
block, err := des.NewTripleDESCipher([]byte(key))
if err != nil {
return ""
}
// 创建CBC加密模式
blockSize := block.BlockSize()
data = PKCS7Padding([]byte(data), blockSize)
iv := make([]byte, blockSize)
mode := cipher.NewCBCEncrypter(block, iv)
// 加密
ciphertext := make([]byte, len(data))
mode.CryptBlocks(ciphertext, []byte(data))
return base64.StdEncoding.EncodeToString(ciphertext)
}
// PKCS7填充
func PKCS7Padding(data []byte, blockSize int) string {
padding := blockSize - len(data)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return string(append(data, padtext...))
}
func generateSignHeepay(agentID, billID, billTime, cardType, cardData, payAmt, notifyURL, timeStamp, key string) string {
// 按照指定顺序拼接参数
signString := fmt.Sprintf("agent_id=%s&bill_id=%s&bill_time=%s&card_type=%s&card_data=%s&pay_amt=%s&notify_url=%s&time_stamp=%s|||%s",
agentID, billID, billTime, cardType, cardData, payAmt, notifyURL, timeStamp, key)
return cryptor.Md5String(signString)
}
func (c *HeepayImpl) SendCard(ctx context.Context, jsonStr string, cardInfo supplier.RedeemCardInfo, attach string, merchantId string, orderInfo order.OrderInfo) (bool, string) {
agentID, err := convertor.ToInt(gojson.Json(jsonStr).Get("agent_id").Tostring())
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("转换agent_id失败", zap.Error(err))
return false, "内部错误请稍后再试试"
}
payAmt, err := convertor.ToFloat(cardInfo.FaceType)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("转换pay_amt失败", zap.Error(err))
return false, "内部错误请稍后再试试"
}
timeStamp := time.Now().Format("20060102150405")
timeStampInt, err := convertor.ToInt(timeStamp)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("转换time_stamp失败", zap.Error(err))
return false, "内部错误请稍后再试试"
}
params := map[string]any{
"agent_id": agentID,
"card_type": int64(10),
"bill_id": attach,
"bill_time": time.Now().Format("20060102150405"),
"card_data": TripleDesEncrypt(fmt.Sprintf("%s,%s,%s", cardInfo.CardNo, cardInfo.Data, cardInfo.FaceType), gojson.Json(jsonStr).Get("3ds_key").Tostring()),
"pay_amt": int64(payAmt),
"notify_url": fmt.Sprintf("%s%s", config.GetConfig().GatewayAddr(), "/notify/Heepay"),
"client_ip": strings.ReplaceAll("127.0.0.1", ".", "_"),
"goods_detail": gojson.Json(jsonStr).Get("goods_detail").Tostring(),
"time_stamp": timeStampInt,
}
// 生成签名
params["sign"] = generateSignHeepay(
convertor.ToString(params["agent_id"].(int64)),
params["bill_id"].(string),
params["bill_time"].(string),
convertor.ToString(params["card_type"].(int64)),
params["card_data"].(string),
convertor.ToString(params["pay_amt"].(int64)),
params["notify_url"].(string),
convertor.ToString(params["time_stamp"].(int64)),
gojson.Json(jsonStr).Get("secret").Tostring(),
)
marshal, err := json.Marshal(params)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
return false, "内部错误请稍后再试试"
}
req := httplib.NewBeegoRequestWithCtx(ctx, "https://pay.Heepay.com/Api/CardPaySubmitService.aspx", "POST").
SetTransport(otelhttp.NewTransport(http.DefaultTransport)).
SetTimeout(time.Second*30, time.Second*30).
Header("Content-Type", "application/json").
Body(params).
Header("Accept-Charset", "utf-8")
response, err := req.String()
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("JD请求失败", zap.Error(err))
return false, ""
}
query, err := url.ParseQuery(response)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("解析失败", zap.Error(err))
return false, "内部数据处理失败"
}
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
if query.Get("ret_code") != "0" {
return false, response
}
return true, "等待兑换"
}
func (c *HeepayImpl) Scan(ctx context.Context, orderInfo order.OrderInfo, roadInfo road.RoadInfo, merchantInfo merchant.MerchantInfo) supplier.ScanData {
ctx, cancel := otelTrace.Span(ctx, "JDCardImpl", "Scan", trace.WithAttributes(
attribute.String("BankOrderId", orderInfo.BankOrderId),
attribute.String("MerchantUid", orderInfo.MerchantUid),
attribute.String("ExValue", orderInfo.ExValue),
))
defer cancel()
cdata := supplier.RedeemCardInfo{}
err := json.Unmarshal([]byte(orderInfo.ExValue), &cdata)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("格式化数据失败", zap.String("ExValue", orderInfo.ExValue))
return supplier.ScanData{Status: "-1", Msg: "订单有有误,请稍后再试"}
}
ok, str := c.SendCard(ctx, roadInfo.Params, cdata, orderInfo.BankOrderId, orderInfo.MerchantOrderId, orderInfo)
var scanData supplier.ScanData
if !ok {
scanData = supplier.ScanData{
Status: "-1",
Msg: "订单有有误,请稍后再试:" + str,
BankNo: orderInfo.MerchantOrderId,
OrderNo: orderInfo.BankOrderId,
ReturnData: str,
}
return scanData
}
scanData.Status = "00"
scanData.OrderNo = orderInfo.BankOrderId
scanData.BankNo = orderInfo.MerchantOrderId
scanData.OrderPrice = strconv.FormatFloat(orderInfo.OrderAmount, 'f', 2, 64)
scanData.ReturnData = str
return scanData
}
// KMEncrypt 加密卡密
func (c *HeepayImpl) kMEncrypt(kf, appSecret string) (string, error) {
ctx := context.Background()
secret := utils.GetMD5LOWER(appSecret)[:16] // 加密秘钥
block, err := aes.NewCipher([]byte(secret))
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Joker: AesDecrypt failed to NewCipher")
return "", err
}
// 数据填充
plaintext := utils.PadType(kf)
iv := "0102030405060708"
mode := cipher.NewCBCEncrypter(block, []byte(iv))
mode.CryptBlocks(plaintext, plaintext)
return base64.StdEncoding.EncodeToString(plaintext), nil
}
type HeepayNotifyResponse struct {
RetCode int `json:"ret_code"` // 返回结果代码 0=接收成功
RetMsg string `json:"ret_msg"` // 返回码信息提示
AgentID int `json:"agent_id"` // 商户编号
BillID string `json:"bill_id"` // 商户系统内部的订单号
JnetBillNo string `json:"jnet_bill_no"` // 汇付宝交易号
BillStatus string `json:"bill_status"` // 单据状态
CardRealAmt float64 `json:"card_real_amt"` // 收到的卡的实际面值金额
CardSettleAmt float64 `json:"card_settle_amt"` // 卡的结算金额
CardDetailData string `json:"card_detail_data"` // 卡明细信息
ExtParam string `json:"ext_param"` // 商户自定义参数或扩展参数
UcardKind int `json:"ucard_kind"` // 卡种类型
Sign string `json:"sign"` // 签名结果
}
func (c *HeepayImpl) PayNotify() {
ctx := c.Ctx.Request.Context()
// 从GET参数中获取数据
retCode, err := c.GetInt("ret_code")
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("获取ret_code失败", zap.Error(err))
c.Ctx.WriteString("FAIL")
return
}
agentID, err := c.GetInt("agent_id")
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("获取agent_id失败", zap.Error(err))
c.Ctx.WriteString("FAIL")
return
}
cardRealAmt, err := c.GetFloat("card_real_amt")
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("获取card_real_amt失败", zap.Error(err))
c.Ctx.WriteString("FAIL")
return
}
cardSettleAmt, err := c.GetFloat("card_settle_amt")
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("获取card_settle_amt失败", zap.Error(err))
c.Ctx.WriteString("FAIL")
return
}
ucardKind, err := c.GetInt("ucard_kind")
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("获取ucard_kind失败", zap.Error(err))
c.Ctx.WriteString("FAIL")
return
}
notifyData := HeepayNotifyResponse{
RetCode: retCode,
RetMsg: c.GetString("ret_msg"),
AgentID: agentID,
BillID: c.GetString("bill_id"),
JnetBillNo: c.GetString("jnet_bill_no"),
BillStatus: c.GetString("bill_status"),
CardRealAmt: cardRealAmt,
CardSettleAmt: cardSettleAmt,
CardDetailData: c.GetString("card_detail_data"),
ExtParam: c.GetString("ext_param"),
UcardKind: ucardKind,
Sign: c.GetString("sign"),
}
// 获取订单信息
orderInfo := order.GetOrderByBankOrderId(ctx, notifyData.BillID)
if orderInfo.BankOrderId == "" {
otelTrace.Logger.WithContext(ctx).Error("【Heepay】回调的订单号不存在订单号=", zap.String("bill_id", notifyData.BillID))
c.Ctx.WriteString("FAIL")
return
}
// 获取通道信息
roadInfo := road.GetRoadInfoByRoadUid(ctx, orderInfo.RoadUid)
if roadInfo.RoadUid == "" {
otelTrace.Logger.WithContext(ctx).Error("【Heepay】支付通道已经关闭或者删除不进行回调")
c.Ctx.WriteString("FAIL")
return
}
// 验证签名
signString := fmt.Sprintf("ret_code=%d&ret_msg=%s&agent_id=%d&bill_id=%s&jnet_bill_no=%s&bill_status=%s&card_real_amt=%.2f&card_settle_amt=%.2f&card_detail_data=%s&ext_param=%s&ucard_kind=%d|||%s",
notifyData.RetCode, notifyData.RetMsg, notifyData.AgentID, notifyData.BillID, notifyData.JnetBillNo,
notifyData.BillStatus, notifyData.CardRealAmt, notifyData.CardSettleAmt, notifyData.CardDetailData,
notifyData.ExtParam, notifyData.UcardKind, gojson.Json(roadInfo.Params).Get("secret").Tostring())
calculatedSign := cryptor.Md5String(signString)
if calculatedSign != notifyData.Sign {
otelTrace.Logger.WithContext(ctx).Error("【Heepay】签名验证失败")
c.Ctx.WriteString("FAIL")
return
}
// 处理订单状态
if notifyData.RetCode == 0 {
isOk := false
if notifyData.CardRealAmt == orderInfo.OrderAmount {
// 订单支付成功
isOk = service.SolvePaySuccess(ctx, orderInfo.BankOrderId, orderInfo.FactAmount, notifyData.JnetBillNo, "支付成功")
} else {
isOk = service.SolvePayFail(ctx, orderInfo.BankOrderId, notifyData.RetMsg, fmt.Sprintf("金额异议 卡面金额:%.2f 实际金额:%.2f%s", orderInfo.OrderAmount, float64(notifyData.CardRealAmt), convertor.ToString(notifyData)))
}
if isOk {
c.Ctx.WriteString("SUCCESS")
} else {
c.Ctx.WriteString("FAIL")
}
}
// 订单支付失败
isOk := service.SolvePayFail(ctx, orderInfo.BankOrderId, notifyData.RetMsg, "")
if isOk {
c.Ctx.WriteString("SUCCESS")
} else {
c.Ctx.WriteString("FAIL")
}
}
func (c *HeepayImpl) PayQuery(orderInfo order.OrderInfo, roadInfo road.RoadInfo) bool {
params := map[string]any{}
ctx := context.Background()
cardData, err := sonic.GetFromString(orderInfo.CardReturnData)
if err != nil {
return false
}
orderId, err := cardData.Get("order_id").String()
if err != nil {
return false
}
params["order_id"] = orderId
params["app_key"] = gojson.Json(roadInfo.Params).Get("appKey").Tostring()
params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
params["sign"] = utils.GetMD5SignMF(params, gojson.Json(roadInfo.Params).Get("appSecret").Tostring())
cfg := config.Config{}
url, err := cfg.GetMFCardQueryUrl()
req := httplib.Post(url)
marshal, err := json.Marshal(params)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
// fmt.Printf("Map转化为byte数组失败,异常:%s\n",zap.Error(err))
return false
}
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
req.Header("Content-Type", "application/json")
req.Body(marshal)
req.Header("Accept-Charset", "utf-8")
response, err := req.String()
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("MF GetToken 请求失败:", zap.Error(err))
return false
}
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
resData, err := sonic.GetFromString(response)
if err != nil {
return false
}
resStatus, err := resData.Get("data").Get("status").Int64()
if err != nil {
return false
}
resCode, err := resData.Get("code").Int64()
if err != nil {
return false
}
if resCode == 0 && resStatus == 9 {
return true
}
return false
}
func (c *HeepayImpl) PayQueryV2(orderInfo order.OrderInfo, roadInfo road.RoadInfo) supply_model.MsgModel {
params := map[string]any{}
ctx := context.Background()
cardData, err := sonic.GetFromString(orderInfo.CardReturnData)
if err != nil {
return supply_model.CardMsgErr
}
orderId, err := cardData.Get("order_id").String()
if err != nil {
return supply_model.CardMsgErr
}
params["order_id"] = orderId
params["app_key"] = gojson.Json(roadInfo.Params).Get("appKey").Tostring()
params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
params["sign"] = utils.GetMD5SignMF(params, gojson.Json(roadInfo.Params).Get("appSecret").Tostring())
cfg := config.Config{}
url, err := cfg.GetMFCardQueryUrl()
req := httplib.Post(url)
marshal, err := json.Marshal(params)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
// fmt.Printf("Map转化为byte数组失败,异常:%s\n",zap.Error(err))
return supply_model.DataErr
}
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
req.Header("Content-Type", "application/json")
req.Body(marshal)
req.Header("Accept-Charset", "utf-8")
response, err := req.String()
if err != nil {
return supply_model.RemoteDataErr
}
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
resData, err := sonic.GetFromString(response)
if err != nil {
return supply_model.RemoteDataErr
}
resStatus, err := resData.Get("data").Get("status").Int64()
if err != nil {
return supply_model.RemoteDataErr
}
resCode, err := resData.Get("code").Int64()
if err != nil {
return supply_model.RemoteDataErr
}
if resCode == 0 {
switch resStatus {
case 9:
return supply_model.RemoteSuccess
case 2, 3, 4:
return supply_model.RemoteDataDealing
case 7:
return supply_model.RemoteDataHandErr
case 8:
return supply_model.RemoteDataHealingErr
}
}
return supply_model.RemoteDataErr
}
func (c *HeepayImpl) PayFor(info payfor.PayforInfo) string {
return ""
}
func (c *HeepayImpl) PayForQuery(payFor payfor.PayforInfo) (string, string) {
cfg := config.Config{}
url, err := cfg.GetMFCardQueryUrl()
ctx := context.Background()
if err != nil {
return config.PAYFOR_FAIL, ""
}
params := map[string]string{}
params["order_id"] = payFor.BankOrderId
params["app_key"] = gojson.Json("").Get("appKey").Tostring()
req := httplib.Post(url)
marshal, err := json.Marshal(params)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Map转化为byte数组失败,异常。", zap.Error(err))
// fmt.Printf("Map转化为byte数组失败,异常:%s\n",zap.Error(err))
return config.PAYFOR_FAIL, "内部错误请稍后再试试"
}
otelTrace.Logger.WithContext(ctx).Info("请求参数:" + string(marshal))
req.Header("Content-Type", "application/json")
req.Body(marshal)
req.Header("Accept-Charset", "utf-8")
response, err := req.String()
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("MF GetToken 请求失败:", zap.Error(err))
return config.PAYFOR_FAIL, ""
}
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:" + response)
if gojson.Json(response).Get("code").Tostring() == "" {
otelTrace.Logger.WithContext(ctx).Error("远程调用失败")
return config.PAYFOR_BANKING, ""
}
if gojson.Json(response).Get("code").Tostring() == "0" {
type data struct {
OrderID int64 `json:"order_id"`
CardNo string `json:"card_no"`
CardPwd string `json:"card_pwd"`
Status int `json:"status"`
RspInfo string `json:"rsp_info"`
FaceVal int `json:"face_val"`
Amount int `json:"amount"`
Discount string `json:"discount"`
}
var d data
err2 := json.Unmarshal([]byte(gojson.Json(response).Get("data").Tostring()), &d)
if err2 != nil {
return config.PAYFOR_FAIL, ""
}
if d.Status == 9 {
return config.PAYFOR_SUCCESS, ""
}
if d.Status == 4 {
return config.PAYFOR_BANKING, ""
}
if d.Status == 7 || d.Status == 8 {
return config.PAYFOR_FAIL, ""
}
}
otelTrace.Logger.WithContext(ctx).Error("远程调用失败")
return config.PAYFOR_BANKING, ""
}
func (c *HeepayImpl) BalanceQuery(roadInfo road.RoadInfo) float64 {
return 0.00
}
func (c *HeepayImpl) PayForNotify() string {
return ""
}

View File

@@ -0,0 +1,130 @@
package third_party
import (
"context"
"gateway/internal/models/order"
"gateway/internal/service/supplier"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestHeepayImpl_SendCard(t *testing.T) {
tests := []struct {
name string
jsonStr string
cardInfo supplier.RedeemCardInfo
attach string
merchantId string
orderInfo order.OrderInfo
want bool
wantMsg string
}{
{
name: "正常充值",
jsonStr: `{
"agent_id": "2340960",
"secret": "2C3566FF9E354DBE99BF0C63",
"3ds_key": "E6340D5A314B42E0B5DC307A",
"goods_detail": "{\"name\":\"测试商品\"}"
}`,
cardInfo: supplier.RedeemCardInfo{
CardNo: "1234567890123456",
Data: "123456",
FaceType: "100.00",
},
attach: uuid.New().String(),
merchantId: uuid.New().String(),
orderInfo: order.OrderInfo{
BankOrderId: uuid.New().String(),
MerchantOrderId: uuid.New().String(),
OrderAmount: 100.00,
FactAmount: 100.00,
},
want: true,
wantMsg: "等待兑换",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &HeepayImpl{}
ctx := context.Background()
got, gotMsg := c.SendCard(ctx, tt.jsonStr, tt.cardInfo, tt.attach, tt.merchantId, tt.orderInfo)
assert.Equal(t, tt.want, got)
assert.Equal(t, tt.wantMsg, gotMsg)
})
}
}
func TestGenerateSignHeepay(t *testing.T) {
tests := []struct {
name string
agentID string
billID string
billTime string
cardType string
cardData string
payAmt string
notifyURL string
timeStamp string
key string
wantLength int
}{
{
name: "正常签名",
agentID: "1234567",
billID: "test_order_123",
billTime: time.Now().Format("20060102150405"),
cardType: "10",
cardData: "test_card_data",
payAmt: "100.00",
notifyURL: "http://test.com/notify",
timeStamp: time.Now().Format("20060102150405"),
key: "test_key",
wantLength: 32, // MD5 签名长度
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateSignHeepay(tt.agentID, tt.billID, tt.billTime, tt.cardType, tt.cardData, tt.payAmt, tt.notifyURL, tt.timeStamp, tt.key)
assert.Equal(t, tt.wantLength, len(got))
})
}
}
func TestTripleDesEncrypt(t *testing.T) {
tests := []struct {
name string
data string
key string
wantErr bool
}{
{
name: "正常加密",
data: "test_data",
key: "test_key_1234567890123456",
wantErr: false,
},
{
name: "密钥长度不足",
data: "test_data",
key: "short_key",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TripleDesEncrypt(tt.data, tt.key)
if tt.wantErr {
assert.Empty(t, got)
} else {
assert.NotEmpty(t, got)
}
})
}
}

View File

@@ -48,15 +48,15 @@ func (c *NinjaCardImpl) HasDependencyHTML() bool {
func generateNinjaSign(ctx context.Context, params map[string]interface{}, key string) string {
keys := maputil.Keys(params)
sort.Strings(keys)
sign_ := ""
sign := ""
for _, key2 := range keys {
sign_ += key2 + "=" + convertor.ToString(params[key2]) + "&"
sign += key2 + "=" + convertor.ToString(params[key2]) + "&"
}
sign_ += "secret=" + key
otelTrace.Logger.WithContext(ctx).Info("构造sign参数", zap.Any("sign", sign_))
return utils.EncodeMd5Str(sign_)
sign += "secret=" + key
return utils.EncodeMd5Str(sign)
}
// SendCard 发送卡密
func (c *NinjaCardImpl) SendCard(ctx context.Context, jsonStr string, orderInfo order.OrderInfo, cardInfo supplier.RedeemCardInfo, attach string) (bool, string) {
cfg := config.Config{}
@@ -103,12 +103,12 @@ func (c *NinjaCardImpl) SendCard(ctx context.Context, jsonStr string, orderInfo
}
var resData AutoGenerated
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:", zap.Any("response", response), zap.Any("解析结果", resData))
err = sonic.Unmarshal([]byte(response), &resData)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("json解析失败", zap.Error(err), zap.Any("response", response))
return false, "内部数据处理失败"
}
otelTrace.Logger.WithContext(ctx).Info("远端请求返回数据:", zap.Any("response", response), zap.Any("解析结果", resData))
if resData.Code == 200 {
return true, response