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

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"
}