Compare commits

...

25 Commits

Author SHA1 Message Date
66d8d660f2 f 2026-03-04 20:07:41 +08:00
6e3c268b82 Merge branch 'main' of http://1.117.67.95:3000/team/tyc-server-v2 2026-03-02 16:51:15 +08:00
6b8e7eada6 f 2026-03-02 16:51:05 +08:00
Mrx
6a627dc474 f 2026-02-28 12:45:25 +08:00
ed35631900 f 2026-02-24 18:49:36 +08:00
d80076e2c7 f 2026-02-24 18:42:06 +08:00
2d40d589e2 Merge branch 'main' of http://1.117.67.95:3000/team/tyc-server-v2 2026-02-24 16:48:03 +08:00
2d7e241b76 add ali captcha 2026-02-24 16:47:46 +08:00
Mrx
de78857139 f 2026-02-24 15:25:05 +08:00
Mrx
15c0d20508 f 2026-02-24 15:14:33 +08:00
Mrx
e1f62efecd f 2026-02-24 15:00:40 +08:00
86bda66271 f 2026-02-12 19:48:23 +08:00
f5e8fa6558 f 2026-02-12 18:57:13 +08:00
be6aef3730 f 2026-02-12 17:29:03 +08:00
356b422879 f 2026-02-12 17:04:10 +08:00
c2591eec44 f 2026-02-12 16:54:09 +08:00
3d531301cb f 2026-02-12 16:48:21 +08:00
7aa875af19 f 2026-02-12 16:39:53 +08:00
2b8add736f f 2026-02-12 16:25:01 +08:00
ce34e426c4 f 2026-02-12 15:42:09 +08:00
ca6dcc1b24 f 2026-02-12 15:16:54 +08:00
07bf234b30 f 2026-02-06 14:12:07 +08:00
33a8baefb7 f 2026-02-06 14:05:53 +08:00
dc4fcbf857 f 2026-02-06 13:34:49 +08:00
9648fcd1ec f 2026-02-02 19:35:58 +08:00
45 changed files with 5442 additions and 730 deletions

1
.gitignore vendored
View File

@@ -20,3 +20,4 @@ data/*
/tmp/
/app/api
**/__debug_bin*.exe

View File

@@ -51,10 +51,11 @@ type (
type (
QueryServiceReq {
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
CaptchaVerifyParam string `json:"captchaVerifyParam,optional"`
}
QueryServiceResp {
Id string `json:"id"`

View File

@@ -0,0 +1,19 @@
syntax = "v1"
info (
title: "天远异步回调"
desc: "天远车辆类接口异步回调入口"
version: "v1"
)
// 该服务只提供第三方回调入口,不依赖登录态
@server (
prefix: api/v1
group: tianyuan
)
service main {
@doc "天远车辆类接口异步回调"
@handler vehicleCallback
post /tianyuan/vehicle/callback
}

View File

@@ -0,0 +1,39 @@
syntax = "v1"
info (
title: "上传"
desc: "图片上传,用于行驶证等需 URL 的接口"
version: "v1"
)
type (
UploadImageReq {
ImageBase64 string `json:"image_base64" validate:"required"` // 图片 base64不含 data URL 前缀)
}
UploadImageResp {
Url string `json:"url"` // 可公网访问的图片 URL
}
ServeUploadedFileReq {
FileName string `path:"fileName"` // 文件名,如 uuid.jpg
}
// 实际由 Handler 根据 FilePath/ContentType 写文件流,不返回 JSON
ServeUploadedFileResp {
FilePath string `json:"-"` // 内部:本地文件路径
ContentType string `json:"-"` // 内部Content-Type
}
)
@server (
prefix: api/v1
group: upload
)
service main {
@doc "上传图片,返回可访问 URL如行驶证限制 3MB"
@handler UploadImage
post /upload/image (UploadImageReq) returns (UploadImageResp)
@doc "访问已上传文件(供第三方或前端通过返回的 URL 拉取)"
@handler ServeUploadedFile
get /upload/file/:fileName (ServeUploadedFileReq) returns (ServeUploadedFileResp)
}

View File

@@ -141,8 +141,26 @@ service main {
type (
sendSmsReq {
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
CaptchaVerifyParam string `json:"captchaVerifyParam"`
}
)
//============================> captcha v1 <============================
@server (
prefix: api/v1
group: captcha
)
service main {
@doc "get encrypted scene id for aliyun captcha"
@handler getEncryptedSceneId
post /captcha/encryptedSceneId returns (GetEncryptedSceneIdResp)
}
type (
GetEncryptedSceneIdResp {
EncryptedSceneId string `json:"encryptedSceneId"`
}
)

View File

@@ -14,6 +14,8 @@ import "./front/product.api"
import "./front/agent.api"
import "./front/app.api"
import "./front/authorization.api"
import "./front/upload.api"
import "./front/tianyuan.api"
// 后台
import "./admin/auth.api"
import "./admin/menu.api"

View File

@@ -17,15 +17,28 @@ VerifyCode:
SignName: "天远查"
TemplateCode: "SMS_302641455"
ValidTime: 300
Captcha:
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
SceneID: "wynt39to"
EKey: "xdhf5JbWVmFXx+2K+6kBk2aH++GtQBEI8Gmzdeen90o="
Encrypt:
SecretKey: "ff83609b2b24fc73196aac3d3dfb874f"
Alipay:
AppID: "2021004161631930"
PrivateKey: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCRrZNr8DNs4LhPSulTLEg4RLREWVSFGS+Nl5Q2FxQ8DgkUYV+p3kfi4XmB2W/Ruz4egPxEB0V/xj75OktVphVKY8rI6OaNnVoFVe5NqGa5MTj3wLwBIv/hMHA1VAru2KLIv9R1FR7LpWmreHSkpJ65CD2mZqYuMCekOfzMQZIGgSagEU4my0bLbFWw7M3qZz4vm2KUtm4Ew28OUJDkqygjPzXgS5l5niYQvqPjiQNdnTtoIcNcHo07tS8lmf/hdgq9EtVfY7Y0brESfgvOoVJeg1hTHEj0hyWnnWPeA4HD2izANP/5ObRX4ZVqpVju+7PSpbeFd71fxbR1blAVnrqTAgMBAAECggEASpkwHN3r9507xJ7/zG+oq+fCyB1WgrHbAA7W/rviyL4HOECE1F/XP/9mUXAfKq9PqB81D0EJ/dxu8wE/AqUB0g44EZnyNiKVrpXKakoKEFt8aKJxo8NgdNhxHV3kG1skQNi62xntoysZaY1NbeI+xVHLACMghhZytk5bfd02Ac3rMBz3X8Cl1R+3mgU0zFc5f476VRxywiRQM+QNJIaHDNB4vw1TKI0K92mEKD8lOuNZD7d5TCBZi3r08t8FFAkMjIMDiFvFRFmAqMg3NyaIGUkLVDU2zUP0Vlzmo9ghCV9hluqDqeP4RhxQydOw+rxGBk+crYQBhPyYOI/I9PFXAQKBgQDHSRRTPqYbCfztmwk3AIH7VN6izyU3FljEXAsdf+UVJpRa8429J3e+sB96jxhiwVlCzX4CDjsa/Pu0iQQx22a0AZs5GTE0MJ1FVydfGlyqF6/hRS4TswSkklW3be7/KDAjgj2+/wap+mN7rRmDkdvxgCJG6MiWuRAthhg/g16wIwKBgQC7Iu7D4yQXRKheL6p6pbMtE+oD58/EJ2vO8ZUz3LiPc9pZ6+bp4nkBP6JOuYiB5jkzWQifKe6hsXpv06kWzaBEzz4f4SUpWDmdBchNoct3pB/k66FaxHLO/pG4RV86hqscqTdutmdC62bbwM6yCtJ+3rS5rlCxDGQkGJbM+wM60QKBgH5nQyYeCbwC1NRdTzX883VYerLoEyHi4cEC5OX8NnD4/IbIDzJYc2KXUhAp7XzOSPDPaMqi/ih7KKh1dByvnnA0yKEp8oS5BThzNHzlOruEtMF9YOGL3jkIvKfRahOcCRSsyr94AWEVeb57qEBE5y5CaPtzMbAwiCtn779xc0DjAoGAZwEGXWokDm6rIhSoiJO2OQSyFW4+LSDptWHCF2bRa5yAPmiblHck1awaAa0b1yxKpdnG5hzljbirxOvDMZsDMXzFHDUICGbYZ3asVxbMcNE1AQM1sElbTFZRDRWaIhPIEaGOsnDSC8KYvjK1UsikLlMVNPMe1SUV5cxnDPLJR1ECgYEAw8M09uLylPtfGq7oyE2R6xC2kUA8EJ6aapJgUs/UZ6dtjtvudbYzUo0Cgnb12hpN3hfLc5O0/P4nRzZ72Hm43cMiRNLJi4BYCa0m/mCxq+RcoBWYQTIraHnR17yIQhxt5IBRVjgbvYCnryx5Jd5wjOvv7DdnGFJLepzSJwlGqeU="
AppID: "2021006121698606"
PrivateKey: "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCx3TqaCUgZIMfG8Lu8fqkKqKLzONB9NBp4qCM2CAgxkUK1fd48XZ3ANDzfzNoeIbLbR/VL3ZUgyX/F8jO7Cit71JS58X9b1ngeRyBdaiBhLZgHb7Ngg0f1zDJuq8H6/i3Tiu9PaH0iOFKeaFCCpkakFyXjp//rqkcj2bPfa6lX0bD4YcILz8DC3iPy/cXHrHwaBryhYouYA3bKFROHykfF5D+tS2PZdR13BiDVElwDuquZCMdRdISfTw8snp5HSxA0TyRZIiGrtU3WvSKxYi2s8smZl/uYN1uj4w4GOivnTrkBdLy6DBqvgqRsCeYDDV5UyjtbAhvksZH60EWS3HTfAgMBAAECggEAK4dv+xjAa13pZpet6nC5ICGrV4kVBT9GJzdG/scycicRw2cdh3qFy+884qy4yN0Ib8AJmVqOT6rguWoQHPtdLv4Us/kVaT1wwkA3/ISnjgDhjxhYNwuKBe7GfO1OGQYx4u7CqJVy4ngUSC5RXdghu7Dqle+co2lV5cE20zv/Ar2nYo4p9fxcl/XAttqdRyby3ge6zJZP46Ru4CHzFzyUsrJYC575R3Jq68Zrr/+v0BmpEm/wAmoQE8W0ElMMH+Jw0twj+4l6PaUcq9oUVIL9wzl7ay1B6dKyxEOyinGYrmd65NzsTu8HhEpFdvQ+1O27XyahLsNWpSamJlV23ns/EQKBgQDd2UiQQRrj+itfa1/Zehy8MstEICw73LMDk129yNG9cU8U4y5vaTJkAcDTfme08B3uVVhsIUhJsFdgnh4ayyYW3jsB5BBBgszhwYQciH7f/3nE3rcX/TIs6yQAQkPhQi2RNSdn3SrCIhxgu9TwrZIDyQYJ6kk/9qEmZd93RrhOBwKBgQDNPpS1jFt4l08kzCHzfTvTbIu5z/6NPxmRSpcrdkoOAUfzlKSkRnKJuzkFSJEmWpFXILfNiza1dBYncg60Lu58yXpaGlLbVmxeFem+DdQm55lgl28d8Ssg6nlCTqE/tjB5LZ6qn8KEkWSj6gXH37pU1XFZh0A3+EIytnL7NnrsaQKBgGMaGUw3iSemLZHmiV7BKez4U80PAjOLl3xVbF7HQsp5v3X5NlkWiSgbkGPp57HwQa6h+Wn0RDKGz8GdYJ1fephklb92fbyGDbgblkSYxPSTT3Yed3QD61IdiGuFLoWF5o0jTYMcTWmDi2G7BpitMLj4J/Zt7mLgbYSVpYnG0bYpAoGAS7Tfya/CNdMqQFqD03rITI5nY9zS+mriFXO8Gy4A1vWmArU7ndTWfvNubwJ7d/hEUC0jX1AQmBH/8gDiZ5hAJAt1dDLtiTZxtqrCk3YqYUdgjf6N4C+LRxL2M30pgYTEkI5BTpKrf5bZ1pSGGVnvM0egDfQTvhF26ZnfA8buxLECgYAssWTLQ2Ou2NJ8N3HJx54kpKLwf218ulINsH6opcot22hbVfj0rc7lAcMe9FTGgmSgrbZG9QqAC2BpSHDeSC4C57iSYhG86tq/jwvNsV1miPV9RRj3CvwZkbHzCwqhRvCPS/QUgTzG3Z9pljYLkZjuMkb9jp814GbwEGPeoucefw=="
AlipayPublicKey: ""
AppCertPath: "etc/merchant/appCertPublicKey_2021004161631930.crt"
AppCertPath: "etc/merchant/appCertPublicKey_2021006121698606.crt"
AlipayCertPath: "etc/merchant/alipayCertPublicKey_RSA2.crt"
AlipayRootCertPath: "etc/merchant/alipayRootCert.crt"
AppIDBak: "2021004161631930"
PrivateKeyBak: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCRrZNr8DNs4LhPSulTLEg4RLREWVSFGS+Nl5Q2FxQ8DgkUYV+p3kfi4XmB2W/Ruz4egPxEB0V/xj75OktVphVKY8rI6OaNnVoFVe5NqGa5MTj3wLwBIv/hMHA1VAru2KLIv9R1FR7LpWmreHSkpJ65CD2mZqYuMCekOfzMQZIGgSagEU4my0bLbFWw7M3qZz4vm2KUtm4Ew28OUJDkqygjPzXgS5l5niYQvqPjiQNdnTtoIcNcHo07tS8lmf/hdgq9EtVfY7Y0brESfgvOoVJeg1hTHEj0hyWnnWPeA4HD2izANP/5ObRX4ZVqpVju+7PSpbeFd71fxbR1blAVnrqTAgMBAAECggEASpkwHN3r9507xJ7/zG+oq+fCyB1WgrHbAA7W/rviyL4HOECE1F/XP/9mUXAfKq9PqB81D0EJ/dxu8wE/AqUB0g44EZnyNiKVrpXKakoKEFt8aKJxo8NgdNhxHV3kG1skQNi62xntoysZaY1NbeI+xVHLACMghhZytk5bfd02Ac3rMBz3X8Cl1R+3mgU0zFc5f476VRxywiRQM+QNJIaHDNB4vw1TKI0K92mEKD8lOuNZD7d5TCBZi3r08t8FFAkMjIMDiFvFRFmAqMg3NyaIGUkLVDU2zUP0Vlzmo9ghCV9hluqDqeP4RhxQydOw+rxGBk+crYQBhPyYOI/I9PFXAQKBgQDHSRRTPqYbCfztmwk3AIH7VN6izyU3FljEXAsdf+UVJpRa8429J3e+sB96jxhiwVlCzX4CDjsa/Pu0iQQx22a0AZs5GTE0MJ1FVydfGlyqF6/hRS4TswSkklW3be7/KDAjgj2+/wap+mN7rRmDkdvxgCJG6MiWuRAthhg/g16wIwKBgQC7Iu7D4yQXRKheL6p6pbMtE+oD58/EJ2vO8ZUz3LiPc9pZ6+bp4nkBP6JOuYiB5jkzWQifKe6hsXpv06kWzaBEzz4f4SUpWDmdBchNoct3pB/k66FaxHLO/pG4RV86hqscqTdutmdC62bbwM6yCtJ+3rS5rlCxDGQkGJbM+wM60QKBgH5nQyYeCbwC1NRdTzX883VYerLoEyHi4cEC5OX8NnD4/IbIDzJYc2KXUhAp7XzOSPDPaMqi/ih7KKh1dByvnnA0yKEp8oS5BThzNHzlOruEtMF9YOGL3jkIvKfRahOcCRSsyr94AWEVeb57qEBE5y5CaPtzMbAwiCtn779xc0DjAoGAZwEGXWokDm6rIhSoiJO2OQSyFW4+LSDptWHCF2bRa5yAPmiblHck1awaAa0b1yxKpdnG5hzljbirxOvDMZsDMXzFHDUICGbYZ3asVxbMcNE1AQM1sElbTFZRDRWaIhPIEaGOsnDSC8KYvjK1UsikLlMVNPMe1SUV5cxnDPLJR1ECgYEAw8M09uLylPtfGq7oyE2R6xC2kUA8EJ6aapJgUs/UZ6dtjtvudbYzUo0Cgnb12hpN3hfLc5O0/P4nRzZ72Hm43cMiRNLJi4BYCa0m/mCxq+RcoBWYQTIraHnR17yIQhxt5IBRVjgbvYCnryx5Jd5wjOvv7DdnGFJLepzSJwlGqeU="
AlipayPublicKeyBak: ""
AppCertPathBak: "etc/merchant/bak/appCertPublicKey_2021004161631930.crt"
AlipayCertPathBak: "etc/merchant/bak/alipayCertPublicKey_RSA2.crt"
AlipayRootCertPathBak: "etc/merchant/bak/alipayRootCert.crt"
IsProduction: true
NotifyUrl: "https://www.tianyuancha.cn/api/v1/pay/alipay/callback"
ReturnURL: "https://www.tianyuancha.cn/payment/result"
@@ -52,10 +65,10 @@ Ali:
Code: "d55b58829efb41c8aa8e86769cba4844"
SystemConfig:
ThreeVerify: false
CommissionSafeMode: false # 佣金安全防御模式true-冻结模式false-直接结算模式
CommissionSafeMode: false # 佣金安全防御模式true-冻结模式false-直接结算模式
WechatH5:
AppID: "wxa581992dc74d860e"
AppSecret: "4de1fbf521712247542d49907fcd5dbf"
AppID: "wxe323cdc072127c27"
AppSecret: "e68035927b054a20b8bfdb7f0caffebf"
WechatMini:
AppID: "wx781abb66b3368963" # 小程序的AppID
AppSecret: "c7d02cdb0fc23c35c93187af9243b00d" # 小程序的AppSecret
@@ -79,4 +92,8 @@ Tianyuanapi:
Timeout: 60
Authorization:
FileBaseURL: "https://www.tianyuancha.cn/api/v1/auth-docs" # 授权书文件访问基础URL
ExtensionTime: 24 # 佣金解冻延迟时间单位24小时
Upload:
FileBaseURL: "https://www.tianyuancha.cn/api/v1/upload/file" # 上传图片访问基础 URL
TempFileMaxAgeH: 24 # 临时文件保留时长小时超时自动删除0 表示默认 24
PublicBaseURL: "https://www.tianyuancha.cn"
ExtensionTime: 24 # 佣金解冻延迟时间单位24小时

View File

@@ -19,6 +19,16 @@ VerifyCode:
SignName: "天远查"
TemplateCode: "SMS_302641455"
ValidTime: 300
Captcha:
# 建议与短信相同的 AccessKey或单独为验证码创建子账号
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
# 验证码服务 Endpoint国内一般为 captcha.cn-shanghai.aliyuncs.com
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
# 阿里云控制台中该场景的 SceneId请替换为真实值
SceneID: "wynt39to"
# 验证码控制台中的 ekey通常为 Base64 字符串),用于生成 EncryptedSceneId
EKey: ""
Encrypt:
SecretKey: "ff83609b2b24fc73196aac3d3dfb874f"
WestConfig:
@@ -37,6 +47,13 @@ Alipay:
AppCertPath: "etc/merchant/appCertPublicKey_2021006121698606.crt"
AlipayCertPath: "etc/merchant/alipayCertPublicKey_RSA2.crt"
AlipayRootCertPath: "etc/merchant/alipayRootCert.crt"
AppIDBak: "2021004161631930"
PrivateKeyBak: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCRrZNr8DNs4LhPSulTLEg4RLREWVSFGS+Nl5Q2FxQ8DgkUYV+p3kfi4XmB2W/Ruz4egPxEB0V/xj75OktVphVKY8rI6OaNnVoFVe5NqGa5MTj3wLwBIv/hMHA1VAru2KLIv9R1FR7LpWmreHSkpJ65CD2mZqYuMCekOfzMQZIGgSagEU4my0bLbFWw7M3qZz4vm2KUtm4Ew28OUJDkqygjPzXgS5l5niYQvqPjiQNdnTtoIcNcHo07tS8lmf/hdgq9EtVfY7Y0brESfgvOoVJeg1hTHEj0hyWnnWPeA4HD2izANP/5ObRX4ZVqpVju+7PSpbeFd71fxbR1blAVnrqTAgMBAAECggEASpkwHN3r9507xJ7/zG+oq+fCyB1WgrHbAA7W/rviyL4HOECE1F/XP/9mUXAfKq9PqB81D0EJ/dxu8wE/AqUB0g44EZnyNiKVrpXKakoKEFt8aKJxo8NgdNhxHV3kG1skQNi62xntoysZaY1NbeI+xVHLACMghhZytk5bfd02Ac3rMBz3X8Cl1R+3mgU0zFc5f476VRxywiRQM+QNJIaHDNB4vw1TKI0K92mEKD8lOuNZD7d5TCBZi3r08t8FFAkMjIMDiFvFRFmAqMg3NyaIGUkLVDU2zUP0Vlzmo9ghCV9hluqDqeP4RhxQydOw+rxGBk+crYQBhPyYOI/I9PFXAQKBgQDHSRRTPqYbCfztmwk3AIH7VN6izyU3FljEXAsdf+UVJpRa8429J3e+sB96jxhiwVlCzX4CDjsa/Pu0iQQx22a0AZs5GTE0MJ1FVydfGlyqF6/hRS4TswSkklW3be7/KDAjgj2+/wap+mN7rRmDkdvxgCJG6MiWuRAthhg/g16wIwKBgQC7Iu7D4yQXRKheL6p6pbMtE+oD58/EJ2vO8ZUz3LiPc9pZ6+bp4nkBP6JOuYiB5jkzWQifKe6hsXpv06kWzaBEzz4f4SUpWDmdBchNoct3pB/k66FaxHLO/pG4RV86hqscqTdutmdC62bbwM6yCtJ+3rS5rlCxDGQkGJbM+wM60QKBgH5nQyYeCbwC1NRdTzX883VYerLoEyHi4cEC5OX8NnD4/IbIDzJYc2KXUhAp7XzOSPDPaMqi/ih7KKh1dByvnnA0yKEp8oS5BThzNHzlOruEtMF9YOGL3jkIvKfRahOcCRSsyr94AWEVeb57qEBE5y5CaPtzMbAwiCtn779xc0DjAoGAZwEGXWokDm6rIhSoiJO2OQSyFW4+LSDptWHCF2bRa5yAPmiblHck1awaAa0b1yxKpdnG5hzljbirxOvDMZsDMXzFHDUICGbYZ3asVxbMcNE1AQM1sElbTFZRDRWaIhPIEaGOsnDSC8KYvjK1UsikLlMVNPMe1SUV5cxnDPLJR1ECgYEAw8M09uLylPtfGq7oyE2R6xC2kUA8EJ6aapJgUs/UZ6dtjtvudbYzUo0Cgnb12hpN3hfLc5O0/P4nRzZ72Hm43cMiRNLJi4BYCa0m/mCxq+RcoBWYQTIraHnR17yIQhxt5IBRVjgbvYCnryx5Jd5wjOvv7DdnGFJLepzSJwlGqeU="
AlipayPublicKeyBak: ""
AppCertPathBak: "etc/merchant/bak/appCertPublicKey_2021004161631930.crt"
AlipayCertPathBak: "etc/merchant/bak/alipayCertPublicKey_RSA2.crt"
AlipayRootCertPathBak: "etc/merchant/bak/alipayRootCert.crt"
IsProduction: true
NotifyUrl: "https://www.tianyuancha.cn/api/v1/pay/alipay/callback"
ReturnURL: "https://www.tianyuancha.cn/payment/result"
@@ -63,7 +80,7 @@ Ali:
Code: "d55b58829efb41c8aa8e86769cba4844"
SystemConfig:
ThreeVerify: true
CommissionSafeMode: false # 佣金安全防御模式true-冻结模式false-直接结算模式
CommissionSafeMode: false # 佣金安全防御模式true-冻结模式false-直接结算模式
WechatH5:
AppID: "wxa581992dc74d860e"
AppSecret: "4de1fbf521712247542d49907fcd5dbf"
@@ -88,4 +105,7 @@ Tianyuanapi:
Timeout: 60
Authorization:
FileBaseURL: "https://www.tianyuancha.cn/api/v1/auth-docs" # 授权书文件访问基础URL
ExtensionTime: 24 # 佣金解冻延迟时间单位24小时
Upload:
FileBaseURL: "https://www.tianyuancha.cn/api/v1/upload/file" # 上传图片访问基础 URL行驶证等
PublicBaseURL: "https://www.tianyuancha.cn"
ExtensionTime: 24 # 佣金解冻延迟时间单位24小时

View File

@@ -11,6 +11,7 @@ type Config struct {
CacheRedis cache.CacheConf
JwtAuth JwtAuth // JWT 鉴权相关配置
VerifyCode VerifyCode
Captcha CaptchaConfig
Encrypt Encrypt
Alipay AlipayConfig
Wxpay WxpayConfig
@@ -20,12 +21,15 @@ type Config struct {
SystemConfig SystemConfig
WechatH5 WechatH5Config
Authorization AuthorizationConfig // 授权书配置
Upload UploadConfig // 图片上传(行驶证等)配置
WechatMini WechatMiniConfig
Query QueryConfig
AdminConfig AdminConfig
AdminPromotion AdminPromotion
TaxConfig TaxConfig
ExtensionTime int64
// PublicBaseURL 前台访问域名,用于构造对外可访问链接(上传图片、回调等),例如 https://www.tianyuancha.cn
PublicBaseURL string
ExtensionTime int64
}
// JwtAuth 用于 JWT 鉴权配置
@@ -42,6 +46,13 @@ type VerifyCode struct {
TemplateCode string
ValidTime int
}
type CaptchaConfig struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
EKey string
}
type Encrypt struct {
SecretKey string
}
@@ -53,9 +64,16 @@ type AlipayConfig struct {
AppCertPath string // 应用公钥证书路径
AlipayCertPath string // 支付宝公钥证书路径
AlipayRootCertPath string // 根证书路径
IsProduction bool
NotifyUrl string
ReturnURL string
// Bak 仅用于 [2026-01-25 16:38:17, 2026-02-02 18:26) 区间内订单退款
AppIDBak string
PrivateKeyBak string
AlipayPublicKeyBak string
AppCertPathBak string
AlipayCertPathBak string
AlipayRootCertPathBak string
IsProduction bool
NotifyUrl string
ReturnURL string
}
type WxpayConfig struct {
AppID string
@@ -129,3 +147,9 @@ type TianyuanapiConfig struct {
type AuthorizationConfig struct {
FileBaseURL string // 授权书文件访问基础URL
}
// UploadConfig 图片上传(行驶证等)配置,临时存储,按 hash 去重
type UploadConfig struct {
FileBaseURL string `json:",optional"` // 上传文件访问基础 URL如 https://xxx/api/v1/upload/file
TempFileMaxAgeH int `json:",optional"` // 临时文件保留时长小时超时删除0 表示默认 24 小时
}

View File

@@ -1,6 +1,7 @@
package auth
import (
"context"
"net/http"
"tyc-server/app/main/api/internal/logic/auth"
@@ -23,7 +24,8 @@ func SendSmsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
result.ParamValidateErrorResult(r, w, err)
return
}
l := auth.NewSendSmsLogic(r.Context(), svcCtx)
ctx := context.WithValue(r.Context(), auth.UserAgentContextKey, r.Header.Get("User-Agent"))
l := auth.NewSendSmsLogic(ctx, svcCtx)
err := l.SendSms(&req)
result.HttpResult(r, w, nil, err)
}

View File

@@ -0,0 +1,17 @@
package captcha
import (
"net/http"
"tyc-server/app/main/api/internal/logic/captcha"
"tyc-server/app/main/api/internal/svc"
"tyc-server/common/result"
)
func GetEncryptedSceneIdHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := captcha.NewGetEncryptedSceneIdLogic(r.Context(), svcCtx)
resp, err := l.GetEncryptedSceneId()
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -22,10 +22,13 @@ import (
app "tyc-server/app/main/api/internal/handler/app"
auth "tyc-server/app/main/api/internal/handler/auth"
authorization "tyc-server/app/main/api/internal/handler/authorization"
captcha "tyc-server/app/main/api/internal/handler/captcha"
notification "tyc-server/app/main/api/internal/handler/notification"
pay "tyc-server/app/main/api/internal/handler/pay"
product "tyc-server/app/main/api/internal/handler/product"
query "tyc-server/app/main/api/internal/handler/query"
tianyuan "tyc-server/app/main/api/internal/handler/tianyuan"
upload "tyc-server/app/main/api/internal/handler/upload"
user "tyc-server/app/main/api/internal/handler/user"
"tyc-server/app/main/api/internal/svc"
@@ -770,7 +773,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserDisableInterceptor, serverCtx.UserAuthInterceptor},
[]rest.Middleware{serverCtx.UserAuthInterceptor},
[]rest.Route{
{
Method: http.MethodGet,
@@ -810,7 +813,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserDisableInterceptor, serverCtx.UserAuthInterceptor},
[]rest.Middleware{serverCtx.UserAuthInterceptor},
[]rest.Route{
{
Method: http.MethodPost,
@@ -830,7 +833,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserDisableInterceptor, serverCtx.UserAuthInterceptor},
[]rest.Middleware{serverCtx.UserAuthInterceptor},
[]rest.Route{
{
Method: http.MethodGet,
@@ -880,7 +883,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthInterceptor, serverCtx.UserDisableInterceptor},
[]rest.Middleware{serverCtx.AuthInterceptor},
[]rest.Route{
{
Method: http.MethodPost,
@@ -952,6 +955,18 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// get encrypted scene id for aliyun captcha
Method: http.MethodPost,
Path: "/captcha/encryptedSceneId",
Handler: captcha.GetEncryptedSceneIdHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
@@ -987,7 +1002,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserDisableInterceptor, serverCtx.UserAuthInterceptor},
[]rest.Middleware{serverCtx.UserAuthInterceptor},
[]rest.Route{
{
Method: http.MethodPost,
@@ -1039,7 +1054,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthInterceptor, serverCtx.UserDisableInterceptor},
[]rest.Middleware{serverCtx.AuthInterceptor},
[]rest.Route{
{
// query service agent
@@ -1059,7 +1074,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserDisableInterceptor, serverCtx.UserAuthInterceptor},
[]rest.Middleware{serverCtx.UserAuthInterceptor},
[]rest.Route{
{
// query service
@@ -1075,7 +1090,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserDisableInterceptor, serverCtx.UserAuthInterceptor},
[]rest.Middleware{serverCtx.UserAuthInterceptor},
[]rest.Route{
{
// 生成分享链接
@@ -1148,6 +1163,36 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// 天远车辆类接口异步回调
Method: http.MethodPost,
Path: "/tianyuan/vehicle/callback",
Handler: tianyuan.VehicleCallbackHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// 访问已上传文件(供第三方或前端通过返回的 URL 拉取)
Method: http.MethodGet,
Path: "/upload/file/:fileName",
Handler: upload.ServeUploadedFileHandler(serverCtx),
},
{
// 上传图片,返回可访问 URL如行驶证限制 3MB
Method: http.MethodPost,
Path: "/upload/image",
Handler: upload.UploadImageHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
@@ -1179,7 +1224,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthInterceptor, serverCtx.UserDisableInterceptor},
[]rest.Middleware{serverCtx.AuthInterceptor},
[]rest.Route{
{
// 绑定手机号

View File

@@ -0,0 +1,33 @@
package tianyuan
import (
"net/http"
tianyuanlogic "tyc-server/app/main/api/internal/logic/tianyuan"
"tyc-server/app/main/api/internal/svc"
"tyc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
// VehicleCallbackHandler 天远车辆类接口异步回调入口
// 约定:第三方在回调 URL 上携带 order_no / api_id 等标识,例如:/api/v1/tianyuan/vehicle/callback?order_no=Q_xxx&api_id=QCXG1U4U
// 回调 Body 为该接口最终的 JSON 结果。
func VehicleCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := tianyuanlogic.NewVehicleCallbackLogic(r.Context(), svcCtx)
if err := l.Handle(r); err != nil {
// 对第三方尽量返回 200避免无限重试这里使用统一 result 封装错误
result.HttpResult(r, w, map[string]interface{}{
"code": 500,
"msg": "fail",
}, err)
return
}
httpx.OkJson(w, map[string]interface{}{
"code": 200,
"msg": "success",
})
}
}

View File

@@ -0,0 +1,47 @@
package upload
import (
"net/http"
"os"
"tyc-server/app/main/api/internal/logic/upload"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/common/result"
"tyc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ServeUploadedFileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ServeUploadedFileReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := upload.NewServeUploadedFileLogic(r.Context(), svcCtx)
resp, err := l.ServeUploadedFile(&req)
if err != nil {
result.HttpResult(r, w, nil, err)
return
}
if resp != nil && resp.FilePath != "" {
f, openErr := os.Open(resp.FilePath)
if openErr != nil {
result.HttpResult(r, w, nil, openErr)
return
}
defer f.Close()
w.Header().Set("Content-Type", resp.ContentType)
w.WriteHeader(http.StatusOK)
_, _ = f.WriteTo(w)
return
}
httpx.OkJson(w, resp)
}
}

View File

@@ -0,0 +1,29 @@
package upload
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"tyc-server/app/main/api/internal/logic/upload"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/common/result"
"tyc-server/pkg/lzkit/validator"
)
func UploadImageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UploadImageReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := upload.NewUploadImageLogic(r.Context(), svcCtx)
resp, err := l.UploadImage(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -75,16 +75,19 @@ func (l *AdminRefundOrderLogic) getAndValidateOrder(orderId int64, refundAmount
return order, nil
}
// handleAlipayRefund 处理支付宝退款
// handleAlipayRefund 处理支付宝退款(仅 [2026-01-25 16:38:17, 2026-02-02 18:26) 区间内订单走 bak 商户号)
func (l *AdminRefundOrderLogic) handleAlipayRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) {
// 调用支付宝退款接口
refundResp, err := l.svcCtx.AlipayService.AliRefund(l.ctx, order.OrderNo, req.RefundAmount)
orderPayTime := &order.CreateTime
if order.PayTime.Valid {
orderPayTime = &order.PayTime.Time
}
refundNo := l.generateRefundNo(order.OrderNo)
// 按订单记录的商户号 payment_merchant 选择支付宝商户;老订单未写入时由 AliRefund 内部按时间区间兜底。
refundResp, err := l.svcCtx.AlipayService.AliRefund(l.ctx, order.PaymentMerchant, order.OrderNo, req.RefundAmount, orderPayTime, refundNo)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "AdminRefundOrder, 支付宝退款失败 err: %v", err)
}
refundNo := l.generateRefundNo(order.OrderNo)
if refundResp.IsSuccess() {
// 支付宝退款成功,创建成功记录
err = l.createRefundRecordAndUpdateOrder(order, req, refundNo, refundResp.TradeNo, model.OrderStatusRefunded, model.OrderRefundStatusSuccess)
@@ -144,6 +147,7 @@ func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Or
OrderId: order.Id,
UserId: order.UserId,
ProductId: order.ProductId,
PaymentMerchant: order.PaymentMerchant,
RefundAmount: req.RefundAmount,
RefundReason: l.createNullString(req.RefundReason),
Status: refundStatus, // 使用传入的状态,不再硬编码
@@ -172,6 +176,7 @@ func (l *AdminRefundOrderLogic) createRefundRecordOnly(order *model.Order, req *
OrderId: order.Id,
UserId: order.UserId,
ProductId: order.ProductId,
PaymentMerchant: order.PaymentMerchant,
RefundAmount: req.RefundAmount,
RefundReason: l.createNullString(req.RefundReason),
Status: refundStatus,
@@ -185,9 +190,9 @@ func (l *AdminRefundOrderLogic) createRefundRecordOnly(order *model.Order, req *
return nil
}
// generateRefundNo 生成退款单号
// generateRefundNo 生成退款单号(同一订单多次退款需唯一,避免 unique_refund_no 冲突)
func (l *AdminRefundOrderLogic) generateRefundNo(orderNo string) string {
return fmt.Sprintf("%s%s", RefundNoPrefix, orderNo)
return fmt.Sprintf("%s%s-%d", RefundNoPrefix, orderNo, time.Now().UnixMilli())
}
// createNullString 创建 sql.NullString

View File

@@ -6,6 +6,7 @@ import (
"math/rand"
"time"
"tyc-server/common/xerr"
"tyc-server/pkg/captcha"
"tyc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
@@ -34,7 +35,21 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo
}
}
// UserAgentContextKey 用于从 context 读取 User-Agent如判断是否微信
const UserAgentContextKey = "user_agent"
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
userAgent, _ := l.ctx.Value(UserAgentContextKey).(string)
cfg := l.svcCtx.Config.Captcha
if err := captcha.VerifyWithUserAgent(captcha.Config{
AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret,
EndpointURL: cfg.EndpointURL,
SceneID: cfg.SceneID,
}, req.CaptchaVerifyParam, userAgent); err != nil {
return err
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {

View File

@@ -0,0 +1,38 @@
package captcha
import (
"context"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/pkg/captcha"
"github.com/zeromicro/go-zero/core/logx"
)
type GetEncryptedSceneIdLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetEncryptedSceneIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEncryptedSceneIdLogic {
return &GetEncryptedSceneIdLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetEncryptedSceneIdLogic) GetEncryptedSceneId() (resp *types.GetEncryptedSceneIdResp, err error) {
cfg := l.svcCtx.Config.Captcha
encrypted, genErr := captcha.GenerateEncryptedSceneID(cfg.SceneID, cfg.EKey, 3600)
if genErr != nil {
// 记录日志,返回通用错误
l.Errorf("generate encrypted scene id error: %+v", genErr)
return nil, genErr
}
return &types.GetEncryptedSceneIdResp{
EncryptedSceneId: encrypted,
}, nil
}

View File

@@ -32,22 +32,58 @@ func NewAlipayCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Al
}
func (l *AlipayCallbackLogic) AlipayCallback(w http.ResponseWriter, r *http.Request) error {
notification, err := l.svcCtx.AlipayService.HandleAliPaymentNotification(r)
if err != nil {
logx.Errorf("支付宝支付回调,%v", err)
// 先解析表单,拿到 out_trade_no 用于查找订单和对应商户号
if err := r.ParseForm(); err != nil {
logx.Errorf("支付宝支付回调,解析请求表单失败: %v", err)
return nil
}
// 根据订单号前缀判断订单类型
orderNo := notification.OutTradeNo
orderNo := r.FormValue("out_trade_no")
if orderNo == "" {
logx.Errorf("支付宝支付回调,缺少 out_trade_no")
return nil
}
// 根据订单号前缀判断订单类型,并查出对应商户标识
var merchant string
if strings.HasPrefix(orderNo, "Q_") {
// 查询订单处理
return l.handleQueryOrderPayment(w, notification)
// 查询订单
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo)
if err != nil {
logx.Errorf("支付宝支付回调,查询订单失败: %v", err)
return nil
}
merchant = order.PaymentMerchant
} else if strings.HasPrefix(orderNo, "A_") {
// 代理会员订单处理
return l.handleAgentVipOrderPayment(w, notification)
// 代理会员订单
agentOrder, err := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, orderNo)
if err != nil {
logx.Errorf("支付宝支付回调,查询代理会员订单失败: %v", err)
return nil
}
merchant = agentOrder.PaymentMerchant
} else {
// 兼容旧订单,假设没有前缀的是查询订单
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo)
if err != nil {
logx.Errorf("支付宝支付回调(旧订单),查询订单失败: %v", err)
return nil
}
merchant = order.PaymentMerchant
}
notification, err := l.svcCtx.AlipayService.HandleAliPaymentNotification(merchant, r.Form)
if err != nil {
logx.Errorf("支付宝支付回调,验签失败: %v", err)
return nil
}
// 再次根据订单号前缀分发到具体处理函数
if strings.HasPrefix(orderNo, "Q_") {
return l.handleQueryOrderPayment(w, notification)
} else if strings.HasPrefix(orderNo, "A_") {
return l.handleAgentVipOrderPayment(w, notification)
} else {
return l.handleQueryOrderPayment(w, notification)
}
}
@@ -218,7 +254,10 @@ func (l *AlipayCallbackLogic) handleRefund(order *model.AgentMembershipRechargeO
return refundErr
}
} else {
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
// 支付宝退款按订单记录的商户号 payment_merchant 走对应商户;
// 老订单若未写入商户号,则在 AliRefund 内按时间区间兜底。
orderPayTime := order.CreateTime
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.PaymentMerchant, order.OrderNo, order.Amount, &orderPayTime, "")
if refundErr != nil {
return refundErr
}

View File

@@ -28,10 +28,11 @@ type PaymentLogic struct {
svcCtx *svc.ServiceContext
}
type PaymentTypeResp struct {
amount float64
outTradeNo string
description string
orderID int64
amount float64
outTradeNo string
description string
orderID int64
payMerchantID string
}
func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic {
@@ -79,7 +80,8 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
} else if req.PayMethod == "alipay" {
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
// 支付宝按订单写入的商户标识one/two创建支付订单
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.payMerchantID, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
} else if req.PayMethod == "appleiap" {
prepayData = l.svcCtx.ApplePayService.GetIappayAppID(paymentTypeResp.outTradeNo)
}
@@ -186,12 +188,24 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
amount = 0.01
}
var orderID int64
// 默认支付宝商户号为 one若为代理推广订单存在 AgentIdentifier则使用 two。
paymentMerchant := ""
if req.PayMethod == "alipay" {
if data.AgentIdentifier != "" {
paymentMerchant = "two"
} else {
paymentMerchant = "one"
}
}
order := model.Order{
OrderNo: outTradeNo,
UserId: userID,
ProductId: product.Id,
PaymentPlatform: req.PayMethod,
PaymentScene: "app",
PaymentMerchant: paymentMerchant,
Amount: amount,
Status: "pending",
}
@@ -228,7 +242,13 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert)
}
}
return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName, orderID: orderID}, nil
return &PaymentTypeResp{
amount: amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: orderID,
payMerchantID: paymentMerchant,
}, nil
}
func (l *PaymentLogic) AgentVipOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) {
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
@@ -274,20 +294,32 @@ func (l *PaymentLogic) AgentVipOrderPayment(req *types.PaymentReq, session sqlx.
if user.Inside == 1 {
amount = 0.01
}
paymentMerchant := ""
if req.PayMethod == "alipay" {
paymentMerchant = "one"
}
agentMembershipRechargeOrder := model.AgentMembershipRechargeOrder{
OrderNo: req.Id,
UserId: userID,
AgentId: agentModel.Id,
Amount: amount,
PaymentMethod: req.PayMethod,
LevelName: agentVipCache.Type,
Status: "pending",
OrderNo: req.Id,
UserId: userID,
AgentId: agentModel.Id,
Amount: amount,
PaymentMethod: req.PayMethod,
PaymentMerchant: paymentMerchant,
LevelName: agentVipCache.Type,
Status: "pending",
}
_, err = l.svcCtx.AgentMembershipRechargeOrderModel.Insert(l.ctx, session, &agentMembershipRechargeOrder)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理会员充值订单失败: %+v", err)
}
return &PaymentTypeResp{amount: amount, outTradeNo: req.Id, description: fmt.Sprintf("%s代理会员充值", agentMembershipConfig.LevelName)}, nil
return &PaymentTypeResp{
amount: amount,
outTradeNo: req.Id,
description: fmt.Sprintf("%s代理会员充值", agentMembershipConfig.LevelName),
payMerchantID: paymentMerchant,
}, nil
}
func (l *PaymentLogic) agentParsing(agentIdentifier string) (*types.AgentIdentifier, error) {
key, decodeErr := hex.DecodeString("8e3e7a2f60edb49221e953b9c029ed10")

View File

@@ -221,7 +221,10 @@ func (l *WechatPayCallbackLogic) handleRefund(order *model.AgentMembershipRechar
return refundErr
}
} else {
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
// 支付宝退款按订单记录的商户号 payment_merchant 走对应商户;
// 老订单若未写入商户号,则在 AliRefund 内按时间区间兜底。
orderPayTime := order.CreateTime
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.PaymentMerchant, order.OrderNo, order.Amount, &orderPayTime, "")
if refundErr != nil {
return refundErr
}

View File

@@ -62,9 +62,16 @@ func (l *QueryDetailByOrderIdLogic) QueryDetailByOrderId(req *types.QueryDetailB
}
// 检查订单状态
if order.Status != "paid" {
// - pending未支付阻止查看
// - refunded已退款允许进入但前端只展示“查询为空订单已退款”提示不再展示报告数据
// - 其他异常状态:按未找到处理
if order.Status == "pending" {
return nil, errors.Wrapf(xerr.NewErrMsg("订单未支付,无法查看报告"), "")
}
if order.Status != "paid" && order.Status != "refunded" {
// 使用逻辑层“未找到”错误码,前端按空结果处理
return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "订单状态不支持查看报告, status=%s", order.Status)
}
// 获取报告信息
queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, req.OrderId)
@@ -105,6 +112,15 @@ func (l *QueryDetailByOrderIdLogic) QueryDetailByOrderId(req *types.QueryDetailB
}
query.ProductName = product.ProductName
query.Product = product.ProductEn
// 已退款订单:不再返回具体报告数据,仅告知前端“查询为空,订单已退款”
if order.Status == "refunded" {
query.QueryState = "failed" // 前端据此走空状态 + 退款提示
query.QueryData = []types.QueryItem{} // 不返回任何模块数据
if query.QueryParams == nil { // 参数可选保留,这里保险起见给个空 map
query.QueryParams = map[string]interface{}{}
}
}
return &types.QueryDetailByOrderIdResp{
Query: query,
}, nil

View File

@@ -53,9 +53,16 @@ func (l *QueryDetailByOrderNoLogic) QueryDetailByOrderNo(req *types.QueryDetailB
}
// 检查订单状态
if order.Status != "paid" {
// - pending未支付阻止查看
// - refunded已退款允许进入但前端只展示“查询为空订单已退款”提示不再展示报告数据
// - 其他异常状态:按未找到处理
if order.Status == "pending" {
return nil, errors.Wrapf(xerr.NewErrMsg("订单未支付,无法查看报告"), "")
}
if order.Status != "paid" && order.Status != "refunded" {
// 使用逻辑层“未找到”错误码,前端按空结果处理
return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "订单状态不支持查看报告, status=%s", order.Status)
}
// 获取报告信息
queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id)
@@ -96,6 +103,15 @@ func (l *QueryDetailByOrderNoLogic) QueryDetailByOrderNo(req *types.QueryDetailB
}
query.Product = product.ProductEn
query.ProductName = product.ProductName
// 已退款订单:不再返回具体报告数据,仅告知前端“查询为空,订单已退款”
if order.Status == "refunded" {
query.QueryState = "failed" // 前端据此走空状态 + 退款提示
query.QueryData = []types.QueryItem{} // 不返回任何模块数据
if query.QueryParams == nil { // 参数可选保留,这里保险起见给个空 map
query.QueryParams = map[string]interface{}{}
}
}
return &types.QueryDetailByOrderNoResp{
Query: query,
}, nil

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,187 @@
package tianyuan
import (
"context"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"time"
"tyc-server/app/main/api/internal/service"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/model"
"tyc-server/common/xerr"
"tyc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
// VehicleCallbackLogic 处理天远车辆类接口的异步回调
// 设计目标:
// - 按 order_no + api_id 找到对应的查询记录
// - 解密原有 query_data[]APIResponseData更新或追加当前 api_id 的结果
// - 再次加密写回 query.query_data必要时将 query_state 从 pending 更新为 success
type VehicleCallbackLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewVehicleCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VehicleCallbackLogic {
return &VehicleCallbackLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// 与 paySuccessNotify 中一致:仅通过异步回调回写结果的车辆接口
var asyncVehicleApiIDs = map[string]bool{
"QCXG1U4U": true, "QCXG3Y6B": true, "QCXG3Z3L": true, "QCXGP00W": true,
}
// allAsyncVehicleReceived 判断 apiList 中所有“异步车辆接口”是否均已收到回调Success 为 true
func allAsyncVehicleReceived(apiList []service.APIResponseData) bool {
for _, item := range apiList {
if asyncVehicleApiIDs[item.ApiID] && !item.Success {
return false
}
}
return true
}
// Handle 入口:直接接收原始 *http.Request方便读取 query / body
func (l *VehicleCallbackLogic) Handle(r *http.Request) error {
apiID := r.URL.Query().Get("api_id")
orderNo := r.URL.Query().Get("order_no")
if apiID == "" || orderNo == "" {
return errors.Wrapf(
xerr.NewErrMsg("缺少 api_id 或 order_no"),
"tianyuan vehicle callback, api_id=%s, order_no=%s", apiID, orderNo,
)
}
l.Infof("tianyuan vehicle callback start, api_id=%s, order_no=%s", apiID, orderNo)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "读取回调 Body 失败: %v", err)
}
l.Infof("tianyuan vehicle callback body received, api_id=%s, order_no=%s, body_len=%d", apiID, orderNo, len(bodyBytes))
// 回调包裹格式通常为 { code:0, msg:\"成功\", order_id:\"...\", data:{...} }
// 这里只把 data 字段提取出来存到 QueryData 里,便于前端直接渲染。
type callbackWrapper struct {
Code json.RawMessage `json:"code"`
Msg json.RawMessage `json:"msg"`
OrderID json.RawMessage `json:"order_id"`
Data json.RawMessage `json:"data"`
Extra map[string]any `json:"-"` // 预留
}
var wrapper callbackWrapper
payload := bodyBytes
if len(bodyBytes) > 0 {
if err := json.Unmarshal(bodyBytes, &wrapper); err != nil {
l.Errorf("tianyuan vehicle callback unmarshal body failed, api_id=%s, order_no=%s, err=%v", apiID, orderNo, err)
} else if len(wrapper.Data) > 0 {
payload = wrapper.Data
l.Infof("tianyuan vehicle callback extracted data field, api_id=%s, order_no=%s, data_len=%d", apiID, orderNo, len(payload))
}
}
// 1. 根据订单号找到订单
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo)
if err != nil {
if err == model.ErrNotFound {
return errors.Wrapf(xerr.NewErrMsg("未找到订单"), "order_no=%s", orderNo)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败, order_no=%s, err=%v", orderNo, err)
}
// 2. 根据订单ID找到对应查询记录
query, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id)
if err != nil {
if err == model.ErrNotFound {
return errors.Wrapf(xerr.NewErrMsg("未找到查询记录"), "order_no=%s, order_id=%d", orderNo, order.Id)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询 query 记录失败, order_id=%d, err=%v", order.Id, err)
}
// 3. 获取加密密钥
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解析 AES 密钥失败: %v", decodeErr)
}
// 4. 解密 query_data反序列化为 []APIResponseData
var apiList []service.APIResponseData
if query.QueryData.Valid && query.QueryData.String != "" {
decrypted, decErr := crypto.AesDecrypt(query.QueryData.String, key)
if decErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密 query_data 失败: %v", decErr)
}
if len(decrypted) > 0 {
if err := json.Unmarshal(decrypted, &apiList); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解析 query_data 为 APIResponseData 列表失败: %v", err)
}
}
}
// 5. 更新或追加当前 api_id 的结果
nowStr := time.Now().Format("2006-01-02 15:04:05")
updated := false
for i := range apiList {
if apiList[i].ApiID == apiID {
apiList[i].Data = json.RawMessage(payload)
apiList[i].Success = true
apiList[i].Timestamp = nowStr
apiList[i].Error = ""
updated = true
break
}
}
if !updated {
apiList = append(apiList, service.APIResponseData{
ApiID: apiID,
Data: json.RawMessage(payload),
Success: true,
Timestamp: nowStr,
})
}
// 6. 重新序列化并加密,写回查询记录
merged, err := json.Marshal(apiList)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "序列化合并后的 query_data 失败: %v", err)
}
enc, encErr := crypto.AesEncrypt(merged, key)
if encErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密合并后的 query_data 失败: %v", encErr)
}
query.QueryData.String = enc
query.QueryData.Valid = true
// 仅当所有异步车辆接口均已有回调结果时,才将 pending 置为 success并触发代理结算
wasPending := query.QueryState == "pending"
didSetSuccess := wasPending && allAsyncVehicleReceived(apiList)
if didSetSuccess {
query.QueryState = "success"
}
if err := l.svcCtx.QueryModel.UpdateWithVersion(l.ctx, nil, query); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新查询记录失败, query_id=%d, err=%v", query.Id, err)
}
if didSetSuccess {
if agentErr := l.svcCtx.AgentService.AgentProcess(l.ctx, order); agentErr != nil {
l.Errorf("tianyuan vehicle callback, AgentProcess failed, order_no=%s, err=%v", orderNo, agentErr)
// 不因代理处理失败而整体失败,回调已成功落库
}
}
l.Infof("tianyuan vehicle callback handled, order_no=%s, api_id=%s, query_id=%d", orderNo, apiID, query.Id)
return nil
}

View File

@@ -0,0 +1,66 @@
package upload
import (
"context"
"os"
"path/filepath"
"strings"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type ServeUploadedFileLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewServeUploadedFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServeUploadedFileLogic {
return &ServeUploadedFileLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ServeUploadedFileLogic) ServeUploadedFile(req *types.ServeUploadedFileReq) (resp *types.ServeUploadedFileResp, err error) {
fileName := strings.TrimSpace(req.FileName)
if fileName == "" {
return nil, errors.Wrap(xerr.NewErrMsg("缺少文件名"), "fileName empty")
}
// 只允许文件名,禁止路径穿越
if strings.Contains(fileName, "..") || filepath.Base(fileName) != fileName {
return nil, errors.Wrap(xerr.NewErrMsg("非法文件名"), fileName)
}
candidates := []string{
"data/uploads",
"../data/uploads",
"../../data/uploads",
"../../../data/uploads",
}
for _, c := range candidates {
abs, _ := filepath.Abs(c)
fullPath := filepath.Join(abs, fileName)
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
contentType := "image/jpeg"
if strings.HasSuffix(strings.ToLower(fileName), ".png") {
contentType = "image/png"
} else if strings.HasSuffix(strings.ToLower(fileName), ".gif") {
contentType = "image/gif"
} else if strings.HasSuffix(strings.ToLower(fileName), ".webp") {
contentType = "image/webp"
}
return &types.ServeUploadedFileResp{
FilePath: fullPath,
ContentType: contentType,
}, nil
}
}
return nil, errors.Wrapf(xerr.NewErrMsg("文件不存在"), "fileName=%s", fileName)
}

View File

@@ -0,0 +1,137 @@
package upload
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
const maxImageSize = 3 * 1024 * 1024 // 3MB
const defaultTempFileMaxAgeH = 24
type UploadImageLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUploadImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadImageLogic {
return &UploadImageLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UploadImageLogic) UploadImage(req *types.UploadImageReq) (resp *types.UploadImageResp, err error) {
decoded, decErr := base64.StdEncoding.DecodeString(req.ImageBase64)
if decErr != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("图片 base64 格式错误"), "%v", decErr)
}
if len(decoded) > maxImageSize {
return nil, errors.Wrapf(xerr.NewErrMsg("图片不能超过 3M"), "size=%d", len(decoded))
}
// 按文件内容 hash 命名,相同文件复用同一 URL避免重复传输与刷流量
hashSum := sha256.Sum256(decoded)
hashHex := hex.EncodeToString(hashSum[:])
fileName := hashHex + ".jpg"
dir := l.uploadStoragePath()
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建上传目录失败: %v", err)
}
filePath := filepath.Join(dir, fileName)
// 若已存在同 hash 文件,直接返回 URL不重复写入
if _, statErr := os.Stat(filePath); statErr == nil {
url := l.buildURL(fileName)
logx.Infof("upload image dedup by hash, file=%s", fileName)
return &types.UploadImageResp{Url: url}, nil
}
if err := os.WriteFile(filePath, decoded, 0644); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存图片失败: %v", err)
}
// 异步清理过期临时文件,不阻塞响应
go l.deleteOldUploads(dir)
url := l.buildURL(fileName)
logx.Infof("upload image ok, file=%s", fileName)
return &types.UploadImageResp{Url: url}, nil
}
func (l *UploadImageLogic) buildURL(fileName string) string {
baseURL := l.svcCtx.Config.Upload.FileBaseURL
if baseURL == "" && l.svcCtx.Config.PublicBaseURL != "" {
// 兜底:如果未单独配置 Upload.FileBaseURL则使用公共域名拼接默认上传路径
baseURL = strings.TrimRight(l.svcCtx.Config.PublicBaseURL, "/") + "/api/v1/upload/file"
}
if baseURL == "" {
return ""
}
return fmt.Sprintf("%s/%s", baseURL, fileName)
}
func (l *UploadImageLogic) uploadStoragePath() string {
candidates := []string{
"data/uploads",
"../data/uploads",
"../../data/uploads",
"../../../data/uploads",
}
for _, c := range candidates {
abs, _ := filepath.Abs(c)
if err := os.MkdirAll(abs, 0755); err == nil {
return abs
}
}
abs, _ := filepath.Abs(candidates[0])
return abs
}
// deleteOldUploads 删除目录下超过保留时长的临时文件
func (l *UploadImageLogic) deleteOldUploads(dir string) {
maxAgeH := l.svcCtx.Config.Upload.TempFileMaxAgeH
if maxAgeH <= 0 {
maxAgeH = defaultTempFileMaxAgeH
}
cutoff := time.Now().Add(-time.Duration(maxAgeH) * time.Hour)
entries, err := os.ReadDir(dir)
if err != nil {
l.Errorf("deleteOldUploads ReadDir: %v", err)
return
}
for _, e := range entries {
if e.IsDir() {
continue
}
path := filepath.Join(dir, e.Name())
info, err := os.Stat(path)
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
if err := os.Remove(path); err != nil {
l.Errorf("deleteOldUploads Remove %s: %v", path, err)
} else {
l.Infof("deleteOldUploads removed %s", path)
}
}
}
}

View File

@@ -38,7 +38,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 加密手机号失败: %+v", err)
}
// 检查验证码(开发环境可跳过)
// 短信验证码校验(开发环境可跳过)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)

View File

@@ -36,6 +36,26 @@ var payload struct {
OrderID int64 `json:"order_id"`
}
// 仅通过异步回调回写结果的车辆接口 ApiID
var asyncVehicleApiIDs = map[string]bool{
"QCXG1U4U": true, // 车辆里程混合
"QCXG3Y6B": true, // 车辆维保简版
"QCXG3Z3L": true, // 车辆维保详细版
"QCXGP00W": true, // 车辆出险详版
}
func isAllAsyncVehicleQuery(responseData []service.APIResponseData) bool {
if len(responseData) == 0 {
return false
}
for _, r := range responseData {
if !asyncVehicleApiIDs[r.ApiID] {
return false
}
}
return true
}
func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
// 从任务的负载中解码数据
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
@@ -76,11 +96,31 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
return fmt.Errorf("解密参数失败: %+v", aesdecryptErr)
}
// 为避免 base64 图片等大字段撑爆 query.query_params 列,这里对参数做一次瘦身:
// 删除纯 base64 图片字段(如 vlphoto_data / photo_data / image_base64仅保留必要的文本信息。
var originalParams map[string]interface{}
if err := json.Unmarshal(decryptData, &originalParams); err != nil {
return fmt.Errorf("解析原始参数失败: %+v", err)
}
delete(originalParams, "vlphoto_data")
delete(originalParams, "photo_data")
delete(originalParams, "image_base64")
slimBytes, err := json.Marshal(originalParams)
if err != nil {
return fmt.Errorf("序列化精简后的参数失败: %+v", err)
}
slimEncrypted, err := crypto.AesEncrypt(slimBytes, key)
if err != nil {
return fmt.Errorf("加密精简后的参数失败: %+v", err)
}
query := &model.Query{
OrderId: order.Id,
UserId: order.UserId,
ProductId: product.Id,
QueryParams: data.Params,
OrderId: order.Id,
UserId: order.UserId,
ProductId: product.Id,
// 仅保存精简后的参数,避免 base64 图片占满列
QueryParams: slimEncrypted,
QueryState: "pending",
}
result, insertQueryErr := l.svcCtx.QueryModel.Insert(ctx, nil, query)
@@ -100,7 +140,7 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
return fmt.Errorf("获取插入后的查询记录失败: %+v", err)
}
// 解析解密后的参数获取用户信息
// 解析解密后的参数获取用户信息(这里依然使用完整参数,后续会在写回时再次瘦身)
var userInfo map[string]interface{}
if err := json.Unmarshal(decryptData, &userInfo); err != nil {
return fmt.Errorf("解析用户信息失败: %+v", err)
@@ -120,26 +160,40 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
downloadURL := l.buildAuthorizationDownloadURL(authDoc.FileName)
userInfo["authorization_url"] = downloadURL
// 重新序列化用户信息
// 1完整参数包含图片和授权书 URL用于后续调用天远接口
updatedDecryptData, marshalErr := json.Marshal(userInfo)
if marshalErr != nil {
logx.Errorf("序列化用户信息失败: %v", marshalErr)
} else {
// 重新加密更新后的数据
encryptedUpdatedData, encryptErr := crypto.AesEncrypt(updatedDecryptData, key)
if encryptErr != nil {
logx.Errorf("重新加密用户信息失败: %v", encryptErr)
decryptData = updatedDecryptData
// 2精简版本去掉图片类大字段仅用于持久化到 query.QueryParams
slimUserInfo := make(map[string]interface{}, len(userInfo))
for k, v := range userInfo {
slimUserInfo[k] = v
}
delete(slimUserInfo, "vlphoto_data")
delete(slimUserInfo, "photo_data")
delete(slimUserInfo, "image_base64")
slimUpdatedBytes, marshalSlimErr := json.Marshal(slimUserInfo)
if marshalSlimErr != nil {
logx.Errorf("序列化精简用户信息失败: %v", marshalSlimErr)
} else {
// 更新查询记录中的参数
query.QueryParams = string(encryptedUpdatedData)
updateParamsErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query)
if updateParamsErr != nil {
logx.Errorf("更新查询参数失败: %v", updateParamsErr)
// 重新加密精简后的数据写回数据库
encryptedUpdatedData, encryptErr := crypto.AesEncrypt(slimUpdatedBytes, key)
if encryptErr != nil {
logx.Errorf("重新加密精简用户信息失败: %v", encryptErr)
} else {
logx.Infof("成功更新查询参数包含授权书URL: %s", downloadURL)
query.QueryParams = string(encryptedUpdatedData)
updateParamsErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query)
if updateParamsErr != nil {
logx.Errorf("更新查询参数失败: %v", updateParamsErr)
} else {
logx.Infof("成功更新查询参数包含授权书URL: %s", downloadURL)
}
}
}
decryptData = updatedDecryptData
}
}
@@ -152,7 +206,8 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
responseData = []service.APIResponseData{}
} else {
var processErr error
responseData, processErr = l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
// 传入订单号,用于在 ApiRequestService 中为异步车辆接口生成回调地址return_url
responseData, processErr = l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id, order.OrderNo)
if processErr != nil {
return l.handleError(ctx, processErr, order, query)
}
@@ -160,18 +215,16 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
// 计算成功模块的总成本价
totalCostPrice := 0.0
if responseData != nil {
for _, item := range responseData {
if item.Success {
// 根据API ID查找功能模块
feature, err := l.svcCtx.FeatureModel.FindOneByApiId(ctx, item.ApiID)
if err != nil {
logx.Errorf("查找功能模块失败, API ID: %s, 错误: %v", item.ApiID, err)
continue
}
// 累加成本价
totalCostPrice += feature.CostPrice
for _, item := range responseData {
if item.Success {
// 根据API ID查找功能模块
feature, err := l.svcCtx.FeatureModel.FindOneByApiId(ctx, item.ApiID)
if err != nil {
logx.Errorf("查找功能模块失败, API ID: %s, 错误: %v", item.ApiID, err)
continue
}
// 累加成本价
totalCostPrice += feature.CostPrice
}
}
@@ -204,11 +257,14 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
return l.handleError(ctx, err, order, query)
}
query.QueryState = "success"
updateQueryErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query)
if updateQueryErr != nil {
updateQueryErr = fmt.Errorf("修改查询状态失败: %v", updateQueryErr)
return l.handleError(ctx, updateQueryErr, order, query)
// 若当前产品全部为异步车辆接口(结果通过回调回写),则保持 pending由回调再置为 success
if !isAllAsyncVehicleQuery(responseData) {
query.QueryState = "success"
updateQueryErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query)
if updateQueryErr != nil {
updateQueryErr = fmt.Errorf("修改查询状态失败: %v", updateQueryErr)
return l.handleError(ctx, updateQueryErr, order, query)
}
}
err = l.svcCtx.AgentService.AgentProcess(ctx, order)
@@ -266,8 +322,13 @@ func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error
logx.Infof("已发起微信退款申请, orderID: %d, amount: %f", order.Id, order.Amount)
return asynq.SkipRetry
} else {
// 支付宝退款为同步结果,这里直接根据返回结果更新订单和佣金/钱包
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
// 支付宝退款为同步结果,优先按订单记录的 payment_merchant 选择商户;
// 老订单若未写入商户号,则在 AliRefund 内按时间区间兜底。
orderPayTime := &order.CreateTime
if order.PayTime.Valid {
orderPayTime = &order.PayTime.Time
}
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.PaymentMerchant, order.OrderNo, order.Amount, orderPayTime, "")
if refundErr != nil {
logx.Error(refundErr)
return asynq.SkipRetry

View File

@@ -5,7 +5,7 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strconv"
"sync/atomic"
"time"
@@ -16,9 +16,38 @@ import (
"github.com/smartwalle/alipay/v3"
)
// bak 支付宝仅用于 [AlipayBakRefundStart, AlipayBakRefundEnd) 区间内支付订单的退款,区间外使用正式 clientCST
var (
AlipayBakRefundStart = time.Date(2026, 1, 25, 16, 38, 17, 0, time.FixedZone("CST", 8*3600)) // Sun Jan 25 2026 16:38:17 GMT+0800 之前用正式
AlipayBakRefundEnd = time.Date(2026, 2, 2, 18, 26, 0, 0, time.FixedZone("CST", 8*3600)) // 2026-02-02 18:26 之后用正式
)
type AliPayService struct {
config config.AlipayConfig
AlipayClient *alipay.Client
config config.AlipayConfig
AlipayClient *alipay.Client
AlipayClientBak *alipay.Client // 仅用于 [2026-01-25 16:38:17, 2026-02-02 18:26) 区间内订单的退款
}
// clientForMerchant 根据商户标识与订单时间选择对应的支付宝 client。
// - merchant == "two" 且 Bak 存在:优先返回 Bak
// - merchant == "one" 或空:默认返回主商户;
// - merchant 为空且 orderPayTime 落在备份时间区间:兼容老订单,走 Bak。
func (a *AliPayService) clientForMerchant(merchant string, orderPayTime *time.Time) *alipay.Client {
// 显式指定 two则优先走 Bak
if merchant == "two" && a.AlipayClientBak != nil {
return a.AlipayClientBak
}
// 显式指定 one 或其他未知标识,一律走主商户
if merchant == "one" || merchant == "" {
// 对于老订单未写入 merchant 的情况,继续保留时间区间兜底逻辑
if merchant == "" && orderPayTime != nil && a.AlipayClientBak != nil &&
!orderPayTime.Before(AlipayBakRefundStart) && orderPayTime.Before(AlipayBakRefundEnd) {
return a.AlipayClientBak
}
return a.AlipayClient
}
// 兜底:未知标识时仍走主商户,避免因为配置问题导致整体不可用
return a.AlipayClient
}
// NewAliPayService 是一个构造函数,用于初始化 AliPayService
@@ -44,14 +73,34 @@ func NewAliPayService(c config.Config) *AliPayService {
panic(fmt.Sprintf("加载根证书失败: %v", err))
}
return &AliPayService{
svc := &AliPayService{
config: c.Alipay,
AlipayClient: client,
}
// 初始化 bak 支付宝客户端(仅用于 [2026-01-25 16:38:17, 2026-02-02 18:26) 区间内订单的退款)
if c.Alipay.AppIDBak != "" && c.Alipay.PrivateKeyBak != "" {
bakClient, err := alipay.New(c.Alipay.AppIDBak, c.Alipay.PrivateKeyBak, c.Alipay.IsProduction)
if err != nil {
panic(fmt.Sprintf("创建支付宝 bak 客户端失败: %v", err))
}
if err = bakClient.LoadAppCertPublicKeyFromFile(c.Alipay.AppCertPathBak); err != nil {
panic(fmt.Sprintf("加载 bak 应用公钥证书失败: %v", err))
}
if err = bakClient.LoadAlipayCertPublicKeyFromFile(c.Alipay.AlipayCertPathBak); err != nil {
panic(fmt.Sprintf("加载 bak 支付宝公钥证书失败: %v", err))
}
if err = bakClient.LoadAliPayRootCertFromFile(c.Alipay.AlipayRootCertPathBak); err != nil {
panic(fmt.Sprintf("加载 bak 根证书失败: %v", err))
}
svc.AlipayClientBak = bakClient
}
return svc
}
func (a *AliPayService) CreateAlipayAppOrder(amount float64, subject string, outTradeNo string) (string, error) {
client := a.AlipayClient
func (a *AliPayService) CreateAlipayAppOrder(merchant string, amount float64, subject string, outTradeNo string) (string, error) {
client := a.clientForMerchant(merchant, nil)
totalAmount := lzUtils.ToAlipayAmount(amount)
// 构造移动支付请求
p := alipay.TradeAppPay{
@@ -74,8 +123,8 @@ func (a *AliPayService) CreateAlipayAppOrder(amount float64, subject string, out
}
// CreateAlipayH5Order 创建支付宝H5支付订单
func (a *AliPayService) CreateAlipayH5Order(amount float64, subject string, outTradeNo string) (string, error) {
client := a.AlipayClient
func (a *AliPayService) CreateAlipayH5Order(merchant string, amount float64, subject string, outTradeNo string) (string, error) {
client := a.clientForMerchant(merchant, nil)
totalAmount := lzUtils.ToAlipayAmount(amount)
// 构造H5支付请求
p := alipay.TradeWapPay{
@@ -97,8 +146,9 @@ func (a *AliPayService) CreateAlipayH5Order(amount float64, subject string, outT
return payUrl.String(), nil
}
// CreateAlipayOrder 根据平台类型创建支付宝支付订单
func (a *AliPayService) CreateAlipayOrder(ctx context.Context, amount float64, subject string, outTradeNo string) (string, error) {
// CreateAlipayOrder 根据平台类型和商户标识创建支付宝支付订单
// merchant: 商户标识,目前约定 "one"=主商户, "two"=备商户
func (a *AliPayService) CreateAlipayOrder(ctx context.Context, merchant string, amount float64, subject string, outTradeNo string) (string, error) {
// 根据 ctx 中的 platform 判断平台
platform, platformOk := ctx.Value("platform").(string)
if !platformOk {
@@ -107,52 +157,58 @@ func (a *AliPayService) CreateAlipayOrder(ctx context.Context, amount float64, s
switch platform {
case model.PlatformApp:
// 调用App支付的创建方法
return a.CreateAlipayAppOrder(amount, subject, outTradeNo)
return a.CreateAlipayAppOrder(merchant, amount, subject, outTradeNo)
case model.PlatformH5:
// 调用H5支付的创建方法并传入 returnUrl
return a.CreateAlipayH5Order(amount, subject, outTradeNo)
return a.CreateAlipayH5Order(merchant, amount, subject, outTradeNo)
default:
return "", fmt.Errorf("不支持的支付平台: %s", platform)
}
}
// AliRefund 发起支付宝退款
func (a *AliPayService) AliRefund(ctx context.Context, outTradeNo string, refundAmount float64) (*alipay.TradeRefundRsp, error) {
// AliRefund 发起支付宝退款
// merchant: 支付商户标识one/two。为空时按老逻辑仅在备份时间区间内使用 Bak。
// orderPayTime 为订单支付时间(或创建时间);用于老订单按时间区间选择商户;传 nil 则忽略时间区间。
// outRequestNo 为商户退款请求号,同一笔退款需唯一;传空则使用 "refund-"+outTradeNo重试时建议传入唯一号避免支付宝报重复。
func (a *AliPayService) AliRefund(ctx context.Context, merchant string, outTradeNo string, refundAmount float64, orderPayTime *time.Time, outRequestNo string) (*alipay.TradeRefundRsp, error) {
client := a.clientForMerchant(merchant, orderPayTime)
if outRequestNo == "" {
outRequestNo = fmt.Sprintf("refund-%s", outTradeNo)
}
refund := alipay.TradeRefund{
OutTradeNo: outTradeNo,
RefundAmount: lzUtils.ToAlipayAmount(refundAmount),
OutRequestNo: fmt.Sprintf("refund-%s", outTradeNo),
OutRequestNo: outRequestNo,
}
// 发起退款请求
refundResp, err := a.AlipayClient.TradeRefund(ctx, refund)
refundResp, err := client.TradeRefund(ctx, refund)
if err != nil {
return nil, fmt.Errorf("支付宝退款请求错误:%v", err)
}
return refundResp, nil
}
// HandleAliPaymentNotification 支付宝支付回调
func (a *AliPayService) HandleAliPaymentNotification(r *http.Request) (*alipay.Notification, error) {
// 解析表单
err := r.ParseForm()
if err != nil {
return nil, fmt.Errorf("解析请求表单失败:%v", err)
}
// HandleAliPaymentNotification 支付宝支付回调验签。
// 由上层根据 out_trade_no 查出订单并传入对应商户标识 merchant。
func (a *AliPayService) HandleAliPaymentNotification(merchant string, form url.Values) (*alipay.Notification, error) {
client := a.clientForMerchant(merchant, nil)
// 解析并验证通知DecodeNotification 会自动验证签名
notification, err := a.AlipayClient.DecodeNotification(r.Form)
notification, err := client.DecodeNotification(form)
if err != nil {
return nil, fmt.Errorf("验证签名失败: %v", err)
}
return notification, nil
}
func (a *AliPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*alipay.TradeQueryRsp, error) {
// QueryOrderStatus 按商户标识查询支付宝订单状态
func (a *AliPayService) QueryOrderStatus(ctx context.Context, merchant string, outTradeNo string) (*alipay.TradeQueryRsp, error) {
client := a.clientForMerchant(merchant, nil)
queryRequest := alipay.TradeQuery{
OutTradeNo: outTradeNo,
}
// 发起查询请求
resp, err := a.AlipayClient.TradeQuery(ctx, queryRequest)
resp, err := client.TradeQuery(ctx, queryRequest)
if err != nil {
return nil, fmt.Errorf("查询支付宝订单失败: %v", err)
}

View File

@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -13,6 +14,7 @@ import (
"tyc-server/app/main/api/internal/config"
tianyuanapi "tyc-server/app/main/api/internal/service/tianyuanapi_sdk"
"tyc-server/app/main/model"
"tyc-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/tidwall/gjson"
@@ -49,6 +51,16 @@ func NewApiRequestService(c config.Config, featureModel model.FeatureModel, prod
}
}
// keysOfMap 返回 map 的 key 列表,便于 debug 日志展示参数结构而不是完整明文
func keysOfMap(m map[string]interface{}) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
type APIResponseData struct {
ApiID string `json:"apiID"`
Data json.RawMessage `json:"data"` // 这里用 RawMessage 来存储原始的 data
@@ -58,7 +70,8 @@ type APIResponseData struct {
}
// ProcessRequests 处理请求
func (a *ApiRequestService) ProcessRequests(params []byte, productID int64) ([]APIResponseData, error) {
// orderNo: 当前查询对应的订单号用于为异步车辆类接口生成回调地址return_url
func (a *ApiRequestService) ProcessRequests(params []byte, productID int64, orderNo string) ([]APIResponseData, error) {
var ctx, cancel = context.WithCancel(context.Background())
defer cancel()
build := a.productFeatureModel.SelectBuilder().Where(squirrel.Eq{
@@ -85,6 +98,27 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID int64) ([]A
if len(featureList) == 0 {
return nil, errors.New("处理请求错误,产品无对应接口功能")
}
// 在原始 params 上附加 order_no供异步车辆类接口自动生成回调地址使用
var baseParams map[string]interface{}
if err := json.Unmarshal(params, &baseParams); err != nil {
logx.Errorf("解析查询参数失败, Params: %s, Error: %v", string(params), err)
return nil, fmt.Errorf("解析查询参数失败: %w", err)
}
// 参数转换:将 mobile 转换为 mobile_no
if mobile, exists := baseParams["mobile"]; exists {
baseParams["mobile_no"] = mobile
}
if orderNo != "" {
baseParams["order_no"] = orderNo
}
paramsWithOrder, err := json.Marshal(baseParams)
if err != nil {
logx.Errorf("序列化查询参数失败, Params: %s, Error: %v", string(params), err)
return nil, fmt.Errorf("序列化查询参数失败: %w", err)
}
var (
wg sync.WaitGroup
resultsCh = make(chan APIResponseData, len(featureList))
@@ -118,7 +152,7 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID int64) ([]A
tryCount := 0
for {
tryCount++
resp, preprocessErr = a.PreprocessRequestApi(params, feature.ApiId)
resp, preprocessErr = a.PreprocessRequestApi(paramsWithOrder, feature.ApiId)
if preprocessErr == nil {
break
}
@@ -129,6 +163,9 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID int64) ([]A
}
}
if preprocessErr != nil {
// 在这里添加日志记录入参
logx.Errorf("API请求处理器失败, ApiID: %s, Params: %s, Error: %v", feature.ApiId, string(paramsWithOrder), preprocessErr)
result.Timestamp = timestamp
result.Error = preprocessErr.Error()
result.Data = resp
@@ -175,6 +212,9 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
"BehaviorRiskScan": (*ApiRequestService).ProcessBehaviorRiskScanRequest,
"YYSYBE08": (*ApiRequestService).ProcessYYSYBE08Request,
"YYSY09CD": (*ApiRequestService).ProcessYYSY09CDRequest,
"QCXGGB2Q": (*ApiRequestService).ProcessQCXGGB2QRequest,
"QCXGYTS2": (*ApiRequestService).ProcessQCXGYTS2Request,
"QCXG5F3A": (*ApiRequestService).ProcessQCXG5F3ARequest,
"FLXG0687": (*ApiRequestService).ProcessFLXG0687Request,
"FLXG3D56": (*ApiRequestService).ProcessFLXG3D56Request,
"FLXG0V4B": (*ApiRequestService).ProcesFLXG0V4BRequest,
@@ -197,6 +237,39 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
"IVYZ3P9M": (*ApiRequestService).ProcessIVYZ3P9MRequest,
"FLXG7E8F": (*ApiRequestService).ProcessFLXG7E8FRequest,
"QCXG9P1C": (*ApiRequestService).ProcessQCXG9P1CFRequest,
// 车辆类接口(传参后续配置,先透传缓存 params
"QCXG4D2E": (*ApiRequestService).ProcessQCXG4D2ERequest,
"QCXG5U0Z": (*ApiRequestService).ProcessQCXG5U0ZRequest,
"QCXG1U4U": (*ApiRequestService).ProcessQCXG1U4URequest,
"QCXGY7F2": (*ApiRequestService).ProcessQCXGY7F2Request,
"QCXG1H7Y": (*ApiRequestService).ProcessQCXG1H7YRequest,
"QCXG4I1Z": (*ApiRequestService).ProcessQCXG4I1ZRequest,
"QCXG3Y6B": (*ApiRequestService).ProcessQCXG3Y6BRequest,
"QCXG3Z3L": (*ApiRequestService).ProcessQCXG3Z3LRequest,
"QCXGP00W": (*ApiRequestService).ProcessQCXGP00WRequest,
"QCXG6B4E": (*ApiRequestService).ProcessQCXG6B4ERequest,
// 核验工具verify feature.md
"IVYZ9K7F": (*ApiRequestService).ProcessIVYZ9K7FRequest,
"IVYZA1B3": (*ApiRequestService).ProcessIVYZA1B3Request,
"IVYZ6M8P": (*ApiRequestService).ProcessIVYZ6M8PRequest,
"JRZQ8B3C": (*ApiRequestService).ProcessJRZQ8B3CRequest,
"YYSY3M8S": (*ApiRequestService).ProcessYYSY3M8SRequest,
"YYSYK9R4": (*ApiRequestService).ProcessYYSYK9R4Request,
"YYSYF2T7": (*ApiRequestService).ProcessYYSYF2T7Request,
"YYSYK8R3": (*ApiRequestService).ProcessYYSYK8R3Request,
"YYSYS9W1": (*ApiRequestService).ProcessYYSYS9W1Request,
"YYSYE7V5": (*ApiRequestService).ProcessYYSYE7V5Request,
"YYSYP0T4": (*ApiRequestService).ProcessYYSYP0T4Request,
"YYSY6F2B": (*ApiRequestService).ProcessYYSY6F2BRequest,
"YYSY9E4A": (*ApiRequestService).ProcessYYSY9E4ARequest,
"QYGL5F6A": (*ApiRequestService).ProcessQYGL5F6ARequest,
"JRZQACAB": (*ApiRequestService).ProcessJRZQACABRequest,
"JRZQ0B6Y": (*ApiRequestService).ProcessJRZQ0B6YRequest,
// 新增司法涉诉类接口
"QYGL66SL": (*ApiRequestService).ProcessQYGL66SLRequest,
"FLXG3A9B": (*ApiRequestService).ProcessFLXG3A9BRequest,
"QYGL2S0W": (*ApiRequestService).ProcessQYGL2S0WRequest,
"FLXGDEA9": (*ApiRequestService).ProcessFLXGDEA9Request,
}
// PreprocessRequestApi 调用指定的请求处理函数
@@ -1104,6 +1177,382 @@ func (a *ApiRequestService) ProcessQYGL6F2DRequest(params []byte) ([]byte, error
return nil, fmt.Errorf("响应code错误%s", code.String())
}
// ProcessQCXGGB2QRequest 人车核验简版
func (a *ApiRequestService) ProcessQCXGGB2QRequest(params []byte) ([]byte, error) {
plateNo := gjson.GetBytes(params, "plate_no")
carplateType := gjson.GetBytes(params, "carplate_type")
name := gjson.GetBytes(params, "name")
if !plateNo.Exists() || !carplateType.Exists() || !name.Exists() {
return nil, errors.New("api请求, QCXGGB2Q, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXGGB2Q", map[string]interface{}{
"plate_no": plateNo.String(),
"carplate_type": carplateType.String(),
"name": name.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessQCXGYTS2Request 人车核验详版
func (a *ApiRequestService) ProcessQCXGYTS2Request(params []byte) ([]byte, error) {
plateNo := gjson.GetBytes(params, "plate_no")
carplateType := gjson.GetBytes(params, "carplate_type")
name := gjson.GetBytes(params, "name")
if !plateNo.Exists() || !carplateType.Exists() || !name.Exists() {
return nil, errors.New("api请求, QCXGYTS2, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXGYTS2", map[string]interface{}{
"plate_no": plateNo.String(),
"carplate_type": carplateType.String(),
"name": name.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessQCXG5F3ARequest 名下车辆(车牌)
func (a *ApiRequestService) ProcessQCXG5F3ARequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card")
name := gjson.GetBytes(params, "name")
if !idCard.Exists() || !name.Exists() {
return nil, errors.New("api请求, QCXG5F3A, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXG5F3A", map[string]interface{}{
"id_card": idCard.String(),
"name": name.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// processVehicleApiPassThrough 车辆类接口通用透传:将缓存 params 原样传给天远(传参后续按接口文档再细化)
func (a *ApiRequestService) processVehicleApiPassThrough(params []byte, apiID string) ([]byte, error) {
var m map[string]interface{}
if err := json.Unmarshal(params, &m); err != nil {
return nil, fmt.Errorf("api请求, %s, 解析参数失败: %w", apiID, err)
}
logx.Infof("vehicle api passthrough, api_id=%s, params_keys=%v", apiID, keysOfMap(m))
resp, err := a.tianyuanapi.CallInterface(apiID, m)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// 车辆 API 按 md 从缓存 params 提取字段并调用天远
func (a *ApiRequestService) ProcessQCXG4D2ERequest(params []byte) ([]byte, error) {
m := map[string]interface{}{}
if err := json.Unmarshal(params, &m); err != nil {
return nil, fmt.Errorf("api请求, QCXG4D2E, 解析参数失败: %w", err)
}
body := map[string]interface{}{}
if v, ok := m["user_type"].(string); ok && v != "" {
body["user_type"] = v
} else {
body["user_type"] = "1"
}
if v, ok := m["id_card"].(string); ok {
body["id_card"] = v
} else {
return nil, errors.New("api请求, QCXG4D2E, 缺少 id_card")
}
resp, err := a.tianyuanapi.CallInterface("QCXG4D2E", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG5U0ZRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
if !vin.Exists() || vin.String() == "" {
return nil, errors.New("api请求, QCXG5U0Z, 缺少 vin_code")
}
resp, err := a.tianyuanapi.CallInterface("QCXG5U0Z", map[string]interface{}{"vin_code": vin.String()})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG1U4URequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "image_url"}, nil)
orderNo := gjson.GetBytes(params, "order_no").String()
if body["vin_code"] == nil || body["image_url"] == nil || orderNo == "" {
return nil, errors.New("api请求, QCXG1U4U, 缺少必填参数 vin_code/image_url/order_no")
}
logx.Infof("vehicle api request QCXG1U4U, order_no=%s, vin_code=%v, image_url=%v", orderNo, body["vin_code"], body["image_url"])
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG1U4U")
resp, err := a.tianyuanapi.CallInterface("QCXG1U4U", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXGY7F2Request(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "vehicle_location", "first_registrationdate"}, nil)
if body["vin_code"] == nil || body["vehicle_location"] == nil || body["first_registrationdate"] == nil {
return nil, errors.New("api请求, QCXGY7F2, 缺少必填参数 vin_code/vehicle_location/first_registrationdate")
}
logx.Infof("vehicle api request QCXGY7F2, vin_code=%v, vehicle_location=%v, first_registrationdate=%v", body["vin_code"], body["vehicle_location"], body["first_registrationdate"])
resp, err := a.tianyuanapi.CallInterface("QCXGY7F2", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG1H7YRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
plate := gjson.GetBytes(params, "car_license")
if !vin.Exists() || vin.String() == "" || !plate.Exists() || plate.String() == "" {
return nil, errors.New("api请求, QCXG1H7Y, 缺少 vin_code 或 car_license")
}
// 天远侧字段为 vin_code + plate_no这里将前端 car_license 映射为 plate_no
body := map[string]interface{}{
"vin_code": vin.String(),
"plate_no": plate.String(),
}
logx.Infof("vehicle api request QCXG1H7Y, vin_code=%s, plate_no=%s", vin.String(), plate.String())
resp, err := a.tianyuanapi.CallInterface("QCXG1H7Y", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG4I1ZRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
if !vin.Exists() || vin.String() == "" {
return nil, errors.New("api请求, QCXG4I1Z, 缺少 vin_code")
}
resp, err := a.tianyuanapi.CallInterface("QCXG4I1Z", map[string]interface{}{"vin_code": vin.String()})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG3Y6BRequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code"}, nil)
orderNo := gjson.GetBytes(params, "order_no").String()
if body["vin_code"] == nil || orderNo == "" {
return nil, errors.New("api请求, QCXG3Y6B, 缺少必填参数 vin_code/order_no")
}
logx.Infof("vehicle api request QCXG3Y6B, order_no=%s, vin_code=%v", orderNo, body["vin_code"])
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG3Y6B")
resp, err := a.tianyuanapi.CallInterface("QCXG3Y6B", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG3Z3LRequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code"}, nil)
orderNo := gjson.GetBytes(params, "order_no").String()
if body["vin_code"] == nil || orderNo == "" {
return nil, errors.New("api请求, QCXG3Z3L, 缺少必填参数 vin_code/order_no")
}
logx.Infof("vehicle api request QCXG3Z3L, order_no=%s, vin_code=%v", orderNo, body["vin_code"])
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG3Z3L")
resp, err := a.tianyuanapi.CallInterface("QCXG3Z3L", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXGP00WRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
orderNo := gjson.GetBytes(params, "order_no").String()
vlphoto := gjson.GetBytes(params, "vlphoto_data")
if !vin.Exists() || vin.String() == "" || orderNo == "" || !vlphoto.Exists() || vlphoto.String() == "" {
return nil, errors.New("api请求, QCXGP00W, 缺少必填参数 vin_code/order_no/vlphoto_data")
}
logx.Infof("vehicle api request QCXGP00W, order_no=%s, vin_code=%s, vlphoto_data_len=%d", orderNo, vin.String(), len(vlphoto.String()))
key, err := hex.DecodeString(a.config.Encrypt.SecretKey)
if err != nil {
return nil, fmt.Errorf("api请求, QCXGP00W, 密钥解析失败: %w", err)
}
encData, err := crypto.AesEncrypt([]byte(vlphoto.String()), key)
if err != nil {
return nil, fmt.Errorf("api请求, QCXGP00W, 加密行驶证数据失败: %w", err)
}
resp, err := a.tianyuanapi.CallInterface("QCXGP00W", map[string]interface{}{
"vin_code": vin.String(),
"return_url": a.buildVehicleCallbackURL(orderNo, "QCXGP00W"),
"data": encData,
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG6B4ERequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
if !vin.Exists() || vin.String() == "" {
return nil, errors.New("api请求, QCXG6B4E, 缺少 vin_code")
}
auth := gjson.GetBytes(params, "authorized").String()
if auth == "" {
auth = "1"
}
// 天远文档字段名为 VINCode、Authorized
resp, err := a.tianyuanapi.CallInterface("QCXG6B4E", map[string]interface{}{
"vin_code": vin.String(),
"authorized": auth,
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// processVerifyPassThrough 核验类接口:缓存 params 已含 mobile_no/id_card/name 等,原样传天远
func (a *ApiRequestService) processVerifyPassThrough(params []byte, apiID string) ([]byte, error) {
var m map[string]interface{}
if err := json.Unmarshal(params, &m); err != nil {
return nil, fmt.Errorf("api请求, %s, 解析参数失败: %w", apiID, err)
}
logx.Infof("verify api passthrough, api_id=%s, params_keys=%v", apiID, keysOfMap(m))
resp, err := a.tianyuanapi.CallInterface(apiID, m)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessIVYZ9K7FRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "IVYZ9K7F")
}
func (a *ApiRequestService) ProcessIVYZA1B3Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "IVYZA1B3")
}
func (a *ApiRequestService) ProcessIVYZ6M8PRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "IVYZ6M8P")
}
func (a *ApiRequestService) ProcessJRZQ8B3CRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "JRZQ8B3C")
}
func (a *ApiRequestService) ProcessYYSY3M8SRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSY3M8S")
}
func (a *ApiRequestService) ProcessYYSYK9R4Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSYK9R4")
}
func (a *ApiRequestService) ProcessYYSYF2T7Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSYF2T7")
}
func (a *ApiRequestService) ProcessYYSYK8R3Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSYK8R3")
}
func (a *ApiRequestService) ProcessYYSYS9W1Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSYS9W1")
}
func (a *ApiRequestService) ProcessYYSYE7V5Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSYE7V5")
}
func (a *ApiRequestService) ProcessYYSYP0T4Request(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSYP0T4")
}
func (a *ApiRequestService) ProcessYYSY6F2BRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSY6F2B")
}
func (a *ApiRequestService) ProcessYYSY9E4ARequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "YYSY9E4A")
}
func (a *ApiRequestService) ProcessQYGL5F6ARequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "QYGL5F6A")
}
func (a *ApiRequestService) ProcessJRZQACABRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "JRZQACAB")
}
func (a *ApiRequestService) ProcessJRZQ0B6YRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "JRZQ0B6Y")
}
// QYGL66SL 企业司法涉诉(简版)
func (a *ApiRequestService) ProcessQYGL66SLRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "QYGL66SL")
}
// FLXG3A9B 限高被执行人
func (a *ApiRequestService) ProcessFLXG3A9BRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "FLXG3A9B")
}
// QYGL2S0W 失信被执行人
func (a *ApiRequestService) ProcessQYGL2S0WRequest(params []byte) ([]byte, error) {
return a.processVerifyPassThrough(params, "QYGL2S0W")
}
// FLXGDEA9 本人不良
func (a *ApiRequestService) ProcessFLXGDEA9Request(params []byte) ([]byte, error) {
var m map[string]interface{}
if err := json.Unmarshal(params, &m); err != nil {
return nil, fmt.Errorf("api请求, FLXGDEA9, 解析参数失败: %w", err)
}
// 授权由后端默认传 1前端与查询服务不再感知 authorized
if v, ok := m["authorized"]; !ok || v == "" {
m["authorized"] = "1"
}
resp, err := a.tianyuanapi.CallInterface("FLXGDEA9", m)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// buildVehicleBody 从 params 中取 required 与 optional 键,仅非空才写入 body
func buildVehicleBody(params []byte, required, optional []string) map[string]interface{} {
body := make(map[string]interface{})
for _, k := range required {
v := gjson.GetBytes(params, k)
if v.Exists() {
body[k] = v.String()
}
}
for _, k := range optional {
v := gjson.GetBytes(params, k)
if v.Exists() && v.String() != "" {
body[k] = v.String()
}
}
return body
}
// buildVehicleCallbackURL 生成车辆类接口的异步回调地址
// 使用 PublicBaseURL 作为对外域名配置,路径固定为 /api/v1/tianyuan/vehicle/callback
// 并通过查询参数携带 order_no 与 api_id 以便后端识别具体查询与模块。
func (a *ApiRequestService) buildVehicleCallbackURL(orderNo, apiID string) string {
base := strings.TrimRight(a.config.PublicBaseURL, "/")
if base == "" {
// 兜底:如果未配置 URLDomain则使用相对路径交给网关/部署层补全域名
return fmt.Sprintf("/api/v1/tianyuan/vehicle/callback?order_no=%s&api_id=%s", orderNo, apiID)
}
return fmt.Sprintf("%s/api/v1/tianyuan/vehicle/callback?order_no=%s&api_id=%s", base, orderNo, apiID)
}
// ProcessQCXG7A2BRequest 名下车辆
func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card")

View File

@@ -64,6 +64,38 @@ type EntLawsuitReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
}
// EnterpriseLawsuitSimple 企业司法涉诉简版QYGL66SL仅企业名称授权信息由后端补齐
type EnterpriseLawsuitSimpleReq struct {
EntName string `json:"ent_name" validate:"required"`
}
// LimitHighExecuted 限高被执行人FLXG3A9B
type LimitHighExecutedReq struct {
Name string `json:"name" validate:"required,name"`
IDCard string `json:"id_card" validate:"required,idCard"`
Mobile string `json:"mobile" validate:"required,mobile"`
Authorized string `json:"authorized"` // 可选,后端默认 1
}
// DishonestExecuted 失信被执行人QYGL2S0W
type DishonestExecutedReq struct {
Type string `json:"type"` // 可选,后端默认 per
Name string `json:"name" validate:"required,name"`
IDCard string `json:"id_card" validate:"required,idCard"`
}
// PersonalBadRecord 本人不良FLXGDEA9姓名 + 身份证(授权由 ApiRequest 默认传 1
type PersonalBadRecordReq struct {
Name string `json:"name" validate:"required,name"`
IDCard string `json:"id_card" validate:"required,idCard"`
}
// TocPersonEnterprisePro 人企关系加强版预查询:仅身份证号
type TocPersonEnterpriseProReq struct {
IDCard string `json:"id_card" validate:"required,idCard"`
}
type TocPhoneThreeElements struct {
Name string `json:"name" validate:"required,name"`
IDCard string `json:"id_card" validate:"required,idCard"`
@@ -83,6 +115,16 @@ type TocDualMarriage struct {
NameWoman string `json:"name_woman" validate:"required,name"`
IDCardWoman string `json:"id_card_woman" validate:"required,idCard"`
}
// TocDualMarriageReq 双人婚姻状态查询(含手机号与验证码)
type TocDualMarriageReq struct {
NameMan string `json:"name_man" validate:"required,name"`
IDCardMan string `json:"id_card_man" validate:"required,idCard"`
NameWoman string `json:"name_woman" validate:"required,name"`
IDCardWoman string `json:"id_card_woman" validate:"required,idCard"`
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
}
type TocPersonVehicleVerification struct {
Name string `json:"name" validate:"required,name"`
CarType string `json:"car_type" validate:"required"`
@@ -112,8 +154,170 @@ type AgentQueryData struct {
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 名下车辆(数量) QCXG4D2E仅 user_type(1-ETC开户人 2-车辆所有人 3-ETC经办人)+id_card无验证码
type TocVehiclesUnderNameCountReq struct {
UserType string `json:"user_type"` // 必填,默认 1
IDCard string `json:"id_card" validate:"required,idCard"`
}
// 车辆静态信息/过户详版等 仅 vin_code车辆类不要求手机号与验证码
type TocVehicleVinCodeReq struct {
VinCode string `json:"vin_code" validate:"required"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆里程记录(混合) QCXG1U4U
type TocVehicleMileageMixedReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no"`
// 回调地址由后端在调用第三方接口时自动生成,不再由前端透传
ReturnURL string `json:"return_url"`
ImageURL string `json:"image_url" validate:"required"`
RegURL string `json:"reg_url"`
EngineNumber string `json:"engine_number"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 二手车VIN估值 QCXGY7F2
type TocVehicleVinValuationReq struct {
VinCode string `json:"vin_code" validate:"required"`
VehicleName string `json:"vehicle_name"`
VehicleLocation string `json:"vehicle_location" validate:"required"`
FirstRegistrationDate string `json:"first_registrationdate" validate:"required"` // yyyy-MM
Color string `json:"color"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆过户简版 QCXG1H7Y
type TocVehicleTransferSimpleReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆维保简版 QCXG3Y6B
type TocVehicleMaintenanceSimpleReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no"`
// 回调地址由后端在调用第三方接口时自动生成,不再由前端透传
ReturnURL string `json:"return_url"`
ImageURL string `json:"image_url"`
RegURL string `json:"reg_url"`
EngineNumber string `json:"engine_number"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆维保详细版 QCXG3Z3L
type TocVehicleMaintenanceDetailReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no"`
// 回调地址由后端在调用第三方接口时自动生成,不再由前端透传
ReturnURL string `json:"return_url"`
ImageURL string `json:"image_url"`
EngineNumber string `json:"engine_number"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆出险详版 QCXGP00Wvlphoto_data 加密后以 data 字段提交
type TocVehicleClaimDetailReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no"`
// 回调地址由后端在调用第三方接口时自动生成,不再由前端透传
ReturnURL string `json:"return_url"`
VlphotoData string `json:"vlphoto_data" validate:"required"` // 行驶证图片 base64加密后传 API 的 data
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆出险记录核验 QCXG6B4EVINCodeAuthorized 由后端默认传 1
type TocVehicleClaimVerifyReq struct {
VINCode string `json:"vin_code" validate:"required"`
Authorized string `json:"authorized"` // 可选,后端默认 1
Mobile string `json:"mobile"`
Code string `json:"code"`
}
type AgentIdentifier struct {
Product string `json:"product"`
AgentID int64 `json:"agent_id"`
Price string `json:"price"`
}
// --------------- 核验工具verify feature.md---------------
// 公安二要素 IVYZ9K7F请求用 mobile缓存/API 用 mobile_no
type TocVerifyAuthTwoReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"` // 前端传 mobile
IDCard string `json:"id_card" validate:"required,idCard"`
Name string `json:"name" validate:"required,name"`
}
// 公安三要素 IVYZA1B3photo_data, id_card, name
type TocVerifyAuthThreeReq struct {
PhotoData string `json:"photo_data" validate:"required"` // 人像 base64
IDCard string `json:"id_card" validate:"required,idCard"`
Name string `json:"name" validate:"required,name"`
}
// 职业资格证书 IVYZ6M8Pid_card, name
type TocVerifyCertReq struct {
IDCard string `json:"id_card" validate:"required,idCard"`
Name string `json:"name" validate:"required,name"`
}
// 个人消费能力 JRZQ8B3C
type TocVerifyConsumptionReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
IDCard string `json:"id_card" validate:"required,idCard"`
Name string `json:"name" validate:"required,name"`
}
// 运营商二要素 YYSY3M8S
type TocVerifyYysTwoReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
Name string `json:"name" validate:"required,name"`
}
// 全网手机三要素 YYSYK9R4
type TocVerifyYysThreeReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
IDCard string `json:"id_card" validate:"required,idCard"`
Name string `json:"name" validate:"required,name"`
}
// 仅手机号YYSYF2T7/YYSYK8R3/YYSYS9W1/YYSYE7V5/YYSYP0T4/YYSY9E4A
type TocVerifyMobileOnlyReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
}
// 手机消费区间 YYSY6F2B
type TocVerifyYysConsumptionReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
Authorized string `json:"authorized" validate:"required"` // 0/1
}
// 名下企业关联 QYGL5F6Aid_card 选填
type TocVerifyEntRelationReq struct {
IDCard string `json:"id_card"` // 可选
}
// 银行卡四要素 JRZQACAB
type TocVerifyBankFourReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
IDCard string `json:"id_card" validate:"required,idCard"`
BankCard string `json:"bank_card" validate:"required"`
Name string `json:"name" validate:"required,name"`
}
// 银行卡黑名单 JRZQ0B6Y
type TocVerifyBankBlackReq struct {
MobileNo string `json:"mobile" validate:"required,mobile"`
IDCard string `json:"id_card" validate:"required,idCard"`
Name string `json:"name" validate:"required,name"`
BankCard string `json:"bank_card" validate:"required"`
}

View File

@@ -950,13 +950,13 @@ type AdminUpdateOrderResp struct {
}
type AdminUpdatePlatformUserReq struct {
Id int64 `path:"id"` // 用户ID
Mobile *string `json:"mobile,optional"` // 手机号
Password *string `json:"password,optional"` // 密码
Nickname *string `json:"nickname,optional"` // 昵称
Info *string `json:"info,optional"` // 备注信息
Inside *int64 `json:"inside,optional"` // 是否内部用户 1-是 0-否
Disable *int64 `json:"disable,optional"` // 是否封禁 0-可用 1-禁用
Id int64 `path:"id"` // 用户ID
Mobile *string `json:"mobile,optional"` // 手机号
Password *string `json:"password,optional"` // 密码
Nickname *string `json:"nickname,optional"` // 昵称
Info *string `json:"info,optional"` // 备注信息
Inside *int64 `json:"inside,optional"` // 是否内部用户 1-是 0-否
Disable *int64 `json:"disable,optional"` // 是否封禁 0-可用 1-禁用
}
type AdminUpdatePlatformUserResp struct {
@@ -1547,6 +1547,10 @@ type GetCommissionResp struct {
List []Commission `json:"list"` // 查询列表
}
type GetEncryptedSceneIdResp struct {
EncryptedSceneId string `json:"encryptedSceneId"`
}
type GetLinkDataReq struct {
LinkIdentifier string `form:"link_identifier"`
}
@@ -2050,10 +2054,11 @@ type QueryRetryResp struct {
}
type QueryServiceReq struct {
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
CaptchaVerifyParam string `json:"captchaVerifyParam,optional"`
}
type QueryServiceResp struct {
@@ -2115,6 +2120,15 @@ type SaveAgentMembershipUserConfigReq struct {
PriceRatio float64 `json:"price_ratio"`
}
type ServeUploadedFileReq struct {
FileName string `path:"fileName"` // 文件名,如 uuid.jpg
}
type ServeUploadedFileResp struct {
FilePath string `json:"-"` // 内部:本地文件路径
ContentType string `json:"-"` // 内部Content-Type
}
type TimeRangeReport struct {
Commission float64 `json:"commission"` // 佣金
Report int `json:"report"` // 报告量
@@ -2170,6 +2184,14 @@ type UpdateRoleResp struct {
Success bool `json:"success"` // 是否成功
}
type UploadImageReq struct {
ImageBase64 string `json:"image_base64" validate:"required"` // 图片 base64不含 data URL 前缀)
}
type UploadImageResp struct {
Url string `json:"url"` // 可公网访问的图片 URL
}
type User struct {
Id int64 `json:"id"`
Mobile string `json:"mobile"`
@@ -2228,6 +2250,7 @@ type GetAppVersionResp struct {
}
type SendSmsReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
CaptchaVerifyParam string `json:"captchaVerifyParam"`
}

View File

@@ -10,8 +10,6 @@ import (
"time"
"tyc-server/common/globalkey"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/builder"
@@ -19,6 +17,7 @@ import (
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
"tyc-server/common/globalkey"
)
var (
@@ -65,6 +64,7 @@ type (
LevelName string `db:"level_name"` // 会员级别,如 VIPSVIPnormal
Amount float64 `db:"amount"` // 充值金额
PaymentMethod string `db:"payment_method"` // 支付方式:支付宝,微信,苹果支付,其他
PaymentMerchant string `db:"payment_merchant"` // 支付商户标识,例如 one/two
OrderNo string `db:"order_no"` // 交易号
PlatformOrderId sql.NullString `db:"platform_order_id"` // 支付平台订单号
Status string `db:"status"`
@@ -89,11 +89,11 @@ func (m *defaultAgentMembershipRechargeOrderModel) Insert(ctx context.Context, s
tycAgentMembershipRechargeOrderOrderNoKey := fmt.Sprintf("%s%v", cacheTycAgentMembershipRechargeOrderOrderNoPrefix, data.OrderNo)
tycAgentMembershipRechargeOrderPlatformOrderIdKey := fmt.Sprintf("%s%v", cacheTycAgentMembershipRechargeOrderPlatformOrderIdPrefix, data.PlatformOrderId)
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentMembershipRechargeOrderRowsExpectAutoSet)
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentMembershipRechargeOrderRowsExpectAutoSet)
if session != nil {
return session.ExecCtx(ctx, query, data.UserId, data.AgentId, data.LevelName, data.Amount, data.PaymentMethod, data.OrderNo, data.PlatformOrderId, data.Status, data.DeleteTime, data.DelState, data.Version)
return session.ExecCtx(ctx, query, data.UserId, data.AgentId, data.LevelName, data.Amount, data.PaymentMethod, data.PaymentMerchant, data.OrderNo, data.PlatformOrderId, data.Status, data.DeleteTime, data.DelState, data.Version)
}
return conn.ExecCtx(ctx, query, data.UserId, data.AgentId, data.LevelName, data.Amount, data.PaymentMethod, data.OrderNo, data.PlatformOrderId, data.Status, data.DeleteTime, data.DelState, data.Version)
return conn.ExecCtx(ctx, query, data.UserId, data.AgentId, data.LevelName, data.Amount, data.PaymentMethod, data.PaymentMerchant, data.OrderNo, data.PlatformOrderId, data.Status, data.DeleteTime, data.DelState, data.Version)
}, tycAgentMembershipRechargeOrderIdKey, tycAgentMembershipRechargeOrderOrderNoKey, tycAgentMembershipRechargeOrderPlatformOrderIdKey)
}
@@ -165,9 +165,9 @@ func (m *defaultAgentMembershipRechargeOrderModel) Update(ctx context.Context, s
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentMembershipRechargeOrderRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
return session.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.PaymentMerchant, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}
return conn.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
return conn.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.PaymentMerchant, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}, tycAgentMembershipRechargeOrderIdKey, tycAgentMembershipRechargeOrderOrderNoKey, tycAgentMembershipRechargeOrderPlatformOrderIdKey)
}
@@ -189,9 +189,9 @@ func (m *defaultAgentMembershipRechargeOrderModel) UpdateWithVersion(ctx context
sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentMembershipRechargeOrderRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
return session.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.PaymentMerchant, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
}
return conn.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
return conn.ExecCtx(ctx, query, newData.UserId, newData.AgentId, newData.LevelName, newData.Amount, newData.PaymentMethod, newData.PaymentMerchant, newData.OrderNo, newData.PlatformOrderId, newData.Status, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
}, tycAgentMembershipRechargeOrderIdKey, tycAgentMembershipRechargeOrderOrderNoKey, tycAgentMembershipRechargeOrderPlatformOrderIdKey)
if err != nil {
return err

View File

@@ -10,8 +10,6 @@ import (
"time"
"tyc-server/common/globalkey"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/builder"
@@ -19,6 +17,7 @@ import (
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
"tyc-server/common/globalkey"
)
var (
@@ -63,6 +62,7 @@ type (
ProductId int64 `db:"product_id"` // 产品ID软关联到产品表
PaymentPlatform string `db:"payment_platform"` // 支付平台(支付宝、微信、苹果内购、其他)
PaymentScene string `db:"payment_scene"` // 支付场景App、H5、微信小程序、公众号
PaymentMerchant string `db:"payment_merchant"` // 支付商户标识,例如 one/two
PlatformOrderId sql.NullString `db:"platform_order_id"` // 支付平台订单号
Amount float64 `db:"amount"` // 支付金额
Status string `db:"status"` // 支付状态
@@ -90,11 +90,11 @@ func (m *defaultOrderModel) Insert(ctx context.Context, session sqlx.Session, da
tycOrderIdKey := fmt.Sprintf("%s%v", cacheTycOrderIdPrefix, data.Id)
tycOrderOrderNoKey := fmt.Sprintf("%s%v", cacheTycOrderOrderNoPrefix, data.OrderNo)
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, orderRowsExpectAutoSet)
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, orderRowsExpectAutoSet)
if session != nil {
return session.ExecCtx(ctx, query, data.OrderNo, data.UserId, data.ProductId, data.PaymentPlatform, data.PaymentScene, data.PlatformOrderId, data.Amount, data.Status, data.DelState, data.Version, data.PayTime, data.RefundTime, data.CloseTime, data.DeleteTime, data.SalesCost)
return session.ExecCtx(ctx, query, data.OrderNo, data.UserId, data.ProductId, data.PaymentPlatform, data.PaymentScene, data.PaymentMerchant, data.PlatformOrderId, data.Amount, data.Status, data.DelState, data.Version, data.PayTime, data.RefundTime, data.CloseTime, data.DeleteTime, data.SalesCost)
}
return conn.ExecCtx(ctx, query, data.OrderNo, data.UserId, data.ProductId, data.PaymentPlatform, data.PaymentScene, data.PlatformOrderId, data.Amount, data.Status, data.DelState, data.Version, data.PayTime, data.RefundTime, data.CloseTime, data.DeleteTime, data.SalesCost)
return conn.ExecCtx(ctx, query, data.OrderNo, data.UserId, data.ProductId, data.PaymentPlatform, data.PaymentScene, data.PaymentMerchant, data.PlatformOrderId, data.Amount, data.Status, data.DelState, data.Version, data.PayTime, data.RefundTime, data.CloseTime, data.DeleteTime, data.SalesCost)
}, tycOrderIdKey, tycOrderOrderNoKey)
}
@@ -145,9 +145,9 @@ func (m *defaultOrderModel) Update(ctx context.Context, session sqlx.Session, ne
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, orderRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id)
return session.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PaymentMerchant, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id)
}
return conn.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id)
return conn.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PaymentMerchant, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id)
}, tycOrderIdKey, tycOrderOrderNoKey)
}
@@ -168,9 +168,9 @@ func (m *defaultOrderModel) UpdateWithVersion(ctx context.Context, session sqlx.
sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, orderRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id, oldVersion)
return session.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PaymentMerchant, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id, oldVersion)
}
return conn.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id, oldVersion)
return conn.ExecCtx(ctx, query, newData.OrderNo, newData.UserId, newData.ProductId, newData.PaymentPlatform, newData.PaymentScene, newData.PaymentMerchant, newData.PlatformOrderId, newData.Amount, newData.Status, newData.DelState, newData.Version, newData.PayTime, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.SalesCost, newData.Id, oldVersion)
}, tycOrderIdKey, tycOrderOrderNoKey)
if err != nil {
return err

View File

@@ -10,8 +10,6 @@ import (
"time"
"tyc-server/common/globalkey"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/builder"
@@ -19,6 +17,7 @@ import (
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
"tyc-server/common/globalkey"
)
var (
@@ -64,6 +63,7 @@ type (
OrderId int64 `db:"order_id"` // 关联的订单ID
UserId int64 `db:"user_id"` // 用户ID
ProductId int64 `db:"product_id"` // 产品ID
PaymentMerchant string `db:"payment_merchant"` // 退款对应的支付商户标识,例如 one/two
PlatformRefundId sql.NullString `db:"platform_refund_id"` // 支付平台退款单号
RefundAmount float64 `db:"refund_amount"` // 退款金额
RefundReason sql.NullString `db:"refund_reason"` // 退款原因
@@ -91,11 +91,11 @@ func (m *defaultOrderRefundModel) Insert(ctx context.Context, session sqlx.Sessi
tycOrderRefundPlatformRefundIdKey := fmt.Sprintf("%s%v", cacheTycOrderRefundPlatformRefundIdPrefix, data.PlatformRefundId)
tycOrderRefundRefundNoKey := fmt.Sprintf("%s%v", cacheTycOrderRefundRefundNoPrefix, data.RefundNo)
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, orderRefundRowsExpectAutoSet)
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, orderRefundRowsExpectAutoSet)
if session != nil {
return session.ExecCtx(ctx, query, data.RefundNo, data.OrderId, data.UserId, data.ProductId, data.PlatformRefundId, data.RefundAmount, data.RefundReason, data.Status, data.DelState, data.Version, data.RefundTime, data.CloseTime, data.DeleteTime)
return session.ExecCtx(ctx, query, data.RefundNo, data.OrderId, data.UserId, data.ProductId, data.PaymentMerchant, data.PlatformRefundId, data.RefundAmount, data.RefundReason, data.Status, data.DelState, data.Version, data.RefundTime, data.CloseTime, data.DeleteTime)
}
return conn.ExecCtx(ctx, query, data.RefundNo, data.OrderId, data.UserId, data.ProductId, data.PlatformRefundId, data.RefundAmount, data.RefundReason, data.Status, data.DelState, data.Version, data.RefundTime, data.CloseTime, data.DeleteTime)
return conn.ExecCtx(ctx, query, data.RefundNo, data.OrderId, data.UserId, data.ProductId, data.PaymentMerchant, data.PlatformRefundId, data.RefundAmount, data.RefundReason, data.Status, data.DelState, data.Version, data.RefundTime, data.CloseTime, data.DeleteTime)
}, tycOrderRefundIdKey, tycOrderRefundPlatformRefundIdKey, tycOrderRefundRefundNoKey)
}
@@ -167,9 +167,9 @@ func (m *defaultOrderRefundModel) Update(ctx context.Context, session sqlx.Sessi
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, orderRefundRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id)
return session.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PaymentMerchant, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id)
}
return conn.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id)
return conn.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PaymentMerchant, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id)
}, tycOrderRefundIdKey, tycOrderRefundPlatformRefundIdKey, tycOrderRefundRefundNoKey)
}
@@ -191,9 +191,9 @@ func (m *defaultOrderRefundModel) UpdateWithVersion(ctx context.Context, session
sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, orderRefundRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id, oldVersion)
return session.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PaymentMerchant, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id, oldVersion)
}
return conn.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id, oldVersion)
return conn.ExecCtx(ctx, query, newData.RefundNo, newData.OrderId, newData.UserId, newData.ProductId, newData.PaymentMerchant, newData.PlatformRefundId, newData.RefundAmount, newData.RefundReason, newData.Status, newData.DelState, newData.Version, newData.RefundTime, newData.CloseTime, newData.DeleteTime, newData.Id, oldVersion)
}, tycOrderRefundIdKey, tycOrderRefundPlatformRefundIdKey, tycOrderRefundRefundNoKey)
if err != nil {
return err

View File

@@ -5,7 +5,7 @@ $DB_URL = "tyc:5vg67b3UNHu8@tcp(127.0.0.1:22001)/tyc"
$OUTPUT_DIR = "./model"
$TEMPLATE_DIR = "../template"
# 表名列表
# 表名列表(按需开启)
$tables = @(
# "agent"
# "agent_active_stat",
@@ -16,7 +16,7 @@ $tables = @(
# "agent_commission_deduction"
# "agent_link",
# "agent_membership_config",
# "agent_membership_recharge_order"
"agent_membership_recharge_order"
# "agent_membership_user_config",
# "agent_order",
# "agent_platform_deduction"
@@ -29,15 +29,15 @@ $tables = @(
# "agent_withdrawal_tax_exemption"
# "feature"
# "global_notifications"
# "order"
# "order_refund"
"order"
"order_refund"
# "product",
# "product_feature"
# "query",
# "query_cleanup_log"
# "query_cleanup_detail"
# "query_cleanup_config"
"user"
# "user"
# "user_auth"
# "user_temp"
# "example"
@@ -55,7 +55,6 @@ $tables = @(
# "admin_promotion_link_stats_history"
# "admin_promotion_order"
# "query_user_record"
)
# 为每个表生成模型

90
deploy/script/m.sql Normal file
View File

@@ -0,0 +1,90 @@
-- =========================
-- 1. 表结构变更:新增 payment_merchant
-- =========================
-- 1.1 order 表:增加支付商户标识
ALTER TABLE `order`
ADD COLUMN `payment_merchant` varchar(64) NOT NULL DEFAULT '' COMMENT '支付商户标识,例如 one/two' AFTER `payment_scene`;
-- 1.2 order_refund 表:增加支付商户标识
ALTER TABLE `order_refund`
ADD COLUMN `payment_merchant` varchar(64) NOT NULL DEFAULT '' COMMENT '退款对应的支付商户标识,例如 one/two' AFTER `product_id`;
-- 1.3 agent_membership_recharge_order 表:增加支付商户标识
ALTER TABLE `agent_membership_recharge_order`
ADD COLUMN `payment_merchant` varchar(64) NOT NULL DEFAULT '' COMMENT '支付商户标识,例如 one/two' AFTER `payment_method`;
-- =========================
-- 2. 历史数据初始化one / two
-- 约定:
-- - one当前主支付宝商户
-- - two当前 bak 支付宝商户
-- 时间区间:
-- [2026-01-25 16:38:17, 2026-02-02 18:26:00)
-- =========================
-- 2.1 order 表:按时间区间映射 one / two
-- 仅处理支付宝订单payment_platform='alipay'
-- 区间内用 two其余用 one
-- 时间优先用 pay_timepay_time 为空则用 create_time
-- 2.1.1 全部支付宝订单默认标记为 one
UPDATE `order`
SET
payment_merchant = 'one'
WHERE
payment_platform = 'alipay'
AND del_state = 0;
-- 2.1.2 区间内的支付宝订单标记为 two
UPDATE `order`
SET
payment_merchant = 'two'
WHERE
payment_platform = 'alipay'
AND del_state = 0
AND (
(
pay_time IS NOT NULL
AND pay_time >= '2026-01-25 16:38:17'
AND pay_time < '2026-02-02 18:26:00'
)
OR (
pay_time IS NULL
AND create_time >= '2026-01-25 16:38:17'
AND create_time < '2026-02-02 18:26:00'
)
);
-- 2.2 agent_membership_recharge_order 表:按创建时间映射 one / two
-- 仅处理支付宝支付payment_method='alipay'
-- 区间内创建的订单标记为 two其余为 one
-- 2.2.1 所有支付宝代理会员充值订单默认标记为 one
UPDATE `agent_membership_recharge_order`
SET
payment_merchant = 'one'
WHERE
payment_method = 'alipay'
AND del_state = 0;
-- 2.2.2 区间内的支付宝代理会员订单标记为 two
UPDATE `agent_membership_recharge_order`
SET
payment_merchant = 'two'
WHERE
payment_method = 'alipay'
AND del_state = 0
AND create_time >= '2026-01-25 16:38:17'
AND create_time < '2026-02-02 18:26:00';
-- 2.3 order_refund 表:跟随对应订单的 payment_merchant
-- 直接复制 order.payment_merchant避免逻辑重复
UPDATE `order_refund` r
JOIN `order` o ON r.order_id = o.id
SET
r.payment_merchant = o.payment_merchant
WHERE
r.del_state = 0
AND o.del_state = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
-- 司法涉诉、婚姻状况等二级分类小项产品(与前端 inquireCategories 配置对应)
-- 执行前请确认 id 不与现有 product 表冲突;若已有 31+ 可改为更大起始 id 或去掉 id 让其自增
INSERT INTO `product` (`id`, `create_time`, `update_time`, `delete_time`, `del_state`, `version`, `product_name`, `product_en`, `description`, `notes`, `cost_price`, `sell_price`) VALUES
(31, NOW(), NOW(), NULL, 0, 0, '企业司法涉诉', 'toc_EnterpriseLawsuit', '<p>企业司法涉诉服务,可查询企业涉诉与执行信息,帮助您评估企业法律风险。</p>', NULL, 0.50, 28.80),
(32, NOW(), NOW(), NULL, 0, 0, '被执行人', 'toc_ExecutedPerson', '<p>被执行人信息查询,帮助您了解个人或企业是否被列为被执行人及其执行案件情况。</p>', NULL, 0.36, 18.80),
(33, NOW(), NOW(), NULL, 0, 0, '限制高消费', 'toc_LimitHigh', '<p>限制高消费信息查询,可核查个人或企业是否被采取限制高消费措施。</p>', NULL, 0.36, 18.80),
(34, NOW(), NOW(), NULL, 0, 0, '个人婚姻状态', 'toc_PersonalMarriageStatus', '<p>个人婚姻登记状态查询,帮助您了解个人的婚姻状态(未登记/结婚/离婚)。</p>', NULL, 1.00, 39.90),
(35, NOW(), NOW(), NULL, 0, 0, '婚姻状态查询(登记时间版)', 'toc_MarriageStatusRegisterTime', '<p>按登记时间查询婚姻状态,提供婚姻登记时间维度的核查服务。</p>', NULL, 1.00, 39.90),
(36, NOW(), NOW(), NULL, 0, 0, '婚姻状态查询(补证版)', 'toc_MarriageStatusSupplement', '<p>补证版婚姻状态查询,适用于补领婚姻登记证明等场景的婚姻状态核查。</p>', NULL, 1.00, 39.90),
(37, NOW(), NOW(), NULL, 0, 0, '婚姻状态核验', 'toc_MarriageStatusVerify', '<p>婚姻状态核验服务,对指定人员的婚姻状态进行快速核验。</p>', NULL, 1.00, 39.90),
(38, NOW(), NOW(), NULL, 0, 0, '双人婚姻状态(登记时间版)', 'toc_DualMarriageStatusRegisterTime', '<p>双人婚姻状态按登记时间查询,可同时核查双方婚姻登记状态及登记时间信息。</p>', NULL, 1.50, 59.90);
COMMIT;

View File

@@ -0,0 +1,285 @@
-- =============================================================================
-- 修复订单 Q_176967208700018143d9f6支付宝已退款成功但库内未完成
-- =============================================================================
--
-- 【原因简述】
-- 后台发起支付宝退款后,支付宝侧已退款成功,但创建退款记录时因 unique_refund_no
-- 冲突Duplicate entry 'refund-Q_176967208700018143d9f6')导致 createRefundRecordAndUpdateOrder
-- 失败,后续流程未执行:订单未置为 refunded、未写退款记录、未扣代理佣金与钱包。
--
-- 【本单实际退款金额】支付宝已退 99.5 元(非整单金额)
--
-- 【与 adminrefundorderlogic 退款链路对照】
-- 代码路径AdminRefundOrder -> handleAlipayRefund -> createRefundRecordAndUpdateOrder + HandleCommissionAndWalletDeduction
--
-- handleAlipayRefund 成功分支:
-- 1) createRefundRecordAndUpdateOrder(order, req, refundNo, refundResp.TradeNo, ...)
-- -> 事务内Insert order_refundrefund_no, platform_refund_id=TradeNo, order_id, user_id, product_id, refund_amount, status=success, refund_time=NOW()
-- -> 事务内Update order仅 code 中赋了 status=refunded未显式设 refund_time/version
-- 2) HandleCommissionAndWalletDeduction(ctx, svcCtx, nil, order, req.RefundAmount)
-- -> 该订单下 agent_commission按 refundAmount 比例冲减refunded_amount 增加,满额则 status=2UpdateWithVersion
-- -> 按代理汇总扣减额agent_wallet 先扣冻结再扣可用UpdateWithVersion
-- -> agent_wallet_transaction 插入 type=refund、金额为负的流水
--
-- 本 SQL 对应关系:
-- Step 2 = createRefundRecordAndUpdateOrder 的 Insert order_refundplatform_refund_id 修复时无支付宝 TradeNo 填 NULL可从支付宝补
-- Step 3 = createRefundRecordAndUpdateOrder 的 Update order并显式补全 refund_time、version+1
-- Step 4 = HandleCommissionAndWalletDeduction 对 agent_commission 的冲减(按 99.5 比例)
-- Step 5 = HandleCommissionAndWalletDeduction 对 agent_wallet 扣减 + agent_wallet_transaction 插入
--
-- 【执行前请确认】
-- 1该订单在支付宝侧已退款成功2已备份相关表或先在测试环境执行。
-- Step 25 已包在事务中:任一步报错请执行 ROLLBACK若 step 1 未查到订单_order_not_found=1勿执行后续。
-- =============================================================================
-- 与表字段 collation 一致,避免 #1267 Illegal mix of collations若表为 utf8mb4_general_ci 则改为 COLLATE utf8mb4_general_ci
SET
@order_no = CONVERT(
'Q_176967208700018143d9f6' USING utf8mb4
) COLLATE utf8mb4_general_ci;
-- 本单实际退款金额(元)
SET @repair_refund_amount = 99.5;
-- 1) 取订单信息
SELECT
id,
user_id,
product_id,
amount,
version INTO @order_id,
@user_id,
@product_id,
@order_amount,
@order_version
FROM `order`
WHERE
order_no = @order_no
AND del_state = 0
LIMIT 1;
SET @refund_amount = @repair_refund_amount;
-- 若无记录则说明订单号错误,终止
SELECT IF(@order_id IS NULL, 1, 0) AS _order_not_found;
-- 若 _order_not_found=1 请勿继续执行后续语句
-- ---------- 以下为事务:任一步报错请执行 ROLLBACK ----------
START TRANSACTION;
-- 2) 补写退款记录(若该订单尚无成功状态的退款记录)
-- 代码中 platform_refund_id 来自支付宝 refundResp.TradeNo修复时无则填 NULL如有支付宝退款单号可事后 UPDATE 补上
INSERT INTO
order_refund (
refund_no,
order_id,
user_id,
product_id,
platform_refund_id,
refund_amount,
refund_reason,
status,
del_state,
version,
refund_time,
close_time,
delete_time
)
SELECT
CONCAT(
'refund-',
@order_no,
'-repair'
),
@order_id,
@user_id,
@product_id,
NULL,
@refund_amount,
NULL,
'success',
0,
0,
NOW(),
NULL,
NULL
FROM (
SELECT 1
) _one
WHERE
NOT EXISTS (
SELECT 1
FROM order_refund
WHERE
order_id = @order_id
AND status = 'success'
AND del_state = 0
);
-- 3) 订单状态改为已退款
UPDATE `order`
SET
status = 'refunded',
refund_time = NOW(),
version = version + 1,
update_time = NOW()
WHERE
id = @order_id
AND del_state = 0
AND status = 'paid';
-- 4) 代理佣金:按本次退款 99.5 元在该订单的佣金上比例冲减refunded_amount 增加,满额则 status=2
SET
@total_available = (
SELECT COALESCE(
SUM(amount - refunded_amount), 0
)
FROM agent_commission
WHERE
order_id = @order_id
AND del_state = 0
);
UPDATE agent_commission
SET
refunded_amount = LEAST(
amount,
refunded_amount + @repair_refund_amount * (amount - refunded_amount) / NULLIF(@total_available, 0)
),
status = CASE
WHEN LEAST(
amount,
refunded_amount + @repair_refund_amount * (amount - refunded_amount) / NULLIF(@total_available, 0)
) >= amount THEN 2
ELSE status
END,
version = version + 1,
update_time = NOW()
WHERE
order_id = @order_id
AND del_state = 0
AND @total_available > 0;
-- 5) 代理钱包扣减 + 流水(仅对尚未存在本单退款流水的代理扣减,避免重复执行导致重复扣款)
DROP TEMPORARY TABLE IF EXISTS _repair_wallet_snapshot;
CREATE TEMPORARY TABLE _repair_wallet_snapshot (
agent_id BIGINT NOT NULL,
balance_before DECIMAL(20, 4) NOT NULL,
frozen_before DECIMAL(20, 4) NOT NULL,
total_deduct DECIMAL(20, 4) NOT NULL,
PRIMARY KEY (agent_id)
);
-- 按 99.5 元在该订单各代理间按“可退佣金”比例分配扣减额(与 step 4 一致),仅扣减额>0 的代理
INSERT INTO
_repair_wallet_snapshot (
agent_id,
balance_before,
frozen_before,
total_deduct
)
SELECT
agent_id,
balance_before,
frozen_before,
total_deduct
FROM (
SELECT
c.agent_id, w.balance AS balance_before, w.frozen_balance AS frozen_before, @repair_refund_amount * COALESCE(
SUM(c.amount - c.refunded_amount), 0
) / NULLIF(@total_available, 0) AS total_deduct
FROM
agent_commission c
JOIN agent_wallet w ON w.agent_id = c.agent_id
AND w.del_state = 0
WHERE
c.order_id = @order_id
AND c.del_state = 0
AND @total_available > 0
AND NOT EXISTS (
SELECT 1
FROM agent_wallet_transaction t
WHERE
t.agent_id = c.agent_id
AND t.transaction_id = @order_no
AND t.transaction_type = 'refund'
AND t.del_state = 0
)
GROUP BY
c.agent_id, w.balance, w.frozen_balance
) _agent_deduct
WHERE
total_deduct > 0;
-- 从钱包扣减:优先扣冻结余额,不足再扣可用余额
UPDATE agent_wallet w
INNER JOIN _repair_wallet_snapshot r ON w.agent_id = r.agent_id
SET
w.frozen_balance = w.frozen_balance - LEAST(
r.total_deduct,
w.frozen_balance
),
w.balance = w.balance - (
r.total_deduct - LEAST(
r.total_deduct,
w.frozen_balance
)
),
w.version = w.version + 1,
w.update_time = NOW()
WHERE
w.del_state = 0;
-- 插入退款流水(金额为负数)
INSERT INTO
agent_wallet_transaction (
delete_time,
del_state,
version,
agent_id,
transaction_type,
amount,
balance_before,
balance_after,
frozen_balance_before,
frozen_balance_after,
transaction_id,
related_user_id,
remark
)
SELECT
NULL,
0,
0,
r.agent_id,
'refund',
- r.total_deduct,
r.balance_before,
r.balance_before - (
r.total_deduct - LEAST(
r.total_deduct,
r.frozen_before
)
),
r.frozen_before,
r.frozen_before - LEAST(
r.total_deduct,
r.frozen_before
),
@order_no,
NULL,
'订单退款修复(支付宝已退,库内补单)'
FROM _repair_wallet_snapshot r;
DROP TEMPORARY TABLE IF EXISTS _repair_wallet_snapshot;
COMMIT;
-- ---------- 事务结束 ----------
-- 6) 校验(可选)
-- 订单应为 refunded
-- SELECT id, order_no, status, refund_time FROM `order` WHERE id = @order_id;
-- 应有 success 退款记录refund_amount = 99.5
-- SELECT * FROM order_refund WHERE order_id = @order_id AND del_state = 0;
-- 佣金 refunded_amount 按 99.5 比例增加,满额则 status=2
-- SELECT id, agent_id, order_id, amount, refunded_amount, status FROM agent_commission WHERE order_id = @order_id;

9
go.mod
View File

@@ -6,9 +6,9 @@ toolchain go1.23.4
require (
github.com/Masterminds/squirrel v1.5.4
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/tea v1.3.13
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/bytedance/sonic v1.13.0
github.com/cenkalti/backoff/v4 v4.3.0
@@ -37,16 +37,17 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/captcha-20230305 v1.1.3 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/aliyun/credentials-go v1.4.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic/loader v0.2.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

33
go.sum
View File

@@ -13,6 +13,8 @@ github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do2
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
@@ -22,6 +24,8 @@ github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+M
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
@@ -46,6 +50,8 @@ github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
@@ -64,6 +70,8 @@ github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTs
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA=
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
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/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -83,6 +91,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
@@ -326,10 +336,13 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -342,6 +355,9 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -356,10 +372,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -370,6 +389,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -384,20 +406,27 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -406,6 +435,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
@@ -420,6 +451,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

82
pkg/captcha/aliyun.go Normal file
View File

@@ -0,0 +1,82 @@
package captcha
import (
"os"
"strings"
"tyc-server/common/xerr"
captcha20230305 "github.com/alibabacloud-go/captcha-20230305/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
// Config 阿里云验证码配置(与 api internal config 解耦,供 pkg 使用)
type Config struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
}
// isWeChatUserAgent 判断 User-Agent 是否为微信内置浏览器(含 MicroMessenger
func isWeChatUserAgent(ua string) bool {
return strings.Contains(ua, "MicroMessenger")
}
// VerifyWithUserAgent 根据 User-Agent 与 captchaVerifyParam 校验。微信请求直接通过。
func VerifyWithUserAgent(cfg Config, captchaVerifyParam string, userAgent string) error {
if isWeChatUserAgent(userAgent) {
return nil
}
return Verify(cfg, captchaVerifyParam)
}
// Verify 校验前端传入的 captchaVerifyParam。异常时视为通过以保证业务可用。
func Verify(cfg Config, captchaVerifyParam string) error {
if os.Getenv("ENV") == "development" {
return nil
}
if captchaVerifyParam == "" {
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")
}
clientCfg := &openapi.Config{
AccessKeyId: tea.String(cfg.AccessKeyID),
AccessKeySecret: tea.String(cfg.AccessKeySecret),
}
clientCfg.Endpoint = tea.String(cfg.EndpointURL)
clientCfg.ConnectTimeout = tea.Int(5000)
clientCfg.ReadTimeout = tea.Int(5000)
client, err := captcha20230305.NewClient(clientCfg)
if err != nil {
logx.Errorf("init aliyun captcha client error: %+v", err)
return nil
}
req := &captcha20230305.VerifyIntelligentCaptchaRequest{
SceneId: tea.String(cfg.SceneID),
CaptchaVerifyParam: tea.String(captchaVerifyParam),
}
resp, err := client.VerifyIntelligentCaptcha(req)
if err != nil {
logx.Errorf("verify aliyun captcha error: %+v", err)
return nil
}
if resp.Body == nil || resp.Body.Result == nil {
logx.Errorf("verify aliyun captcha empty result, resp: %+v", resp)
return nil
}
if tea.BoolValue(resp.Body.Result.VerifyResult) {
return nil
}
verifyCode := tea.StringValue(resp.Body.Result.VerifyCode)
logx.Errorf("verify aliyun captcha failed, code: %s", verifyCode)
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "aliyun captcha verify fail, code: %s", verifyCode)
}

View File

@@ -0,0 +1,32 @@
package captcha
import (
"encoding/base64"
"fmt"
"time"
lzcrypto "tyc-server/pkg/lzkit/crypto"
)
// GenerateEncryptedSceneID 按阿里云文档生成 EncryptedSceneId仅适用于 V3 架构加密模式)。
// 明文格式: sceneId&timestamp&expireTime
// 加密: AES-256-CBC + PKCS7Padding结果为 Base64( IV(16字节) + ciphertext )
func GenerateEncryptedSceneID(sceneId, ekey string, expireSeconds int) (string, error) {
if expireSeconds <= 0 || expireSeconds > 86400 {
expireSeconds = 3600
}
ts := time.Now().Unix() // 秒级时间戳
plaintext := fmt.Sprintf("%s&%d&%d", sceneId, ts, expireSeconds)
keyBytes, err := base64.StdEncoding.DecodeString(ekey)
if err != nil {
return "", fmt.Errorf("decode ekey error: %w", err)
}
if len(keyBytes) != 32 {
return "", fmt.Errorf("invalid ekey length, need 32 bytes after base64 decode, got %d", len(keyBytes))
}
// 复用已有的 AES-CBC + PKCS7 实现,输出即为 Base64(IV + ciphertext)
return lzcrypto.AesEncrypt([]byte(plaintext), keyBytes)
}