Add 'server/' from commit 'e64421295fff83acbb6d6ab3d3b27f3ef8368f00'
git-subtree-dir: server git-subtree-mainline:c4e617ada7git-subtree-split:e64421295f
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const jwtSecret = "slogan-agent-jwt-secret-2026"
|
||||
|
||||
type JwtClaims struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
AgentId int64 `json:"agent_id,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GetJwtSecret() string {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
func ParseToken(tokenStr string) (*JwtClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(*JwtClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
var publicPaths = []string{
|
||||
"/user/login",
|
||||
"/user/register",
|
||||
"/hairstyle/list",
|
||||
"/api.json",
|
||||
"/member/order/notify",
|
||||
}
|
||||
|
||||
func Auth(r *ghttp.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
// 公开路径(精确匹配)
|
||||
for _, p := range publicPaths {
|
||||
if path == p {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// workspace 文件通过前缀匹配放行(浏览器图片请求不带 Authorization)
|
||||
if strings.HasPrefix(path, "/workspace/") {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
||||
r.Response.WriteJson(ghttp.DefaultHandlerResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: "未登录或登录已过期",
|
||||
})
|
||||
r.Exit()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := ParseToken(auth[7:])
|
||||
if err != nil {
|
||||
r.Response.WriteJson(ghttp.DefaultHandlerResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: "登录已过期,请重新登录",
|
||||
})
|
||||
r.Exit()
|
||||
return
|
||||
}
|
||||
|
||||
r.SetCtxVar("userId", claims.UserId)
|
||||
r.SetCtxVar("role", claims.Role)
|
||||
r.SetCtxVar("agentId", claims.AgentId)
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func GetUserId(r *ghttp.Request) int64 {
|
||||
v := r.GetCtxVar("userId")
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
return v.Int64()
|
||||
}
|
||||
|
||||
func GetRole(r *ghttp.Request) string {
|
||||
v := r.GetCtxVar("role")
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return v.String()
|
||||
}
|
||||
|
||||
func GetAgentId(r *ghttp.Request) int64 {
|
||||
v := r.GetCtxVar("agentId")
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
return v.Int64()
|
||||
}
|
||||
|
||||
func CheckAdmin(r *ghttp.Request) bool {
|
||||
return GetRole(r) == "admin"
|
||||
}
|
||||
|
||||
func CheckAgent(r *ghttp.Request) bool {
|
||||
return GetRole(r) == "agent"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func prepareInsertData(data any) map[string]any {
|
||||
m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}})
|
||||
delete(m, "id")
|
||||
m["created_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
m["updated_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
delete(m, "deleted_at")
|
||||
return m
|
||||
}
|
||||
|
||||
func InsertAndReturnId(ctx context.Context, table string, data any) (id int64, err error) {
|
||||
m := prepareInsertData(data)
|
||||
r, err := g.DB().Model(table).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if r == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
|
||||
func GetOneByPk[T any](ctx context.Context, table string, pk int64) (res *T, err error) {
|
||||
r, err := g.DB().Model(table).Ctx(ctx).
|
||||
Cache(gdb.CacheOption{Duration: CacheTTL(), Name: table + "_GetOneByPk_" + gconv.String(pk)}).
|
||||
Where("id", pk).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateByPk(ctx context.Context, table string, pk int64, data any) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Data(data).Where("id", pk).Update()
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteByPk(ctx context.Context, table string, pk int64) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Unscoped().Where("id", pk).Delete()
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var (
|
||||
cacheTTL time.Duration
|
||||
cacheTTLOnce sync.Once
|
||||
)
|
||||
|
||||
// CacheTTL returns the database query cache TTL from config
|
||||
func CacheTTL() time.Duration {
|
||||
cacheTTLOnce.Do(func() {
|
||||
cacheTTL = time.Duration(g.Cfg().MustGet(context.Background(), "database.cache.ttl", 60).Int()) * time.Second
|
||||
})
|
||||
return cacheTTL
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
var allowedImageExt = map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".webp": true}
|
||||
|
||||
// SaveUploadedFile 保存上传文件到 workspace/{subDir},返回访问路径 /workspace/{subDir}/{filename}
|
||||
func SaveUploadedFile(file *ghttp.UploadFile, subDir string) (string, error) {
|
||||
if file == nil {
|
||||
return "", errors.New("未收到文件")
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if !allowedImageExt[ext] {
|
||||
return "", errors.New("仅支持 jpg/jpeg/png/webp 格式")
|
||||
}
|
||||
if file.Size > 10*1024*1024 {
|
||||
return "", errors.New("单张图片不能超过 10MB")
|
||||
}
|
||||
dir := filepath.Join("workspace", subDir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||||
path := filepath.Join(dir, filename)
|
||||
if _, err := file.Save(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "/" + filepath.ToSlash(filepath.Join("workspace", subDir, filename)), nil
|
||||
}
|
||||
|
||||
// RemoveWorkspaceFile 删除 workspace 下文件(路径穿越防护)
|
||||
func RemoveWorkspaceFile(url string) error {
|
||||
rel := strings.TrimPrefix(url, "/workspace/")
|
||||
if rel == "" || strings.Contains(rel, "..") {
|
||||
return errors.New("非法文件路径")
|
||||
}
|
||||
abs := filepath.Join("workspace", rel)
|
||||
if _, err := os.Stat(abs); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(abs)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
var Httpserver = g.Server()
|
||||
|
||||
func init() {
|
||||
err := gtime.SetTimeZone("Asia/Shanghai")
|
||||
if err != nil {
|
||||
panic("设置时区失败")
|
||||
}
|
||||
Httpserver.SetOpenApiPath("/api.json")
|
||||
// 全局 panic 恢复(最先注册,作为最外层包裹)
|
||||
Httpserver.BindMiddlewareDefault(ghttp.MiddlewareHandlerResponse)
|
||||
// CORS - allow all origins
|
||||
Httpserver.BindMiddlewareDefault(func(r *ghttp.Request) {
|
||||
r.Response.CORS(r.Response.DefaultCORSOptions())
|
||||
r.Middleware.Next()
|
||||
})
|
||||
// JWT 鉴权
|
||||
Httpserver.BindMiddlewareDefault(Auth)
|
||||
}
|
||||
|
||||
// RouteRegister 根据控制器结构体名称自动注册路由
|
||||
func RouteRegister(controllers []interface{}) {
|
||||
re := regexp.MustCompile("[A-Z]")
|
||||
for _, t := range controllers {
|
||||
sName := reflect.ValueOf(t).Elem().Type().Name()
|
||||
convertedStr := re.ReplaceAllStringFunc(sName, func(s string) string {
|
||||
return fmt.Sprintf("-%s", strings.ToLower(s))
|
||||
})
|
||||
convertedStr = strings.ReplaceAll(convertedStr, "_", "-")
|
||||
if len(convertedStr) > 0 && convertedStr[0] == '-' {
|
||||
convertedStr = convertedStr[1:]
|
||||
}
|
||||
Httpserver.Group("/"+convertedStr, func(group *ghttp.RouterGroup) {
|
||||
group.Bind(t)
|
||||
})
|
||||
}
|
||||
go Httpserver.Run()
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ImageFileToBase64 reads an image file and returns a data:image/...;base64 string.
|
||||
func ImageFileToBase64(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext := strings.ToLower(pathExt(path))
|
||||
mime := "image/png"
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
mime = "image/jpeg"
|
||||
case ".gif":
|
||||
mime = "image/gif"
|
||||
case ".webp":
|
||||
mime = "image/webp"
|
||||
}
|
||||
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
// pathExt extracts the extension from a path.
|
||||
func pathExt(path string) string {
|
||||
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
|
||||
if path[i] == '.' {
|
||||
return path[i:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BuildSchemaRequest validates input values against a JSON schema definition,
|
||||
// fills in default values for missing optional fields,
|
||||
// and returns the result matching the schema's nested structure.
|
||||
//
|
||||
// The schema format follows test.json convention:
|
||||
//
|
||||
// {
|
||||
// "section": {
|
||||
// "field_name": {
|
||||
// "type": "string|integer|number|boolean|array|object",
|
||||
// "required": true|false,
|
||||
// "default": value,
|
||||
// "enum": [...],
|
||||
// "min": number,
|
||||
// "max": number,
|
||||
// "max_chars": number,
|
||||
// "min_items": number,
|
||||
// "max_items": number
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// input is a flat map like {"prompt": "hello", "duration": 5}.
|
||||
// Fields not present in input but with a "default" in the schema are filled automatically.
|
||||
// Nodes without "type" are treated as grouping sections and recursed into.
|
||||
// When validate is false, required/type/range/enum checks are skipped (only structure + defaults).
|
||||
func BuildSchemaRequest(schema map[string]any, input map[string]any, validate bool) (map[string]any, error) {
|
||||
result := make(map[string]any)
|
||||
for key, val := range schema {
|
||||
fieldDef, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
result[key] = val
|
||||
continue
|
||||
}
|
||||
if _, hasType := fieldDef["type"]; hasType {
|
||||
processed, err := processField(key, fieldDef, input, validate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if processed != nil {
|
||||
result[key] = processed
|
||||
}
|
||||
continue
|
||||
}
|
||||
nested, err := BuildSchemaRequest(fieldDef, input, validate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nested) > 0 {
|
||||
result[key] = nested
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func processField(name string, def map[string]any, input map[string]any, validate bool) (any, error) {
|
||||
fieldType, _ := def["type"].(string)
|
||||
required, _ := def["required"].(bool)
|
||||
|
||||
rawVal, exists := input[name]
|
||||
if !exists {
|
||||
if validate && required {
|
||||
return nil, fmt.Errorf("%s", def["description"])
|
||||
}
|
||||
if dflt, ok := def["default"]; ok {
|
||||
return convertDefault(dflt, fieldType), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !validate {
|
||||
return rawVal, nil
|
||||
}
|
||||
|
||||
switch fieldType {
|
||||
case "string":
|
||||
s, ok := rawVal.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' must be a string", name)
|
||||
}
|
||||
if maxChars, ok := def["max_chars"].(float64); ok && len([]rune(s)) > int(maxChars) {
|
||||
return nil, fmt.Errorf("'%s' exceeds max length of %d", name, int(maxChars))
|
||||
}
|
||||
if enum, ok := def["enum"].([]any); ok && len(enum) > 0 {
|
||||
if !containsValue(enum, s) {
|
||||
return nil, fmt.Errorf("'%s' must be one of %v", name, enum)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
|
||||
case "integer":
|
||||
v, err := toInt(rawVal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s' must be an integer", name)
|
||||
}
|
||||
if minVal, ok := def["min"].(float64); ok && v < int(minVal) {
|
||||
return nil, fmt.Errorf("'%s' must be >= %d", name, int(minVal))
|
||||
}
|
||||
if maxVal, ok := def["max"].(float64); ok && v > int(maxVal) {
|
||||
return nil, fmt.Errorf("'%s' must be <= %d", name, int(maxVal))
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case "number":
|
||||
v, ok := rawVal.(float64)
|
||||
if !ok {
|
||||
if iv, err := toInt(rawVal); err == nil {
|
||||
v = float64(iv)
|
||||
} else {
|
||||
return nil, fmt.Errorf("'%s' must be a number", name)
|
||||
}
|
||||
}
|
||||
if minVal, ok := def["min"].(float64); ok && v < minVal {
|
||||
return nil, fmt.Errorf("'%s' must be >= %v", name, minVal)
|
||||
}
|
||||
if maxVal, ok := def["max"].(float64); ok && v > maxVal {
|
||||
return nil, fmt.Errorf("'%s' must be <= %v", name, maxVal)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case "boolean":
|
||||
_, ok := rawVal.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' must be a boolean", name)
|
||||
}
|
||||
return rawVal, nil
|
||||
|
||||
case "array":
|
||||
arr, ok := rawVal.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' must be an array", name)
|
||||
}
|
||||
if minItems, ok := def["min_items"].(float64); ok && len(arr) < int(minItems) {
|
||||
return nil, fmt.Errorf("'%s' must have at least %d items", name, int(minItems))
|
||||
}
|
||||
if maxItems, ok := def["max_items"].(float64); ok && len(arr) > int(maxItems) {
|
||||
return nil, fmt.Errorf("'%s' must have at most %d items", name, int(maxItems))
|
||||
}
|
||||
if itemsDef, ok := def["items"].(map[string]any); ok {
|
||||
items, err := processArrayItems(arr, itemsDef, validate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s': %w", name, err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
return rawVal, nil
|
||||
}
|
||||
|
||||
func processArrayItems(arr []any, itemsDef map[string]any, validate bool) ([]any, error) {
|
||||
itemType, _ := itemsDef["type"].(string)
|
||||
if itemType != "object" {
|
||||
return arr, nil
|
||||
}
|
||||
props, _ := itemsDef["properties"].(map[string]any)
|
||||
if props == nil {
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
result := make([]any, len(arr))
|
||||
for i, item := range arr {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
result[i] = item
|
||||
continue
|
||||
}
|
||||
processed, err := BuildSchemaRequest(props, itemMap, validate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("item[%d]: %w", i, err)
|
||||
}
|
||||
result[i] = processed
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toInt(v any) (int, error) {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return int(val), nil
|
||||
case int:
|
||||
return val, nil
|
||||
case int64:
|
||||
return int(val), nil
|
||||
case json.Number:
|
||||
n, err := val.Int64()
|
||||
return int(n), err
|
||||
default:
|
||||
return 0, fmt.Errorf("cannot convert %T to int", v)
|
||||
}
|
||||
}
|
||||
|
||||
func convertDefault(dflt any, fieldType string) any {
|
||||
if fieldType == "integer" {
|
||||
if f, ok := dflt.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return dflt
|
||||
}
|
||||
|
||||
func containsValue(arr []any, val any) bool {
|
||||
for _, v := range arr {
|
||||
if v == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user