This commit is contained in:
2026-02-12 15:16:54 +08:00
parent 07bf234b30
commit ca6dcc1b24
21 changed files with 2594 additions and 111 deletions

View File

@@ -20,6 +20,7 @@ type Config struct {
SystemConfig SystemConfig
WechatH5 WechatH5Config
Authorization AuthorizationConfig // 授权书配置
Upload UploadConfig // 图片上传(行驶证等)配置
WechatMini WechatMiniConfig
Query QueryConfig
AdminConfig AdminConfig
@@ -136,3 +137,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

@@ -26,6 +26,8 @@ import (
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 +772,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 +812,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 +832,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 +882,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,
@@ -987,7 +989,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 +1041,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 +1061,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 +1077,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 +1150,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 +1211,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

@@ -86,6 +86,23 @@ var productHandlers = map[string]queryHandlerFunc{
"toc_VehicleMaintenanceDetail": runVehicleMaintenanceDetailReq,
"toc_VehicleClaimDetail": runVehicleClaimDetailReq,
"toc_VehicleClaimVerify": runVehicleClaimVerifyReq,
// 核验工具verify feature.md
"toc_PoliceTwoFactors": runVerifyAuthTwoReq,
"toc_PoliceThreeFactors": runVerifyAuthThreeReq,
"toc_ProfessionalCertificate": runVerifyCertReq,
"toc_PersonalConsumptionCapacityLevel": runVerifyConsumptionReq, // 个人消费能力(沿用现有 product_en
"toc_OperatorTwoFactors": runVerifyYysTwoReq,
"toc_MobileThreeFactors": runVerifyYysThreeReq,
"toc_NumberRecycle": runVerifyMobileOnlyReq,
"toc_MobileEmptyCheck": runVerifyMobileOnlyReq,
"toc_MobilePortability": runVerifyMobileOnlyReq,
"toc_MobileOnlineStatus": runVerifyMobileOnlyReq,
"toc_MobileOnlineDuration": runVerifyMobileOnlyReq,
"toc_MobileAttribution": runVerifyMobileOnlyReq,
"toc_MobileConsumptionRange": runVerifyYysConsumptionReq,
"toc_EnterpriseRelation": runVerifyEntRelationReq,
"toc_BankcardFourFactors": runVerifyBankFourReq,
"toc_BankcardBlacklist": runVerifyBankBlackReq,
}
func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) {
@@ -242,8 +259,10 @@ func runVehicleVinCodeReq(l *QueryServiceLogic, decryptData []byte, product stri
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
return map[string]interface{}{
"vin_code": data.VinCode,
@@ -259,13 +278,15 @@ func runVehicleMileageMixedReq(l *QueryServiceLogic, decryptData []byte, product
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
"return_url": data.ReturnURL,
"image_url": data.ImageURL,
"vin_code": data.VinCode,
"image_url": data.ImageURL,
}, nil
}
@@ -278,8 +299,10 @@ func runVehicleVinValuationReq(l *QueryServiceLogic, decryptData []byte, product
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
return map[string]interface{}{
"vin_code": data.VinCode,
@@ -297,13 +320,15 @@ func runVehicleTransferSimpleReq(l *QueryServiceLogic, decryptData []byte, produ
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
return map[string]interface{}{"vin_code": data.VinCode}, nil
}
// runVehicleMaintenanceSimpleReq 车辆维保简版 QCXG3Y6B仅必填 vin_code, return_url
// runVehicleMaintenanceSimpleReq 车辆维保简版 QCXG3Y6B仅必填 vin_code;回调地址后端自动生成
func runVehicleMaintenanceSimpleReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleMaintenanceSimpleReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
@@ -312,16 +337,18 @@ func runVehicleMaintenanceSimpleReq(l *QueryServiceLogic, decryptData []byte, pr
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
"return_url": data.ReturnURL,
"vin_code": data.VinCode,
}, nil
}
// runVehicleMaintenanceDetailReq 车辆维保详细版 QCXG3Z3L仅必填 vin_code, return_url
// runVehicleMaintenanceDetailReq 车辆维保详细版 QCXG3Z3L仅必填 vin_code;回调地址后端自动生成
func runVehicleMaintenanceDetailReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleMaintenanceDetailReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
@@ -330,16 +357,18 @@ func runVehicleMaintenanceDetailReq(l *QueryServiceLogic, decryptData []byte, pr
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
"return_url": data.ReturnURL,
"vin_code": data.VinCode,
}, nil
}
// runVehicleClaimDetailReq 车辆出险详版 QCXGP00W仅必填 vin_code, return_url, vlphoto_datavlphoto_data 由 API 层加密为 data
// runVehicleClaimDetailReq 车辆出险详版 QCXGP00W仅必填 vin_code, vlphoto_data;回调地址后端自动生成vlphoto_data 由 API 层加密为 data
func runVehicleClaimDetailReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleClaimDetailReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
@@ -348,12 +377,14 @@ func runVehicleClaimDetailReq(l *QueryServiceLogic, decryptData []byte, product
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
"return_url": data.ReturnURL,
"vlphoto_data": data.VlphotoData,
}, nil
}
@@ -367,12 +398,151 @@ func runVehicleClaimVerifyReq(l *QueryServiceLogic, decryptData []byte, product
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
auth := data.Authorized
if auth == "" {
auth = "1"
}
return map[string]interface{}{
"vin_code": data.VINCode,
"authorized": data.Authorized,
"authorized": auth,
}, nil
}
// --------------- 核验工具 handlers ---------------
func runVerifyAuthTwoReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyAuthTwoReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyAuthThreeReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyAuthThreeReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"photo_data": data.PhotoData, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyCertReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyCertReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyConsumptionReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyConsumptionReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyYysTwoReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyYysTwoReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "name": data.Name}, nil
}
func runVerifyYysThreeReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyYysThreeReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyMobileOnlyReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyMobileOnlyReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo}, nil
}
func runVerifyYysConsumptionReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyYysConsumptionReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "authorized": data.Authorized}, nil
}
func runVerifyEntRelationReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyEntRelationReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
out := map[string]interface{}{}
if data.IDCard != "" {
out["id_card"] = data.IDCard
}
return out, nil
}
func runVerifyBankFourReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyBankFourReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{
"mobile_no": data.MobileNo,
"id_card": data.IDCard,
"bank_card": data.BankCard,
"name": data.Name,
}, nil
}
func runVerifyBankBlackReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyBankBlackReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{
"mobile_no": data.MobileNo,
"id_card": data.IDCard,
"name": data.Name,
"bank_card": data.BankCard,
}, nil
}

View File

@@ -0,0 +1,164 @@
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,
)
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "读取回调 Body 失败: %v", err)
}
// 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(bodyBytes)
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(bodyBytes),
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,138 @@
package upload
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"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 == "" {
baseURL = l.svcCtx.Config.AdminPromotion.URLDomain
if baseURL != "" {
baseURL = baseURL + "/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

@@ -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 {
@@ -152,7 +172,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 +181,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 +223,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)

View File

@@ -60,7 +60,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{
@@ -87,6 +88,19 @@ 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 {
return nil, fmt.Errorf("解析查询参数失败: %w", err)
}
if orderNo != "" {
baseParams["order_no"] = orderNo
}
paramsWithOrder, err := json.Marshal(baseParams)
if err != nil {
return nil, fmt.Errorf("序列化查询参数失败: %w", err)
}
var (
wg sync.WaitGroup
resultsCh = make(chan APIResponseData, len(featureList))
@@ -120,7 +134,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
}
@@ -213,6 +227,23 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
"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,
}
// PreprocessRequestApi 调用指定的请求处理函数
@@ -1235,10 +1266,12 @@ func (a *ApiRequestService) ProcessQCXG5U0ZRequest(params []byte) ([]byte, error
}
func (a *ApiRequestService) ProcessQCXG1U4URequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "return_url", "image_url"}, nil)
if body["vin_code"] == nil || body["return_url"] == nil || body["image_url"] == nil {
return nil, errors.New("api请求, QCXG1U4U, 缺少必填参数 vin_code/return_url/image_url")
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")
}
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG1U4U")
resp, err := a.tianyuanapi.CallInterface("QCXG1U4U", body)
if err != nil {
return nil, err
@@ -1283,10 +1316,12 @@ func (a *ApiRequestService) ProcessQCXG4I1ZRequest(params []byte) ([]byte, error
}
func (a *ApiRequestService) ProcessQCXG3Y6BRequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "return_url"}, nil)
if body["vin_code"] == nil || body["return_url"] == nil {
return nil, errors.New("api请求, QCXG3Y6B, 缺少必填参数 vin_code/return_url")
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")
}
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG3Y6B")
resp, err := a.tianyuanapi.CallInterface("QCXG3Y6B", body)
if err != nil {
return nil, err
@@ -1295,10 +1330,12 @@ func (a *ApiRequestService) ProcessQCXG3Y6BRequest(params []byte) ([]byte, error
}
func (a *ApiRequestService) ProcessQCXG3Z3LRequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "return_url"}, nil)
if body["vin_code"] == nil || body["return_url"] == nil {
return nil, errors.New("api请求, QCXG3Z3L, 缺少必填参数 vin_code/return_url")
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")
}
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG3Z3L")
resp, err := a.tianyuanapi.CallInterface("QCXG3Z3L", body)
if err != nil {
return nil, err
@@ -1308,10 +1345,10 @@ func (a *ApiRequestService) ProcessQCXG3Z3LRequest(params []byte) ([]byte, error
func (a *ApiRequestService) ProcessQCXGP00WRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
returnURL := gjson.GetBytes(params, "return_url")
orderNo := gjson.GetBytes(params, "order_no").String()
vlphoto := gjson.GetBytes(params, "vlphoto_data")
if !vin.Exists() || vin.String() == "" || !returnURL.Exists() || returnURL.String() == "" || !vlphoto.Exists() || vlphoto.String() == "" {
return nil, errors.New("api请求, QCXGP00W, 缺少必填参数 vin_code/return_url/vlphoto_data")
if !vin.Exists() || vin.String() == "" || orderNo == "" || !vlphoto.Exists() || vlphoto.String() == "" {
return nil, errors.New("api请求, QCXGP00W, 缺少必填参数 vin_code/order_no/vlphoto_data")
}
key, err := hex.DecodeString(a.config.Encrypt.SecretKey)
if err != nil {
@@ -1323,7 +1360,7 @@ func (a *ApiRequestService) ProcessQCXGP00WRequest(params []byte) ([]byte, error
}
resp, err := a.tianyuanapi.CallInterface("QCXGP00W", map[string]interface{}{
"vin_code": vin.String(),
"return_url": returnURL.String(),
"return_url": a.buildVehicleCallbackURL(orderNo, "QCXGP00W"),
"data": encData,
})
if err != nil {
@@ -1334,14 +1371,17 @@ func (a *ApiRequestService) ProcessQCXGP00WRequest(params []byte) ([]byte, error
func (a *ApiRequestService) ProcessQCXG6B4ERequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
auth := gjson.GetBytes(params, "authorized")
if !vin.Exists() || vin.String() == "" || !auth.Exists() {
return nil, errors.New("api请求, QCXG6B4E, 缺少 vin_code 或 authorized")
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{}{
"VINCode": vin.String(),
"Authorized": auth.String(),
"Authorized": auth,
})
if err != nil {
return nil, err
@@ -1349,6 +1389,68 @@ func (a *ApiRequestService) ProcessQCXG6B4ERequest(params []byte) ([]byte, error
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)
}
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")
}
// buildVehicleBody 从 params 中取 required 与 optional 键,仅非空才写入 body
func buildVehicleBody(params []byte, required, optional []string) map[string]interface{} {
body := make(map[string]interface{})
@@ -1367,6 +1469,18 @@ func buildVehicleBody(params []byte, required, optional []string) map[string]int
return body
}
// buildVehicleCallbackURL 生成车辆类接口的异步回调地址
// 当前使用 AdminPromotion.URLDomain 作为域名配置,路径固定为 /api/v1/tianyuan/vehicle/callback
// 并通过查询参数携带 order_no 与 api_id 以便后端识别具体查询与模块。
func (a *ApiRequestService) buildVehicleCallbackURL(orderNo, apiID string) string {
base := strings.TrimRight(a.config.AdminPromotion.URLDomain, "/")
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

@@ -129,23 +129,24 @@ type TocVehiclesUnderNameCountReq struct {
IDCard string `json:"id_card" validate:"required,idCard"`
}
// 车辆静态信息/过户详版等 仅 vin_code
// 车辆静态信息/过户详版等 仅 vin_code(车辆类不要求手机号与验证码)
type TocVehicleVinCodeReq struct {
VinCode string `json:"vin_code" validate:"required"`
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"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" validate:"required"`
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" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 二手车VIN估值 QCXGY7F2
@@ -155,60 +156,136 @@ type TocVehicleVinValuationReq struct {
VehicleLocation string `json:"vehicle_location" validate:"required"`
FirstRegistrationDate string `json:"first_registrationdate" validate:"required"` // yyyy-MM
Color string `json:"color"`
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
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" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
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" validate:"required"`
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" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
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" validate:"required"`
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" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
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" validate:"required"`
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" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
Mobile string `json:"mobile"`
Code string `json:"code"`
}
// 车辆出险记录核验 QCXG6B4EVINCode + Authorized(0/1)
// 车辆出险记录核验 QCXG6B4EVINCodeAuthorized 由后端默认传 1
type TocVehicleClaimVerifyReq struct {
VINCode string `json:"vin_code" validate:"required"`
Authorized string `json:"authorized" validate:"required"` // 0:否 1:是
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"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 {
@@ -2115,6 +2115,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 +2179,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"`