feat: add toolbox query, upload module, update config and gitignore
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,6 +22,8 @@ data/*
|
|||||||
/app/api
|
/app/api
|
||||||
/app/main/api/main
|
/app/main/api/main
|
||||||
/app/main/api/_*
|
/app/main/api/_*
|
||||||
|
__debug_bin.exe
|
||||||
|
**/.../
|
||||||
|
|
||||||
|
|
||||||
# 文档目录
|
# 文档目录
|
||||||
|
|||||||
29
app/main/api/desc/front/toolbox.api
Normal file
29
app/main/api/desc/front/toolbox.api
Normal 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"`
|
||||||
|
}
|
||||||
|
)
|
||||||
44
app/main/api/desc/front/upload.api
Normal file
44
app/main/api/desc/front/upload.api
Normal 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"`
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -14,6 +14,8 @@ import "./front/product.api"
|
|||||||
import "./front/agent.api"
|
import "./front/agent.api"
|
||||||
import "./front/app.api"
|
import "./front/app.api"
|
||||||
import "./front/authorization.api"
|
import "./front/authorization.api"
|
||||||
|
import "./front/toolbox.api"
|
||||||
|
import "./front/upload.api"
|
||||||
// 后台
|
// 后台
|
||||||
import "./admin/auth.api"
|
import "./admin/auth.api"
|
||||||
import "./admin/menu.api"
|
import "./admin/menu.api"
|
||||||
@@ -29,3 +31,6 @@ import "./admin/admin_query.api"
|
|||||||
import "./admin/admin_agent.api"
|
import "./admin/admin_agent.api"
|
||||||
import "./admin/admin_api.api"
|
import "./admin/admin_api.api"
|
||||||
import "./admin/admin_role_api.api"
|
import "./admin/admin_role_api.api"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ Tianyuanapi:
|
|||||||
Key: "04c6b4c559be6d5ba5351c04c8713a64"
|
Key: "04c6b4c559be6d5ba5351c04c8713a64"
|
||||||
BaseURL: "https://api.tianyuanapi.com"
|
BaseURL: "https://api.tianyuanapi.com"
|
||||||
Timeout: 60
|
Timeout: 60
|
||||||
|
tianxingjuhe:
|
||||||
|
url: "https://apis.tianapi.com"
|
||||||
|
key: "4ceffb1ffb95b83230b9a9c9df2467e1"
|
||||||
|
timeout: 30
|
||||||
Authorization:
|
Authorization:
|
||||||
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
|
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
|
||||||
Promotion:
|
Promotion:
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ Tianyuanapi:
|
|||||||
Key: "04c6b4c559be6d5ba5351c04c8713a64"
|
Key: "04c6b4c559be6d5ba5351c04c8713a64"
|
||||||
BaseURL: "https://api.tianyuanapi.com"
|
BaseURL: "https://api.tianyuanapi.com"
|
||||||
Timeout: 60
|
Timeout: 60
|
||||||
|
tianxingjuhe:
|
||||||
|
url: "https://apis.tianapi.com"
|
||||||
|
key: "4ceffb1ffb95b83230b9a9c9df2467e1"
|
||||||
|
timeout: 30
|
||||||
Authorization:
|
Authorization:
|
||||||
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
|
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
|
||||||
Promotion:
|
Promotion:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Config struct {
|
|||||||
AdminConfig AdminConfig
|
AdminConfig AdminConfig
|
||||||
TaxConfig TaxConfig
|
TaxConfig TaxConfig
|
||||||
Promotion PromotionConfig // 推广链接配置
|
Promotion PromotionConfig // 推广链接配置
|
||||||
|
Tianxingjuhe TianxingjuheConfig // 天行聚合API配置
|
||||||
}
|
}
|
||||||
|
|
||||||
// JwtAuth 用于 JWT 鉴权配置
|
// JwtAuth 用于 JWT 鉴权配置
|
||||||
@@ -126,3 +127,10 @@ type PromotionConfig struct {
|
|||||||
PromotionDomain string // 推广域名(用于生成短链)
|
PromotionDomain string // 推广域名(用于生成短链)
|
||||||
OfficialDomain string // 正式站点域名(短链重定向的目标域名)
|
OfficialDomain string // 正式站点域名(短链重定向的目标域名)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TianxingjuheConfig 天行聚合API配置
|
||||||
|
type TianxingjuheConfig struct {
|
||||||
|
URL string // API基础URL
|
||||||
|
Key string // API密钥
|
||||||
|
Timeout int // 超时时间(秒),默认30秒
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import (
|
|||||||
pay "qnc-server/app/main/api/internal/handler/pay"
|
pay "qnc-server/app/main/api/internal/handler/pay"
|
||||||
product "qnc-server/app/main/api/internal/handler/product"
|
product "qnc-server/app/main/api/internal/handler/product"
|
||||||
query "qnc-server/app/main/api/internal/handler/query"
|
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"
|
user "qnc-server/app/main/api/internal/handler/user"
|
||||||
"qnc-server/app/main/api/internal/svc"
|
"qnc-server/app/main/api/internal/svc"
|
||||||
|
|
||||||
@@ -1091,6 +1093,46 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
rest.WithPrefix("/api/v1"),
|
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(
|
server.AddRoutes(
|
||||||
[]rest.Route{
|
[]rest.Route{
|
||||||
{
|
{
|
||||||
|
|||||||
25
app/main/api/internal/handler/toolbox/toolboxqueryhandler.go
Normal file
25
app/main/api/internal/handler/toolbox/toolboxqueryhandler.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/main/api/internal/handler/upload/serveuploadhandler.go
Normal file
26
app/main/api/internal/handler/upload/serveuploadhandler.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/main/api/internal/handler/upload/uploadimagehandler.go
Normal file
25
app/main/api/internal/handler/upload/uploadimagehandler.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/main/api/internal/logic/toolbox/toolboxquerylogic.go
Normal file
47
app/main/api/internal/logic/toolbox/toolboxquerylogic.go
Normal 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
|
||||||
|
}
|
||||||
67
app/main/api/internal/logic/upload/serveuploadlogic.go
Normal file
67
app/main/api/internal/logic/upload/serveuploadlogic.go
Normal 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
|
||||||
|
}
|
||||||
93
app/main/api/internal/logic/upload/uploadimagelogic.go
Normal file
93
app/main/api/internal/logic/upload/uploadimagelogic.go
Normal 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"
|
||||||
|
}
|
||||||
@@ -159,7 +159,7 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
|
|||||||
encryptData = encryptedEmptyData
|
encryptData = encryptedEmptyData
|
||||||
} else {
|
} else {
|
||||||
// 正常模式:调用API请求服务
|
// 正常模式:调用API请求服务
|
||||||
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
|
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id, order.OrderNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return l.handleError(ctx, err, order, query)
|
return l.handleError(ctx, err, order, query)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"qnc-server/app/main/api/internal/config"
|
"qnc-server/app/main/api/internal/config"
|
||||||
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
|
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
|
||||||
"qnc-server/app/main/model"
|
"qnc-server/app/main/model"
|
||||||
|
"qnc-server/pkg/lzkit/crypto"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -75,7 +77,8 @@ type APIResponseData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ProcessRequests 处理请求
|
// 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())
|
var ctx, cancel = context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -112,6 +115,25 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]
|
|||||||
if len(featureList) == 0 {
|
if len(featureList) == 0 {
|
||||||
return nil, errors.New("处理请求错误,产品无对应接口功能")
|
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 (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
resultsCh = make(chan APIResponseData, len(featureList))
|
resultsCh = make(chan APIResponseData, len(featureList))
|
||||||
@@ -152,7 +174,7 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]
|
|||||||
tryCount := 0
|
tryCount := 0
|
||||||
for {
|
for {
|
||||||
tryCount++
|
tryCount++
|
||||||
resp, preprocessErr = a.PreprocessRequestApi(params, feature.ApiId)
|
resp, preprocessErr = a.PreprocessRequestApi(paramsWithOrder, feature.ApiId)
|
||||||
if preprocessErr == nil {
|
if preprocessErr == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -224,7 +246,21 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
|
|||||||
"QYGL6F2D": (*ApiRequestService).ProcessQYGL6F2DRequest,
|
"QYGL6F2D": (*ApiRequestService).ProcessQYGL6F2DRequest,
|
||||||
"JRZQ8203": (*ApiRequestService).ProcessJRZQ8203Request,
|
"JRZQ8203": (*ApiRequestService).ProcessJRZQ8203Request,
|
||||||
"JRZQ4AA8": (*ApiRequestService).ProcessJRZQ4AA8Request,
|
"JRZQ4AA8": (*ApiRequestService).ProcessJRZQ4AA8Request,
|
||||||
|
"QCXGGB2Q": (*ApiRequestService).ProcessQCXGGB2QRequest,
|
||||||
|
"QCXGYTS2": (*ApiRequestService).ProcessQCXGYTS2Request,
|
||||||
|
"QCXG5F3A": (*ApiRequestService).ProcessQCXG5F3ARequest,
|
||||||
"QCXG7A2B": (*ApiRequestService).ProcessQCXG7A2BRequest,
|
"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,
|
"DWBG8B4D": (*ApiRequestService).ProcessDWBG8B4DRequest,
|
||||||
"DWBG6A2C": (*ApiRequestService).ProcessDWBG6A2CRequest,
|
"DWBG6A2C": (*ApiRequestService).ProcessDWBG6A2CRequest,
|
||||||
"JRZQ4B6C": (*ApiRequestService).ProcessJRZQ4B6CRequest,
|
"JRZQ4B6C": (*ApiRequestService).ProcessJRZQ4B6CRequest,
|
||||||
@@ -1126,24 +1162,290 @@ func (a *ApiRequestService) ProcessQYGL6F2DRequest(params []byte) ([]byte, error
|
|||||||
return nil, fmt.Errorf("响应code错误%s", code.String())
|
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 名下车辆
|
// ProcessQCXG7A2BRequest 名下车辆
|
||||||
func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) {
|
func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) {
|
||||||
idCard := gjson.GetBytes(params, "id_card")
|
idCard := gjson.GetBytes(params, "id_card")
|
||||||
if !idCard.Exists() {
|
if !idCard.Exists() {
|
||||||
return nil, errors.New("api请求, QCXG7A2B, 获取相关参数失败")
|
return nil, errors.New("api请求, QCXG7A2B, 获取相关参数失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.tianyuanapi.CallInterface("QCXG7A2B", map[string]interface{}{
|
resp, err := a.tianyuanapi.CallInterface("QCXG7A2B", map[string]interface{}{
|
||||||
"id_card": idCard.String(),
|
"id_card": idCard.String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertTianyuanResponse(resp)
|
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 三要素
|
// ProcessYYSY09CDRequest 三要素
|
||||||
func (a *ApiRequestService) ProcessYYSY09CDRequest(params []byte) ([]byte, error) {
|
func (a *ApiRequestService) ProcessYYSY09CDRequest(params []byte) ([]byte, error) {
|
||||||
name := gjson.GetBytes(params, "name")
|
name := gjson.GetBytes(params, "name")
|
||||||
|
|||||||
1984
app/main/api/internal/service/apirequestService2.md
Normal file
1984
app/main/api/internal/service/apirequestService2.md
Normal file
File diff suppressed because it is too large
Load Diff
150
app/main/api/internal/service/tianxingjuhe_sdk/client.go
Normal file
150
app/main/api/internal/service/tianxingjuhe_sdk/client.go
Normal 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
|
||||||
|
}
|
||||||
@@ -7,9 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"qnc-server/app/main/model"
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
"qnc-server/app/main/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TianyuanapiCallLogService 天元API调用记录服务
|
// TianyuanapiCallLogService 天元API调用记录服务
|
||||||
|
|||||||
8276
app/main/api/internal/service/toolboxService.go
Normal file
8276
app/main/api/internal/service/toolboxService.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import (
|
|||||||
"qnc-server/app/main/api/internal/config"
|
"qnc-server/app/main/api/internal/config"
|
||||||
"qnc-server/app/main/api/internal/middleware"
|
"qnc-server/app/main/api/internal/middleware"
|
||||||
"qnc-server/app/main/api/internal/service"
|
"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"
|
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
|
||||||
"qnc-server/app/main/model"
|
"qnc-server/app/main/model"
|
||||||
"time"
|
"time"
|
||||||
@@ -99,6 +100,7 @@ type ServiceContext struct {
|
|||||||
DictService *service.DictService
|
DictService *service.DictService
|
||||||
ImageService *service.ImageService
|
ImageService *service.ImageService
|
||||||
AuthorizationService *service.AuthorizationService
|
AuthorizationService *service.AuthorizationService
|
||||||
|
ToolboxService *service.ToolboxService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServiceContext 创建服务上下文
|
// NewServiceContext 创建服务上下文
|
||||||
@@ -201,6 +203,23 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
dictService := service.NewDictService(adminDictTypeModel, adminDictDataModel)
|
dictService := service.NewDictService(adminDictTypeModel, adminDictDataModel)
|
||||||
imageService := service.NewImageService()
|
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(
|
asynqServer := asynq.NewServer(
|
||||||
asynq.RedisClientOpt{Addr: c.CacheRedis[0].Host, Password: c.CacheRedis[0].Pass},
|
asynq.RedisClientOpt{Addr: c.CacheRedis[0].Host, Password: c.CacheRedis[0].Pass},
|
||||||
@@ -296,6 +315,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
DictService: dictService,
|
DictService: dictService,
|
||||||
ImageService: imageService,
|
ImageService: imageService,
|
||||||
AuthorizationService: authorizationService,
|
AuthorizationService: authorizationService,
|
||||||
|
ToolboxService: toolboxService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2199,6 +2199,10 @@ type RoleListItem struct {
|
|||||||
MenuIds []string `json:"menu_ids"` // 关联的菜单ID列表
|
MenuIds []string `json:"menu_ids"` // 关联的菜单ID列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServeUploadFileReq struct {
|
||||||
|
Name string `path:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
type ShortLinkRedirectResp struct {
|
type ShortLinkRedirectResp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2251,6 +2255,16 @@ type TeamStatisticsResp struct {
|
|||||||
MonthNewMembers int64 `json:"month_new_members"` // 本月新增成员
|
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 {
|
type UpdateMenuReq struct {
|
||||||
Id string `path:"id"` // 菜单ID
|
Id string `path:"id"` // 菜单ID
|
||||||
Pid *string `json:"pid,optional"` // 父菜单ID
|
Pid *string `json:"pid,optional"` // 父菜单ID
|
||||||
@@ -2334,6 +2348,14 @@ type UpgradeSubordinateResp struct {
|
|||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UploadImageReq struct {
|
||||||
|
ImageBase64 string `json:"image_base64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadImageResp struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
|
|||||||
Reference in New Issue
Block a user