Add 'server/' from commit 'e64421295fff83acbb6d6ab3d3b27f3ef8368f00'

git-subtree-dir: server
git-subtree-mainline: c4e617ada7
git-subtree-split: e64421295f
This commit is contained in:
2026-08-04 15:02:35 +08:00
176 changed files with 13532 additions and 0 deletions
+34
View File
@@ -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
}
+91
View File
@@ -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"
}
+55
View File
@@ -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
}
+22
View File
@@ -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
}
+51
View File
@@ -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)
}
+50
View File
@@ -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()
}
+249
View File
@@ -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
}