135 lines
4.0 KiB
Go
135 lines
4.0 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"path/filepath"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
commonHttp "video-factory/common"
|
||
|
||
"video-factory/shortdrama/controller"
|
||
"video-factory/shortdrama/service"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/net/ghttp"
|
||
"github.com/gogf/gf/v2/util/gvalid"
|
||
|
||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||
)
|
||
|
||
func init() {
|
||
gvalid.RegisterRule("password-complex", func(ctx context.Context, input gvalid.RuleFuncInput) error {
|
||
pwd := input.Value.String()
|
||
var hasUpper, hasLower, hasDigit, hasSpecial bool
|
||
for _, c := range pwd {
|
||
switch {
|
||
case c >= 'A' && c <= 'Z':
|
||
hasUpper = true
|
||
case c >= 'a' && c <= 'z':
|
||
hasLower = true
|
||
case c >= '0' && c <= '9':
|
||
hasDigit = true
|
||
default:
|
||
hasSpecial = true
|
||
}
|
||
}
|
||
if !hasUpper {
|
||
return errors.New("密码必须包含大写字母")
|
||
}
|
||
if !hasLower {
|
||
return errors.New("密码必须包含小写字母")
|
||
}
|
||
if !hasDigit {
|
||
return errors.New("密码必须包含数字")
|
||
}
|
||
if !hasSpecial {
|
||
return errors.New("密码必须包含特殊符号")
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func main() {
|
||
// ==================== API 路由(通过 RouteRegister 自动注册,遵循 ai-agent 规范) ====================
|
||
commonHttp.RouteRegister([]interface{}{
|
||
controller.Drama,
|
||
controller.Scene,
|
||
controller.Character,
|
||
controller.Prop,
|
||
controller.Bgm,
|
||
controller.Episode,
|
||
controller.Generation,
|
||
controller.ModelConfig,
|
||
controller.UserModelConfig,
|
||
controller.User,
|
||
controller.Agent,
|
||
controller.Customer,
|
||
controller.Transaction,
|
||
controller.PaymentOrder,
|
||
controller.PaymentChannelTrade,
|
||
controller.PaymentConfig,
|
||
controller.RegionPricing,
|
||
})
|
||
|
||
// ==================== Workspace 文件服务(鉴权保护) ====================
|
||
// 通过 BindHandler 代替 AddStaticPath,确保经过 JWT 中间件鉴权
|
||
commonHttp.Httpserver.BindHandler("/workspace/*", func(r *ghttp.Request) {
|
||
relPath := strings.TrimPrefix(r.URL.Path, "/workspace/")
|
||
if relPath == "" || strings.Contains(relPath, "..") {
|
||
r.Response.WriteStatus(http.StatusForbidden)
|
||
return
|
||
}
|
||
filePath := filepath.Join("workspace", relPath)
|
||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||
r.Response.WriteStatus(http.StatusNotFound)
|
||
return
|
||
}
|
||
r.Response.ServeFile(filePath)
|
||
})
|
||
|
||
// ==================== 前端静态资源服务(SPA,前后端合并部署) ====================
|
||
// 静态资源目录为 ui-src/dist:本地是 npm run build 产物,Docker 运行镜像由
|
||
// ui-builder 阶段将 dist 注入 /app/ui-src/dist,本地与容器路径一致,无需拷贝。
|
||
// 不用 AddStaticPath:GF 静态前缀有边界检查(前缀后一字符必须为 /),
|
||
// "/" 前缀无法命中 /assets/* 子路径,只能服务 "/" 本身。
|
||
// 前端使用 hash 路由,浏览器只请求 / 与 /assets/*,BindHandler("/*") 精确覆盖即可,
|
||
// 无需 SPA fallback;具体 API 路由与 /workspace/* 优先级更高不受影响,
|
||
// 且 auth 中间件仅放行 GET / 与 GET /assets/*,其余路径一律走鉴权
|
||
if st, err := os.Stat("ui-src/dist"); err == nil && st.IsDir() {
|
||
commonHttp.Httpserver.BindHandler("/*", func(r *ghttp.Request) {
|
||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||
if strings.Contains(path, "..") {
|
||
r.Response.WriteStatus(http.StatusNotFound)
|
||
return
|
||
}
|
||
if path == "" || path == "index.html" {
|
||
path = "index.html"
|
||
}
|
||
filePath := filepath.Join("ui-src/dist", path)
|
||
if st, err := os.Stat(filePath); err == nil && !st.IsDir() {
|
||
r.Response.ServeFile(filePath)
|
||
return
|
||
}
|
||
r.Response.WriteStatus(http.StatusNotFound)
|
||
})
|
||
}
|
||
|
||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||
defer stop()
|
||
|
||
// 启动时恢复未完成的视频生成轮询
|
||
service.GenerationService.StartVideoPoller(ctx)
|
||
|
||
g.Log().Info(ctx, "service started on :3006")
|
||
|
||
<-ctx.Done()
|
||
g.Log().Info(ctx, "shutting down...")
|
||
time.Sleep(3 * time.Second) // 等待当前任务完成
|
||
g.Log().Info(ctx, "bye")
|
||
}
|