1
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const jwtSecret = "video-factory-jwt-secret-2024"
|
||||
|
||||
func GetJwtSecret() string {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
type JwtClaims struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
AgentId int64 `json:"agent_id,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package middleware
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
@@ -34,7 +32,7 @@ func Auth(r *ghttp.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := service.UserService.ParseToken(auth[7:])
|
||||
claims, err := ParseToken(auth[7:])
|
||||
if err != nil {
|
||||
r.Response.WriteJson(ghttp.DefaultHandlerResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
@@ -0,0 +1,83 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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).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
|
||||
}
|
||||
|
||||
func DeleteByDrama(ctx context.Context, table string, dramaId int64) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
func ListPageByDrama[T any](ctx context.Context, table string, dramaId int64, page, pageSize int) (res []*T, total int, err error) {
|
||||
m := g.DB().Model(table).Ctx(ctx).Where("drama_id", dramaId)
|
||||
if pageSize == -1 {
|
||||
r, err := m.OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res = make([]*T, 0)
|
||||
err = r.Structs(&res)
|
||||
return res, len(res), err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make([]*T, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package http
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"video-factory/shortdrama/middleware"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
@@ -29,7 +27,7 @@ func init() {
|
||||
r.Middleware.Next()
|
||||
})
|
||||
// JWT 鉴权
|
||||
Httpserver.BindMiddlewareDefault(middleware.Auth)
|
||||
Httpserver.BindMiddlewareDefault(Auth)
|
||||
}
|
||||
|
||||
// RouteRegister 根据控制器结构体名称自动注册路由
|
||||
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"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 ""
|
||||
}
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
# Video Factory 项目文档
|
||||
|
||||
## 概述
|
||||
|
||||
Video Factory 是一个 AI 驱动的短视频自动生成平台。用户创建短剧项目,配置演员/场景/道具等素材,通过 AI 生成剧集脚本并调用视频生成 API 自动产出视频。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|---|------|
|
||||
| 语言 | Go 1.22+ |
|
||||
| Web 框架 | GoFrame v2 (github.com/gogf/gf/v2) |
|
||||
| 数据库 | SQLite(通过 GoFrame ORM) |
|
||||
| 认证 | JWT (golang-jwt/jwt/v5) |
|
||||
| 密码 | bcrypt (golang.org/x/crypto/bcrypt) |
|
||||
| AI 模型 | OpenAI 兼容 API(可对接 DeepSeek/Kimi/Qwen 等) |
|
||||
| 前端 | Vue 3 + Vite(video-factory-ui) |
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
video-factory/
|
||||
├── main.go # 入口:路由注册、静态文件、启动轮询
|
||||
├── common/
|
||||
│ └── http/
|
||||
│ └── http.go # RouteRegister — 自动注册路由
|
||||
├── shortdrama/
|
||||
│ ├── controller/ # HTTP 控制器层(17 个)
|
||||
│ │ ├── drama_controller.go # 短剧 CRUD
|
||||
│ │ ├── scene_controller.go # 场景 CRUD
|
||||
│ │ ├── character_controller.go # 演员 CRUD
|
||||
│ │ ├── prop_controller.go # 道具 CRUD
|
||||
│ │ ├── bgm_controller.go # 背景音 CRUD
|
||||
│ │ ├── episode_controller.go # 剧集 CRUD + 脚本生成
|
||||
│ │ ├── generation_controller.go # 视频生成/轮询/分段
|
||||
│ │ ├── user_controller.go # 用户登录/改密
|
||||
│ │ ├── agent_controller.go # 代理商管理
|
||||
│ │ ├── customer_controller.go # 客户管理
|
||||
│ │ ├── transaction_controller.go # 交易记录
|
||||
│ │ ├── payment_order_controller.go # 支付订单
|
||||
│ │ ├── payment_channel_trade_controller.go # 支付渠道流水
|
||||
│ │ ├── payment_config_controller.go # 支付配置
|
||||
│ │ ├── model_config_controller.go # 模型配置
|
||||
│ │ ├── user_model_config_controller.go # 用户模型配置
|
||||
│ │ └── region_pricing_controller.go # 区域定价
|
||||
│ ├── service/ # 业务逻辑层(17 个)
|
||||
│ │ ├── drama_service.go # 短剧 CRUD + 工作空间管理
|
||||
│ │ ├── scene_service.go # 场景
|
||||
│ │ ├── character_service.go # 演员(含文件保存)
|
||||
│ │ ├── prop_service.go # 道具
|
||||
│ │ ├── background_music_service.go # 背景音
|
||||
│ │ ├── episode_service.go # 剧集 + AI 脚本生成
|
||||
│ │ ├── generation_service.go # 视频生成核心流水线
|
||||
│ │ ├── user_service.go # 登录/JWT
|
||||
│ │ ├── agent_service.go # 代理商
|
||||
│ │ ├── customer_service.go # 客户
|
||||
│ │ ├── transaction_service.go # 交易
|
||||
│ │ ├── payment_order_service.go # 支付(微信/支付宝/线下)
|
||||
│ │ ├── payment_channel_trade_service.go # 渠道流水
|
||||
│ │ ├── payment_config_service.go # 支付配置
|
||||
│ │ ├── model_config_service.go # 模型配置(缓存)
|
||||
│ │ ├── user_model_config_service.go # 用户模型配置(合并系统+用户)
|
||||
│ │ └── region_pricing_service.go # 区域定价(缓存)
|
||||
│ ├── dao/ # 数据访问层(17 个,每表一个)
|
||||
│ │ ├── drama_dao.go
|
||||
│ │ ├── scene_dao.go
|
||||
│ │ ├── character_dao.go
|
||||
│ │ ├── episode_dao.go
|
||||
│ │ ├── prop_dao.go
|
||||
│ │ ├── background_music_dao.go
|
||||
│ │ ├── generation_task_dao.go
|
||||
│ │ ├── user_dao.go
|
||||
│ │ ├── agent_profile_dao.go
|
||||
│ │ ├── customer_profile_dao.go
|
||||
│ │ ├── account_transaction_dao.go
|
||||
│ │ ├── payment_order_dao.go
|
||||
│ │ ├── payment_channel_trade_dao.go
|
||||
│ │ ├── payment_config_dao.go
|
||||
│ │ ├── model_config_dao.go
|
||||
│ │ ├── user_model_config_dao.go
|
||||
│ │ └── region_pricing_dao.go
|
||||
│ ├── model/
|
||||
│ │ ├── entity/ # 数据库实体(每表一个)
|
||||
│ │ ├── dto/ # 请求/响应结构体(含 g.Meta 路由信息)
|
||||
│ │ ├── domain/ # 领域模型
|
||||
│ │ │ └── shot.go # 镜头模型 + 镜头转文本方法
|
||||
│ │ ├── agent_output.go # AI Agent 输出解析
|
||||
│ │ └── segment_output.go # 分段生成输出类型
|
||||
│ ├── agent/ # AI Agent 模块
|
||||
│ │ ├── types.go # 类型定义
|
||||
│ │ ├── chat_model.go # LLM API 调用(OpenAI 兼容)
|
||||
│ │ ├── react_agent.go # ReAct 智能体引擎
|
||||
│ │ ├── tools.go # Agent 工具集
|
||||
│ │ └── context.go # Agent 上下文
|
||||
│ ├── middleware/
|
||||
│ │ └── auth_middleware.go # JWT 鉴权中间件
|
||||
│ ├── config/
|
||||
│ │ └── shot_duration.go # 镜头时长提示词
|
||||
│ └── consts/
|
||||
│ ├── public/
|
||||
│ │ ├── table_name.go # 数据库表名常量
|
||||
│ │ └── content_type.go # 内容类型/字段定义
|
||||
│ └── status.go # 剧集/任务状态常量
|
||||
└── workspace/ # 上传文件存储目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构模式
|
||||
|
||||
分层结构:**Controller → Service → DAO → SQLite**
|
||||
|
||||
```
|
||||
HTTP 请求
|
||||
↓
|
||||
Controller(接收请求、参数校验、返回响应)
|
||||
↓
|
||||
Service(业务逻辑、事务管理、AI 调用)
|
||||
↓
|
||||
DAO(数据访问、ORM 操作)
|
||||
↓
|
||||
SQLite
|
||||
```
|
||||
|
||||
- 每一层都是独立的包,通过包级变量暴露单例(如 `var DramaService = new(dramaService)`)
|
||||
- 每张数据库表对应一个 DAO、一个 Service、一个 Controller(17×17 一对一映射)
|
||||
|
||||
### 路由注册机制
|
||||
|
||||
`RouteRegister`(`common/http/http.go`)通过反射获取结构体名,转为 kebab-case 作为 URL 前缀:
|
||||
|
||||
```go
|
||||
type scene struct{} // → 前缀 /scene
|
||||
type modelConfig struct{} // → 前缀 /model-config
|
||||
```
|
||||
|
||||
GoFrame v2 将 Controller 方法名转为 kebab-case 拼接到前缀后:
|
||||
|
||||
```go
|
||||
// scene_controller.go
|
||||
func (c *scene) List(ctx, req) → GET /scene/list
|
||||
func (c *scene) Add(ctx, req) → POST /scene/add
|
||||
```
|
||||
|
||||
完整路由表(详见下方 API 章节)。
|
||||
|
||||
---
|
||||
|
||||
## 数据库
|
||||
|
||||
使用 SQLite,`init()` 函数自动建表,支持旧表迁移(ALTER TABLE ADD COLUMN 兼容)。
|
||||
|
||||
### 短剧相关表(6 张)
|
||||
|
||||
| 表名 | 实体 | 说明 |
|
||||
|------|------|------|
|
||||
| `short_drama` | Drama | 短剧项目(标题/类型/时长/分辨率/配置) |
|
||||
| `short_drama_episode` | Episode | 剧集(序号/标题/脚本/状态/视频URL) |
|
||||
| `short_drama_character` | Character | 演员(名称/描述/声音文件/形象文件) |
|
||||
| `short_drama_scene` | Scene | 场景(名称/描述/图片) |
|
||||
| `short_drama_prop` | Prop | 道具(名称/描述/图片) |
|
||||
| `short_drama_background_music` | BackgroundMusic | 背景音乐(名称/文件) |
|
||||
|
||||
### 生成任务表(1 张)
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `short_drama_generation_task` | 视频生成任务(剧集/分段/状态/任务ID/脚本/模型名) |
|
||||
|
||||
### 用户/代理商/客户表(3 张)
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `user` | 用户(角色:admin/agent/customer) |
|
||||
| `agent_profile` | 代理商资料(最大客户数/到期时间/区域保护) |
|
||||
| `customer_profile` | 客户资料(关联代理商/余额) |
|
||||
|
||||
### 支付相关表(3 张)
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `payment_order` | 支付订单(用户/金额/渠道/状态/类型) |
|
||||
| `payment_channel_trade` | 渠道流水(支付单号/渠道单号/状态) |
|
||||
| `payment_config` | 支付配置(渠道/商户ID/密钥) |
|
||||
|
||||
### 其他表(4 张)
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `short_drama_model_config` | 模型配置(系统级,模型列表/参数) |
|
||||
| `user_model_config` | 用户模型配置(用户自定义 API Key/模型选择) |
|
||||
| `account_transaction` | 账户交易流水(充值/扣费) |
|
||||
| `region_pricing` | 区域定价(省份/地区/套餐/价格) |
|
||||
|
||||
---
|
||||
|
||||
## API 路由表
|
||||
|
||||
所有请求均需 JWT 鉴权(除 `/user/login`),统一 JSON 响应格式 `{"code":0,"message":"OK","data":...}`。
|
||||
|
||||
### 短剧管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/drama/list` | 短剧列表(分页) |
|
||||
| POST | `/drama/create` | 创建短剧 |
|
||||
| GET | `/drama/get` | 短剧详情(含关联演员/场景/道具/背景音) |
|
||||
| POST | `/drama/update` | 更新短剧 |
|
||||
| POST | `/drama/delete` | 删除短剧(级联删除关联数据) |
|
||||
| GET | `/drama/field-definitions` | 获取内容类型字段定义 |
|
||||
|
||||
### 场景
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/scene/list` | 场景列表 |
|
||||
| POST | `/scene/add` | 添加场景(支持上传图片) |
|
||||
| POST | `/scene/update` | 更新场景 |
|
||||
| POST | `/scene/delete` | 删除场景 |
|
||||
|
||||
### 演员
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/character/list` | 演员列表 |
|
||||
| POST | `/character/add` | 添加演员(支持上传声音/形象) |
|
||||
| POST | `/character/update` | 更新演员 |
|
||||
| POST | `/character/delete` | 删除演员 |
|
||||
|
||||
### 道具
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/prop/list` | 道具列表 |
|
||||
| POST | `/prop/add` | 添加道具 |
|
||||
| POST | `/prop/update` | 更新道具 |
|
||||
| POST | `/prop/delete` | 删除道具 |
|
||||
|
||||
### 背景音乐
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/bgm/list` | 背景音乐列表 |
|
||||
| POST | `/bgm/add` | 添加背景音乐 |
|
||||
| POST | `/bgm/update` | 更新背景音乐 |
|
||||
| POST | `/bgm/delete` | 删除背景音乐 |
|
||||
|
||||
### 剧集
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/episode/list` | 剧集列表(分页) |
|
||||
| POST | `/episode/add` | 添加剧集 |
|
||||
| POST | `/episode/update` | 更新剧集 |
|
||||
| POST | `/episode/delete` | 删除剧集 |
|
||||
| POST | `/episode/generate-script` | AI 生成脚本 |
|
||||
|
||||
### 视频生成
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/generation/generate` | 提交一集视频生成任务 |
|
||||
| GET | `/generation/poll` | 轮询生成状态 |
|
||||
| GET | `/generation/episode/task` | 获取剧集所有生成任务 |
|
||||
| POST | `/generation/segment/continue` | 继续分段生成(含反馈) |
|
||||
| POST | `/generation/segment/feedback` | 分段反馈 |
|
||||
|
||||
### 用户
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/user/login` | 登录(公开路径) |
|
||||
| POST | `/user/change-password` | 修改密码 |
|
||||
|
||||
### 模型配置
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/model-config/get-model-list` | 模型列表 |
|
||||
| POST | `/model-config/save-model-config` | 保存模型配置 |
|
||||
| GET | `/user-model-config/get-user-config` | 获取用户模型配置 |
|
||||
| POST | `/user-model-config/save-user-config` | 保存用户模型配置 |
|
||||
| GET | `/user-model-config/get-user-model-list` | 用户可用模型列表 |
|
||||
|
||||
### 支付
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/payment-order/prepay` | 创建支付订单 |
|
||||
| GET | `/payment-order/status` | 查询支付状态 |
|
||||
| POST | `/payment-order/confirm-offline` | 线下确认 |
|
||||
| ALL | `/payment-order/notify-wechat` | 微信回调 |
|
||||
| ALL | `/payment-order/notify-alipay` | 支付宝回调 |
|
||||
| GET | `/payment-channel-trade/list-by-order` | 订单渠道流水 |
|
||||
| GET | `/payment-config/get` | 支付配置 |
|
||||
| POST | `/payment-config/save` | 保存支付配置 |
|
||||
|
||||
### 代理商/客户/交易
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/agent/list` | 代理商列表 |
|
||||
| POST | `/agent/create` | 创建代理商 |
|
||||
| POST | `/agent/update` | 更新代理商 |
|
||||
| ALL | `/agent/get` | 代理商详情 |
|
||||
| POST | `/agent/create-renew-order` | 创建续费订单 |
|
||||
| GET | `/agent/list-renewals` | 续费记录 |
|
||||
| GET | `/customer/list` | 客户列表 |
|
||||
| POST | `/customer/create` | 创建客户 |
|
||||
| POST | `/customer/update` | 更新客户 |
|
||||
| ALL | `/customer/detail` | 客户详情 |
|
||||
| GET | `/transaction/list` | 交易流水 |
|
||||
| POST | `/transaction/recharge` | 充值 |
|
||||
|
||||
### 区域定价
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/region-pricing/list` | 定价列表 |
|
||||
| POST | `/region-pricing/save` | 保存定价 |
|
||||
| ALL | `/region-pricing/delete` | 删除定价 |
|
||||
| ALL | `/region-pricing/regions` | 区域列表 |
|
||||
| ALL | `/region-pricing/cascades` | 级联数据 |
|
||||
|
||||
---
|
||||
|
||||
## 核心业务流程
|
||||
|
||||
### 1. 创建短剧
|
||||
|
||||
```
|
||||
用户创建短剧 → 填写标题/类型/时长 → 添加演员/场景/道具/背景音 → 添加剧集
|
||||
```
|
||||
|
||||
- 短剧内容类型:短剧、漫剧、广告视频
|
||||
- 创建时自动创建 workspace 目录结构
|
||||
|
||||
### 2. 生成脚本
|
||||
|
||||
```
|
||||
用户选择剧集 → 点击"生成脚本"
|
||||
→ 系统加载短剧上下文(演员/场景/道具列表)
|
||||
→ 调用 LLM chat completion(简单对话,非 ReAct)
|
||||
→ 模型返回 JSON 格式的镜头数组(Shot[])
|
||||
→ 解析验证,失败则重试修正
|
||||
```
|
||||
|
||||
每个镜头包含:时间起止、画面描述、台词、旁白、运镜、景别、出演人物、场景、道具。
|
||||
|
||||
### 3. 生成视频(核心流程)
|
||||
|
||||
```
|
||||
用户选择剧集 → 点击"生成视频"
|
||||
┌─ 1. 余额检查(客户角色)并预扣费
|
||||
├─ 2. 按视频模型约束将剧集时长拆分为多段(15-60s/段)
|
||||
├─ 3. 并行或串行生成每段:
|
||||
│ ├─ a. 如果脚本是 JSON 镜头格式 → 直接按时间提取段内容
|
||||
│ ├─ b. 否则 → ReAct Agent 循环(最多 15 步):
|
||||
│ │ 思考 → 工具调用 → 观察结果 → 最终输出
|
||||
│ │ 工具:parse_script / analyze_script_for_episode / generate_scene_image
|
||||
│ ├─ c. 解析 Agent 输出为 SegmentOutput
|
||||
│ ├─ d. 提取角色/场景参考图片
|
||||
│ ├─ e. 构建视频 API 请求体并提交
|
||||
│ └─ f. 保存 video_task_id
|
||||
├─ 4. 串行模式下等待前段完成(用于首帧参考)
|
||||
└─ 5. 后台轮询(每 15s)检查视频生成状态
|
||||
```
|
||||
|
||||
#### 分段策略(`calcSegDurs`)
|
||||
|
||||
根据视频模型的最大时长限制,将整集拆分为若干段:
|
||||
- 短剧(默认):每段 15-60 秒
|
||||
- 漫剧:每段 15-30 秒
|
||||
- 广告视频:整段 15-60 秒
|
||||
|
||||
#### ReAct Agent 工具
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `parse_script` | 将原始剧本按 `---` 拆分为多集,提取标题和内容 |
|
||||
| `analyze_script_for_episode` | 分析单集剧本,按空行拆分为场景,识别出场演员 |
|
||||
| `generate_scene_image` | 从场景库中匹配图片返回 base64 |
|
||||
|
||||
### 4. 视频模型对接
|
||||
|
||||
项目通过 OpenAI 兼容 API 对接视频生成模型。流程:
|
||||
|
||||
1. 构建请求体(含角色参考图URL、场景图片、脚本描述)
|
||||
2. POST 提交视频生成任务
|
||||
3. 轮询任务状态(pending → generating → completed/failed)
|
||||
4. 完成后拼接各段视频为最终输出
|
||||
|
||||
---
|
||||
|
||||
## 权限与角色
|
||||
|
||||
| 角色 | 说明 |
|
||||
|------|------|
|
||||
| `admin` | 管理员,查看所有数据 |
|
||||
| `agent` | 代理商,管理名下客户,查看区域客户数据 |
|
||||
| `customer` | 客户,仅查看自己的数据,生成视频扣费 |
|
||||
|
||||
登录默认账号:
|
||||
- admin / Tongli686^*^(管理员)
|
||||
- test1 / Tongli686^*^(代理商)
|
||||
|
||||
---
|
||||
|
||||
## AI Agent 模块
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
agent/
|
||||
├── types.go # ChatMessage/ToolInfo/ToolCall 类型定义
|
||||
├── chat_model.go # OpenAI 兼容 API 调用(含重试/限流处理)
|
||||
├── react_agent.go # ReAct 循环引擎(思考→行动→观察→重复)
|
||||
├── tools.go # 工具注册与实现
|
||||
└── context.go # 上下文(DramaID 注入)
|
||||
```
|
||||
|
||||
### 调用模式
|
||||
|
||||
**简单 Chat Completion**(脚本生成用):
|
||||
```
|
||||
System Prompt + User Input → LLM → JSON 输出
|
||||
```
|
||||
|
||||
**ReAct Agent**(视频生成用):
|
||||
```
|
||||
System Prompt + User Input
|
||||
→ LLM 思考 → 工具调用(parse_script) → 观察结果
|
||||
→ LLM 思考 → 工具调用(analyze_script_for_episode) → 观察结果
|
||||
→ LLM 思考 → 工具调用(generate_scene_image) → 观察结果
|
||||
→ LLM 最终回答 → 输出 JSON
|
||||
```
|
||||
|
||||
最大 15 步循环,支持指数退避重试。
|
||||
|
||||
---
|
||||
|
||||
## JWT 认证
|
||||
|
||||
- Token 在 `/user/login` 获取
|
||||
- 所有请求(除 `/user/login`)需在 Header 携带 `Authorization: Bearer <token>`
|
||||
- 中间件从 token 解析 `userId`、`role`、`agentId` 注入请求上下文
|
||||
- 过期时间默认 7 天
|
||||
|
||||
---
|
||||
|
||||
## 文件存储
|
||||
|
||||
上传文件统一存储在 `workspace/{短剧标题}/` 目录下:
|
||||
|
||||
```
|
||||
workspace/{短剧标题}/
|
||||
├── 产出视频/
|
||||
├── 演员形象/
|
||||
├── 演员声音/
|
||||
├── 场景/
|
||||
├── 道具/
|
||||
└── 背景音乐/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
- `config.yml`:镜头时长规则(按内容类型)、画风约束
|
||||
- `.env`:开发环境代理目标(前端项目)
|
||||
- 用户模型配置存数据库 `short_drama_model_config` 和 `user_model_config`
|
||||
|
||||
### 模型配置继承
|
||||
|
||||
```
|
||||
系统模型配置(short_drama_model_config)
|
||||
└── 用户模型配置(user_model_config,可选覆盖 API Key/模型选择)
|
||||
└── GetMergedConfig() 合并两者,用户配置优先
|
||||
```
|
||||
|
||||
系统配置 + 用户配置 → `MergedModelConfig`(运行时最终配置)
|
||||
@@ -0,0 +1,251 @@
|
||||
# Video Factory 后端项目缺陷与优化说明
|
||||
|
||||
> 基于当前代码实际扫描(2026-07-24 最新)。已在此会话中修复的项不重复列出。
|
||||
|
||||
---
|
||||
|
||||
## 一、安全缺陷
|
||||
|
||||
### 1.1 JWT 密钥硬编码
|
||||
|
||||
**文件**: `shortdrama/service/user_service.go:15`
|
||||
|
||||
```go
|
||||
const jwtSecret = "video-factory-jwt-secret-2024"
|
||||
```
|
||||
|
||||
密钥硬编码源码中,所有部署共用同一密钥。泄露后可用任意用户 Token 伪造身份。
|
||||
|
||||
**修复**: 从环境变量 `JWT_SECRET` 读取。
|
||||
|
||||
**严重程度**: 🔴 严重
|
||||
|
||||
---
|
||||
|
||||
### 1.2 支付签名使用 MD5
|
||||
|
||||
**文件**: `shortdrama/service/payment_order_service.go:362,447`
|
||||
|
||||
- 微信支付使用 MD5 签名(微信推荐 HMAC-SHA256)
|
||||
- 支付宝使用 `md5.Sum([]byte(raw + privateKey))` 而非标准 RSA2
|
||||
|
||||
**严重程度**: 🔴 严重
|
||||
|
||||
---
|
||||
|
||||
### 1.3 支付通知 URL 为空
|
||||
|
||||
**文件**: `shortdrama/service/payment_order_service.go:309,384`
|
||||
|
||||
```go
|
||||
"notify_url": "", // 需要实际外网可访问地址
|
||||
```
|
||||
|
||||
微信和支付宝通知地址均为空,支付渠道无法回调通知订单状态变更。
|
||||
|
||||
**严重程度**: 🔴 严重
|
||||
|
||||
---
|
||||
|
||||
### 1.4 支付回调错误被静默忽略
|
||||
|
||||
**文件**: `shortdrama/controller/payment_order_controller.go:67,76`
|
||||
|
||||
```go
|
||||
_ = service.PaymentService.HandleNotify(ctx, "wechat", body)
|
||||
```
|
||||
|
||||
`HandleNotify` 返回值被忽略,即使处理失败也返回 `SUCCESS`,支付渠道不会重试,可能导致资金损失。
|
||||
|
||||
**严重程度**: 🔴 严重
|
||||
|
||||
---
|
||||
|
||||
### 1.5 微信支付 IP 写死
|
||||
|
||||
**文件**: `shortdrama/service/payment_order_service.go:308`
|
||||
|
||||
```go
|
||||
"spbill_create_ip": "127.0.0.1",
|
||||
```
|
||||
|
||||
全部走 localhost 可能触发微信风控。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 1.6 API Key 明文写入日志
|
||||
|
||||
**文件**: `shortdrama/service/episode_service.go:216`
|
||||
|
||||
```go
|
||||
g.Log().Warningf(ctx, "... ApiKey=%q ...", modelCfg.ApiKey)
|
||||
```
|
||||
|
||||
模型 API Key 在日志中明文输出。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 1.7 订单号随机性弱
|
||||
|
||||
**文件**: `shortdrama/service/payment_order_service.go:290`
|
||||
|
||||
```go
|
||||
r := rand.Intn(10000) // 仅 10000 种
|
||||
return fmt.Sprintf("PAY%s%04d", now.Format("20060102150405"), r)
|
||||
```
|
||||
|
||||
使用 `math/rand`(非加密安全),同一秒内仅 10000 种订单号,高并发可重复。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 1.8 支付 nonce 使用 math/rand
|
||||
|
||||
**文件**: `shortdrama/service/payment_order_service.go:457`
|
||||
|
||||
```go
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
```
|
||||
|
||||
nonce 可被预测。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 1.9 CORS 未限制来源
|
||||
|
||||
**文件**: `common/http/http.go:28`
|
||||
|
||||
```go
|
||||
r.Response.CORS(r.Response.DefaultCORSOptions())
|
||||
```
|
||||
|
||||
允许所有来源跨域访问。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 1.10 客户默认密码为手机号后 6 位
|
||||
|
||||
**文件**: `shortdrama/service/customer_service.go:57-61`
|
||||
|
||||
默认密码 = 手机号后 6 位,且 `bcryptGenerate` 错误被忽略(失败时密码为空)。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 1.11 API Key 明文存储
|
||||
|
||||
**文件**: `shortdrama/dao/payment_config_dao.go:25`, `dao/user_model_config_dao.go:34,71`
|
||||
|
||||
支付 API Key、AppSecret、PrivateKey 及用户模型 API Key 在 SQLite 中明文存储。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
## 二、严重逻辑缺陷
|
||||
|
||||
### 2.1 并行模式永久死代码
|
||||
|
||||
**文件**: `shortdrama/service/generation_service.go:95`
|
||||
|
||||
```go
|
||||
// 注释说"默认使用并行生成模式"
|
||||
mode = "serial"
|
||||
```
|
||||
|
||||
`mode` 参数被无条件覆盖为 `"serial"`,下方完整的并行 goroutine 代码行(约 30 行)完全不可达。注释与行为矛盾。
|
||||
|
||||
**严重程度**: 🔴 严重
|
||||
|
||||
---
|
||||
|
||||
## 三、代码质量问题
|
||||
|
||||
### 3.1 generation_service.go 文件过大
|
||||
|
||||
**文件**: `shortdrama/service/generation_service.go`(2688 行,60+ 个函数)
|
||||
|
||||
职责涵盖:Agent 调用、视频提交/合并、ffmpeg 操作、轮询引擎、缓存、prompt 构建、上下文构建、文件操作等。
|
||||
|
||||
**建议**: 拆分为 `generation.go`、`poller.go`、`prompt.go`、`fileops.go`、`ffmpeg.go`。
|
||||
|
||||
**严重程度**: 🟡 中等
|
||||
|
||||
---
|
||||
|
||||
## 四、架构与设计问题
|
||||
|
||||
### 4.1 DAO 用 init() 执行 DDL 迁移
|
||||
|
||||
**文件**: `shortdrama/dao/*dao.go`(17 个文件)
|
||||
|
||||
DDL 散落在 `init()` 中,执行顺序依赖包导入顺序,SQLite `DROP COLUMN` 被静默忽略,无回滚,无超时,不可测试。
|
||||
|
||||
**建议**: 使用 migrate/sqlite 等迁移工具统一管理。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 4.2 缺少审计日志
|
||||
|
||||
所有 CRUD 操作(短剧、演员、场景、代理商、充值等)均无审计记录。充值无操作人身份记录。
|
||||
|
||||
**建议**: 关键操作记录 `(actor_id, action, target_type, target_id, detail)`。
|
||||
|
||||
**严重程度**: 🟡 中等
|
||||
|
||||
---
|
||||
|
||||
### 4.3 视频轮询器无指数退避
|
||||
|
||||
**文件**: `shortdrama/service/generation_service.go:1630`
|
||||
|
||||
```go
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
```
|
||||
|
||||
恒定 15s 轮询,任务长时间 RUNNING 时无谓消耗 DB。
|
||||
|
||||
**建议**: 实现指数退避(15s → 30s → 60s → 120s)。
|
||||
|
||||
**严重程度**: 🟡 中等
|
||||
|
||||
---
|
||||
|
||||
## 五、测试覆盖
|
||||
|
||||
### 5.1 无单元测试
|
||||
|
||||
约 100+ `.go` 文件,`_test.go` 数量为 **0**。Agent 调用、分段算法、支付签名、权限校验、DAO 操作等核心逻辑均无覆盖。
|
||||
|
||||
**严重程度**: 🟠 重要
|
||||
|
||||
---
|
||||
|
||||
### 5.2 无集成测试
|
||||
|
||||
API 端点无集成测试,无法验证路由、参数解析、响应格式。DDL 迁移完全不可测试。
|
||||
|
||||
**严重程度**: 🟡 中等
|
||||
|
||||
---
|
||||
|
||||
## 严重程度汇总
|
||||
|
||||
| 级别 | 含义 | 数量 |
|
||||
|------|------|------|
|
||||
| 🔴 严重 | 安全性/功能性问题 | 5 |
|
||||
| 🟠 重要 | 有实际风险 | 12 |
|
||||
| 🟡 中等 | 代码质量/架构 | 4 |
|
||||
| 🟢 轻微 | 代码整洁性 | 0 |
|
||||
@@ -3,7 +3,6 @@ module video-factory
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/Eyevinn/mp4ff v0.53.0
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Eyevinn/mp4ff v0.53.0 h1:aK4OF9gFwjrqvHRf0znEMCUVupgRFR1LB8OVpw9z1RA=
|
||||
github.com/Eyevinn/mp4ff v0.53.0/go.mod h1:AhC+bOI7GSZmzuN4zFY9U76qMedbHI+8BdQXWrC9+8U=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
@@ -27,8 +25,6 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 h1:KLS68SWS2W749x7e+eCCOO3UD2Sbw+bIbLEPR8o1FXw=
|
||||
|
||||
@@ -3,11 +3,20 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
commonHttp "video-factory/common"
|
||||
|
||||
commonHttp "video-factory/common/http"
|
||||
"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"
|
||||
@@ -67,12 +76,32 @@ func main() {
|
||||
controller.RegionPricing,
|
||||
})
|
||||
|
||||
// ==================== 静态文件服务(workspace 目录) ====================
|
||||
commonHttp.Httpserver.AddStaticPath("/workspace", "workspace")
|
||||
// ==================== 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)
|
||||
})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// 启动时恢复未完成的视频生成轮询
|
||||
service.GenerationService.StartVideoPoller(context.Background())
|
||||
service.GenerationService.StartVideoPoller(ctx)
|
||||
|
||||
// 保持运行
|
||||
select {}
|
||||
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")
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -27,16 +27,16 @@ type ModelConfig struct {
|
||||
// CallChatModel 调用大模型聊天接口(OpenAI 兼容格式)
|
||||
func CallChatModel(ctx context.Context, cfg *ModelConfig, req *ChatRequest) (*ChatResponse, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("模型配置不能为空")
|
||||
return nil, fmt.Errorf("model config cannot be empty")
|
||||
}
|
||||
if cfg.APIKey == "" {
|
||||
return nil, fmt.Errorf("APIKey 未配置")
|
||||
return nil, fmt.Errorf("APIKey not configured")
|
||||
}
|
||||
if cfg.ModelName == "" {
|
||||
return nil, fmt.Errorf("模型名称未配置")
|
||||
return nil, fmt.Errorf("model name not configured")
|
||||
}
|
||||
if cfg.BaseURL == "" {
|
||||
return nil, fmt.Errorf("API 地址未配置")
|
||||
return nil, fmt.Errorf("API address not configured")
|
||||
}
|
||||
|
||||
timeout := cfg.Timeout
|
||||
@@ -56,12 +56,12 @@ func CallChatModel(ctx context.Context, cfg *ModelConfig, req *ChatRequest) (*Ch
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 3
|
||||
}
|
||||
g.Log().Infof(ctx, "ChatAPI 开始调用 model=%s timeout=%v max_retries=%d body_size=%d", cfg.ModelName, timeout, maxRetries, len(body))
|
||||
g.Log().Debugf(ctx, "ChatAPI 开始调用 model=%s timeout=%v max_retries=%d body_size=%d", cfg.ModelName, timeout, maxRetries, len(body))
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
wait := time.Duration(1<<(attempt-1)) * time.Second
|
||||
g.Log().Infof(ctx, "ChatAPI 限流重试第%d次(等待%v)", attempt, wait)
|
||||
g.Log().Infof(ctx, "ChatAPI 重试第%d次(等待%v)", attempt, wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
@@ -71,14 +71,13 @@ func CallChatModel(ctx context.Context, cfg *ModelConfig, req *ChatRequest) (*Ch
|
||||
|
||||
result, doErr := doChatRequest(ctx, url, cfg.APIKey, body, timeout)
|
||||
if doErr == nil {
|
||||
g.Log().Infof(ctx, "ChatAPI 调用成功 url=%s tool_calls=%d content_len=%d",
|
||||
g.Log().Debugf(ctx, "ChatAPI 调用成功 url=%s tool_calls=%d content_len=%d",
|
||||
url, len(result.ToolCalls), len(result.Content))
|
||||
g.Log().Printf(ctx, "ChatAPI 返回内容:\n%s", result.Content)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
lastErr = doErr
|
||||
g.Log().Warningf(ctx, "ChatAPI 请求失败(attempt=%d/%d): %v", attempt+1, maxRetries+1, doErr)
|
||||
g.Log().Warningf(ctx, "ChatAPI request failed (attempt=%d/%d): %v", attempt+1, maxRetries+1, doErr)
|
||||
// 只有限流或服务端错误才重试
|
||||
errStr := lastErr.Error()
|
||||
if !strings.Contains(errStr, "limit_requests") &&
|
||||
@@ -90,14 +89,14 @@ func CallChatModel(ctx context.Context, cfg *ModelConfig, req *ChatRequest) (*Ch
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Errorf(ctx, "ChatAPI %d次重试后最终失败: %v", maxRetries+1, lastErr)
|
||||
g.Log().Errorf(ctx, "ChatAPI failed after %d retries: %v", maxRetries+1, lastErr)
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func doChatRequest(ctx context.Context, url, apiKey string, body []byte, timeout time.Duration) (*ChatResponse, error) {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
return nil, fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
@@ -107,20 +106,20 @@ func doChatRequest(ctx context.Context, url, apiKey string, body []byte, timeout
|
||||
resp, err := client.Do(httpReq)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败(耗时%v): %w", elapsed, err)
|
||||
return nil, fmt.Errorf("request failed (elapsed %v): %w", elapsed, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败(状态码=%d): %w", resp.StatusCode, err)
|
||||
return nil, fmt.Errorf("read response failed (status=%d): %w", resp.StatusCode, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("API返回错误状态码=%d body=%s", resp.StatusCode, string(respBody))
|
||||
return nil, fmt.Errorf("API response error status=%d body=%s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "ChatAPI 响应完成 status=%d body_len=%d elapsed=%v",
|
||||
g.Log().Debugf(ctx, "ChatAPI 响应完成 status=%d body_len=%d elapsed=%v",
|
||||
resp.StatusCode, len(respBody), elapsed)
|
||||
|
||||
return parseRespBody(ctx, respBody)
|
||||
@@ -258,13 +257,13 @@ func toAPIMessages(msgs []*ChatMessage) []apiMessage {
|
||||
func parseRespBody(ctx context.Context, data []byte) (*ChatResponse, error) {
|
||||
var resp apiRespBody
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %s", string(data))
|
||||
return nil, fmt.Errorf("parse response failed: %s", string(data))
|
||||
}
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("API错误(code=%s): %s", resp.Error.Code, resp.Error.Message)
|
||||
return nil, fmt.Errorf("API error(code=%s): %s", resp.Error.Code, resp.Error.Message)
|
||||
}
|
||||
if len(resp.Choices) == 0 {
|
||||
return nil, fmt.Errorf("响应为空")
|
||||
return nil, fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
msg := resp.Choices[0].Message
|
||||
@@ -272,7 +271,7 @@ func parseRespBody(ctx context.Context, data []byte) (*ChatResponse, error) {
|
||||
|
||||
// 检测 finish_reason 是否为 length(被 max_tokens 截断)
|
||||
if resp.Choices[0].FinishReason == "length" {
|
||||
g.Log().Warningf(ctx, "ChatAPI 响应被截断(finish_reason=length), 当前content_len=%d, 请考虑增大max_tokens", len(msg.Content))
|
||||
g.Log().Warningf(ctx, "ChatAPI response truncated (finish_reason=length), content_len=%d, consider increasing max_tokens", len(msg.Content))
|
||||
}
|
||||
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
|
||||
})
|
||||
elapsed := time.Since(startTime)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("step %d: 模型调用失败: %w", step, err)
|
||||
return "", fmt.Errorf("step %d: model call failed: %w", step, err)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "ReAct step %d/%d: 模型返回 (耗时 %v), content_len=%d, ToolCalls=%d",
|
||||
@@ -71,7 +71,7 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
|
||||
for _, tc := range result.ToolCalls {
|
||||
tool := a.findTool(tc.Name)
|
||||
if tool == nil {
|
||||
g.Log().Warningf(ctx, "ReAct step %d: 未知工具: %s", step+1, tc.Name)
|
||||
g.Log().Warningf(ctx, "ReAct step %d: unknown tool: %s", step+1, tc.Name)
|
||||
messages = append(messages, &ChatMessage{
|
||||
Role: RoleTool,
|
||||
Content: fmt.Sprintf("未知工具: %s", tc.Name),
|
||||
@@ -82,7 +82,7 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
|
||||
}
|
||||
|
||||
if tc.Arguments == "" {
|
||||
g.Log().Warningf(ctx, "ReAct step %d: 工具 %s 参数为空,跳过", step+1, tc.Name)
|
||||
g.Log().Warningf(ctx, "ReAct step %d: tool %s arguments empty, skipping", step+1, tc.Name)
|
||||
messages = append(messages, &ChatMessage{
|
||||
Role: RoleTool,
|
||||
Content: "工具参数为空",
|
||||
@@ -94,7 +94,7 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
|
||||
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
|
||||
g.Log().Warningf(ctx, "ReAct step %d: 参数解析失败: %v", step+1, err)
|
||||
g.Log().Warningf(ctx, "ReAct step %d: argument parse failed: %v", step+1, err)
|
||||
messages = append(messages, &ChatMessage{
|
||||
Role: RoleTool,
|
||||
Content: fmt.Sprintf("参数解析失败: %v", err),
|
||||
@@ -111,7 +111,7 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
|
||||
toolElapsed := time.Since(toolStart)
|
||||
if err != nil {
|
||||
output = fmt.Sprintf("工具执行失败: %v", err)
|
||||
g.Log().Warningf(ctx, "ReAct step %d: 工具 %s 执行失败 (耗时 %v): %v", step+1, tc.Name, toolElapsed, err)
|
||||
g.Log().Warningf(ctx, "ReAct step %d: tool %s execution failed (elapsed %v): %v", step+1, tc.Name, toolElapsed, err)
|
||||
} else {
|
||||
truncated := output
|
||||
if len(truncated) > 200 {
|
||||
@@ -129,8 +129,8 @@ func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error)
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Errorf(ctx, "ReAct 达到最大步骤数 %d,生成未完成", a.maxStep)
|
||||
return "", fmt.Errorf("达到最大步骤数 %d,生成未完成", a.maxStep)
|
||||
g.Log().Errorf(ctx, "ReAct reached max steps %d, generation incomplete", a.maxStep)
|
||||
return "", fmt.Errorf("max steps reached %d, generation incomplete", a.maxStep)
|
||||
}
|
||||
|
||||
func (a *ReActAgent) findTool(name string) *ToolInfo {
|
||||
|
||||
@@ -2,12 +2,10 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"video-factory/common"
|
||||
|
||||
"video-factory/shortdrama/dao"
|
||||
|
||||
@@ -42,7 +40,7 @@ func parseScriptTool() *ToolInfo {
|
||||
Func: func(ctx context.Context, args map[string]any) (string, error) {
|
||||
rawScript, _ := args["raw_script"].(string)
|
||||
if rawScript == "" {
|
||||
return "", fmt.Errorf("剧本内容不能为空")
|
||||
return "", fmt.Errorf("script content cannot be empty")
|
||||
}
|
||||
|
||||
episodeTexts := strings.Split(rawScript, "---")
|
||||
@@ -121,7 +119,7 @@ func analyzeScriptForEpisodeTool() *ToolInfo {
|
||||
duration := int(durationFloat)
|
||||
|
||||
if scriptContent == "" {
|
||||
return "", fmt.Errorf("剧本内容不能为空")
|
||||
return "", fmt.Errorf("script content cannot be empty")
|
||||
}
|
||||
if duration <= 0 {
|
||||
duration = 60
|
||||
@@ -233,19 +231,13 @@ func generateSceneImageTool() *ToolInfo {
|
||||
"type": "string",
|
||||
"description": "画面描述(场景设定、演员动作、镜头角度等)",
|
||||
},
|
||||
"style": map[string]any{
|
||||
"type": "string",
|
||||
"description": "整体风格",
|
||||
},
|
||||
},
|
||||
"required": []string{"visual_description", "style"},
|
||||
"required": []string{"visual_description"},
|
||||
},
|
||||
Func: func(ctx context.Context, args map[string]any) (string, error) {
|
||||
visualDesc, _ := args["visual_description"].(string)
|
||||
style, _ := args["style"].(string)
|
||||
episodeIdx, _ := args["episode_index"].(float64)
|
||||
sceneIdx, _ := args["scene_index"].(float64)
|
||||
_ = style
|
||||
|
||||
dramaId := GetDramaID(ctx)
|
||||
var imgBase64 string
|
||||
@@ -303,19 +295,5 @@ func cleanEpisodeTitle(title string) string {
|
||||
}
|
||||
|
||||
func readImageFileAsBase64(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(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
|
||||
return common.ImageFileToBase64(path)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
// GetShotDurationPrompt 从 config.yml 读取镜头时长约束并生成提示词文本
|
||||
func GetShotDurationPrompt(contentType string) string {
|
||||
typeMap := g.Cfg().MustGet(context.TODO(), "shotDuration."+contentType).Map()
|
||||
func GetShotDurationPrompt(ctx context.Context, contentType string) string {
|
||||
typeMap := g.Cfg().MustGet(ctx, "shotDuration."+contentType).Map()
|
||||
if len(typeMap) == 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -37,8 +37,8 @@ func GetShotDurationPrompt(contentType string) string {
|
||||
}
|
||||
|
||||
// GetStylePrompt 从 config.yml 读取画风/质量/防崩坏约束并生成提示词文本
|
||||
func GetStylePrompt(contentType string) string {
|
||||
m := g.Cfg().MustGet(context.TODO(), "shotDuration."+contentType).Map()
|
||||
func GetStylePrompt(ctx context.Context, contentType string) string {
|
||||
m := g.Cfg().MustGet(ctx, "shotDuration."+contentType).Map()
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"video-factory/common"
|
||||
|
||||
"video-factory/shortdrama/middleware"
|
||||
"video-factory/shortdrama/dao"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
@@ -22,9 +24,13 @@ func (c *customer) Create(ctx context.Context, req *dto.CreateCustomerReq) (res
|
||||
agentId := req.AgentId
|
||||
if agentId == 0 {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
agentId = middleware.GetUserId(r)
|
||||
agentId = common.GetUserId(r)
|
||||
}
|
||||
user, err := service.CustomerService.CreateCustomer(ctx, req.Phone, req.Name, req.Address, agentId)
|
||||
user, err := dao.User.GetOne(ctx, agentId)
|
||||
if err != nil || user == nil || user.Role != "agent" {
|
||||
return nil, errors.New("agent not found")
|
||||
}
|
||||
user, err = service.CustomerService.CreateCustomer(ctx, req.Phone, req.Name, req.Address, agentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -41,6 +47,6 @@ func (c *customer) Detail(ctx context.Context, req *struct{ Id int64 }) (res *dt
|
||||
|
||||
func (c *customer) Update(ctx context.Context, req *dto.UpdateCustomerReq) (res *struct{}, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
agentId := middleware.GetUserId(r)
|
||||
agentId := common.GetUserId(r)
|
||||
return nil, service.CustomerService.UpdateCustomer(ctx, agentId, req)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"video-factory/common"
|
||||
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/dao"
|
||||
"video-factory/shortdrama/middleware"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
@@ -20,20 +20,20 @@ var Drama = new(drama)
|
||||
// checkDramaAccess 校验客户只能操作自己的短剧
|
||||
func checkDramaAccess(ctx context.Context, dramaId int64) error {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
role := middleware.GetRole(r)
|
||||
role := common.GetRole(r)
|
||||
if role != "customer" {
|
||||
return nil
|
||||
}
|
||||
userId := middleware.GetUserId(r)
|
||||
userId := common.GetUserId(r)
|
||||
d, err := dao.Drama.GetOne(ctx, dramaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d == nil {
|
||||
return fmt.Errorf("短剧不存在")
|
||||
return fmt.Errorf("drama not found")
|
||||
}
|
||||
if d.UserId != userId {
|
||||
return fmt.Errorf("无权操作该短剧")
|
||||
return fmt.Errorf("no permission to operate on this drama")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -42,8 +42,8 @@ func checkDramaAccess(ctx context.Context, dramaId int64) error {
|
||||
|
||||
func (c *drama) List(ctx context.Context, req *dto.ListDramaReq) (res *dto.ListDramaRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
role := middleware.GetRole(r)
|
||||
userId := middleware.GetUserId(r)
|
||||
role := common.GetRole(r)
|
||||
userId := common.GetUserId(r)
|
||||
list, total, err := service.DramaService.List(ctx, req.Page, req.PageSize, role, userId, req.Keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -53,7 +53,7 @@ func (c *drama) List(ctx context.Context, req *dto.ListDramaReq) (res *dto.ListD
|
||||
|
||||
func (c *drama) Create(ctx context.Context, req *dto.CreateDramaReq) (res *dto.CreateDramaRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
userId := common.GetUserId(r)
|
||||
id, err := service.DramaService.Create(ctx, req.Title, req.Type, req.Config, req.AspectRatio, int64(req.EpisodeDuration), req.Resolution, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -67,14 +67,14 @@ func (c *drama) Get(ctx context.Context, req *dto.GetDramaReq) (res *dto.GetDram
|
||||
return nil, err
|
||||
}
|
||||
if drama == nil {
|
||||
return nil, fmt.Errorf("短剧不存在")
|
||||
return nil, fmt.Errorf("drama not found")
|
||||
}
|
||||
// 权限校验
|
||||
r := g.RequestFromCtx(ctx)
|
||||
role := middleware.GetRole(r)
|
||||
userId := middleware.GetUserId(r)
|
||||
role := common.GetRole(r)
|
||||
userId := common.GetUserId(r)
|
||||
if role == "customer" && drama.UserId != userId {
|
||||
return nil, fmt.Errorf("无权操作该短剧")
|
||||
return nil, fmt.Errorf("no permission to operate on this drama")
|
||||
}
|
||||
|
||||
d, characters, scenes, props, backgroundMusic, err := service.DramaService.Get(ctx, drama)
|
||||
|
||||
@@ -2,8 +2,8 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"video-factory/common"
|
||||
|
||||
"video-factory/shortdrama/middleware"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
@@ -16,7 +16,7 @@ var PaymentOrder = new(paymentOrder)
|
||||
|
||||
func (c *paymentOrder) Prepay(ctx context.Context, req *dto.PrepayReq) (res *dto.PrepayRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
userId := common.GetUserId(r)
|
||||
|
||||
orderType := req.OrderType
|
||||
if orderType == "" {
|
||||
@@ -43,6 +43,16 @@ func (c *paymentOrder) Status(ctx context.Context, req *dto.PaymentStatusReq) (r
|
||||
return &dto.PaymentStatusRes{OrderNo: req.OrderNo, Status: status, Amount: amount}, nil
|
||||
}
|
||||
|
||||
func (c *paymentOrder) List(ctx context.Context, req *dto.ListPaymentOrderReq) (res *dto.ListPaymentOrderRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := common.GetUserId(r)
|
||||
list, total, err := service.PaymentService.ListOrders(ctx, userId, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.ListPaymentOrderRes{List: list, Total: total}, nil
|
||||
}
|
||||
|
||||
func (c *paymentOrder) ConfirmOffline(ctx context.Context, req *dto.ConfirmOfflineReq) (res *dto.ConfirmOfflineRes, err error) {
|
||||
if err := service.PaymentService.ConfirmOffline(ctx, req.OrderNo); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,8 +3,8 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"video-factory/common"
|
||||
|
||||
"video-factory/shortdrama/middleware"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
@@ -19,7 +19,7 @@ func (c *transaction) List(ctx context.Context, req *dto.ListTransactionReq) (re
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := req.UserId
|
||||
if userId == 0 {
|
||||
userId = middleware.GetUserId(r)
|
||||
userId = common.GetUserId(r)
|
||||
}
|
||||
list, total, err := service.TransactionService.ListByUser(ctx, userId, req.Type, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,8 +3,8 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"video-factory/common"
|
||||
|
||||
"video-factory/shortdrama/middleware"
|
||||
"video-factory/shortdrama/model/dto"
|
||||
"video-factory/shortdrama/service"
|
||||
|
||||
@@ -17,9 +17,9 @@ var UserModelConfig = new(userModelConfig)
|
||||
|
||||
func (c *userModelConfig) GetUserConfig(ctx context.Context, req *dto.GetUserModelConfigReq) (res *dto.GetUserModelConfigRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
userId := common.GetUserId(r)
|
||||
if userId <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
return nil, fmt.Errorf("not logged in")
|
||||
}
|
||||
cfg := service.UserModelConfigService.GetUserConfig(ctx, userId, req.ModelType)
|
||||
return &dto.GetUserModelConfigRes{UserModelConfig: cfg}, nil
|
||||
@@ -27,16 +27,16 @@ func (c *userModelConfig) GetUserConfig(ctx context.Context, req *dto.GetUserMod
|
||||
|
||||
func (c *userModelConfig) SaveUserConfig(ctx context.Context, req *dto.SaveUserModelConfigReq) (res *struct{}, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
userId := common.GetUserId(r)
|
||||
if userId <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
return nil, fmt.Errorf("not logged in")
|
||||
}
|
||||
return nil, service.UserModelConfigService.SaveUserConfigs(ctx, userId, req.Configs)
|
||||
}
|
||||
|
||||
func (c *userModelConfig) GetUserModelList(ctx context.Context, req *dto.GetUserModelListReq) (res *dto.GetUserModelListRes, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
userId := middleware.GetUserId(r)
|
||||
userId := common.GetUserId(r)
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
|
||||
@@ -26,10 +26,14 @@ func init() {
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建 account_transaction 表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create account_transaction table failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_at_user ON "+public.TableNameAccountTransaction+"(user_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "create index idx_at_user failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAccountTransaction+" ADD COLUMN order_no TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column order_no failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_at_user ON "+public.TableNameAccountTransaction+"(user_id)")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAccountTransaction+" ADD COLUMN order_no TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
|
||||
func (d *accountTransactionDao) Insert(ctx context.Context, data *entity.AccountTransaction) (int64, error) {
|
||||
@@ -47,11 +51,18 @@ func (d *accountTransactionDao) ListByUser(ctx context.Context, userId int64, ty
|
||||
if typeFilter != "" {
|
||||
m = m.Where("type", typeFilter)
|
||||
}
|
||||
count, err := m.Count()
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderDesc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var list []*entity.AccountTransaction
|
||||
err = m.Page(page, pageSize).OrderDesc("id").Scan(&list)
|
||||
return list, count, err
|
||||
list = make([]*entity.AccountTransaction, 0)
|
||||
err = r.Structs(&list)
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
@@ -49,23 +49,25 @@ func (d *agentProfileDao) ListAgentWithProfile(ctx context.Context, keyword, pho
|
||||
if expiredAtTo != "" {
|
||||
m = m.Where("ap.expired_at <= ?", expiredAtTo)
|
||||
}
|
||||
total, err := m.Count()
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
m = m.Fields(
|
||||
"u.id", "u.username", "u.phone", "u.name", "u.province", "u.region",
|
||||
"ap.expired_at", "ap.region_protected", "ap.max_customers",
|
||||
)
|
||||
r, total, err := m.OrderAsc("u.id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var rows []*agentListRow
|
||||
err = m.Fields(
|
||||
"u.id", "u.username", "u.phone", "u.name", "u.province", "u.region",
|
||||
"ap.expired_at", "ap.region_protected", "ap.max_customers",
|
||||
).Page(page, pageSize).OrderAsc("u.id").Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return rows, total, nil
|
||||
err = r.Structs(&rows)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
// ListActiveByRegion 查询指定地区的未过期代理商列表
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+public.TableNameAgentProfile+` (
|
||||
@@ -78,9 +80,15 @@ func init() {
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "create agent_profile table failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAgentProfile+" ADD COLUMN expired_at DATETIME")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAgentProfile+" ADD COLUMN region_protected INTEGER NOT NULL DEFAULT 0")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAgentProfile+" DROP COLUMN tier_id")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAgentProfile+" ADD COLUMN expired_at DATETIME"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column expired_at failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAgentProfile+" ADD COLUMN region_protected INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column region_protected failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameAgentProfile+" DROP COLUMN tier_id"); err != nil {
|
||||
g.Log().Warningf(ctx, "drop column tier_id failed: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var BackgroundMusic = &backgroundMusicDao{}
|
||||
@@ -25,82 +24,38 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')), -- 更新时间
|
||||
deleted_at DATETIME -- 删除时间
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建背景音表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create background music table failed: %v", err)
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_bgm_drama_id ON short_drama_background_music(drama_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建背景音表索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create background music table index failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) Insert(ctx context.Context, data *entity.BackgroundMusic) (id int64, err error) {
|
||||
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")
|
||||
r, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.LastInsertId()
|
||||
func (d *backgroundMusicDao) Insert(ctx context.Context, data *entity.BackgroundMusic) (int64, error) {
|
||||
return common.InsertAndReturnId(ctx, public.TableNameBackgroundMusic, data)
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) GetOne(ctx context.Context, id int64) (res *entity.BackgroundMusic, err error) {
|
||||
r, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Where("id", id).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
func (d *backgroundMusicDao) GetOne(ctx context.Context, id int64) (*entity.BackgroundMusic, error) {
|
||||
return common.GetOneByPk[entity.BackgroundMusic](ctx, public.TableNameBackgroundMusic, id)
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) Update(ctx context.Context, id int64, data *entity.BackgroundMusic) error {
|
||||
_, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Data(data).Where("id", id).Update()
|
||||
return err
|
||||
return common.UpdateByPk(ctx, public.TableNameBackgroundMusic, id, data)
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) Delete(ctx context.Context, id int64) error {
|
||||
_, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Unscoped().Where("id", id).Delete()
|
||||
return err
|
||||
return common.DeleteByPk(ctx, public.TableNameBackgroundMusic, id)
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) DeleteByDrama(ctx context.Context, dramaId int64) error {
|
||||
_, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete()
|
||||
return err
|
||||
return common.DeleteByDrama(ctx, public.TableNameBackgroundMusic, dramaId)
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) ListPageByDrama(ctx context.Context, dramaId int64, page, pageSize int) (res []*entity.BackgroundMusic, total int, err error) {
|
||||
m := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Where("drama_id", dramaId)
|
||||
if pageSize == -1 {
|
||||
r, err := m.OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res = make([]*entity.BackgroundMusic, 0)
|
||||
err = r.Structs(&res)
|
||||
return res, len(res), err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make([]*entity.BackgroundMusic, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
return common.ListPageByDrama[entity.BackgroundMusic](ctx, public.TableNameBackgroundMusic, dramaId, page, pageSize)
|
||||
}
|
||||
|
||||
func (d *backgroundMusicDao) ListByDrama(ctx context.Context, dramaId int64) (res []*entity.BackgroundMusic, err error) {
|
||||
r, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").All()
|
||||
r, err := g.DB().Model(public.TableNameBackgroundMusic).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").Limit(200).All()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Character = &characterDao{}
|
||||
@@ -27,82 +26,39 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')), -- 更新时间
|
||||
deleted_at DATETIME -- 删除时间
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建演员表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create character table failed: %v", err)
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_character_drama_id ON short_drama_character(drama_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建演员表索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create character table index failed: %v", err)
|
||||
}
|
||||
}
|
||||
// 迁移:清理旧字段
|
||||
for _, col := range []string{"voice_type", "portrait_url", "image_base64"} {
|
||||
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameCharacter+` DROP COLUMN `+col); err != nil {
|
||||
g.Log().Warningf(ctx, "删除演员表旧字段 %s 失败(可能已删除): %v", col, err)
|
||||
g.Log().Warningf(ctx, "drop character table old column %s failed (may already be deleted): %v", col, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *characterDao) Insert(ctx context.Context, data *entity.Character) (id int64, err error) {
|
||||
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")
|
||||
r, err := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.LastInsertId()
|
||||
func (d *characterDao) Insert(ctx context.Context, data *entity.Character) (int64, error) {
|
||||
return common.InsertAndReturnId(ctx, public.TableNameCharacter, data)
|
||||
}
|
||||
|
||||
func (d *characterDao) GetOne(ctx context.Context, id int64) (res *entity.Character, err error) {
|
||||
r, err := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Where("id", id).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
func (d *characterDao) GetOne(ctx context.Context, id int64) (*entity.Character, error) {
|
||||
return common.GetOneByPk[entity.Character](ctx, public.TableNameCharacter, id)
|
||||
}
|
||||
|
||||
func (d *characterDao) Update(ctx context.Context, id int64, data *entity.Character) error {
|
||||
_, err := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Data(data).Where("id", id).Update()
|
||||
return err
|
||||
return common.UpdateByPk(ctx, public.TableNameCharacter, id, data)
|
||||
}
|
||||
|
||||
func (d *characterDao) Delete(ctx context.Context, id int64) error {
|
||||
_, err := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Unscoped().Where("id", id).Delete()
|
||||
return err
|
||||
return common.DeleteByPk(ctx, public.TableNameCharacter, id)
|
||||
}
|
||||
|
||||
func (d *characterDao) DeleteByDrama(ctx context.Context, dramaId int64) error {
|
||||
_, err := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete()
|
||||
return err
|
||||
return common.DeleteByDrama(ctx, public.TableNameCharacter, dramaId)
|
||||
}
|
||||
|
||||
func (d *characterDao) ListPageByDrama(ctx context.Context, dramaId int64, page, pageSize int) (res []*entity.Character, total int, err error) {
|
||||
m := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Where("drama_id", dramaId)
|
||||
if pageSize == -1 {
|
||||
r, err := m.OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res = make([]*entity.Character, 0)
|
||||
err = r.Structs(&res)
|
||||
return res, len(res), err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make([]*entity.Character, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
return common.ListPageByDrama[entity.Character](ctx, public.TableNameCharacter, dramaId, page, pageSize)
|
||||
}
|
||||
|
||||
@@ -53,20 +53,24 @@ func (d *customerProfileDao) ListCustomerWithAgent(ctx context.Context, agentId
|
||||
m = m.Where("au.name LIKE ? OR au.username LIKE ? OR au.phone LIKE ?", "%"+agentName+"%", "%"+agentName+"%", "%"+agentName+"%")
|
||||
}
|
||||
|
||||
var rows []*customerListRow
|
||||
total, err := m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
err = m.Fields(
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
m = m.Fields(
|
||||
"u.id", "u.phone", "u.name", "u.province", "u.region", "u.address", "u.created_at",
|
||||
"cp.agent_id", "cp.balance",
|
||||
"au.name agent_name",
|
||||
).Page(page, pageSize).OrderAsc("u.id").Scan(&rows)
|
||||
)
|
||||
r, total, err := m.OrderAsc("u.id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return rows, total, nil
|
||||
var rows []*customerListRow
|
||||
err = r.Structs(&rows)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -77,9 +81,11 @@ func init() {
|
||||
balance INTEGER NOT NULL DEFAULT 0
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建 customer_profile 表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create customer_profile table failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_cp_agent ON "+public.TableNameCustomerProfile+"(agent_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "create index idx_cp_agent failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_cp_agent ON "+public.TableNameCustomerProfile+"(agent_id)")
|
||||
}
|
||||
|
||||
func (d *customerProfileDao) Get(ctx context.Context, userId int64) (*entity.CustomerProfile, error) {
|
||||
|
||||
+30
-12
@@ -31,28 +31,43 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')), -- 更新时间
|
||||
deleted_at DATETIME -- 删除时间
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建短剧表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create drama table failed: %v", err)
|
||||
}
|
||||
// 对已存在的旧表补充唯一索引
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_drama_user_id ON short_drama(user_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建短剧用户索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create drama user index failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_short_drama_title ON "+public.TableNameDrama+"(title)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建短剧标题唯一索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create drama title unique index failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN type TEXT NOT NULL DEFAULT '短剧'"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column type failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column user_id failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN config TEXT NOT NULL DEFAULT '{}'"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column config failed: %v", err)
|
||||
}
|
||||
// 旧表迁移:补充新字段(忽略已存在的错误)
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN type TEXT NOT NULL DEFAULT '短剧'")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN config TEXT NOT NULL DEFAULT '{}'")
|
||||
// 数据迁移:将旧 style 字段值写入 config(仅对 config 为空的旧记录生效)
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNameDrama+" SET config = '{\"题材\":\"' || style || '\"}', type = '短剧' WHERE config = '{}' AND style != '' AND style IS NOT NULL")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN aspect_ratio TEXT NOT NULL DEFAULT ''")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN resolution TEXT NOT NULL DEFAULT '720P'")
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNameDrama+" SET config = '{\"题材\":\"' || style || '\"}', type = '短剧' WHERE config = '{}' AND style != '' AND style IS NOT NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "migrate style to config failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN aspect_ratio TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column aspect_ratio failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" ADD COLUMN resolution TEXT NOT NULL DEFAULT '720P'"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column resolution failed: %v", err)
|
||||
}
|
||||
// 清理已废弃的 min_shot_duration / max_shot_duration 字段(之前 ALTER TABLE 加进去的旧列)
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" DROP COLUMN min_shot_duration")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" DROP COLUMN max_shot_duration")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" DROP COLUMN min_shot_duration"); err != nil {
|
||||
g.Log().Warningf(ctx, "drop column min_shot_duration failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameDrama+" DROP COLUMN max_shot_duration"); err != nil {
|
||||
g.Log().Warningf(ctx, "drop column max_shot_duration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dramaDao) IncrementEpCount(ctx context.Context, dramaId int64) error {
|
||||
@@ -85,6 +100,9 @@ func (d *dramaDao) Insert(ctx context.Context, data *entity.Drama) (id int64, er
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Episode = &episodeDao{}
|
||||
@@ -28,10 +27,10 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')), -- 更新时间
|
||||
deleted_at DATETIME -- 删除时间
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建剧集表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create episode table failed: %v", err)
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_episode_drama_id ON short_drama_episode(drama_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建剧集表索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create episode table index failed: %v", err)
|
||||
}
|
||||
}
|
||||
// 迁移:删除 generation_mode 列(已由模型配置动态决定)
|
||||
@@ -41,7 +40,7 @@ func init() {
|
||||
// 迁移:清理旧字段
|
||||
for _, col := range []string{"tech_script", "script_path", "tech_script_path", "description_path", "duration"} {
|
||||
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameEpisode+` DROP COLUMN `+col); err != nil {
|
||||
g.Log().Warningf(ctx, "删除剧集表旧字段 %s 失败(可能已删除): %v", col, err)
|
||||
g.Log().Warningf(ctx, "drop episode table old column %s failed (may already be deleted): %v", col, err)
|
||||
}
|
||||
}
|
||||
// 迁移:添加 description 列
|
||||
@@ -50,35 +49,24 @@ func init() {
|
||||
g.Log().Debugf(ctx, "添加 description 列失败(可能已存在): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *episodeDao) Insert(ctx context.Context, data *entity.Episode) (id int64, err error) {
|
||||
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")
|
||||
r, err := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.LastInsertId()
|
||||
func (d *episodeDao) Insert(ctx context.Context, data *entity.Episode) (int64, error) {
|
||||
return common.InsertAndReturnId(ctx, public.TableNameEpisode, data)
|
||||
}
|
||||
|
||||
func (d *episodeDao) GetOne(ctx context.Context, id int64) (res *entity.Episode, err error) {
|
||||
r, err := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Where("id", id).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
func (d *episodeDao) GetOne(ctx context.Context, id int64) (*entity.Episode, error) {
|
||||
return common.GetOneByPk[entity.Episode](ctx, public.TableNameEpisode, id)
|
||||
}
|
||||
|
||||
func (d *episodeDao) Update(ctx context.Context, id int64, data *entity.Episode) error {
|
||||
_, err := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Data(data).Where("id", id).Update()
|
||||
return err
|
||||
return common.UpdateByPk(ctx, public.TableNameEpisode, id, data)
|
||||
}
|
||||
|
||||
func (d *episodeDao) Delete(ctx context.Context, id int64) error {
|
||||
return common.DeleteByPk(ctx, public.TableNameEpisode, id)
|
||||
}
|
||||
|
||||
func (d *episodeDao) DeleteByDrama(ctx context.Context, dramaId int64) error {
|
||||
return common.DeleteByDrama(ctx, public.TableNameEpisode, dramaId)
|
||||
}
|
||||
|
||||
func (d *episodeDao) UpdateStatus(ctx context.Context, id int64, status string, videoUrl string) error {
|
||||
@@ -90,16 +78,6 @@ func (d *episodeDao) UpdateStatus(ctx context.Context, id int64, status string,
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *episodeDao) Delete(ctx context.Context, id int64) error {
|
||||
_, err := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Unscoped().Where("id", id).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *episodeDao) DeleteByDrama(ctx context.Context, dramaId int64) error {
|
||||
_, err := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *episodeDao) ListPageByDrama(ctx context.Context, dramaId int64, page, pageSize int, keyword ...string) (res []*entity.Episode, total int, err error) {
|
||||
m := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Where("drama_id", dramaId)
|
||||
if len(keyword) > 0 && keyword[0] != "" {
|
||||
|
||||
@@ -30,14 +30,14 @@ func init() {
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建生成任务表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create generation task table failed: %v", err)
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_gt_episode_id ON short_drama_generation_task(episode_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建生成任务剧集索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create generation task episode index failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_gt_status ON short_drama_generation_task(status)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建生成任务状态索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create generation task status index failed: %v", err)
|
||||
}
|
||||
}
|
||||
// 迁移:添加 segment_idx 列(旧表升级)
|
||||
@@ -111,6 +111,9 @@ func (d *generationTaskDao) Insert(ctx context.Context, data *entity.GenerationT
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
|
||||
@@ -237,7 +240,7 @@ func (d *generationTaskDao) ListByStatuses(ctx context.Context, statuses []strin
|
||||
}
|
||||
r, err := g.DB().Model(public.TableNameGenerationTask).Ctx(ctx).
|
||||
Where("status in (?)", statuses).
|
||||
OrderDesc("id").All()
|
||||
OrderDesc("id").Limit(200).All()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func init() {
|
||||
"updated_at DATETIME DEFAULT (datetime('now','localtime'))"+
|
||||
")")
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建模型配置表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create model config table failed: %v", err)
|
||||
}
|
||||
|
||||
// 检测旧表结构:含 base_url / api_key / chat_api_key / is_active / task_callback_url 等旧列 → 迁移
|
||||
@@ -47,7 +47,7 @@ func init() {
|
||||
if hasOldColumns {
|
||||
g.Log().Info(ctx, "检测到旧表结构,开始迁移 model_config...")
|
||||
newTable := public.TableNameModelConfig + "_new"
|
||||
_, _ = g.DB().Exec(ctx,
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE TABLE IF NOT EXISTS "+newTable+" ("+
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"+
|
||||
"model_type TEXT NOT NULL DEFAULT '',"+
|
||||
@@ -60,7 +60,9 @@ func init() {
|
||||
"reference_template TEXT NOT NULL DEFAULT '',"+
|
||||
"created_at DATETIME DEFAULT (datetime('now','localtime')),"+
|
||||
"updated_at DATETIME DEFAULT (datetime('now','localtime'))"+
|
||||
")")
|
||||
")"); err != nil {
|
||||
g.Log().Warningf(ctx, "create temp table for model_config migration failed: %v", err)
|
||||
}
|
||||
|
||||
hasChatApiKey := false
|
||||
if _, err := g.DB().Exec(ctx, "SELECT chat_api_key FROM "+public.TableNameModelConfig+" LIMIT 1"); err == nil {
|
||||
@@ -71,18 +73,22 @@ func init() {
|
||||
if hasChatApiKey {
|
||||
oldRow, _ := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Limit(1).One()
|
||||
if oldRow != nil && !oldRow.IsEmpty() {
|
||||
_, _ = g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
if _, err := g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
"model_type": "chat", "model_name": oldRow["chat_model_name"],
|
||||
"schema": oldRow["chat_schema"],
|
||||
"price": 0, "price_unit": "video",
|
||||
"created_at": now, "updated_at": now,
|
||||
}).Insert()
|
||||
_, _ = g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
}).Insert(); err != nil {
|
||||
g.Log().Warningf(ctx, "insert chat model config failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
"model_type": "video", "model_name": oldRow["video_model_name"],
|
||||
"schema": oldRow["video_schema"],
|
||||
"price": gconv.Int(oldRow["price_per_second"]), "price_unit": "second",
|
||||
"created_at": now, "updated_at": now,
|
||||
}).Insert()
|
||||
}).Insert(); err != nil {
|
||||
g.Log().Warningf(ctx, "insert video model config failed: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
oldRows, _ := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).OrderAsc("id").All()
|
||||
@@ -96,16 +102,22 @@ func init() {
|
||||
if mt == "chat" {
|
||||
unit = "video"
|
||||
}
|
||||
_, _ = g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
if _, err := g.DB().Model(newTable).Ctx(ctx).Data(g.Map{
|
||||
"model_type": mt, "model_name": or["model_name"],
|
||||
"schema": or["schema"],
|
||||
"price": priceVal, "price_unit": unit,
|
||||
"created_at": now, "updated_at": now,
|
||||
}).Insert()
|
||||
}).Insert(); err != nil {
|
||||
g.Log().Warningf(ctx, "insert model config for %s failed: %v", or["model_name"].String(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "DROP TABLE "+public.TableNameModelConfig)
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+newTable+" RENAME TO "+public.TableNameModelConfig)
|
||||
if _, err := g.DB().Exec(ctx, "DROP TABLE "+public.TableNameModelConfig); err != nil {
|
||||
g.Log().Warningf(ctx, "drop old model_config table failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+newTable+" RENAME TO "+public.TableNameModelConfig); err != nil {
|
||||
g.Log().Warningf(ctx, "rename temp table to model_config failed: %v", err)
|
||||
}
|
||||
g.Log().Info(ctx, "model_config 表结构迁移完成")
|
||||
}
|
||||
|
||||
@@ -121,7 +133,7 @@ func init() {
|
||||
if !hasCol {
|
||||
g.Log().Info(ctx, "检测到 model_config 缺少 concurrency_count 列,正在补充...")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameModelConfig+" ADD COLUMN concurrency_count INTEGER NOT NULL DEFAULT 1"); err != nil {
|
||||
g.Log().Warningf(ctx, "补充 concurrency_count 列失败: %v", err)
|
||||
g.Log().Warningf(ctx, "add concurrency_count column failed: %v", err)
|
||||
} else {
|
||||
g.Log().Info(ctx, "concurrency_count 列补充完成")
|
||||
}
|
||||
@@ -140,7 +152,7 @@ func init() {
|
||||
if !hasCol {
|
||||
g.Log().Info(ctx, "检测到 model_config 缺少 first_frame_mapping 列,正在补充...")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameModelConfig+" ADD COLUMN first_frame_mapping TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "补充 first_frame_mapping 列失败: %v", err)
|
||||
g.Log().Warningf(ctx, "add first_frame_mapping column failed: %v", err)
|
||||
} else {
|
||||
g.Log().Info(ctx, "first_frame_mapping 列补充完成")
|
||||
}
|
||||
@@ -159,7 +171,7 @@ func init() {
|
||||
if !hasCol {
|
||||
g.Log().Info(ctx, "检测到 model_config 缺少 reference_template 列,正在补充...")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameModelConfig+" ADD COLUMN reference_template TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "补充 reference_template 列失败: %v", err)
|
||||
g.Log().Warningf(ctx, "add reference_template column failed: %v", err)
|
||||
} else {
|
||||
g.Log().Info(ctx, "reference_template 列补充完成")
|
||||
}
|
||||
@@ -176,7 +188,7 @@ func init() {
|
||||
}
|
||||
for _, m := range defaults {
|
||||
if _, e := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).Data(m).Insert(); e != nil {
|
||||
g.Log().Warningf(ctx, "初始化默认模型配置失败: %v", e)
|
||||
g.Log().Warningf(ctx, "init default model config failed: %v", e)
|
||||
}
|
||||
}
|
||||
g.Log().Info(ctx, "已初始化默认模型配置(chat + video)")
|
||||
@@ -243,7 +255,7 @@ func (d *modelConfigDao) GetActiveModelByType(ctx context.Context, modelType str
|
||||
}
|
||||
|
||||
func (d *modelConfigDao) GetAll(ctx context.Context) (res []*entity.ModelConfig, err error) {
|
||||
r, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).OrderAsc("id").All()
|
||||
r, err := g.DB().Model(public.TableNameModelConfig).Ctx(ctx).OrderAsc("id").Limit(200).All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@ func init() {
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建 payment_channel_trade 表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create payment_channel_trade table failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_pct_order ON "+public.TableNamePaymentChannelTrade+"(order_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "create index idx_pct_order failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_pct_order ON "+public.TableNamePaymentChannelTrade+"(order_id)")
|
||||
}
|
||||
|
||||
func (d *paymentChannelTradeDao) Insert(ctx context.Context, data *entity.PaymentChannelTrade) (int64, error) {
|
||||
|
||||
@@ -29,7 +29,7 @@ func init() {
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建支付配置表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create payment config table failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,19 +31,23 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建 payment_order 表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create payment_order table failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_po_user ON "+public.TableNamePaymentOrder+"(user_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "create index idx_po_user failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_po_user ON "+public.TableNamePaymentOrder+"(user_id)")
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_po_order_type ON payment_order(order_type)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建订单类型索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create order type index failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_po_status ON payment_order(status)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建订单状态索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create order status index failed: %v", err)
|
||||
}
|
||||
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNamePaymentOrder+" ADD COLUMN order_type TEXT NOT NULL DEFAULT 'recharge'")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNamePaymentOrder+" ADD COLUMN order_type TEXT NOT NULL DEFAULT 'recharge'"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column order_type failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *paymentOrderDao) Insert(ctx context.Context, data *entity.PaymentOrder) (int64, error) {
|
||||
@@ -70,7 +74,7 @@ func (d *paymentOrderDao) GetByOrderNo(ctx context.Context, orderNo string) (*en
|
||||
|
||||
func (d *paymentOrderDao) ListByStatus(ctx context.Context, status string) ([]*entity.PaymentOrder, error) {
|
||||
var list []*entity.PaymentOrder
|
||||
err := g.DB().Model(public.TableNamePaymentOrder).Ctx(ctx).Where("status", status).OrderDesc("id").Scan(&list)
|
||||
err := g.DB().Model(public.TableNamePaymentOrder).Ctx(ctx).Where("status", status).OrderDesc("id").Limit(200).Scan(&list)
|
||||
return list, err
|
||||
}
|
||||
|
||||
@@ -93,6 +97,32 @@ func (d *paymentOrderDao) ListByUserAndType(ctx context.Context, userId int64, o
|
||||
err := g.DB().Model(public.TableNamePaymentOrder).Ctx(ctx).
|
||||
Where("user_id", userId).
|
||||
Where("order_type", orderType).
|
||||
OrderDesc("id").Scan(&list)
|
||||
OrderDesc("id").Limit(200).Scan(&list)
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (d *paymentOrderDao) ListPageByUser(ctx context.Context, userId int64, page, pageSize int) (res []*entity.PaymentOrder, total int, err error) {
|
||||
m := g.DB().Model(public.TableNamePaymentOrder).Ctx(ctx).Where("user_id", userId)
|
||||
if pageSize == -1 {
|
||||
r, err := m.OrderDesc("id").All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res = make([]*entity.PaymentOrder, 0)
|
||||
err = r.Structs(&res)
|
||||
return res, len(res), err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderDesc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make([]*entity.PaymentOrder, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
}
|
||||
|
||||
+12
-57
@@ -2,12 +2,11 @@ package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Prop = &propDao{}
|
||||
@@ -26,82 +25,38 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')), -- 更新时间
|
||||
deleted_at DATETIME -- 删除时间
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建道具表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create prop table failed: %v", err)
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_prop_drama_id ON short_drama_prop(drama_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建道具表索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create prop table index failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *propDao) Insert(ctx context.Context, data *entity.Prop) (id int64, err error) {
|
||||
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")
|
||||
r, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.LastInsertId()
|
||||
func (d *propDao) Insert(ctx context.Context, data *entity.Prop) (int64, error) {
|
||||
return common.InsertAndReturnId(ctx, public.TableNameProp, data)
|
||||
}
|
||||
|
||||
func (d *propDao) GetOne(ctx context.Context, id int64) (res *entity.Prop, err error) {
|
||||
r, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Where("id", id).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
func (d *propDao) GetOne(ctx context.Context, id int64) (*entity.Prop, error) {
|
||||
return common.GetOneByPk[entity.Prop](ctx, public.TableNameProp, id)
|
||||
}
|
||||
|
||||
func (d *propDao) Update(ctx context.Context, id int64, data *entity.Prop) error {
|
||||
_, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Data(data).Where("id", id).Update()
|
||||
return err
|
||||
return common.UpdateByPk(ctx, public.TableNameProp, id, data)
|
||||
}
|
||||
|
||||
func (d *propDao) Delete(ctx context.Context, id int64) error {
|
||||
_, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Unscoped().Where("id", id).Delete()
|
||||
return err
|
||||
return common.DeleteByPk(ctx, public.TableNameProp, id)
|
||||
}
|
||||
|
||||
func (d *propDao) DeleteByDrama(ctx context.Context, dramaId int64) error {
|
||||
_, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete()
|
||||
return err
|
||||
return common.DeleteByDrama(ctx, public.TableNameProp, dramaId)
|
||||
}
|
||||
|
||||
func (d *propDao) ListPageByDrama(ctx context.Context, dramaId int64, page, pageSize int) (res []*entity.Prop, total int, err error) {
|
||||
m := g.DB().Model(public.TableNameProp).Ctx(ctx).Where("drama_id", dramaId)
|
||||
if pageSize == -1 {
|
||||
r, err := m.OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res = make([]*entity.Prop, 0)
|
||||
err = r.Structs(&res)
|
||||
return res, len(res), err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make([]*entity.Prop, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
return common.ListPageByDrama[entity.Prop](ctx, public.TableNameProp, dramaId, page, pageSize)
|
||||
}
|
||||
|
||||
func (d *propDao) ListByDrama(ctx context.Context, dramaId int64) (res []*entity.Prop, err error) {
|
||||
r, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").All()
|
||||
r, err := g.DB().Model(public.TableNameProp).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").Limit(200).All()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -26,15 +26,26 @@ func init() {
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "create region_pricing table failed: %v", err)
|
||||
}
|
||||
// add province column for existing databases
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameRegionPricing+" ADD COLUMN province TEXT NOT NULL DEFAULT ''")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameRegionPricing+" ADD COLUMN province TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column province failed: %v", err)
|
||||
}
|
||||
// add max_customers column for existing databases
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameRegionPricing+" ADD COLUMN max_customers INTEGER NOT NULL DEFAULT 0")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameRegionPricing+" ADD COLUMN max_customers INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column max_customers failed: %v", err)
|
||||
}
|
||||
// drop old indexes, recreate with new constraints
|
||||
_, _ = g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_region_pricing_uniq`)
|
||||
_, _ = g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_rp_price`)
|
||||
_, _ = g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_rp_max_customers`)
|
||||
_, _ = g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_rp_single_protected`)
|
||||
if _, err := g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_region_pricing_uniq`); err != nil {
|
||||
g.Log().Warningf(ctx, "drop index idx_region_pricing_uniq failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_rp_price`); err != nil {
|
||||
g.Log().Warningf(ctx, "drop index idx_rp_price failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_rp_max_customers`); err != nil {
|
||||
g.Log().Warningf(ctx, "drop index idx_rp_max_customers failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `DROP INDEX IF EXISTS idx_rp_single_protected`); err != nil {
|
||||
g.Log().Warningf(ctx, "drop index idx_rp_single_protected failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS idx_rp_price ON `+public.TableNameRegionPricing+`(region, protected, price)`); err != nil {
|
||||
g.Log().Warningf(ctx, "create idx_rp_price failed: %v", err)
|
||||
}
|
||||
|
||||
+12
-57
@@ -2,12 +2,11 @@ package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/consts/public"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Scene = &sceneDao{}
|
||||
@@ -26,82 +25,38 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')), -- 更新时间
|
||||
deleted_at DATETIME -- 删除时间
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建场景表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create scene table failed: %v", err)
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"CREATE INDEX IF NOT EXISTS idx_scene_drama_id ON short_drama_scene(drama_id)"); err != nil {
|
||||
g.Log().Warningf(ctx, "创建场景表索引失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create scene table index failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sceneDao) Insert(ctx context.Context, data *entity.Scene) (id int64, err error) {
|
||||
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")
|
||||
r, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.LastInsertId()
|
||||
func (d *sceneDao) Insert(ctx context.Context, data *entity.Scene) (int64, error) {
|
||||
return common.InsertAndReturnId(ctx, public.TableNameScene, data)
|
||||
}
|
||||
|
||||
func (d *sceneDao) GetOne(ctx context.Context, id int64) (res *entity.Scene, err error) {
|
||||
r, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Where("id", id).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
func (d *sceneDao) GetOne(ctx context.Context, id int64) (*entity.Scene, error) {
|
||||
return common.GetOneByPk[entity.Scene](ctx, public.TableNameScene, id)
|
||||
}
|
||||
|
||||
func (d *sceneDao) Update(ctx context.Context, id int64, data *entity.Scene) error {
|
||||
_, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Data(data).Where("id", id).Update()
|
||||
return err
|
||||
return common.UpdateByPk(ctx, public.TableNameScene, id, data)
|
||||
}
|
||||
|
||||
func (d *sceneDao) Delete(ctx context.Context, id int64) error {
|
||||
_, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Unscoped().Where("id", id).Delete()
|
||||
return err
|
||||
return common.DeleteByPk(ctx, public.TableNameScene, id)
|
||||
}
|
||||
|
||||
func (d *sceneDao) DeleteByDrama(ctx context.Context, dramaId int64) error {
|
||||
_, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete()
|
||||
return err
|
||||
return common.DeleteByDrama(ctx, public.TableNameScene, dramaId)
|
||||
}
|
||||
|
||||
func (d *sceneDao) ListPageByDrama(ctx context.Context, dramaId int64, page, pageSize int) (res []*entity.Scene, total int, err error) {
|
||||
m := g.DB().Model(public.TableNameScene).Ctx(ctx).Where("drama_id", dramaId)
|
||||
if pageSize == -1 {
|
||||
r, err := m.OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res = make([]*entity.Scene, 0)
|
||||
err = r.Structs(&res)
|
||||
return res, len(res), err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make([]*entity.Scene, 0)
|
||||
err = r.Structs(&res)
|
||||
return
|
||||
return common.ListPageByDrama[entity.Scene](ctx, public.TableNameScene, dramaId, page, pageSize)
|
||||
}
|
||||
|
||||
func (d *sceneDao) ListByDrama(ctx context.Context, dramaId int64) (res []*entity.Scene, err error) {
|
||||
r, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").All()
|
||||
r, err := g.DB().Model(public.TableNameScene).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").Limit(200).All()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
+54
-18
@@ -29,20 +29,42 @@ func init() {
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "create user table failed: %v", err)
|
||||
}
|
||||
_, _ = g.DB().Exec(ctx, "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_username ON "+public.TableNameUser+"(username) WHERE username != ''")
|
||||
_, _ = g.DB().Exec(ctx, "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_phone ON "+public.TableNameUser+"(phone) WHERE phone != ''")
|
||||
if _, err := g.DB().Exec(ctx, "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_username ON "+public.TableNameUser+"(username) WHERE username != ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "create index idx_user_username failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_phone ON "+public.TableNameUser+"(phone) WHERE phone != ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "create index idx_user_phone failed: %v", err)
|
||||
}
|
||||
// add columns for existing databases
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" ADD COLUMN province TEXT NOT NULL DEFAULT ''")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" ADD COLUMN address TEXT NOT NULL DEFAULT ''")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" ADD COLUMN province TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column province failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" ADD COLUMN address TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "add column address failed: %v", err)
|
||||
}
|
||||
// cleanup legacy columns
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" DROP COLUMN status")
|
||||
_, _ = g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" DROP COLUMN expired_at")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" DROP COLUMN status"); err != nil {
|
||||
g.Log().Warningf(ctx, "drop column status failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUser+" DROP COLUMN expired_at"); err != nil {
|
||||
g.Log().Warningf(ctx, "drop column expired_at failed: %v", err)
|
||||
}
|
||||
// backfill created_at for legacy data
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNameUser+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL")
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNameAccountTransaction+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL")
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentOrder+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL")
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentConfig+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL")
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentChannelTrade+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL")
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNameUser+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "backfill user created_at failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNameAccountTransaction+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "backfill account_transaction created_at failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentOrder+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "backfill payment_order created_at failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentConfig+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "backfill payment_config created_at failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentChannelTrade+" SET created_at = datetime('now','localtime') WHERE created_at IS NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "backfill payment_channel_trade created_at failed: %v", err)
|
||||
}
|
||||
|
||||
// UTC to local timezone migration, run once via PRAGMA user_version
|
||||
var userVersion int
|
||||
@@ -52,16 +74,30 @@ func init() {
|
||||
}
|
||||
if userVersion == 0 {
|
||||
g.Log().Info(ctx, "migration: convert historical UTC time data to local timezone")
|
||||
g.DB().Exec(ctx, "UPDATE "+public.TableNameUser+" SET created_at = datetime(created_at, '+8 hours'), updated_at = datetime(updated_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'")
|
||||
g.DB().Exec(ctx, "UPDATE "+public.TableNameAccountTransaction+" SET created_at = datetime(created_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z' AND created_at IS NOT NULL")
|
||||
g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentOrder+" SET created_at = datetime(created_at, '+8 hours'), updated_at = datetime(updated_at, '+8 hours'), paid_at = datetime(paid_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'")
|
||||
g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentConfig+" SET created_at = datetime(created_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'")
|
||||
g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentChannelTrade+" SET created_at = datetime(created_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'")
|
||||
g.DB().Exec(ctx, "PRAGMA user_version = 1")
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNameUser+" SET created_at = datetime(created_at, '+8 hours'), updated_at = datetime(updated_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'"); err != nil {
|
||||
g.Log().Warningf(ctx, "migrate user UTC to local failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNameAccountTransaction+" SET created_at = datetime(created_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z' AND created_at IS NOT NULL"); err != nil {
|
||||
g.Log().Warningf(ctx, "migrate account_transaction UTC to local failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentOrder+" SET created_at = datetime(created_at, '+8 hours'), updated_at = datetime(updated_at, '+8 hours'), paid_at = datetime(paid_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'"); err != nil {
|
||||
g.Log().Warningf(ctx, "migrate payment_order UTC to local failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentConfig+" SET created_at = datetime(created_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'"); err != nil {
|
||||
g.Log().Warningf(ctx, "migrate payment_config UTC to local failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNamePaymentChannelTrade+" SET created_at = datetime(created_at, '+8 hours') WHERE created_at LIKE '%-%T%:%.%Z' OR created_at LIKE '%-%T%:%s%z'"); err != nil {
|
||||
g.Log().Warningf(ctx, "migrate payment_channel_trade UTC to local failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "PRAGMA user_version = 1"); err != nil {
|
||||
g.Log().Warningf(ctx, "set user_version failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 补充 province 字段到各表的 DDL(仅针对在 province 列存在前入库的老数据)
|
||||
_, _ = g.DB().Exec(ctx, "UPDATE "+public.TableNameUser+" SET province = region WHERE province = '' AND region != ''")
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE "+public.TableNameUser+" SET province = region WHERE province = '' AND region != ''"); err != nil {
|
||||
g.Log().Warningf(ctx, "backfill province from region failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *userDao) Insert(ctx context.Context, data *entity.User) (int64, error) {
|
||||
|
||||
@@ -19,7 +19,9 @@ func init() {
|
||||
ctx := context.Background()
|
||||
// 检测表是否已存在
|
||||
var tableExists bool
|
||||
if r, _ := g.DB().Exec(ctx, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='"+public.TableNameUserModelConfig+"'"); r != nil {
|
||||
if r, err := g.DB().Exec(ctx, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='"+public.TableNameUserModelConfig+"'"); err != nil {
|
||||
g.Log().Warningf(ctx, "check user_model_config table existence failed: %v", err)
|
||||
} else if r != nil {
|
||||
tableExists = true
|
||||
}
|
||||
|
||||
@@ -40,14 +42,17 @@ func init() {
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "创建用户模型配置表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create user model config table failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 表已存在,检测是否包含旧列(base_url),有则迁移到不含 base_url/task_callback_url 的新表
|
||||
r, _ := g.DB().GetAll(ctx, "PRAGMA table_info("+public.TableNameUserModelConfig+")")
|
||||
r, err := g.DB().GetAll(ctx, "PRAGMA table_info("+public.TableNameUserModelConfig+")")
|
||||
var hasBaseUrl bool
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "check user_model_config table columns failed: %v", err)
|
||||
}
|
||||
for _, col := range r {
|
||||
if col["name"].String() == "base_url" {
|
||||
hasBaseUrl = true
|
||||
@@ -73,7 +78,7 @@ func init() {
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`); err != nil {
|
||||
g.Log().Warningf(ctx, "创建新表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create new table failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,7 +102,7 @@ func init() {
|
||||
}
|
||||
|
||||
if _, err := g.DB().Exec(ctx, "DROP TABLE "+public.TableNameUserModelConfig); err != nil {
|
||||
g.Log().Warningf(ctx, "删除旧表失败: %v", err)
|
||||
g.Log().Warningf(ctx, "drop old table failed: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+newTable+" RENAME TO "+public.TableNameUserModelConfig); err != nil {
|
||||
g.Log().Error(ctx, "重命名表失败:", err)
|
||||
@@ -107,7 +112,9 @@ func init() {
|
||||
}
|
||||
|
||||
// 检测是否缺少 concurrency_count 列(已有新表结构但字段不全)
|
||||
if r, _ := g.DB().GetAll(ctx, "PRAGMA table_info("+public.TableNameUserModelConfig+")"); r != nil {
|
||||
if r, err := g.DB().GetAll(ctx, "PRAGMA table_info("+public.TableNameUserModelConfig+")"); err != nil {
|
||||
g.Log().Warningf(ctx, "check user_model_config concurrency_count column failed: %v", err)
|
||||
} else if r != nil {
|
||||
hasCol := false
|
||||
for _, col := range r {
|
||||
if col["name"].String() == "concurrency_count" {
|
||||
@@ -118,7 +125,7 @@ func init() {
|
||||
if !hasCol {
|
||||
g.Log().Info(ctx, "检测到 user_model_config 缺少 concurrency_count 列,正在补充...")
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE "+public.TableNameUserModelConfig+" ADD COLUMN concurrency_count INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
g.Log().Warningf(ctx, "补充 concurrency_count 列失败: %v", err)
|
||||
g.Log().Warningf(ctx, "add concurrency_count column failed: %v", err)
|
||||
} else {
|
||||
g.Log().Info(ctx, "concurrency_count 列补充完成")
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ import (
|
||||
|
||||
type RechargeReq struct {
|
||||
g.Meta `path:"/recharge" method:"post" tags:"短剧管理" summary:"充值"`
|
||||
UserId int64 `json:"user_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
UserId int64 `v:"required" json:"user_id"`
|
||||
Amount int64 `v:"required|min:1" json:"amount"`
|
||||
}
|
||||
|
||||
type ListTransactionReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"交易记录"`
|
||||
UserId int64 `json:"user_id"`
|
||||
Type string `json:"type"` // 筛选类型:recharge/deduct/空(全部)
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
UserId int64 `v:"required" json:"user_id"`
|
||||
Type string `json:"type" v:"in:recharge,deduct" dc:"筛选类型:recharge/deduct"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码"`
|
||||
PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页条数"`
|
||||
}
|
||||
|
||||
type ListTransactionRes struct {
|
||||
|
||||
@@ -19,14 +19,14 @@ type CreateAgentReq struct {
|
||||
|
||||
type ListAgentReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"代理商列表"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Keyword string `json:"keyword"`
|
||||
Phone string `json:"phone"`
|
||||
Province string `json:"province"`
|
||||
Region string `json:"region"`
|
||||
ExpiredAtFrom string `json:"expired_at_from"`
|
||||
ExpiredAtTo string `json:"expired_at_to"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码"`
|
||||
PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页条数"`
|
||||
Keyword string `json:"keyword" dc:"搜索关键词"`
|
||||
Phone string `json:"phone" dc:"手机号"`
|
||||
Province string `json:"province" dc:"省份"`
|
||||
Region string `json:"region" dc:"地区"`
|
||||
ExpiredAtFrom string `json:"expired_at_from" dc:"过期时间(起)"`
|
||||
ExpiredAtTo string `json:"expired_at_to" dc:"过期时间(止)"`
|
||||
}
|
||||
|
||||
type ListAgentItem struct {
|
||||
@@ -45,7 +45,7 @@ type ListAgentRes struct {
|
||||
type UpdateAgentReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新代理商"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
Phone string `json:"phone" v:"phone" dc:"手机号"`
|
||||
Name string `v:"required" json:"name"`
|
||||
Province string `json:"province"`
|
||||
Region string `v:"required" json:"region"`
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type ListBackgroundMusicReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"背景音列表"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
type ListBackgroundMusicRes struct {
|
||||
@@ -18,21 +18,21 @@ type ListBackgroundMusicRes struct {
|
||||
|
||||
type AddBackgroundMusicReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"短剧管理" summary:"添加背景音"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"背景音名称"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"背景音名称"`
|
||||
AudioFile *ghttp.UploadFile `json:"audioFile" dc:"背景音文件"`
|
||||
}
|
||||
|
||||
type UpdateBackgroundMusicReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新背景音"`
|
||||
Id int64 `json:"id" dc:"背景音ID"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"背景音名称"`
|
||||
Id int64 `v:"required" json:"id" dc:"背景音ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"背景音名称"`
|
||||
AudioFile *ghttp.UploadFile `json:"audioFile" dc:"背景音文件"`
|
||||
}
|
||||
|
||||
type DeleteBackgroundMusicReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"短剧管理" summary:"删除背景音"`
|
||||
Id int64 `json:"id" dc:"背景音ID"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Id int64 `v:"required" json:"id" dc:"背景音ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type ListCharacterReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"演员列表"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
type ListCharacterRes struct {
|
||||
@@ -18,25 +18,25 @@ type ListCharacterRes struct {
|
||||
|
||||
type AddCharacterReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"短剧管理" summary:"添加演员"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"演员名称"`
|
||||
Description string `v:"required" json:"description" dc:"演员描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"演员名称"`
|
||||
Description string `v:"required|min-length:1|max-length:2000" json:"description" dc:"演员描述"`
|
||||
VoiceFile *ghttp.UploadFile `json:"voiceFile" dc:"声音文件"`
|
||||
PortraitFile *ghttp.UploadFile `json:"portraitFile" dc:"形象文件"`
|
||||
}
|
||||
|
||||
type UpdateCharacterReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新演员"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
CharId int64 `json:"charId" dc:"演员ID"`
|
||||
Name string `v:"required" json:"name" dc:"演员名称"`
|
||||
Description string `v:"required" json:"description" dc:"演员描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
CharId int64 `v:"required" json:"charId" dc:"演员ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"演员名称"`
|
||||
Description string `v:"required|min-length:1|max-length:2000" json:"description" dc:"演员描述"`
|
||||
VoiceFile *ghttp.UploadFile `json:"voiceFile" dc:"声音文件"`
|
||||
PortraitFile *ghttp.UploadFile `json:"portraitFile" dc:"形象文件"`
|
||||
}
|
||||
|
||||
type DeleteCharacterReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"短剧管理" summary:"删除演员"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
CharId int64 `json:"charId" dc:"演员ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
CharId int64 `v:"required" json:"charId" dc:"演员ID"`
|
||||
}
|
||||
|
||||
@@ -8,22 +8,22 @@ import (
|
||||
|
||||
type CreateCustomerReq struct {
|
||||
g.Meta `path:"/create" method:"post" tags:"短剧管理" summary:"创建客户"`
|
||||
Phone string `v:"required" json:"phone"`
|
||||
Name string `v:"required" json:"name"`
|
||||
Address string `v:"required" json:"address"`
|
||||
AgentId int64 `json:"agent_id"`
|
||||
Phone string `v:"required|phone" json:"phone"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name"`
|
||||
Address string `v:"required|min-length:1|max-length:500" json:"address"`
|
||||
AgentId int64 `v:"required" json:"agent_id"`
|
||||
}
|
||||
|
||||
type ListCustomerReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"客户列表"`
|
||||
AgentId int64 `json:"agent_id"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Keyword string `json:"keyword"`
|
||||
Phone string `json:"phone"`
|
||||
Province string `json:"province"`
|
||||
Region string `json:"region"`
|
||||
AgentName string `json:"agent_name"` // 仅admin按代理商搜索
|
||||
AgentId int64 `json:"agent_id" dc:"代理商ID"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码"`
|
||||
PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页条数"`
|
||||
Keyword string `json:"keyword" dc:"搜索关键词"`
|
||||
Phone string `json:"phone" dc:"手机号"`
|
||||
Province string `json:"province" dc:"省份"`
|
||||
Region string `json:"region" dc:"地区"`
|
||||
AgentName string `json:"agent_name" dc:"代理商名称"`
|
||||
}
|
||||
|
||||
type ListCustomerItem struct {
|
||||
@@ -55,7 +55,7 @@ type CustomerDetail struct {
|
||||
type UpdateCustomerReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新客户"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Phone string `json:"phone" v:"phone" dc:"手机号"`
|
||||
Name string `json:"name" dc:"客户名称"`
|
||||
Address string `json:"address" dc:"地址"`
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
type ListDramaReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"短剧列表"`
|
||||
Page int `json:"page" dc:"页码,从1开始"`
|
||||
PageSize int `json:"pageSize" dc:"每页条数"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码,从1开始"`
|
||||
PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数"`
|
||||
Keyword string `json:"keyword" dc:"搜索关键词"`
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ type ListDramaRes struct {
|
||||
|
||||
type ListEpisodeReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"剧集列表"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Page int `json:"page" dc:"页码,从1开始"`
|
||||
PageSize int `json:"pageSize" dc:"每页条数"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码,从1开始"`
|
||||
PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数"`
|
||||
Keyword string `json:"keyword" dc:"搜索关键词"`
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ type ListEpisodeRes struct {
|
||||
|
||||
type CreateDramaReq struct {
|
||||
g.Meta `path:"/create" method:"post" tags:"短剧管理" summary:"创建短剧"`
|
||||
Title string `v:"required" json:"title" dc:"标题"`
|
||||
Type string `v:"required" json:"type" dc:"内容类型(短剧/漫剧/广告视频)"`
|
||||
Title string `v:"required|min-length:1|max-length:200" json:"title" dc:"标题"`
|
||||
Type string `v:"required|in:short_drama,comic_drama,ad_video" json:"type" dc:"内容类型(short_drama/comic_drama/ad_video)"`
|
||||
Config string `json:"config" dc:"类型专属配置(JSON格式)"`
|
||||
AspectRatio string `json:"aspectRatio" dc:"画面比例"`
|
||||
Resolution string `json:"resolution" dc:"分辨率(720P/1080P)"`
|
||||
EpisodeDuration int `json:"episodeDuration" dc:"时长(秒)"`
|
||||
Resolution string `json:"resolution" v:"in:720P,1080P" dc:"分辨率(720P/1080P)"`
|
||||
EpisodeDuration int `json:"episodeDuration" v:"min:1|max:3600" dc:"时长(秒)"`
|
||||
}
|
||||
|
||||
type CreateDramaRes struct {
|
||||
@@ -60,7 +60,7 @@ type CreateDramaRes struct {
|
||||
|
||||
type GetDramaReq struct {
|
||||
g.Meta `path:"/get" method:"get" tags:"短剧管理" summary:"获取短剧详情"`
|
||||
Id int64 `json:"id" dc:"短剧ID"`
|
||||
Id int64 `v:"required" json:"id" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
// GetDramaRes 包含短剧及其关联的演员、剧集、场景、道具、背景音和生成任务
|
||||
@@ -77,20 +77,20 @@ type GetDramaRes struct {
|
||||
// ==================== Update ====================
|
||||
|
||||
type UpdateDramaReq struct {
|
||||
Id int64 `json:"id" dc:"短剧ID"`
|
||||
Title string `v:"required" json:"title" dc:"标题"`
|
||||
Type string `json:"type" dc:"内容类型"`
|
||||
Id int64 `v:"required" json:"id" dc:"短剧ID"`
|
||||
Title string `v:"required|min-length:1|max-length:200" json:"title" dc:"标题"`
|
||||
Type string `json:"type" v:"in:short_drama,comic_drama,ad_video" dc:"内容类型"`
|
||||
Config string `json:"config" dc:"类型专属配置(JSON格式)"`
|
||||
AspectRatio string `json:"aspectRatio" dc:"画面比例"`
|
||||
Resolution string `json:"resolution" dc:"分辨率(720P/1080P)"`
|
||||
EpisodeDuration int `json:"episodeDuration" dc:"时长(秒)"`
|
||||
Resolution string `json:"resolution" v:"in:720P,1080P" dc:"分辨率(720P/1080P)"`
|
||||
EpisodeDuration int `json:"episodeDuration" v:"min:1|max:3600" dc:"时长(秒)"`
|
||||
}
|
||||
|
||||
// ==================== Delete ====================
|
||||
|
||||
type DeleteDramaReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"短剧管理" summary:"删除短剧"`
|
||||
Id int64 `json:"id" dc:"短剧ID"`
|
||||
Id int64 `v:"required" json:"id" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
// ==================== 字段定义 ====================
|
||||
|
||||
@@ -8,39 +8,39 @@ import (
|
||||
|
||||
type AddEpisodeReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"短剧管理" summary:"添加剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Description string `v:"required" json:"description" dc:"剧情描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Title string `v:"required|min-length:1|max-length:200" json:"title" dc:"剧集标题"`
|
||||
Description string `v:"required|min-length:1|max-length:5000" json:"description" dc:"剧情描述"`
|
||||
Script string `v:"required" json:"script" dc:"剧集脚本"`
|
||||
Index int `json:"index" dc:"剧集序号"`
|
||||
Index int `json:"index" v:"min:0" dc:"剧集序号"`
|
||||
}
|
||||
|
||||
type UpdateEpisodeReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Description string `v:"required" json:"description" dc:"剧情描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `v:"required" json:"epId" dc:"剧集ID"`
|
||||
Title string `v:"required|min-length:1|max-length:200" json:"title" dc:"剧集标题"`
|
||||
Description string `v:"required|min-length:1|max-length:5000" json:"description" dc:"剧情描述"`
|
||||
Script string `v:"required" json:"script" dc:"剧集脚本"`
|
||||
Index int `json:"index" dc:"剧集序号"`
|
||||
Index int `json:"index" v:"min:0" dc:"剧集序号"`
|
||||
}
|
||||
|
||||
type DeleteEpisodeReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"短剧管理" summary:"删除剧集"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `v:"required" json:"epId" dc:"剧集ID"`
|
||||
}
|
||||
|
||||
type GenerateEpisodeReq struct {
|
||||
g.Meta `path:"/generate" method:"post" tags:"短剧管理" summary:"单集生成视频"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
Mode string `json:"mode" dc:"生成模式(parallel/serial)"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
EpId int64 `v:"required" json:"epId" dc:"剧集ID"`
|
||||
Mode string `json:"mode" v:"in:parallel,serial" dc:"生成模式(parallel/serial)"`
|
||||
}
|
||||
|
||||
type EpisodePollReq struct {
|
||||
g.Meta `path:"/poll" method:"get" tags:"短剧管理" summary:"轮询剧集生成状态"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
EpId int64 `v:"required" json:"epId" dc:"剧集ID"`
|
||||
}
|
||||
|
||||
type EpisodePollRes struct {
|
||||
@@ -52,9 +52,9 @@ type EpisodePollRes struct {
|
||||
|
||||
type GenerateScriptReq struct {
|
||||
g.Meta `path:"/generate-script" method:"post" tags:"短剧管理" summary:"生成剧集脚本"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Title string `v:"required" json:"title" dc:"剧集标题"`
|
||||
Description string `v:"required" json:"description" dc:"剧情描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Title string `v:"required|min-length:1|max-length:200" json:"title" dc:"剧集标题"`
|
||||
Description string `v:"required|min-length:1|max-length:5000" json:"description" dc:"剧情描述"`
|
||||
}
|
||||
|
||||
type GenerateScriptRes struct {
|
||||
|
||||
@@ -9,20 +9,20 @@ import (
|
||||
// ContinueSegmentReq 继续下一段
|
||||
type ContinueSegmentReq struct {
|
||||
g.Meta `path:"/segment/continue" method:"post" tags:"短剧生成" summary:"确认当前段并继续下一段"`
|
||||
TaskId int64 `json:"taskId" dc:"任务ID"`
|
||||
TaskId int64 `v:"required" json:"taskId" dc:"任务ID"`
|
||||
}
|
||||
|
||||
// FeedbackSegmentReq 反馈重新生成
|
||||
type FeedbackSegmentReq struct {
|
||||
g.Meta `path:"/segment/feedback" method:"post" tags:"短剧生成" summary:"为当前段提供反馈并重新生成"`
|
||||
TaskId int64 `json:"taskId" dc:"任务ID"`
|
||||
Feedback string `json:"feedback" dc:"反馈内容"`
|
||||
TaskId int64 `v:"required" json:"taskId" dc:"任务ID"`
|
||||
Feedback string `v:"max-length:2000" json:"feedback" dc:"反馈内容"`
|
||||
}
|
||||
|
||||
// GetEpisodeTaskReq 获取剧集当前任务
|
||||
type GetEpisodeTaskReq struct {
|
||||
g.Meta `path:"/episode/task" method:"get" tags:"短剧生成" summary:"获取某集当前生成任务"`
|
||||
EpId int64 `json:"epId" dc:"剧集ID"`
|
||||
EpId int64 `v:"required" json:"epId" dc:"剧集ID"`
|
||||
}
|
||||
|
||||
// GetEpisodeTaskRes 获取某集中所有段任务的响应
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
// GetModelConfigListReq 获取模型配置列表(分页)
|
||||
type GetModelConfigListReq struct {
|
||||
g.Meta `path:"/get-model-list" method:"get" tags:"模型配置" summary:"获取模型配置列表"`
|
||||
Page int `json:"page" dc:"页码,默认1"`
|
||||
PageSize int `json:"pageSize" dc:"每页条数,默认20"`
|
||||
ModelType string `json:"modelType" dc:"模型类型筛选: chat/video"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码,默认1"`
|
||||
PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数,默认20"`
|
||||
ModelType string `json:"modelType" v:"in:chat,video" dc:"模型类型筛选: chat/video"`
|
||||
Keyword string `json:"keyword" dc:"模型名称关键词"`
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ type GetModelConfigListRes struct {
|
||||
type SaveModelConfigReq struct {
|
||||
g.Meta `path:"/save-model-config" method:"post" tags:"模型配置" summary:"保存模型配置"`
|
||||
Id int64 `json:"id"`
|
||||
ModelType string `json:"modelType" v:"required" dc:"chat=对话模型 video=视频模型"`
|
||||
ModelName string `json:"modelName" v:"required" dc:"模型名称"`
|
||||
ModelType string `json:"modelType" v:"required|in:chat,video" dc:"chat=对话模型 video=视频模型"`
|
||||
ModelName string `json:"modelName" v:"required|min-length:1|max-length:200" dc:"模型名称"`
|
||||
Schema *gjson.Json `json:"schema" dc:"请求体JSON Schema"`
|
||||
FirstFrameMapping string `json:"firstFrameMapping" dc:"首帧映射字段名,如 input.media"`
|
||||
ReferenceTemplate string `json:"referenceTemplate" dc:"参考图引用模板,如 {\"type\":\"image\",\"url\":\"%s\"}"`
|
||||
Price int `json:"price" dc:"价格(分)"`
|
||||
PriceUnit string `json:"priceUnit" dc:"价格单位(second=每秒/video=每次视频)"`
|
||||
ConcurrencyCount int `json:"concurrencyCount" dc:"并发处理数(同一模型最多同时处理的请求数量)"`
|
||||
Price int `json:"price" v:"min:0" dc:"价格(分)"`
|
||||
PriceUnit string `json:"priceUnit" v:"in:second,video" dc:"价格单位(second=每秒/video=每次视频)"`
|
||||
ConcurrencyCount int `json:"concurrencyCount" v:"min:1" dc:"并发处理数(同一模型最多同时处理的请求数量)"`
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type ListChannelTradeReq struct {
|
||||
g.Meta `path:"/list-by-order" method:"get" tags:"短剧管理" summary:"渠道交易记录"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
OrderId int64 `v:"required" json:"order_id"`
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ type GetPaymentConfigRes struct {
|
||||
|
||||
type SavePaymentConfigReq struct {
|
||||
g.Meta `path:"/save" method:"post" tags:"支付配置" summary:"保存支付配置"`
|
||||
Channel string `json:"channel" v:"required" dc:"支付渠道: wechat/alipay/offline"`
|
||||
ChannelType string `json:"channelType" dc:"支付类型: jsapi/h5/app/native/manual"`
|
||||
AppId string `json:"appId"`
|
||||
MchId string `json:"mchId"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
AppSecret string `json:"appSecret"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Channel string `json:"channel" v:"required|in:wechat,alipay,offline" dc:"支付渠道: wechat/alipay/offline"`
|
||||
ChannelType string `json:"channelType" v:"required|in:jsapi,h5,app,native,manual" dc:"支付类型: jsapi/h5/app/native/manual"`
|
||||
AppId string `json:"appId" dc:"应用ID"`
|
||||
MchId string `json:"mchId" dc:"商户ID"`
|
||||
ApiKey string `json:"apiKey" dc:"API密钥"`
|
||||
AppSecret string `json:"appSecret" dc:"应用密钥"`
|
||||
PrivateKey string `json:"privateKey" dc:"私钥"`
|
||||
PublicKey string `json:"publicKey" dc:"公钥"`
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package dto
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
import (
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type PrepayReq struct {
|
||||
g.Meta `path:"/prepay" method:"post" tags:"短剧管理" summary:"预支付"`
|
||||
Amount int64 `v:"required|min:1" json:"amount"`
|
||||
Channel string `v:"required|in:wechat,alipay,offline" json:"channel"`
|
||||
OrderType string `json:"order_type" dc:"recharge / renewal"`
|
||||
Duration int `json:"duration" dc:"续费年数(renewal时必填)"`
|
||||
OrderType string `json:"order_type" v:"in:recharge,renewal" dc:"recharge / renewal"`
|
||||
Duration int `json:"duration" v:"min:1" dc:"续费年数(renewal时必填)"`
|
||||
}
|
||||
|
||||
type PrepayRes struct {
|
||||
@@ -20,7 +24,7 @@ type PrepayRes struct {
|
||||
|
||||
type PaymentStatusReq struct {
|
||||
g.Meta `path:"/status" method:"get" tags:"短剧管理" summary:"支付状态查询"`
|
||||
OrderNo string `json:"order_no"`
|
||||
OrderNo string `v:"required" json:"order_no"`
|
||||
}
|
||||
|
||||
type PaymentStatusRes struct {
|
||||
@@ -51,3 +55,14 @@ type CreateRenewOrderRes struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type ListPaymentOrderReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"支付订单列表"`
|
||||
Page int `json:"page" dc:"页码"`
|
||||
PageSize int `json:"pageSize" dc:"每页数量"`
|
||||
}
|
||||
|
||||
type ListPaymentOrderRes struct {
|
||||
List []*entity.PaymentOrder `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type ListPropReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"道具列表"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
type ListPropRes struct {
|
||||
@@ -18,23 +18,23 @@ type ListPropRes struct {
|
||||
|
||||
type AddPropReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"短剧管理" summary:"添加道具"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"道具名称"`
|
||||
Description string `v:"required" json:"description" dc:"道具描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"道具名称"`
|
||||
Description string `v:"required|min-length:1|max-length:2000" json:"description" dc:"道具描述"`
|
||||
ImageFile *ghttp.UploadFile `json:"imageFile" dc:"道具图片"`
|
||||
}
|
||||
|
||||
type UpdatePropReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新道具"`
|
||||
Id int64 `json:"id" dc:"道具ID"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"道具名称"`
|
||||
Description string `v:"required" json:"description" dc:"道具描述"`
|
||||
Id int64 `v:"required" json:"id" dc:"道具ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"道具名称"`
|
||||
Description string `v:"required|min-length:1|max-length:2000" json:"description" dc:"道具描述"`
|
||||
ImageFile *ghttp.UploadFile `json:"imageFile" dc:"道具图片"`
|
||||
}
|
||||
|
||||
type DeletePropReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"短剧管理" summary:"删除道具"`
|
||||
Id int64 `json:"id" dc:"道具ID"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Id int64 `v:"required" json:"id" dc:"道具ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
type ListRegionPricingReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"地区定价列表"`
|
||||
Page int `json:"page" dc:"页码,默认1"`
|
||||
PageSize int `json:"pageSize" dc:"每页条数,默认20"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码,默认1"`
|
||||
PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数,默认20"`
|
||||
Keyword string `json:"keyword" dc:"地区名称关键词"`
|
||||
Protected int `json:"protected" d:"-1" dc:"类型筛选: -1=全部 0=普通 1=受保护"`
|
||||
Protected int `json:"protected" d:"-1" v:"in:-1,0,1" dc:"类型筛选: -1=全部 0=普通 1=受保护"`
|
||||
}
|
||||
|
||||
type ListRegionPricingRes struct {
|
||||
@@ -22,11 +22,11 @@ type ListRegionPricingRes struct {
|
||||
type SaveRegionPricingReq struct {
|
||||
g.Meta `path:"/save" method:"post" tags:"短剧管理" summary:"保存地区定价"`
|
||||
Id int64 `json:"id"`
|
||||
Province string `v:"required" json:"province"`
|
||||
Region string `v:"required" json:"region"`
|
||||
Protected int `json:"protected"`
|
||||
Province string `v:"required|min-length:1|max-length:100" json:"province"`
|
||||
Region string `v:"required|min-length:1|max-length:100" json:"region"`
|
||||
Protected int `json:"protected" v:"in:0,1"`
|
||||
Price int64 `v:"required|min:0" json:"price"`
|
||||
MaxCustomers int `json:"max_customers"`
|
||||
MaxCustomers int `json:"max_customers" v:"min:0"`
|
||||
}
|
||||
|
||||
type ListRegionsRes struct {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type ListSceneReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"场景列表"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
type ListSceneRes struct {
|
||||
@@ -18,23 +18,23 @@ type ListSceneRes struct {
|
||||
|
||||
type AddSceneReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"短剧管理" summary:"添加场景"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"场景名称"`
|
||||
Description string `v:"required" json:"description" dc:"场景描述"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"场景名称"`
|
||||
Description string `v:"required|min-length:1|max-length:2000" json:"description" dc:"场景描述"`
|
||||
ImageFile *ghttp.UploadFile `json:"imageFile" dc:"场景图片"`
|
||||
}
|
||||
|
||||
type UpdateSceneReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"短剧管理" summary:"更新场景"`
|
||||
Id int64 `json:"id" dc:"场景ID"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required" json:"name" dc:"场景名称"`
|
||||
Description string `v:"required" json:"description" dc:"场景描述"`
|
||||
Id int64 `v:"required" json:"id" dc:"场景ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `v:"required|min-length:1|max-length:100" json:"name" dc:"场景名称"`
|
||||
Description string `v:"required|min-length:1|max-length:2000" json:"description" dc:"场景描述"`
|
||||
ImageFile *ghttp.UploadFile `json:"imageFile" dc:"场景图片"`
|
||||
}
|
||||
|
||||
type DeleteSceneReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"短剧管理" summary:"删除场景"`
|
||||
Id int64 `json:"id" dc:"场景ID"`
|
||||
DramaId int64 `json:"dramaId" dc:"短剧ID"`
|
||||
Id int64 `v:"required" json:"id" dc:"场景ID"`
|
||||
DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"`
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@ type LoginUser struct {
|
||||
|
||||
type ChangePasswordReq struct {
|
||||
g.Meta `path:"/change-password" method:"post" tags:"短剧管理" summary:"修改密码"`
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
OldPassword string `v:"required" json:"old_password"`
|
||||
NewPassword string `v:"required|min-length:6" json:"new_password"`
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type GetUserModelConfigReq struct {
|
||||
g.Meta `path:"/get-user-config" method:"get" tags:"模型配置" summary:"获取用户模型配置"`
|
||||
ModelType string `json:"modelType" dc:"模型类型(chat/video),为空时返回首个活跃配置"`
|
||||
ModelType string `json:"modelType" v:"in:chat,video" dc:"模型类型(chat/video),为空时返回首个活跃配置"`
|
||||
}
|
||||
|
||||
type GetUserModelConfigRes struct {
|
||||
@@ -17,27 +17,27 @@ type GetUserModelConfigRes struct {
|
||||
|
||||
// SaveUserModelConfigItem 单条用户模型配置
|
||||
type SaveUserModelConfigItem struct {
|
||||
ModelConfigId int64 `json:"modelConfigId" dc:"关联模型配置ID"`
|
||||
ApiKey string `json:"apiKey" dc:"用户API密钥"`
|
||||
EndpointUrl string `json:"endpointUrl" dc:"端点地址(基础域名),如 https://api.example.com"`
|
||||
RequestPath string `json:"requestPath" dc:"接口地址(请求路径),如 /v1/chat/completions"`
|
||||
CallbackPath string `json:"callbackPath" dc:"回调接口地址(回调路径,仅视频模型)"`
|
||||
Temperature float64 `json:"temperature" dc:"温度参数"`
|
||||
MaxTokens int `json:"maxTokens" dc:"最大Token数(不超过系统配置)"`
|
||||
ModelConfigId int64 `json:"modelConfigId" v:"required" dc:"关联模型配置ID"`
|
||||
ApiKey string `json:"apiKey" v:"required|min-length:1|max-length:500" dc:"用户API密钥"`
|
||||
EndpointUrl string `json:"endpointUrl" v:"required|max-length:500" dc:"端点地址(基础域名),如 https://api.example.com"`
|
||||
RequestPath string `json:"requestPath" v:"required|max-length:500" dc:"接口地址(请求路径),如 /v1/chat/completions"`
|
||||
CallbackPath string `json:"callbackPath" v:"max-length:500" dc:"回调接口地址(回调路径,仅视频模型)"`
|
||||
Temperature float64 `json:"temperature" v:"min:0|max:2" dc:"温度参数"`
|
||||
MaxTokens int `json:"maxTokens" v:"min:1|max:1000000" dc:"最大Token数(不超过系统配置)"`
|
||||
ConcurrencyCount int `json:"concurrencyCount" v:"min:1" dc:"并发处理数(必须大于0)"`
|
||||
}
|
||||
|
||||
type SaveUserModelConfigReq struct {
|
||||
g.Meta `path:"/save-user-config" method:"post" tags:"模型配置" summary:"批量保存用户模型配置"`
|
||||
Configs []*SaveUserModelConfigItem `json:"configs" dc:"模型配置列表"`
|
||||
Configs []*SaveUserModelConfigItem `json:"configs" v:"required|min-length:1" dc:"模型配置列表"`
|
||||
}
|
||||
|
||||
// GetUserModelListReq 获取用户模型配置列表(分页)
|
||||
type GetUserModelListReq struct {
|
||||
g.Meta `path:"/get-user-model-list" method:"get" tags:"模型配置" summary:"获取系统模型列表及用户配置状态"`
|
||||
Page int `json:"page" dc:"页码,默认1"`
|
||||
PageSize int `json:"pageSize" dc:"每页条数,默认20"`
|
||||
ModelType string `json:"modelType" dc:"模型类型筛选: chat/video"`
|
||||
Page int `json:"page" d:"1" v:"min:1" dc:"页码,默认1"`
|
||||
PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数,默认20"`
|
||||
ModelType string `json:"modelType" v:"in:chat,video" dc:"模型类型筛选: chat/video"`
|
||||
Keyword string `json:"keyword" dc:"模型名称关键词"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package entity
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type Character struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"演员ID" json:"id" json:"id" dc:"演员ID"`
|
||||
Id int64 `orm:"id" json:"id" dc:"演员ID" json:"id" dc:"演员ID"`
|
||||
DramaId int64 `orm:"drama_id" json:"dramaId" dc:"短剧ID" json:"drama_id" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `orm:"name" json:"name" dc:"演员名称" json:"name" json:"name" dc:"演员名称"`
|
||||
Description string `orm:"description" json:"description" dc:"演员描述" json:"description" json:"description" dc:"演员描述"`
|
||||
Name string `orm:"name" json:"name" dc:"演员名称" json:"name" dc:"演员名称"`
|
||||
Description string `orm:"description" json:"description" dc:"演员描述" json:"description" dc:"演员描述"`
|
||||
VoicePath string `orm:"voice_path" json:"voicePath" dc:"声音文件路径" json:"voice_path" json:"voicePath" dc:"声音文件路径"`
|
||||
PortraitPath string `orm:"portrait_path" json:"portraitPath" dc:"形象文件路径" json:"portrait_path" json:"portraitPath" dc:"形象文件路径"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间" json:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
|
||||
@@ -3,16 +3,16 @@ package entity
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type Drama struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"项目ID" json:"id" json:"id" dc:"项目ID"`
|
||||
Id int64 `orm:"id" json:"id" dc:"项目ID"`
|
||||
UserId int64 `orm:"user_id" json:"userId" dc:"所属用户ID"`
|
||||
Title string `orm:"title" json:"title" dc:"标题" json:"title" json:"title" dc:"标题"`
|
||||
Type string `orm:"type" json:"type" dc:"内容类型(短剧/漫剧/广告视频)" json:"type" json:"type" dc:"内容类型(短剧/漫剧/广告视频)"`
|
||||
Config string `orm:"config" json:"config" dc:"类型专属配置(JSON格式)" json:"config" json:"config" dc:"类型专属配置(JSON格式)"`
|
||||
AspectRatio string `orm:"aspect_ratio" json:"aspectRatio" dc:"画面比例" json:"aspect_ratio" json:"aspectRatio" dc:"画面比例"`
|
||||
Resolution string `orm:"resolution" json:"resolution" dc:"分辨率(720P/1080P)" json:"resolution" json:"resolution" dc:"分辨率(720P/1080P)"`
|
||||
EpisodeDuration int64 `orm:"episode_duration" json:"episodeDuration" dc:"时长(秒)" json:"episode_duration" json:"episodeDuration" dc:"时长(秒)"`
|
||||
EpCount int `orm:"episode_count" json:"epCount" dc:"剧集数量" json:"episode_count" json:"epCount" dc:"剧集数量"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间" json:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间" json:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
DeletedAt *gtime.Time `orm:"deleted_at" json:"deletedAt" dc:"删除时间" json:"deleted_at" json:"deletedAt" dc:"删除时间"`
|
||||
Title string `orm:"title" json:"title" dc:"标题"`
|
||||
Type string `orm:"type" json:"type" dc:"内容类型(短剧/漫剧/广告视频)"`
|
||||
Config string `orm:"config" json:"config" dc:"类型专属配置(JSON格式)"`
|
||||
AspectRatio string `orm:"aspect_ratio" json:"aspectRatio" dc:"画面比例"`
|
||||
Resolution string `orm:"resolution" json:"resolution" dc:"分辨率(720P/1080P)"`
|
||||
EpisodeDuration int64 `orm:"episode_duration" json:"episodeDuration" dc:"时长(秒)"`
|
||||
EpCount int `orm:"episode_count" json:"epCount" dc:"剧集数量"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
DeletedAt *gtime.Time `orm:"deleted_at" json:"deletedAt" dc:"删除时间"`
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package entity
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type Episode struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"剧集ID" json:"id" json:"id" dc:"剧集ID"`
|
||||
Id int64 `orm:"id" json:"id" dc:"剧集ID" json:"id" dc:"剧集ID"`
|
||||
DramaId int64 `orm:"drama_id" json:"dramaId" dc:"短剧ID" json:"drama_id" json:"dramaId" dc:"短剧ID"`
|
||||
Index int `orm:"idx" json:"index" dc:"剧集序号" json:"idx" json:"index" dc:"剧集序号"`
|
||||
Title string `orm:"title" json:"title" dc:"剧集标题" json:"title" json:"title" dc:"剧集标题"`
|
||||
Description string `orm:"description" json:"description" dc:"剧情描述" json:"description" json:"description" dc:"剧情描述"`
|
||||
Script string `orm:"script" json:"script" dc:"剧集脚本" json:"script" json:"script" dc:"剧集脚本"`
|
||||
Status string `orm:"status" json:"status" dc:"生成状态" json:"status" json:"status" dc:"生成状态"`
|
||||
Title string `orm:"title" json:"title" dc:"剧集标题" json:"title" dc:"剧集标题"`
|
||||
Description string `orm:"description" json:"description" dc:"剧情描述" json:"description" dc:"剧情描述"`
|
||||
Script string `orm:"script" json:"script" dc:"剧集脚本" json:"script" dc:"剧集脚本"`
|
||||
Status string `orm:"status" json:"status" dc:"生成状态" json:"status" dc:"生成状态"`
|
||||
VideoUrl string `orm:"video_url" json:"videoUrl" dc:"视频URL" json:"video_url" json:"videoUrl" dc:"视频URL"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间" json:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间" json:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
|
||||
@@ -3,10 +3,10 @@ package entity
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type Prop struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"道具ID" json:"id" json:"id" dc:"道具ID"`
|
||||
Id int64 `orm:"id" json:"id" dc:"道具ID" json:"id" dc:"道具ID"`
|
||||
DramaId int64 `orm:"drama_id" json:"dramaId" dc:"短剧ID" json:"drama_id" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `orm:"name" json:"name" dc:"道具名称" json:"name" json:"name" dc:"道具名称"`
|
||||
Description string `orm:"description" json:"description" dc:"道具描述" json:"description" json:"description" dc:"道具描述"`
|
||||
Name string `orm:"name" json:"name" dc:"道具名称" json:"name" dc:"道具名称"`
|
||||
Description string `orm:"description" json:"description" dc:"道具描述" json:"description" dc:"道具描述"`
|
||||
ImagePath string `orm:"image_path" json:"imagePath" dc:"道具图片路径" json:"image_path" json:"imagePath" dc:"道具图片路径"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间" json:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间" json:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
|
||||
@@ -3,10 +3,10 @@ package entity
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type Scene struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"场景ID" json:"id" json:"id" dc:"场景ID"`
|
||||
Id int64 `orm:"id" json:"id" dc:"场景ID" json:"id" dc:"场景ID"`
|
||||
DramaId int64 `orm:"drama_id" json:"dramaId" dc:"短剧ID" json:"drama_id" json:"dramaId" dc:"短剧ID"`
|
||||
Name string `orm:"name" json:"name" dc:"场景名称" json:"name" json:"name" dc:"场景名称"`
|
||||
Description string `orm:"description" json:"description" dc:"场景描述" json:"description" json:"description" dc:"场景描述"`
|
||||
Name string `orm:"name" json:"name" dc:"场景名称" json:"name" dc:"场景名称"`
|
||||
Description string `orm:"description" json:"description" dc:"场景描述" json:"description" dc:"场景描述"`
|
||||
ImagePath string `orm:"image_path" json:"imagePath" dc:"场景图片路径" json:"image_path" json:"imagePath" dc:"场景图片路径"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间" json:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间" json:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
|
||||
@@ -7,7 +7,7 @@ type User struct {
|
||||
Role string `orm:"role" json:"role"`
|
||||
Username string `orm:"username" json:"username"`
|
||||
Phone string `orm:"phone" json:"phone"`
|
||||
Password string `orm:"password" json:"password"`
|
||||
Password string `orm:"password" json:"-"`
|
||||
Name string `orm:"name" json:"name"`
|
||||
Province string `orm:"province" json:"province"`
|
||||
Region string `orm:"region" json:"region"`
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s *agentService) ExtractRegionFromAddress(ctx context.Context, address str
|
||||
}
|
||||
}
|
||||
if province == "" {
|
||||
return "", "", errors.New("无法从地址中识别出所在省份,请确认地址包含省/直辖市/自治区信息")
|
||||
return "", "", errors.New("unable to identify province from address, ensure it contains province/municipality/autonomous region")
|
||||
}
|
||||
|
||||
rest := strings.TrimSpace(strings.TrimPrefix(address, province))
|
||||
@@ -74,7 +74,7 @@ func (s *agentService) ExtractRegionFromAddress(ctx context.Context, address str
|
||||
}
|
||||
}
|
||||
if region == "" {
|
||||
return "", "", fmt.Errorf("无法从地址中识别出所在城市,请确认地址包含市/州/盟信息")
|
||||
return "", "", fmt.Errorf("unable to identify city from address")
|
||||
}
|
||||
return province, region, nil
|
||||
}
|
||||
@@ -83,12 +83,12 @@ func (s *agentService) ExtractRegionFromAddress(ctx context.Context, address str
|
||||
func (s *agentService) CreateAgent(ctx context.Context, username, password, phone, name, province, region string, regionProtected bool) (*entity.User, error) {
|
||||
existing, _ := dao.User.GetByUsername(ctx, username)
|
||||
if existing != nil {
|
||||
return nil, errors.New("用户名已存在")
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
if phone != "" {
|
||||
existing, _ = dao.User.GetByPhone(ctx, phone)
|
||||
if existing != nil {
|
||||
return nil, errors.New("手机号已被使用")
|
||||
return nil, errors.New("phone already in use")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +108,12 @@ func (s *agentService) CreateAgent(ctx context.Context, username, password, phon
|
||||
|
||||
if regionProtected {
|
||||
if len(activeAgents) > 0 {
|
||||
return nil, errors.New("该地区已有未到期代理商,无法添加受地区保护的代理商")
|
||||
return nil, errors.New("active agent already exists in this region")
|
||||
}
|
||||
} else {
|
||||
for _, a := range activeAgents {
|
||||
if a.RegionProtected == 1 {
|
||||
return nil, errors.New("该地区已有未到期受保护代理商,无法添加普通代理商")
|
||||
return nil, errors.New("protected agent already exists in this region")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func (s *agentService) CreateAgent(ctx context.Context, username, password, phon
|
||||
}
|
||||
pricing, _ := dao.RegionPricing.GetByRegion(ctx, region, protectedVal)
|
||||
if pricing == nil {
|
||||
return nil, errors.New("该地区未配置价格,请先在系统配置中设置地区定价")
|
||||
return nil, errors.New("region pricing not configured")
|
||||
}
|
||||
|
||||
hash, _ := bcryptGenerate(password)
|
||||
@@ -162,7 +162,7 @@ func (s *agentService) CreateAgent(ctx context.Context, username, password, phon
|
||||
func (s *agentService) RenewAgent(ctx context.Context, agentId int64, duration ...int) error {
|
||||
agent, err := dao.User.GetOne(ctx, agentId)
|
||||
if err != nil || agent == nil || agent.Role != "agent" {
|
||||
return errors.New("代理商不存在")
|
||||
return errors.New("agent not found")
|
||||
}
|
||||
|
||||
years := 1
|
||||
@@ -188,7 +188,7 @@ func (s *agentService) RenewAgentByOrder(ctx context.Context, agentId int64, raw
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), ¶ms); err != nil {
|
||||
return errors.New("续费参数异常")
|
||||
return errors.New("invalid renewal parameters")
|
||||
}
|
||||
return s.RenewAgent(ctx, agentId, params.Duration)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s *bgmService) UpdateBackgroundMusic(ctx context.Context, id, dramaId int6
|
||||
return err
|
||||
}
|
||||
if m == nil {
|
||||
return fmt.Errorf("背景音不存在: %d", id)
|
||||
return fmt.Errorf("background music not found: %d", id)
|
||||
}
|
||||
if name != "" {
|
||||
m.Name = name
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *characterService) UpdateCharacter(ctx context.Context, dramaId, charId
|
||||
return err
|
||||
}
|
||||
if c == nil {
|
||||
return fmt.Errorf("演员不存在: %d", charId)
|
||||
return fmt.Errorf("character not found: %d", charId)
|
||||
}
|
||||
if name != "" {
|
||||
c.Name = name
|
||||
@@ -95,12 +95,12 @@ func (s *characterService) SaveCharacterFile(ctx context.Context, dramaId int64,
|
||||
return "", err
|
||||
}
|
||||
if d == nil {
|
||||
return "", fmt.Errorf("短剧不存在: %d", dramaId)
|
||||
return "", fmt.Errorf("drama not found: %d", dramaId)
|
||||
}
|
||||
workspaceDir := WorkspaceDir(d.Title)
|
||||
subPath := filepath.Join(workspaceDir, subDir)
|
||||
if err := os.MkdirAll(subPath, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
return "", fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
name := filename
|
||||
if len(actorName) > 0 && actorName[0] != "" {
|
||||
@@ -110,11 +110,11 @@ func (s *characterService) SaveCharacterFile(ctx context.Context, dramaId int64,
|
||||
dest := filepath.Join(subPath, name)
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建文件失败: %w", err)
|
||||
return "", fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer func() { _ = out.Close() }()
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
@@ -139,16 +139,16 @@ func ValidateUploadFile(ctx context.Context, fileSize int64, fileName string, ca
|
||||
allowedExts = allowedAudioExts
|
||||
maxSize = maxAudioSize
|
||||
default:
|
||||
return fmt.Errorf("不支持的文件类型: %s", category)
|
||||
return fmt.Errorf("unsupported file type: %s", category)
|
||||
}
|
||||
|
||||
if fileSize > maxSize {
|
||||
return fmt.Errorf("文件大小超出限制(最大 %dMB)", maxSize/(1024*1024))
|
||||
return fmt.Errorf("file size exceeds limit (max %dMB)", maxSize/(1024*1024))
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
if !allowedExts[ext] {
|
||||
return fmt.Errorf("不支持的文件格式: %s", ext)
|
||||
return fmt.Errorf("unsupported file format: %s", ext)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -34,24 +34,24 @@ func (s *customerService) CreateCustomer(ctx context.Context, phone, name, addre
|
||||
|
||||
agent, err := dao.User.GetOne(ctx, agentId)
|
||||
if err != nil || agent == nil || agent.Role != "agent" {
|
||||
return nil, errors.New("代理商不存在")
|
||||
return nil, errors.New("agent not found")
|
||||
}
|
||||
ap, _ := dao.AgentProfile.Get(ctx, agentId)
|
||||
if ap != nil && ap.ExpiredAt != nil && ap.ExpiredAt.Before(gtime.Now()) {
|
||||
return nil, errors.New("代理商已过期")
|
||||
return nil, errors.New("agent has expired")
|
||||
}
|
||||
|
||||
ap, _ = dao.AgentProfile.Get(ctx, agentId)
|
||||
if ap != nil && ap.MaxCustomers > 0 {
|
||||
count, _ := dao.CustomerProfile.CountByAgent(ctx, agentId)
|
||||
if count >= ap.MaxCustomers {
|
||||
return nil, fmt.Errorf("已达到最大客户数上限(%d)", ap.MaxCustomers)
|
||||
return nil, fmt.Errorf("max customer limit reached (%d)", ap.MaxCustomers)
|
||||
}
|
||||
}
|
||||
|
||||
existing, _ := dao.User.GetByPhone(ctx, phone)
|
||||
if existing != nil {
|
||||
return nil, errors.New("该手机号已注册")
|
||||
return nil, errors.New("phone number already registered")
|
||||
}
|
||||
|
||||
defaultPwd := phone
|
||||
@@ -89,7 +89,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, phone, name, addre
|
||||
func (s *customerService) CheckBalance(ctx context.Context, customerId int64, durationSec int64) (bool, int64, error) {
|
||||
cp, _ := dao.CustomerProfile.Get(ctx, customerId)
|
||||
if cp == nil {
|
||||
return false, 0, errors.New("客户不存在")
|
||||
return false, 0, errors.New("customer not found")
|
||||
}
|
||||
cfg := ModelConfigService.GetActiveModel(ctx, "video")
|
||||
cost := durationSec * int64(cfg.Price)
|
||||
@@ -100,10 +100,10 @@ func (s *customerService) CheckBalance(ctx context.Context, customerId int64, du
|
||||
func (s *customerService) DeductBalance(ctx context.Context, customerId int64, amount int64, remark, createdBy string) error {
|
||||
cp, err := dao.CustomerProfile.Get(ctx, customerId)
|
||||
if err != nil || cp == nil {
|
||||
return errors.New("客户不存在")
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
if cp.Balance < amount {
|
||||
return errors.New("余额不足")
|
||||
return errors.New("insufficient balance")
|
||||
}
|
||||
newBalance := cp.Balance - amount
|
||||
if err := dao.CustomerProfile.UpdateBalance(ctx, customerId, newBalance); err != nil {
|
||||
@@ -163,7 +163,7 @@ func (s *customerService) GetCustomerDetail(ctx context.Context, id int64) (*dto
|
||||
// UpdateCustomer 更新客户信息
|
||||
func (s *customerService) UpdateCustomer(ctx context.Context, agentId int64, req *dto.UpdateCustomerReq) error {
|
||||
if ap, _ := dao.AgentProfile.Get(ctx, agentId); ap != nil && ap.ExpiredAt != nil && ap.ExpiredAt.Before(gtime.Now()) {
|
||||
return errors.New("代理商已过期,无法编辑客户")
|
||||
return errors.New("agent has expired, cannot edit customer")
|
||||
}
|
||||
|
||||
m := g.Map{}
|
||||
|
||||
@@ -25,7 +25,7 @@ var DramaService = new(dramaService)
|
||||
func (s *dramaService) Create(ctx context.Context, title, contentType, config, aspectRatio string, episodeDuration int64, resolution string, userId int64) (int64, error) {
|
||||
existing, _ := dao.Drama.GetByTitle(ctx, title)
|
||||
if existing != nil {
|
||||
return 0, fmt.Errorf("短剧标题已存在: %s", title)
|
||||
return 0, fmt.Errorf("drama title already exists: %s", title)
|
||||
}
|
||||
|
||||
id, err := dao.Drama.Insert(ctx, &entity.Drama{
|
||||
@@ -42,7 +42,7 @@ func (s *dramaService) Create(ctx context.Context, title, contentType, config, a
|
||||
}
|
||||
|
||||
if err := CreateWorkspace(title); err != nil {
|
||||
g.Log().Warningf(ctx, "创建工作空间目录失败: %v", err)
|
||||
g.Log().Warningf(ctx, "create workspace dir failed: %v", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -83,13 +83,13 @@ func (s *dramaService) Update(ctx context.Context, id int64, title, contentType,
|
||||
return err
|
||||
}
|
||||
if d == nil {
|
||||
return fmt.Errorf("短剧不存在: %d", id)
|
||||
return fmt.Errorf("drama not found: %d", id)
|
||||
}
|
||||
oldTitle := d.Title
|
||||
if title != "" && title != oldTitle {
|
||||
existing, _ := dao.Drama.GetByTitle(ctx, title)
|
||||
if existing != nil {
|
||||
return fmt.Errorf("短剧标题已存在: %s", title)
|
||||
return fmt.Errorf("drama title already exists: %s", title)
|
||||
}
|
||||
d.Title = title
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func (s *dramaService) Delete(ctx context.Context, id int64) error {
|
||||
|
||||
if d != nil {
|
||||
if err := RemoveWorkspace(d.Title); err != nil {
|
||||
g.Log().Warningf(ctx, "删除工作空间目录失败: %v", err)
|
||||
g.Log().Warningf(ctx, "remove workspace dir failed: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -153,17 +153,38 @@ func (s *dramaService) Delete(ctx context.Context, id int64) error {
|
||||
|
||||
// ==================== Workspace ====================
|
||||
|
||||
const workspaceRoot = "workspace"
|
||||
var workspaceRoot string
|
||||
|
||||
func init() {
|
||||
wd, err := os.Getwd()
|
||||
if err == nil {
|
||||
workspaceRoot = filepath.Join(wd, "workspace")
|
||||
} else {
|
||||
workspaceRoot = "workspace"
|
||||
}
|
||||
}
|
||||
|
||||
var workspaceSubdirs = []string{"产出视频", "演员形象", "演员声音", "场景", "道具", "背景音乐"}
|
||||
|
||||
// sanitizeDirName 将短剧标题转为安全的目录名
|
||||
// sanitizeDirName 将标题转为安全的目录名(仅保留字母、数字、中文和下划线)
|
||||
func sanitizeDirName(title string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"/", "_", "\\", "_", ":", "_", "*", "_",
|
||||
"?", "_", "\"", "_", "<", "_", ">", "_", "|", "_",
|
||||
)
|
||||
return replacer.Replace(title)
|
||||
var b strings.Builder
|
||||
b.Grow(len(title))
|
||||
for _, r := range title {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || (r >= 0x4E00 && r <= 0x9FFF) || (r >= 0x3400 && r <= 0x4DBF) {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
b.WriteRune('_')
|
||||
}
|
||||
}
|
||||
result := b.String()
|
||||
for result != "" && result[0] == '_' {
|
||||
result = result[1:]
|
||||
}
|
||||
if result == "" {
|
||||
result = "unnamed"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WorkspaceDir 返回短剧的 workspace 目录路径
|
||||
@@ -176,7 +197,7 @@ func CreateWorkspace(title string) error {
|
||||
root := WorkspaceDir(title)
|
||||
for _, sub := range workspaceSubdirs {
|
||||
if err := os.MkdirAll(filepath.Join(root, sub), 0755); err != nil {
|
||||
return fmt.Errorf("创建工作空间目录 %s 失败: %w", sub, err)
|
||||
return fmt.Errorf("create workspace directory %s failed: %w", sub, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -198,22 +219,22 @@ func SaveScenePropFile(ctx context.Context, dramaId int64, file io.Reader, filen
|
||||
return "", err
|
||||
}
|
||||
if d == nil {
|
||||
return "", fmt.Errorf("短剧不存在: %d", dramaId)
|
||||
return "", fmt.Errorf("drama not found: %d", dramaId)
|
||||
}
|
||||
workspaceDir := WorkspaceDir(d.Title)
|
||||
subPath := filepath.Join(workspaceDir, subDir)
|
||||
if err := os.MkdirAll(subPath, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
return "", fmt.Errorf("create directory failed: %w", err)
|
||||
}
|
||||
ext := filepath.Ext(filename)
|
||||
dest := filepath.Join(subPath, sanitizeDirName(name)+ext)
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建文件失败: %w", err)
|
||||
return "", fmt.Errorf("create file failed: %w", err)
|
||||
}
|
||||
defer func() { _ = out.Close() }()
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
return "", fmt.Errorf("write file failed: %w", err)
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (s *episodeService) AddEpisode(ctx context.Context, dramaId int64, title, d
|
||||
return 0, err
|
||||
}
|
||||
if d == nil {
|
||||
return 0, fmt.Errorf("短剧不存在: %d", dramaId)
|
||||
return 0, fmt.Errorf("drama not found: %d", dramaId)
|
||||
}
|
||||
var epId int64
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
@@ -105,7 +105,7 @@ func (s *episodeService) UpdateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
return err
|
||||
}
|
||||
if e == nil {
|
||||
return fmt.Errorf("剧集不存在: %d", epId)
|
||||
return fmt.Errorf("episode not found: %d", epId)
|
||||
}
|
||||
if title != "" {
|
||||
e.Title = title
|
||||
@@ -126,7 +126,7 @@ func (s *episodeService) UpdateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
return err
|
||||
}
|
||||
if d == nil {
|
||||
return fmt.Errorf("短剧不存在: %d", dramaId)
|
||||
return fmt.Errorf("drama not found: %d", dramaId)
|
||||
}
|
||||
|
||||
needsTaskUpdate := script != ""
|
||||
@@ -140,7 +140,8 @@ func (s *episodeService) UpdateEpisode(ctx context.Context, dramaId, epId int64,
|
||||
if _, e := tx.Model(public.TableNameGenerationTask).Ctx(ctx).Where("episode_id", epId).Delete(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := createPendingTasks(ctx, dramaId, epId, script, tx, d, e.Index); e != nil {
|
||||
if err2 := createPendingTasks(ctx, dramaId, epId, script, tx, d, e.Index); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -201,33 +202,33 @@ func (s *episodeService) DeleteEpisode(ctx context.Context, dramaId, epId int64)
|
||||
func (s *episodeService) GenerateScript(ctx context.Context, dramaId int64, episodeTitle, description string) (script string, err error) {
|
||||
d, err := dao.Drama.GetOne(ctx, dramaId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "GenerateScript 查询短剧失败 dramaId=%d: %v", dramaId, err)
|
||||
g.Log().Errorf(ctx, "GenerateScript query drama failed dramaId=%d: %v", dramaId, err)
|
||||
return
|
||||
}
|
||||
if d == nil {
|
||||
g.Log().Warningf(ctx, "GenerateScript 短剧不存在 dramaId=%d", dramaId)
|
||||
err = fmt.Errorf("短剧不存在")
|
||||
g.Log().Warningf(ctx, "GenerateScript drama not found dramaId=%d", dramaId)
|
||||
err = fmt.Errorf("drama not found")
|
||||
return
|
||||
}
|
||||
|
||||
modelCfg := UserModelConfigService.GetMergedConfig(ctx, d.UserId, "chat")
|
||||
if modelCfg.ApiKey == "" || modelCfg.ModelName == "" {
|
||||
g.Log().Warningf(ctx, "GenerateScript 对话模型未配置 userId=%d ApiKey=%q ModelName=%q", d.UserId, modelCfg.ApiKey, modelCfg.ModelName)
|
||||
err = fmt.Errorf("模型未配置")
|
||||
g.Log().Warningf(ctx, "GenerateScript chat model not configured userId=%d ApiKey=%q ModelName=%q", d.UserId, modelCfg.ApiKey, modelCfg.ModelName)
|
||||
err = fmt.Errorf("model not configured")
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "GenerateScript 开始生成 userId=%d dramaId=%d model=%s baseUrl=%s", d.UserId, dramaId, modelCfg.ModelName, modelCfg.BaseUrl)
|
||||
g.Log().Infof(ctx, "GenerateScript start generating userId=%d dramaId=%d model=%s baseUrl=%s", d.UserId, dramaId, modelCfg.ModelName, modelCfg.BaseUrl)
|
||||
|
||||
// 加载演员/场景/道具上下文
|
||||
genCtx, err := BuildGenerationContext(ctx, d)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "GenerateScript 加载上下文失败 dramaId=%d: %v", dramaId, err)
|
||||
err = fmt.Errorf("加载短剧上下文失败: %w", err)
|
||||
g.Log().Errorf(ctx, "GenerateScript load context failed dramaId=%d: %v", dramaId, err)
|
||||
err = fmt.Errorf("load drama context failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
systemPrompt := PromptService.GetScriptGenerationPrompt(ctx)
|
||||
userInput := s.buildScriptGenUserInput(d, episodeTitle, description, genCtx)
|
||||
userInput := s.buildScriptGenUserInput(ctx, d, episodeTitle, description, genCtx)
|
||||
|
||||
chatCfg := &agent.ModelConfig{
|
||||
ModelName: modelCfg.ModelName,
|
||||
@@ -243,36 +244,36 @@ func (s *episodeService) GenerateScript(ctx context.Context, dramaId int64, epis
|
||||
{Role: agent.RoleSystem, Content: systemPrompt},
|
||||
{Role: agent.RoleUser, Content: userInput},
|
||||
}
|
||||
g.Log().Infof(ctx, "GenerateScript 调用AI模型 model=%s max_tokens=%d prompt_len=%d", chatCfg.ModelName, chatCfg.MaxTokens, len(systemPrompt)+len(userInput))
|
||||
g.Log().Infof(ctx, "GenerateScript calling AI model=%s max_tokens=%d prompt_len=%d", chatCfg.ModelName, chatCfg.MaxTokens, len(systemPrompt)+len(userInput))
|
||||
result, err := agent.CallChatModel(ctx, chatCfg, &agent.ChatRequest{
|
||||
Messages: messages,
|
||||
MaxTokens: chatCfg.MaxTokens,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "GenerateScript AI调用失败: %v", err)
|
||||
err = fmt.Errorf("生成脚本失败: %w", err)
|
||||
g.Log().Errorf(ctx, "GenerateScript AI call failed: %v", err)
|
||||
err = fmt.Errorf("generate script failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
raw := result.Content
|
||||
if raw == "" {
|
||||
g.Log().Warningf(ctx, "GenerateScript AI返回内容为空")
|
||||
err = fmt.Errorf("生成的脚本为空")
|
||||
g.Log().Warningf(ctx, "GenerateScript AI returned empty content")
|
||||
err = fmt.Errorf("generated script is empty")
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "GenerateScript AI返回成功 content_len=%d", len(raw))
|
||||
g.Log().Infof(ctx, "GenerateScript AI returned successfully content_len=%d", len(raw))
|
||||
|
||||
// 尝试解析为JSON shots数组
|
||||
var shots []domain.Shot
|
||||
var parseErr error
|
||||
if parseErr = json.Unmarshal([]byte(raw), &shots); parseErr == nil && len(shots) > 0 {
|
||||
script = raw
|
||||
g.Log().Infof(ctx, "JSON镜头脚本生成成功: 短剧=%s, 剧集=%s, 镜头数=%d", d.Title, episodeTitle, len(shots))
|
||||
g.Log().Infof(ctx, "JSON shot script generated: drama=%s, episode=%s, shots=%d", d.Title, episodeTitle, len(shots))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON解析失败,将错误信息拼入提示词让AI自行修正
|
||||
g.Log().Warningf(ctx, "JSON解析失败,请求AI修正: %v", parseErr)
|
||||
g.Log().Warningf(ctx, "JSON parse failed, requesting AI fix: %v", parseErr)
|
||||
fixPrompt := fmt.Sprintf("你输出的JSON格式有误,请修正后重新输出。\n\n错误信息:%v\n\n你之前输出的内容:\n%s\n\n请只输出修正后的合法JSON数组,不要任何其他内容。", parseErr, raw)
|
||||
retryMsgs := []*agent.ChatMessage{
|
||||
{Role: agent.RoleSystem, Content: systemPrompt},
|
||||
@@ -280,7 +281,7 @@ func (s *episodeService) GenerateScript(ctx context.Context, dramaId int64, epis
|
||||
{Role: agent.RoleAssistant, Content: raw},
|
||||
{Role: agent.RoleUser, Content: fixPrompt},
|
||||
}
|
||||
g.Log().Infof(ctx, "GenerateScript 调用AI修正模型 model=%s max_tokens=%d 目的=JSON脚本修正重试", chatCfg.ModelName, chatCfg.MaxTokens)
|
||||
g.Log().Infof(ctx, "GenerateScript calling AI fix model=%s max_tokens=%d purpose=JSON script fix retry", chatCfg.ModelName, chatCfg.MaxTokens)
|
||||
result2, retryErr := agent.CallChatModel(ctx, chatCfg, &agent.ChatRequest{
|
||||
Messages: retryMsgs,
|
||||
MaxTokens: chatCfg.MaxTokens,
|
||||
@@ -290,23 +291,23 @@ func (s *episodeService) GenerateScript(ctx context.Context, dramaId int64, epis
|
||||
var parseErr2 error
|
||||
if parseErr2 = json.Unmarshal([]byte(result2.Content), &shots2); parseErr2 == nil && len(shots2) > 0 {
|
||||
script = result2.Content
|
||||
g.Log().Infof(ctx, "JSON脚本修正成功: 短剧=%s, 剧集=%s, 镜头数=%d", d.Title, episodeTitle, len(shots2))
|
||||
g.Log().Infof(ctx, "JSON script fix succeeded: drama=%s, episode=%s, shots=%d", d.Title, episodeTitle, len(shots2))
|
||||
return
|
||||
}
|
||||
g.Log().Warningf(ctx, "JSON脚本修正后仍解析失败: %v", parseErr2)
|
||||
g.Log().Warningf(ctx, "JSON script fix still parse failed: %v", parseErr2)
|
||||
} else if retryErr != nil {
|
||||
g.Log().Warningf(ctx, "JSON脚本修正请求失败: %v", retryErr)
|
||||
g.Log().Warningf(ctx, "JSON script fix request failed: %v", retryErr)
|
||||
}
|
||||
|
||||
// 修正仍失败,回退为纯文本
|
||||
script = raw
|
||||
g.Log().Warningf(ctx, "JSON脚本回退为纯文本: 短剧=%s, 剧集=%s, 长度=%d字符", d.Title, episodeTitle, len([]rune(raw)))
|
||||
g.Log().Warningf(ctx, "JSON script fallback to plain text: drama=%s, episode=%s, length=%d chars", d.Title, episodeTitle, len([]rune(raw)))
|
||||
g.Log().Printf(ctx, "模型返回的文本:\n%s", raw)
|
||||
return
|
||||
}
|
||||
|
||||
// buildScriptGenUserInput 构建脚本生成的用户输入提示
|
||||
func (s *episodeService) buildScriptGenUserInput(d *entity.Drama, episodeTitle, description string, genCtx *GenerationContext) string {
|
||||
func (s *episodeService) buildScriptGenUserInput(ctx context.Context, d *entity.Drama, episodeTitle, description string, genCtx *GenerationContext) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "每集时长:%d秒\n\n", d.EpisodeDuration)
|
||||
@@ -354,8 +355,8 @@ func (s *episodeService) buildScriptGenUserInput(d *entity.Drama, episodeTitle,
|
||||
b.WriteString("【输出格式】\nJSON数组,每个元素是一个镜头对象(shot),包含以下字段:\n- index: 镜头序号(从1开始)\n- startTime: 开始时间(格式MM:SS)\n- endTime: 结束时间(格式MM:SS)\n- event: 画面描述——观众在屏幕上直接看到的一切。场景环境、角色动作、表情变化、物体位置等。**这是视频模型生成画面的唯一依据,所有画面内容必须写在这里**,严禁把画面描述放入 narration 字段。不包含旁白和台词。\n- dialogue: 主台词——角色在画面中亲口说出的对白。多人对话用「角色名:台词」格式。旁白和内心独白不属于这里。如果本镜头无人说话,留空即可。\n- narration: 旁白配音——需要配音演员念出来的解说文字。必须是**通过画面无法直接传达**的信息(如角色内心独白、故事背景交代、时间跳跃说明等)。**如果一段文字描述的是观众能直接看到的画面内容,它就不属于旁白,必须放进 event 字段。** 本镜头无旁白时留空即可。\n- cameraMovement: 运镜方式,从以下标准类型中选择一种:固定镜头、推、拉、摇、移、跟、升、降、旋转、晃动、航拍\n- shotSize: 景别,从以下标准类型中选择一种:远景、全景、中景、近景、特写\n- characters: 出演人物数组,填写演员名称,如[\"张三\", \"李四\"],从「可用演员」中选择\n- scene: 场景名称,从「可用场景」中选择\n- props: 道具名称数组,如[\"剑\", \"酒杯\"],从「可用道具」中选择\n\n直接输出JSON数组,不要markdown代码块标记,不要其他任何内容。\n\n")
|
||||
|
||||
b.WriteString("【创作规范】\n\n1. 剧情完整性【核心规则】—— 剧情描述是唯一创作依据,严格按每一句话、每一个细节逐拍还原。\n - 剧情描述中出现的每一句角色说的话,必须一字不差地写入对应镜头的dialogue字段。严禁把台词\"翻译\"成事件描述。\n - 剧情描述中的每一个具体细节(人物的动作、表情反应、对话、环境互动等)都必须忠实地保留在对应镜头的event、narration或dialogue字段中,不能省略、概括或改写为泛化描述。\n - 所有镜头时长之和应等于 %d 秒(50秒至少20个镜头,20-25个镜头为最佳密度)。\n\n2. 场景一致性 —— 每个镜头的 scene 字段值必须与 event 中画面描述的地点一致。\n - 如果 event 描述角色在\"实验室内\",scene 必须是该实验室的名称,不能写成其他地点。\n - 如果角色从一个场景移动到另一个场景,必须先切换 scene(如从「街道」切到「室内」),再描述新地点中的动作。\n\n3. 景别与动作匹配规则 —— 按镜头内容选择正确的 shotSize:\n - shotSize=特写:面部表情、微表情、眼神变化、细节反应(手部小动作、物品细节)。不能用于动作展示。\n - shotSize=近景:对话、上半身互动、情绪交流。能看清面部表情和手势。\n - shotSize=中景:日常动作、手持物体、多人交互。膝盖以上,既有动作又有空间感。\n - shotSize=全景:连贯动作、奔跑、打斗、空间关系。展示全身动作和环境关系。\n - shotSize=远景:环境交代、空镜过渡、场景建立。没有人或人很小,用于转场。\n\n4. 运镜搭配规则 —— 按场景类型选择 cameraMovement:\n - 对话/情绪交流 → 固定镜头或缓推\n - 动作/打斗 → 跟、摇、移\n - 人物进入/离开画面 → 跟、移\n - 情绪揭示 → 推\n - 环境展示 → 摇、升降、航拍\n - 紧张/混乱 → 晃动\n - 固定镜头是默认选项,无特殊需要不使用复杂运镜。\n\n5. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n - 不说话只做反应的角色可用其他景别。\n")
|
||||
b.WriteString(config.GetShotDurationPrompt(d.Type))
|
||||
b.WriteString(config.GetStylePrompt(d.Type))
|
||||
b.WriteString(config.GetShotDurationPrompt(ctx, d.Type))
|
||||
b.WriteString(config.GetStylePrompt(ctx, d.Type))
|
||||
b.WriteString("\n\n6. JSON格式要求:输出必须是合法的 JSON 数组,字符串值中若需使用引号请用「」代替 ASCII 双引号,确保 json.Unmarshal 能正确解析。\n7. 风控规避:视频模型对某些画面有安全审查机制,生成镜头描述时必须规避。政治敏感内容(真实政治人物/标志/当代军警标识)零容忍。暴力血腥画面(刀刺入身体、血溅、断肢)改为反应镜头或切镜回避,不展示伤害过程。当代背景中的军警抓捕动作将「警察/武警」改为「保安/黑衣人」,动作从「按倒铐住」改为「围住请离」。武器描写聚焦持武器的人物(「他握紧长刀,目光凌厉」),不写武器接触身体的画面。暴力结果用间接描写:写周围人反应而非伤口血迹。\n")
|
||||
|
||||
return b.String()
|
||||
@@ -378,7 +379,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
|
||||
// 读取反向提示词
|
||||
negativePrompt := ""
|
||||
if data, err := os.ReadFile("negative_prompt.md"); err == nil {
|
||||
if data, err := os.ReadFile(getDataPath("negative_prompt.md")); err == nil {
|
||||
negativePrompt = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
@@ -521,7 +522,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
// JSON 镜头数组:按镜头分组,累加连续镜头直到触及时长或字符上限
|
||||
var allShots []domain.Shot
|
||||
if err := json.Unmarshal([]byte(script), &allShots); err != nil || len(allShots) == 0 {
|
||||
return fmt.Errorf("解析镜头脚本失败: %w", err)
|
||||
return fmt.Errorf("parse shot script failed: %w", err)
|
||||
}
|
||||
type _shotGroup struct {
|
||||
shots []domain.Shot
|
||||
@@ -646,7 +647,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
|
||||
"num_segments": len(taskGroups),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建第%d段待生成任务失败: %w", i+1, err)
|
||||
return fmt.Errorf("create segment %d task failed: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -674,18 +675,34 @@ func splitScriptForSegment(fullScript string, segStartTime, segDur, totalDur int
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// 纯文本脚本:按时长比例切分字符
|
||||
runes := []rune(fullScript)
|
||||
if len(runes) == 0 || totalDur <= 0 {
|
||||
return fullScript
|
||||
// 纯文本脚本:按段落切分,避免截断句子
|
||||
paragraphs := strings.Split(fullScript, "\n\n")
|
||||
if len(paragraphs) <= 1 || totalDur <= 0 {
|
||||
// 只有一段或无时间信息时直接按字符比例
|
||||
runes := []rune(fullScript)
|
||||
if len(runes) == 0 {
|
||||
return fullScript
|
||||
}
|
||||
startRune := len(runes) * segStartTime / totalDur
|
||||
endRune := len(runes) * (segStartTime + segDur) / totalDur
|
||||
if startRune >= len(runes) {
|
||||
return ""
|
||||
}
|
||||
if endRune > len(runes) {
|
||||
endRune = len(runes)
|
||||
}
|
||||
return string(runes[startRune:endRune])
|
||||
}
|
||||
startRune := len(runes) * segStartTime / totalDur
|
||||
endRune := len(runes) * (segStartTime + segDur) / totalDur
|
||||
if startRune >= len(runes) {
|
||||
startPara := len(paragraphs) * segStartTime / totalDur
|
||||
endPara := len(paragraphs) * (segStartTime + segDur) / totalDur
|
||||
if startPara >= len(paragraphs) {
|
||||
return ""
|
||||
}
|
||||
if endRune > len(runes) {
|
||||
endRune = len(runes)
|
||||
if endPara > len(paragraphs) {
|
||||
endPara = len(paragraphs)
|
||||
}
|
||||
return string(runes[startRune:endRune])
|
||||
if endPara <= startPara {
|
||||
endPara = startPara + 1
|
||||
}
|
||||
return strings.Join(paragraphs[startPara:endPara], "\n\n")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ func (s *modelConfigService) getModelList(ctx context.Context) []*entity.ModelCo
|
||||
if err != nil || len(list) == 0 {
|
||||
return make([]*entity.ModelConfig, 0)
|
||||
}
|
||||
_ = gcache.Set(ctx, cacheKeyModelList, list, 0)
|
||||
_ = gcache.Set(ctx, cacheKeyModelList, list, 300)
|
||||
return list
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (s *paymentConfigService) getPaymentConfigList(ctx context.Context) ([]*ent
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = gcache.Set(ctx, cacheKeyPaymentConfig, list, 0)
|
||||
_ = gcache.Set(ctx, cacheKeyPaymentConfig, list, 300)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -26,62 +27,21 @@ type paymentService struct{}
|
||||
|
||||
var PaymentService = new(paymentService)
|
||||
|
||||
type PaymentConfig struct {
|
||||
Wechat WechatConfig `json:"wechat"`
|
||||
Alipay AlipayConfig `json:"alipay"`
|
||||
}
|
||||
|
||||
type WechatConfig struct {
|
||||
AppId string `json:"app_id"`
|
||||
MchId string `json:"mch_id"`
|
||||
ApiKey string `json:"api_key"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
AppId string `json:"app_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
func (s *paymentService) GetPaymentConfig(channel, channelType string) (WechatConfig, AlipayConfig, error) {
|
||||
ctx := context.Background()
|
||||
switch channel {
|
||||
case "wechat":
|
||||
wc, err := dao.PaymentConfigDao.GetByChannel(ctx, "wechat", channelType)
|
||||
if err != nil {
|
||||
return WechatConfig{}, AlipayConfig{}, err
|
||||
}
|
||||
if wc == nil || wc.AppId == "" || wc.MchId == "" || wc.ApiKey == "" {
|
||||
return WechatConfig{}, AlipayConfig{}, errors.New("微信支付(" + channelType + ")未配置")
|
||||
}
|
||||
return WechatConfig{
|
||||
AppId: wc.AppId,
|
||||
MchId: wc.MchId,
|
||||
ApiKey: wc.ApiKey,
|
||||
AppSecret: wc.AppSecret,
|
||||
}, AlipayConfig{}, nil
|
||||
case "alipay":
|
||||
ac, err := dao.PaymentConfigDao.GetByChannel(ctx, "alipay", channelType)
|
||||
if err != nil {
|
||||
return WechatConfig{}, AlipayConfig{}, err
|
||||
}
|
||||
if ac == nil || ac.AppId == "" || ac.PrivateKey == "" {
|
||||
return WechatConfig{}, AlipayConfig{}, errors.New("支付宝支付(" + channelType + ")未配置")
|
||||
}
|
||||
return WechatConfig{}, AlipayConfig{
|
||||
AppId: ac.AppId,
|
||||
PrivateKey: ac.PrivateKey,
|
||||
PublicKey: ac.PublicKey,
|
||||
}, nil
|
||||
func (s *paymentService) GetPaymentConfig(ctx context.Context, channel, channelType string) (*entity.PaymentConfig, error) {
|
||||
cfg, err := dao.PaymentConfigDao.GetByChannel(ctx, channel, channelType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return WechatConfig{}, AlipayConfig{}, errors.New("不支持的支付渠道")
|
||||
if cfg == nil {
|
||||
return nil, errors.New("payment config not found: " + channel + "/" + channelType)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Prepay 创建支付订单并调起渠道
|
||||
func (s *paymentService) Prepay(ctx context.Context, userId int64, amount int64, channel string, orderType string, duration int) (*entity.PaymentOrder, string, string, string, error) {
|
||||
if amount < 1 {
|
||||
return nil, "", "", "", errors.New("金额必须大于0")
|
||||
return nil, "", "", "", errors.New("amount must be greater than 0")
|
||||
}
|
||||
|
||||
if orderType == "" {
|
||||
@@ -97,12 +57,12 @@ func (s *paymentService) Prepay(ctx context.Context, userId int64, amount int64,
|
||||
case "offline":
|
||||
chanType = "manual"
|
||||
default:
|
||||
return nil, "", "", "", errors.New("不支持的支付渠道")
|
||||
return nil, "", "", "", errors.New("unsupported payment channel")
|
||||
}
|
||||
|
||||
// 外部渠道需要检查支付配置
|
||||
if channel != "offline" {
|
||||
if _, _, err := s.GetPaymentConfig(channel, chanType); err != nil {
|
||||
if _, err := s.GetPaymentConfig(ctx, channel, chanType); err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
}
|
||||
@@ -137,10 +97,16 @@ func (s *paymentService) Prepay(ctx context.Context, userId int64, amount int64,
|
||||
|
||||
switch channel {
|
||||
case "wechat":
|
||||
wechatCfg, _, _ := s.GetPaymentConfig("wechat", chanType)
|
||||
wechatCfg, err := s.GetPaymentConfig(ctx, "wechat", chanType)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
codeUrl, prepayJson, err = s.callWechat(ctx, order, wechatCfg)
|
||||
case "alipay":
|
||||
_, alipayCfg, _ := s.GetPaymentConfig("alipay", chanType)
|
||||
alipayCfg, err := s.GetPaymentConfig(ctx, "alipay", chanType)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
codeUrl, redirectUrl, err = s.callAlipay(ctx, order, alipayCfg)
|
||||
case "offline":
|
||||
// 线下支付无需调起外部渠道,直接返回
|
||||
@@ -168,7 +134,7 @@ func (s *paymentService) Prepay(ctx context.Context, userId int64, amount int64,
|
||||
func (s *paymentService) CreateRenewalOrder(ctx context.Context, agentId int64, duration int, pricingId int64) (*entity.PaymentOrder, error) {
|
||||
agent, err := dao.User.GetOne(ctx, agentId)
|
||||
if err != nil || agent == nil || agent.Role != "agent" {
|
||||
return nil, errors.New("代理商不存在")
|
||||
return nil, errors.New("agent not found")
|
||||
}
|
||||
|
||||
var pricing *entity.RegionPricing
|
||||
@@ -183,7 +149,7 @@ func (s *paymentService) CreateRenewalOrder(ctx context.Context, agentId int64,
|
||||
pricing, err = dao.RegionPricing.GetByRegion(ctx, agent.Region, protectedVal)
|
||||
}
|
||||
if err != nil || pricing == nil {
|
||||
return nil, errors.New("该地区未配置定价")
|
||||
return nil, errors.New("pricing not configured for this region")
|
||||
}
|
||||
|
||||
orderNo := s.generateOrderNo()
|
||||
@@ -225,13 +191,13 @@ func (s *paymentService) CreateRenewalOrder(ctx context.Context, agentId int64,
|
||||
func (s *paymentService) ConfirmOffline(ctx context.Context, orderNo string) error {
|
||||
order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil || order == nil {
|
||||
return errors.New("订单不存在")
|
||||
return errors.New("order not found")
|
||||
}
|
||||
if order.Status != "pending" {
|
||||
return errors.New("订单状态不允许确认收款")
|
||||
return errors.New("order status does not allow confirmation")
|
||||
}
|
||||
if order.Channel != "offline" {
|
||||
return errors.New("仅支持确认线下支付订单")
|
||||
return errors.New("only offline payment orders can be confirmed")
|
||||
}
|
||||
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
@@ -254,7 +220,7 @@ func (s *paymentService) HandleNotify(ctx context.Context, channel string, body
|
||||
|
||||
switch channel {
|
||||
case "wechat":
|
||||
wechatCfg, _, err := s.GetPaymentConfig("wechat", "native")
|
||||
wechatCfg, err := s.GetPaymentConfig(ctx, "wechat", "native")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -263,7 +229,7 @@ func (s *paymentService) HandleNotify(ctx context.Context, channel string, body
|
||||
return err
|
||||
}
|
||||
case "alipay":
|
||||
_, alipayCfg, err := s.GetPaymentConfig("alipay", "native")
|
||||
alipayCfg, err := s.GetPaymentConfig(ctx, "alipay", "native")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -272,12 +238,12 @@ func (s *paymentService) HandleNotify(ctx context.Context, channel string, body
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("未知渠道")
|
||||
return errors.New("unknown channel")
|
||||
}
|
||||
|
||||
order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil || order == nil {
|
||||
return errors.New("订单不存在")
|
||||
return errors.New("order not found")
|
||||
}
|
||||
if order.Status == "success" {
|
||||
return nil // 防止重复回调
|
||||
@@ -310,11 +276,15 @@ func (s *paymentService) HandleNotify(ctx context.Context, channel string, body
|
||||
func (s *paymentService) GetStatus(ctx context.Context, orderNo string) (string, int64, error) {
|
||||
order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil || order == nil {
|
||||
return "", 0, errors.New("订单不存在")
|
||||
return "", 0, errors.New("order not found")
|
||||
}
|
||||
return order.Status, order.Amount, nil
|
||||
}
|
||||
|
||||
func (s *paymentService) ListOrders(ctx context.Context, userId int64, page, pageSize int) ([]*entity.PaymentOrder, int, error) {
|
||||
return dao.PaymentOrder.ListPageByUser(ctx, userId, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *paymentService) generateOrderNo() string {
|
||||
now := time.Now()
|
||||
r := rand.Intn(10000)
|
||||
@@ -323,9 +293,9 @@ func (s *paymentService) generateOrderNo() string {
|
||||
|
||||
// ==================== 微信支付 ====================
|
||||
|
||||
func (s *paymentService) callWechat(ctx context.Context, order *entity.PaymentOrder, cfg WechatConfig) (codeUrl, prepayJson string, err error) {
|
||||
func (s *paymentService) callWechat(ctx context.Context, order *entity.PaymentOrder, cfg *entity.PaymentConfig) (codeUrl, prepayJson string, err error) {
|
||||
if cfg.AppId == "" || cfg.MchId == "" || cfg.ApiKey == "" {
|
||||
return "", "", errors.New("微信支付未配置")
|
||||
return "", "", errors.New("wechat payment not configured")
|
||||
}
|
||||
nonceStr := s.randomStr(32)
|
||||
params := map[string]string{
|
||||
@@ -345,27 +315,27 @@ func (s *paymentService) callWechat(ctx context.Context, order *entity.PaymentOr
|
||||
xmlReq := s.mapToXML(params)
|
||||
resp, err := http.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "text/xml", strings.NewReader(xmlReq))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("调用微信下单失败: %w", err)
|
||||
return "", "", fmt.Errorf("wechat order failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
|
||||
result := s.xmlToMap(string(b))
|
||||
if result["return_code"] != "SUCCESS" || result["result_code"] != "SUCCESS" {
|
||||
return "", "", fmt.Errorf("微信下单失败: %s", result["return_msg"])
|
||||
return "", "", fmt.Errorf("wechat order failed: %s", result["return_msg"])
|
||||
}
|
||||
return result["code_url"], "", nil
|
||||
}
|
||||
|
||||
// verifyWechatNotify 验证微信回调签名,返回 (orderNo, transactionId)
|
||||
func (s *paymentService) verifyWechatNotify(body []byte, cfg WechatConfig) (string, string, error) {
|
||||
func (s *paymentService) verifyWechatNotify(body []byte, cfg *entity.PaymentConfig) (string, string, error) {
|
||||
result := s.xmlToMap(string(body))
|
||||
if result["return_code"] != "SUCCESS" {
|
||||
return "", "", errors.New("微信回调失败")
|
||||
return "", "", errors.New("wechat callback failed")
|
||||
}
|
||||
sign := s.wechatSign(result, cfg.ApiKey)
|
||||
if sign != result["sign"] {
|
||||
return "", "", errors.New("微信回调签名验证失败")
|
||||
return "", "", errors.New("wechat callback signature verification failed")
|
||||
}
|
||||
return result["out_trade_no"], result["transaction_id"], nil
|
||||
}
|
||||
@@ -395,9 +365,9 @@ func (s *paymentService) wechatSign(params map[string]string, apiKey string) str
|
||||
|
||||
// ==================== 支付宝支付 ====================
|
||||
|
||||
func (s *paymentService) callAlipay(ctx context.Context, order *entity.PaymentOrder, cfg AlipayConfig) (codeUrl, redirectUrl string, err error) {
|
||||
func (s *paymentService) callAlipay(ctx context.Context, order *entity.PaymentOrder, cfg *entity.PaymentConfig) (codeUrl, redirectUrl string, err error) {
|
||||
if cfg.AppId == "" || cfg.PrivateKey == "" {
|
||||
return "", "", errors.New("支付宝支付未配置")
|
||||
return "", "", errors.New("alipay payment not configured")
|
||||
}
|
||||
|
||||
bizContent := fmt.Sprintf(`{"out_trade_no":"%s","total_amount":"%.2f","subject":"%s"}`, order.OrderNo, float64(order.Amount)/100, order.Subject)
|
||||
@@ -418,7 +388,7 @@ func (s *paymentService) callAlipay(ctx context.Context, order *entity.PaymentOr
|
||||
|
||||
resp, err := http.PostForm("https://openapi.alipay.com/gateway.do", params)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("调用支付宝下单失败: %w", err)
|
||||
return "", "", fmt.Errorf("alipay order failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
@@ -433,25 +403,25 @@ func (s *paymentService) callAlipay(ctx context.Context, order *entity.PaymentOr
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &result); err != nil {
|
||||
return "", "", errors.New("支付宝返回解析失败")
|
||||
return "", "", errors.New("alipay response parse failed")
|
||||
}
|
||||
if result.Response.Code != "10000" {
|
||||
return "", "", fmt.Errorf("支付宝下单失败: %s", result.Response.SubMsg)
|
||||
return "", "", fmt.Errorf("alipay order failed: %s", result.Response.SubMsg)
|
||||
}
|
||||
return result.Response.QrCode, "", nil
|
||||
}
|
||||
|
||||
func (s *paymentService) verifyAlipayNotify(body []byte, cfg AlipayConfig) (string, string, error) {
|
||||
func (s *paymentService) verifyAlipayNotify(body []byte, cfg *entity.PaymentConfig) (string, string, error) {
|
||||
vals, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return "", "", errors.New("支付宝回调解析失败")
|
||||
return "", "", errors.New("alipay callback parse failed")
|
||||
}
|
||||
if vals.Get("trade_status") != "TRADE_SUCCESS" {
|
||||
return "", "", errors.New("支付宝回调状态非成功")
|
||||
return "", "", errors.New("alipay callback status is not success")
|
||||
}
|
||||
sign := s.alipaySign(vals, cfg.PublicKey)
|
||||
if sign != vals.Get("sign") {
|
||||
return "", "", errors.New("支付宝回调签名验证失败")
|
||||
return "", "", errors.New("alipay callback signature verification failed")
|
||||
}
|
||||
return vals.Get("out_trade_no"), vals.Get("trade_no"), nil
|
||||
}
|
||||
@@ -506,55 +476,27 @@ func (s *paymentService) mapToXML(m map[string]string) string {
|
||||
|
||||
func (s *paymentService) xmlToMap(xmlStr string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
var key, value string
|
||||
inKey := false
|
||||
inValue := false
|
||||
for i := 0; i < len(xmlStr); i++ {
|
||||
c := xmlStr[i]
|
||||
if c == '<' {
|
||||
if i+1 < len(xmlStr) && xmlStr[i+1] == '/' {
|
||||
inKey = false
|
||||
if key != "" && value != "" {
|
||||
result[key] = value
|
||||
key = ""
|
||||
value = ""
|
||||
decoder := xml.NewDecoder(strings.NewReader(xmlStr))
|
||||
var key string
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch t := token.(type) {
|
||||
case xml.StartElement:
|
||||
key = t.Name.Local
|
||||
case xml.CharData:
|
||||
if key != "" {
|
||||
val := strings.TrimSpace(string(t))
|
||||
if val != "" {
|
||||
result[key] = val
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i+1 < len(xmlStr) && xmlStr[i+1] == '!' {
|
||||
continue
|
||||
}
|
||||
if !inKey && !inValue {
|
||||
inKey = true
|
||||
key = ""
|
||||
continue
|
||||
}
|
||||
}
|
||||
if c == '>' {
|
||||
if inKey {
|
||||
inKey = false
|
||||
inValue = true
|
||||
value = ""
|
||||
continue
|
||||
}
|
||||
if inValue {
|
||||
inValue = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if inKey {
|
||||
key += string(c)
|
||||
} else if inValue {
|
||||
value += string(c)
|
||||
case xml.EndElement:
|
||||
key = ""
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// InitDefaultPaymentConfig 初始化默认支付配置(空配置)
|
||||
func InitDefaultPaymentConfig() PaymentConfig {
|
||||
return PaymentConfig{
|
||||
Wechat: WechatConfig{},
|
||||
Alipay: AlipayConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *propService) UpdateProp(ctx context.Context, id, dramaId int64, name, d
|
||||
return err
|
||||
}
|
||||
if c == nil {
|
||||
return fmt.Errorf("道具不存在: %d", id)
|
||||
return fmt.Errorf("prop not found: %d", id)
|
||||
}
|
||||
if name != "" {
|
||||
c.Name = name
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *regionPricingService) getList(ctx context.Context) ([]*entity.RegionPri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = gcache.Set(ctx, cacheKeyRegionPricing, list, 0)
|
||||
_ = gcache.Set(ctx, cacheKeyRegionPricing, list, 300)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *sceneService) UpdateScene(ctx context.Context, id, dramaId int64, name,
|
||||
return err
|
||||
}
|
||||
if c == nil {
|
||||
return fmt.Errorf("场景不存在: %d", id)
|
||||
return fmt.Errorf("scene not found: %d", id)
|
||||
}
|
||||
if name != "" {
|
||||
c.Name = name
|
||||
|
||||
@@ -22,11 +22,11 @@ func (s *transactionService) Insert(ctx context.Context, data *entity.AccountTra
|
||||
|
||||
func (s *transactionService) Recharge(ctx context.Context, customerId int64, amount int64, orderNo, remark string) error {
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("充值金额必须大于0")
|
||||
return fmt.Errorf("recharge amount must be greater than 0")
|
||||
}
|
||||
cp, err := dao.CustomerProfile.Get(ctx, customerId)
|
||||
if err != nil || cp == nil {
|
||||
return fmt.Errorf("客户不存在")
|
||||
return fmt.Errorf("customer not found")
|
||||
}
|
||||
newBalance := cp.Balance + amount
|
||||
if err := dao.CustomerProfile.UpdateBalance(ctx, customerId, newBalance); err != nil {
|
||||
|
||||
@@ -58,10 +58,10 @@ func (s *userModelConfigService) GetUserConfig(ctx context.Context, userId int64
|
||||
}
|
||||
if err != nil || cfg == nil {
|
||||
m := &entity.UserModelConfig{UserId: userId}
|
||||
_ = gcache.Set(ctx, cacheKey, m, 0)
|
||||
_ = gcache.Set(ctx, cacheKey, m, 300)
|
||||
return m
|
||||
}
|
||||
_ = gcache.Set(ctx, cacheKey, cfg, 0)
|
||||
_ = gcache.Set(ctx, cacheKey, cfg, 300)
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"video-factory/common"
|
||||
"video-factory/shortdrama/dao"
|
||||
"video-factory/shortdrama/model/entity"
|
||||
|
||||
@@ -12,56 +13,23 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const jwtSecret = "video-factory-jwt-secret-2024"
|
||||
|
||||
type userService struct{}
|
||||
|
||||
var UserService = new(userService)
|
||||
|
||||
type JwtClaims struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
AgentId int64 `json:"agent_id,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
adminExisting, _ := dao.User.GetByUsername(ctx, "admin")
|
||||
if adminExisting == nil {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("Tongli686^*^"), bcrypt.DefaultCost)
|
||||
_, _ = dao.User.Insert(ctx, &entity.User{
|
||||
Role: "admin",
|
||||
Username: "admin",
|
||||
Password: string(hash),
|
||||
Name: "管理员",
|
||||
})
|
||||
}
|
||||
test1Existing, _ := dao.User.GetByUsername(ctx, "test1")
|
||||
if test1Existing == nil {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("Tongli686^*^"), bcrypt.DefaultCost)
|
||||
_, _ = dao.User.Insert(ctx, &entity.User{
|
||||
Role: "agent",
|
||||
Username: "test1",
|
||||
Password: string(hash),
|
||||
Name: "测试代理1",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userService) Login(ctx context.Context, account, password string) (*entity.User, string, error) {
|
||||
var user *entity.User
|
||||
var err error
|
||||
if account != "" {
|
||||
user, err = dao.User.GetByAccount(ctx, account)
|
||||
} else {
|
||||
return nil, "", errors.New("请输入账号")
|
||||
return nil, "", errors.New("please enter account")
|
||||
}
|
||||
if err != nil || user == nil {
|
||||
return nil, "", errors.New("账号不存在")
|
||||
return nil, "", errors.New("account not found")
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
|
||||
return nil, "", errors.New("密码错误")
|
||||
return nil, "", errors.New("incorrect password")
|
||||
}
|
||||
|
||||
var agentId int64
|
||||
@@ -73,7 +41,7 @@ func (s *userService) Login(ctx context.Context, account, password string) (*ent
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := JwtClaims{
|
||||
claims := common.JwtClaims{
|
||||
UserId: user.Id,
|
||||
Role: user.Role,
|
||||
AgentId: agentId,
|
||||
@@ -82,23 +50,13 @@ func (s *userService) Login(ctx context.Context, account, password string) (*ent
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
},
|
||||
}
|
||||
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(jwtSecret))
|
||||
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(common.GetJwtSecret()))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *userService) 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("无效的 token")
|
||||
}
|
||||
return claims, nil
|
||||
func (s *userService) ParseToken(tokenStr string) (*common.JwtClaims, error) {
|
||||
return common.ParseToken(tokenStr)
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/Eyevinn/mp4ff/mp4"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("用法: split_merged <输入文件> <输出目录> <段数>")
|
||||
os.Exit(1)
|
||||
}
|
||||
inputPath := os.Args[1]
|
||||
outDir := os.Args[2]
|
||||
numSegments, err := strconv.Atoi(os.Args[3])
|
||||
if err != nil || numSegments <= 0 {
|
||||
fmt.Printf("无效的段数: %s\n", os.Args[3])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 读取合并文件
|
||||
f, err := mp4.ReadMP4File(inputPath)
|
||||
if err != nil {
|
||||
fmt.Printf("读取文件失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||
fmt.Printf("创建输出目录失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 收集每轨道按段划分的 sample 范围
|
||||
type trakInfo struct {
|
||||
trak *mp4.TrakBox
|
||||
samples uint32 // 总 sample 数
|
||||
perSeg uint32 // 每段 sample 数
|
||||
}
|
||||
|
||||
var traks []trakInfo
|
||||
for _, trak := range f.Moov.Traks {
|
||||
stbl := trak.Mdia.Minf.Stbl
|
||||
if stbl.Stsz != nil {
|
||||
totalSamples := stbl.Stsz.SampleNumber
|
||||
perSeg := totalSamples / uint32(numSegments)
|
||||
traks = append(traks, trakInfo{
|
||||
trak: trak,
|
||||
samples: totalSamples,
|
||||
perSeg: perSeg,
|
||||
})
|
||||
fmt.Printf("轨道 %s: 总 %d samples, 每段 %d\n", trak.Mdia.Hdlr.HandlerType, totalSamples, perSeg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(traks) == 0 {
|
||||
fmt.Println("未找到可分割的轨道")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 逐段切分
|
||||
for seg := 0; seg < numSegments; seg++ {
|
||||
outPath := filepath.Join(outDir, fmt.Sprintf("seg_%d.mp4", seg))
|
||||
fmt.Printf("切分第 %d 段 -> %s\n", seg, outPath)
|
||||
|
||||
outFile := mp4.NewFile()
|
||||
outFile.Ftyp = f.Ftyp
|
||||
outFile.Moov = f.Moov // 稍后会修改
|
||||
|
||||
// 为每轨道创建仅包含本段 samples 的 stbl
|
||||
for _, ti := range traks {
|
||||
stbl := ti.trak.Mdia.Minf.Stbl
|
||||
startSample := seg * int(ti.perSeg)
|
||||
endSample := startSample + int(ti.perSeg)
|
||||
if seg == numSegments-1 {
|
||||
endSample = int(ti.samples) // 最后一段拿剩余所有 samples
|
||||
}
|
||||
|
||||
fmt.Printf(" 轨道 %s: samples [%d, %d)\n", ti.trak.Mdia.Hdlr.HandlerType, startSample, endSample)
|
||||
|
||||
// 拷贝 stsz entry(只保留本段范围)
|
||||
if stbl.Stsz != nil {
|
||||
newStsz := &mp4.StszBox{}
|
||||
if stbl.Stsz.SampleUniformSize > 0 {
|
||||
newStsz.SampleUniformSize = stbl.Stsz.SampleUniformSize
|
||||
newStsz.SampleNumber = uint32(endSample - startSample)
|
||||
} else {
|
||||
newStsz.SampleSize = make([]uint32, endSample-startSample)
|
||||
copy(newStsz.SampleSize, stbl.Stsz.SampleSize[startSample:endSample])
|
||||
newStsz.SampleNumber = uint32(len(newStsz.SampleSize))
|
||||
}
|
||||
stbl.Stsz = newStsz
|
||||
}
|
||||
|
||||
// 计算本段 chunk 范围:通过 stsc 找到对应的 chunk
|
||||
// 直接根据 perSeg chunks 计算。每段 chunks = totalChunks / numSegments
|
||||
chunkCount := uint32(0)
|
||||
if stbl.Stco != nil {
|
||||
chunkCount = uint32(len(stbl.Stco.ChunkOffset))
|
||||
} else if stbl.Co64 != nil {
|
||||
chunkCount = uint32(len(stbl.Co64.ChunkOffset))
|
||||
}
|
||||
perSegChunks := chunkCount / uint32(numSegments)
|
||||
startChunk := seg * int(perSegChunks)
|
||||
endChunk := startChunk + int(perSegChunks)
|
||||
if seg == numSegments-1 {
|
||||
endChunk = int(chunkCount)
|
||||
}
|
||||
|
||||
// 重新构建 stco(只保留本段 chunk)
|
||||
if stbl.Stco != nil {
|
||||
oldChunks := stbl.Stco.ChunkOffset
|
||||
stbl.Stco.ChunkOffset = oldChunks[startChunk:endChunk]
|
||||
// 调整偏移量:减去 mdat 中本段数据的起始位置
|
||||
baseOffset := oldChunks[startChunk]
|
||||
for i := range stbl.Stco.ChunkOffset {
|
||||
stbl.Stco.ChunkOffset[i] -= baseOffset
|
||||
}
|
||||
}
|
||||
if stbl.Co64 != nil {
|
||||
oldChunks := stbl.Co64.ChunkOffset
|
||||
stbl.Co64.ChunkOffset = oldChunks[startChunk:endChunk]
|
||||
baseOffset := oldChunks[startChunk]
|
||||
for i := range stbl.Co64.ChunkOffset {
|
||||
stbl.Co64.ChunkOffset[i] -= baseOffset
|
||||
}
|
||||
}
|
||||
|
||||
// 重新构建 stsc(只保留本段 chunk 的 entries)
|
||||
if stbl.Stsc != nil {
|
||||
var newEntries []mp4.StscEntry
|
||||
for _, e := range stbl.Stsc.Entries {
|
||||
if e.FirstChunk-1 >= uint32(startChunk) && e.FirstChunk-1 < uint32(endChunk) {
|
||||
newEntries = append(newEntries, mp4.StscEntry{
|
||||
FirstChunk: e.FirstChunk - uint32(startChunk),
|
||||
SamplesPerChunk: e.SamplesPerChunk,
|
||||
SampleDescriptionIndex: e.SampleDescriptionIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
stbl.Stsc.Entries = newEntries
|
||||
}
|
||||
|
||||
// 重设 stts(重建,只保留本段 samples 对应的时间)
|
||||
if stbl.Stts != nil {
|
||||
newStts := &mp4.SttsBox{}
|
||||
currentSample := uint32(0)
|
||||
for i := range stbl.Stts.SampleCount {
|
||||
count := stbl.Stts.SampleCount[i]
|
||||
delta := stbl.Stts.SampleTimeDelta[i]
|
||||
nextSample := currentSample + count
|
||||
|
||||
if nextSample <= uint32(startSample) {
|
||||
currentSample = nextSample
|
||||
continue
|
||||
}
|
||||
if currentSample >= uint32(endSample) {
|
||||
break
|
||||
}
|
||||
|
||||
overlapStart := uint32(0)
|
||||
if currentSample < uint32(startSample) {
|
||||
overlapStart = uint32(startSample) - currentSample
|
||||
}
|
||||
overlapEnd := count
|
||||
if nextSample > uint32(endSample) {
|
||||
overlapEnd = uint32(endSample) - currentSample
|
||||
}
|
||||
|
||||
if overlapStart < overlapEnd {
|
||||
newStts.SampleCount = append(newStts.SampleCount, overlapEnd-overlapStart)
|
||||
newStts.SampleTimeDelta = append(newStts.SampleTimeDelta, delta)
|
||||
}
|
||||
currentSample = nextSample
|
||||
}
|
||||
stbl.Stts = newStts
|
||||
}
|
||||
|
||||
// 修正 stco 偏移:加上 ftyp+moov+mdat_header 偏移
|
||||
mdatDataStart := uint32(0) // 稍后在写入时修正
|
||||
_ = mdatDataStart
|
||||
}
|
||||
|
||||
// 提取本段 mdat 数据
|
||||
mdatStart := uint32(0)
|
||||
if len(traks) > 0 && traks[0].trak.Mdia.Minf.Stbl.Stco != nil {
|
||||
// 使用第一个轨道的第一个 chunk 偏移量作为数据起始位置
|
||||
mdatStart = traks[0].trak.Mdia.Minf.Stbl.Stco.ChunkOffset[0]
|
||||
}
|
||||
|
||||
// 重构 mdat:从合并文件提取本段数据
|
||||
outFile.Mdat = &mp4.MdatBox{}
|
||||
mdatPayloadLen := uint64(0)
|
||||
// 计算本段 mdat 数据长度
|
||||
for _, ti := range traks {
|
||||
stbl := ti.trak.Mdia.Minf.Stbl
|
||||
if stbl.Stsz != nil {
|
||||
if stbl.Stsz.SampleUniformSize > 0 {
|
||||
mdatPayloadLen += uint64(stbl.Stsz.SampleUniformSize) * uint64(stbl.Stsz.SampleNumber)
|
||||
} else {
|
||||
for _, s := range stbl.Stsz.SampleSize {
|
||||
mdatPayloadLen += uint64(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mdatPayloadLen > 0 {
|
||||
segData := make([]byte, mdatPayloadLen)
|
||||
// 从原始 mdat 拷贝
|
||||
offset := uint64(mdatStart)
|
||||
_ = offset
|
||||
copyPos := uint64(0)
|
||||
for _, ti := range traks {
|
||||
stbl := ti.trak.Mdia.Minf.Stbl
|
||||
if stbl.Stsz != nil {
|
||||
if stbl.Stsz.SampleUniformSize > 0 {
|
||||
dataLen := uint64(stbl.Stsz.SampleUniformSize) * uint64(stbl.Stsz.SampleNumber)
|
||||
copy(segData[copyPos:], f.Mdat.Data[offset:offset+dataLen])
|
||||
copyPos += dataLen
|
||||
offset += dataLen
|
||||
} else {
|
||||
for _, s := range stbl.Stsz.SampleSize {
|
||||
copy(segData[copyPos:], f.Mdat.Data[offset:offset+uint64(s)])
|
||||
copyPos += uint64(s)
|
||||
offset += uint64(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outFile.Mdat.SetData(segData)
|
||||
}
|
||||
|
||||
// 更新 mdhd/tkhd/mvhd 时长
|
||||
outFile.Moov.Mvhd = f.Moov.Mvhd // 拷贝原始 mvhd
|
||||
// 简化处理:直接使用原始 mvhd 时长
|
||||
|
||||
// 重建 Children
|
||||
outFile.Children = []mp4.Box{outFile.Ftyp, outFile.Moov, outFile.Mdat}
|
||||
|
||||
// 修正 stco 偏移:调整到实际文件位置
|
||||
combinedBase := outFile.Ftyp.Size() + outFile.Moov.Size() + outFile.Mdat.HeaderSize()
|
||||
for _, ti := range traks {
|
||||
stbl := ti.trak.Mdia.Minf.Stbl
|
||||
if stbl.Stco != nil {
|
||||
for i := range stbl.Stco.ChunkOffset {
|
||||
stbl.Stco.ChunkOffset[i] += uint32(combinedBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := mp4.WriteToFile(outFile, outPath); err != nil {
|
||||
fmt.Printf("写入 %s 失败: %v\n", outPath, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" 完成: %s\n", outPath)
|
||||
}
|
||||
|
||||
fmt.Println("切分完成")
|
||||
}
|
||||
Reference in New Issue
Block a user