feat: add toolbox query, upload module, update config and gitignore

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Mrx
2026-05-21 17:29:35 +08:00
parent 0a0ca7bf9b
commit 144d5507dd
22 changed files with 11184 additions and 8 deletions

2
.gitignore vendored
View File

@@ -22,6 +22,8 @@ data/*
/app/api
/app/main/api/main
/app/main/api/_*
__debug_bin.exe
**/.../
# 文档目录

View File

@@ -0,0 +1,29 @@
syntax = "v1"
info (
title: "工具箱服务"
desc: "免费工具箱(天行聚合等)"
version: "v1"
)
//============================> toolbox v1 <============================
@server (
prefix: api/v1
group: toolbox
)
service main {
@doc "工具箱统一查询"
@handler toolboxQuery
post /toolbox/query (ToolboxQueryReq) returns (ToolboxQueryResp)
}
type (
ToolboxQueryReq {
ToolKey string `json:"tool_key"`
Params map[string]interface{} `json:"params,optional"`
}
ToolboxQueryResp {
ToolKey string `json:"tool_key"`
Result map[string]interface{} `json:"result"`
}
)

View File

@@ -0,0 +1,44 @@
syntax = "v1"
info (
title: "上传服务"
desc: "图片 Base64 上传与文件访问"
version: "v1"
)
//============================> upload v1 <============================
// 访问已上传图片(直接输出二进制,无 JSON 包装)
@server (
prefix: api/v1
group: upload
)
service main {
@doc "访问已上传图片"
@handler serveUpload
get /upload/file/:name (ServeUploadFileReq)
}
type ServeUploadFileReq {
Name string `path:"name"`
}
@server (
prefix: api/v1
group: upload
jwt: JwtAuth
middleware: AuthInterceptor
)
service main {
@doc "图片 Base64 上传"
@handler uploadImage
post /upload/image (UploadImageReq) returns (UploadImageResp)
}
type (
UploadImageReq {
ImageBase64 string `json:"image_base64"`
}
UploadImageResp {
Url string `json:"url"`
}
)

View File

@@ -14,6 +14,8 @@ import "./front/product.api"
import "./front/agent.api"
import "./front/app.api"
import "./front/authorization.api"
import "./front/toolbox.api"
import "./front/upload.api"
// 后台
import "./admin/auth.api"
import "./admin/menu.api"
@@ -29,3 +31,6 @@ import "./admin/admin_query.api"
import "./admin/admin_agent.api"
import "./admin/admin_api.api"
import "./admin/admin_role_api.api"

View File

@@ -89,6 +89,10 @@ Tianyuanapi:
Key: "04c6b4c559be6d5ba5351c04c8713a64"
BaseURL: "https://api.tianyuanapi.com"
Timeout: 60
tianxingjuhe:
url: "https://apis.tianapi.com"
key: "4ceffb1ffb95b83230b9a9c9df2467e1"
timeout: 30
Authorization:
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
Promotion:

View File

@@ -78,6 +78,10 @@ Tianyuanapi:
Key: "04c6b4c559be6d5ba5351c04c8713a64"
BaseURL: "https://api.tianyuanapi.com"
Timeout: 60
tianxingjuhe:
url: "https://apis.tianapi.com"
key: "4ceffb1ffb95b83230b9a9c9df2467e1"
timeout: 30
Authorization:
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
Promotion:

View File

@@ -24,7 +24,8 @@ type Config struct {
Query QueryConfig
AdminConfig AdminConfig
TaxConfig TaxConfig
Promotion PromotionConfig // 推广链接配置
Promotion PromotionConfig // 推广链接配置
Tianxingjuhe TianxingjuheConfig // 天行聚合API配置
}
// JwtAuth 用于 JWT 鉴权配置
@@ -126,3 +127,10 @@ type PromotionConfig struct {
PromotionDomain string // 推广域名(用于生成短链)
OfficialDomain string // 正式站点域名(短链重定向的目标域名)
}
// TianxingjuheConfig 天行聚合API配置
type TianxingjuheConfig struct {
URL string // API基础URL
Key string // API密钥
Timeout int // 超时时间默认30秒
}

View File

@@ -27,6 +27,8 @@ import (
pay "qnc-server/app/main/api/internal/handler/pay"
product "qnc-server/app/main/api/internal/handler/product"
query "qnc-server/app/main/api/internal/handler/query"
toolbox "qnc-server/app/main/api/internal/handler/toolbox"
upload "qnc-server/app/main/api/internal/handler/upload"
user "qnc-server/app/main/api/internal/handler/user"
"qnc-server/app/main/api/internal/svc"
@@ -1091,6 +1093,46 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// 工具箱统一查询
Method: http.MethodPost,
Path: "/toolbox/query",
Handler: toolbox.ToolboxQueryHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// 访问已上传图片
Method: http.MethodGet,
Path: "/upload/file/:name",
Handler: upload.ServeUploadHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthInterceptor},
[]rest.Route{
{
// 图片 Base64 上传
Method: http.MethodPost,
Path: "/upload/image",
Handler: upload.UploadImageHandler(serverCtx),
},
}...,
),
rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret),
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{

View File

@@ -0,0 +1,25 @@
package toolbox
import (
"net/http"
"qnc-server/app/main/api/internal/logic/toolbox"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ToolboxQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ToolboxQueryReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
l := toolbox.NewToolboxQueryLogic(r.Context(), svcCtx)
resp, err := l.ToolboxQuery(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,26 @@
package upload
import (
"net/http"
uploadlogic "qnc-server/app/main/api/internal/logic/upload"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ServeUploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ServeUploadFileReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
l := uploadlogic.NewServeUploadLogic(r.Context(), svcCtx)
if err := l.ServeUpload(req.Name, w); err != nil {
result.HttpResult(r, w, nil, err)
}
}
}

View File

@@ -0,0 +1,25 @@
package upload
import (
"net/http"
uploadlogic "qnc-server/app/main/api/internal/logic/upload"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
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
}
l := uploadlogic.NewUploadImageLogic(r.Context(), svcCtx)
resp, err := l.UploadImage(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,47 @@
package toolbox
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type ToolboxQueryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewToolboxQueryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToolboxQueryLogic {
return &ToolboxQueryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ToolboxQueryLogic) ToolboxQuery(req *types.ToolboxQueryReq) (*types.ToolboxQueryResp, error) {
if req.ToolKey == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("tool_key 不能为空"), "")
}
if l.svcCtx.ToolboxService == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "工具箱服务未初始化")
}
params := req.Params
if params == nil {
params = map[string]interface{}{}
}
result, err := l.svcCtx.ToolboxService.Query(l.ctx, req.ToolKey, params)
if err != nil {
return nil, err
}
return &types.ToolboxQueryResp{
ToolKey: req.ToolKey,
Result: result,
}, nil
}

View File

@@ -0,0 +1,67 @@
package upload
import (
"context"
"net/http"
"os"
"path/filepath"
"strings"
"qnc-server/app/main/api/internal/svc"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type ServeUploadLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewServeUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServeUploadLogic {
return &ServeUploadLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ServeUploadLogic) ServeUpload(name string, w http.ResponseWriter) error {
name = filepath.Base(strings.TrimSpace(name))
if name == "" || name == "." || name == ".." || !isSafeUploadFileName(name) {
return errors.Wrapf(xerr.NewErrMsg("无效的文件名"), "")
}
fullPath := filepath.Join(uploadImageDir, name)
data, err := os.ReadFile(fullPath)
if err != nil {
if os.IsNotExist(err) {
return errors.Wrapf(xerr.NewErrMsg("文件不存在"), "")
}
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "读取文件失败: %v", err)
}
w.Header().Set("Cache-Control", "public, max-age=86400")
switch filepath.Ext(name) {
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".webp":
w.Header().Set("Content-Type", "image/webp")
default:
w.Header().Set("Content-Type", "image/jpeg")
}
_, err = w.Write(data)
return err
}
func isSafeUploadFileName(name string) bool {
for _, c := range name {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' {
continue
}
return false
}
return true
}

View File

@@ -0,0 +1,93 @@
package upload
import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
const uploadImageDir = "uploads/images"
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) (*types.UploadImageResp, error) {
raw := strings.TrimSpace(req.ImageBase64)
if raw == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("image_base64 不能为空"), "")
}
if idx := strings.Index(raw, ","); idx >= 0 {
raw = raw[idx+1:]
}
data, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("图片 Base64 解析失败"), "")
}
if len(data) == 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("图片内容为空"), "")
}
if len(data) > 5*1024*1024 {
return nil, errors.Wrapf(xerr.NewErrMsg("图片不能超过 5MB"), "")
}
ext := detectImageExt(data)
fileName := fmt.Sprintf("%s%s", uuid.NewString(), ext)
dir := uploadImageDir
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建上传目录失败: %v", err)
}
fullPath := filepath.Join(dir, fileName)
if err := os.WriteFile(fullPath, data, 0o644); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存图片失败: %v", err)
}
base := publicAPIBase(l.svcCtx.Config.Promotion.OfficialDomain)
url := fmt.Sprintf("%s/api/v1/upload/file/%s", base, fileName)
return &types.UploadImageResp{Url: url}, nil
}
func detectImageExt(data []byte) string {
if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return ".jpg"
}
if len(data) >= 8 && string(data[0:8]) == "\x89PNG\r\n\x1a\n" {
return ".png"
}
if len(data) >= 6 && string(data[0:6]) == "GIF87a" || string(data[0:6]) == "GIF89a" {
return ".gif"
}
if len(data) >= 12 && string(data[8:12]) == "WEBP" {
return ".webp"
}
return ".jpg"
}
func publicAPIBase(officialDomain string) string {
base := strings.TrimRight(officialDomain, "/")
if base != "" {
return base
}
return "http://127.0.0.1:8888"
}

View File

@@ -159,7 +159,7 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
encryptData = encryptedEmptyData
} else {
// 正常模式调用API请求服务
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id, order.OrderNo)
if err != nil {
return l.handleError(ctx, err, order, query)
}

View File

@@ -2,12 +2,14 @@ package service
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"qnc-server/app/main/api/internal/config"
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
"qnc-server/app/main/model"
"qnc-server/pkg/lzkit/crypto"
"sort"
"strings"
"sync"
@@ -75,7 +77,8 @@ type APIResponseData struct {
}
// ProcessRequests 处理请求
func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]byte, error) {
// orderNo: 当前查询订单号,供异步车辆类接口生成 return_url 回调地址
func (a *ApiRequestService) ProcessRequests(params []byte, productID string, orderNo string) ([]byte, error) {
var ctx, cancel = context.WithCancel(context.Background())
defer cancel()
@@ -112,6 +115,25 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]
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)
}
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))
@@ -152,7 +174,7 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]
tryCount := 0
for {
tryCount++
resp, preprocessErr = a.PreprocessRequestApi(params, feature.ApiId)
resp, preprocessErr = a.PreprocessRequestApi(paramsWithOrder, feature.ApiId)
if preprocessErr == nil {
break
}
@@ -224,7 +246,21 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
"QYGL6F2D": (*ApiRequestService).ProcessQYGL6F2DRequest,
"JRZQ8203": (*ApiRequestService).ProcessJRZQ8203Request,
"JRZQ4AA8": (*ApiRequestService).ProcessJRZQ4AA8Request,
"QCXGGB2Q": (*ApiRequestService).ProcessQCXGGB2QRequest,
"QCXGYTS2": (*ApiRequestService).ProcessQCXGYTS2Request,
"QCXG5F3A": (*ApiRequestService).ProcessQCXG5F3ARequest,
"QCXG7A2B": (*ApiRequestService).ProcessQCXG7A2BRequest,
"QCXG9P1C": (*ApiRequestService).ProcessQCXG9P1CFRequest,
"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,
"DWBG8B4D": (*ApiRequestService).ProcessDWBG8B4DRequest,
"DWBG6A2C": (*ApiRequestService).ProcessDWBG6A2CRequest,
"JRZQ4B6C": (*ApiRequestService).ProcessJRZQ4B6CRequest,
@@ -1126,24 +1162,290 @@ 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)
}
// ProcessQCXG7A2BRequest 名下车辆
func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card")
if !idCard.Exists() {
return nil, errors.New("api请求, QCXG7A2B, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXG7A2B", map[string]interface{}{
"id_card": idCard.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessQCXG9P1CFRequest 名下车辆车牌查询 A
func (a *ApiRequestService) ProcessQCXG9P1CFRequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card")
name := gjson.GetBytes(params, "name")
mobile := gjson.GetBytes(params, "mobile")
if !idCard.Exists() || !name.Exists() || !mobile.Exists() {
return nil, errors.New("api请求, QCXG9P1C, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXG9P1C", map[string]interface{}{
"id_card": idCard.String(),
"authorized": "1",
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
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")
}
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"
}
resp, err := a.tianyuanapi.CallInterface("QCXG6B4E", map[string]interface{}{
"vin_code": vin.String(),
"authorized": auth,
})
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 生成车辆类接口的异步回调地址
func (a *ApiRequestService) buildVehicleCallbackURL(orderNo, apiID string) string {
base := strings.TrimRight(a.config.Promotion.OfficialDomain, "/")
if base == "" {
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)
}
// ProcessYYSY09CDRequest 三要素
func (a *ApiRequestService) ProcessYYSY09CDRequest(params []byte) ([]byte, error) {
name := gjson.GetBytes(params, "name")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,150 @@
package tianxingjuhe
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Client 天行聚合API客户端
type Client struct {
baseURL string
key string
client *http.Client
}
// Config 客户端配置
type Config struct {
BaseURL string // API基础URL
Key string // API密钥
Timeout int // 超时时间(秒)
}
// Response 通用API响应结构
type Response struct {
Code int `json:"code"` // 状态码200表示成功
Msg string `json:"msg"` // 返回说明
Result interface{} `json:"result"` // 返回结果集,具体内容根据接口而定
}
// NewClient 创建新的客户端实例
func NewClient(config Config) (*Client, error) {
if config.BaseURL == "" {
return nil, fmt.Errorf("baseURL不能为空")
}
if config.Key == "" {
return nil, fmt.Errorf("key不能为空")
}
return &Client{
baseURL: config.BaseURL,
key: config.Key,
client: &http.Client{
Timeout: time.Duration(config.Timeout) * time.Second,
},
}, nil
}
// Get 发送GET请求
func (c *Client) Get(endpoint string, params map[string]interface{}) (*Response, error) {
// 构建完整URL
fullURL := fmt.Sprintf("%s/%s", c.baseURL, endpoint)
// 添加请求参数
queryParams := url.Values{}
for key, value := range params {
queryParams.Set(key, fmt.Sprintf("%v", value))
}
// 添加key参数
queryParams.Set("key", c.key)
// 拼接查询参数
if len(queryParams) > 0 {
fullURL = fmt.Sprintf("%s?%s", fullURL, queryParams.Encode())
}
// 创建HTTP请求
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Tianxingjuhe-Go-SDK/1.0.0")
// 发送请求
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var apiResp Response
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
return &apiResp, nil
}
// Post 发送POST请求
func (c *Client) Post(endpoint string, params map[string]interface{}) (*Response, error) {
// 构建完整URL
fullURL := fmt.Sprintf("%s/%s", c.baseURL, endpoint)
// 添加key参数
if params == nil {
params = make(map[string]interface{})
}
params["key"] = c.key
// 序列化请求体
requestBody, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 创建HTTP请求
req, err := http.NewRequest("POST", fullURL, bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Tianxingjuhe-Go-SDK/1.0.0")
// 发送请求
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var apiResp Response
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
return &apiResp, nil
}

View File

@@ -7,9 +7,10 @@ import (
"fmt"
"time"
"qnc-server/app/main/model"
"github.com/Masterminds/squirrel"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/model"
)
// TianyuanapiCallLogService 天元API调用记录服务

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import (
"qnc-server/app/main/api/internal/config"
"qnc-server/app/main/api/internal/middleware"
"qnc-server/app/main/api/internal/service"
tianxingjuhe "qnc-server/app/main/api/internal/service/tianxingjuhe_sdk"
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
"qnc-server/app/main/model"
"time"
@@ -99,6 +100,7 @@ type ServiceContext struct {
DictService *service.DictService
ImageService *service.ImageService
AuthorizationService *service.AuthorizationService
ToolboxService *service.ToolboxService
}
// NewServiceContext 创建服务上下文
@@ -201,6 +203,23 @@ func NewServiceContext(c config.Config) *ServiceContext {
dictService := service.NewDictService(adminDictTypeModel, adminDictDataModel)
imageService := service.NewImageService()
tianxingjuheTimeout := c.Tianxingjuhe.Timeout
if tianxingjuheTimeout <= 0 {
tianxingjuheTimeout = 30
}
tianxingjuheClient, tjErr := tianxingjuhe.NewClient(tianxingjuhe.Config{
BaseURL: c.Tianxingjuhe.URL,
Key: c.Tianxingjuhe.Key,
Timeout: tianxingjuheTimeout,
})
if tjErr != nil {
logx.Errorf("初始化天行聚合API失败: %+v", tjErr)
}
var toolboxService *service.ToolboxService
if tianxingjuheClient != nil {
toolboxService = service.NewToolboxService(tianxingjuheClient)
}
// ============================== 异步任务服务 ==============================
asynqServer := asynq.NewServer(
asynq.RedisClientOpt{Addr: c.CacheRedis[0].Host, Password: c.CacheRedis[0].Pass},
@@ -296,6 +315,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
DictService: dictService,
ImageService: imageService,
AuthorizationService: authorizationService,
ToolboxService: toolboxService,
}
}

View File

@@ -2199,6 +2199,10 @@ type RoleListItem struct {
MenuIds []string `json:"menu_ids"` // 关联的菜单ID列表
}
type ServeUploadFileReq struct {
Name string `path:"name"`
}
type ShortLinkRedirectResp struct {
}
@@ -2251,6 +2255,16 @@ type TeamStatisticsResp struct {
MonthNewMembers int64 `json:"month_new_members"` // 本月新增成员
}
type ToolboxQueryReq struct {
ToolKey string `json:"tool_key"`
Params map[string]interface{} `json:"params,optional"`
}
type ToolboxQueryResp struct {
ToolKey string `json:"tool_key"`
Result map[string]interface{} `json:"result"`
}
type UpdateMenuReq struct {
Id string `path:"id"` // 菜单ID
Pid *string `json:"pid,optional"` // 父菜单ID
@@ -2334,6 +2348,14 @@ type UpgradeSubordinateResp struct {
Success bool `json:"success"`
}
type UploadImageReq struct {
ImageBase64 string `json:"image_base64"`
}
type UploadImageResp struct {
Url string `json:"url"`
}
type User struct {
Id string `json:"id"`
Mobile string `json:"mobile"`