diff --git a/main.go b/main.go
index e7a0caa..c08fbe3 100644
--- a/main.go
+++ b/main.go
@@ -19,6 +19,7 @@ func main() {
controller.Agent,
controller.Customer,
controller.Transaction,
+ controller.Payment,
})
// ==================== 静态文件服务(workspace 目录) ====================
diff --git a/short_drama.db b/short_drama.db
index d12106e..72fa758 100644
Binary files a/short_drama.db and b/short_drama.db differ
diff --git a/shortdrama/consts/public/table_name.go b/shortdrama/consts/public/table_name.go
index ea658e1..f656481 100644
--- a/shortdrama/consts/public/table_name.go
+++ b/shortdrama/consts/public/table_name.go
@@ -1,17 +1,20 @@
package public
const (
- TableNameDrama = "short_drama"
- TableNameCharacter = "short_drama_character"
- TableNameEpisode = "short_drama_episode"
- TableNameGenerationTask = "short_drama_generation_task"
- TableNameModelConfig = "short_drama_model_config"
- TableNameScene = "short_drama_scene"
- TableNameProp = "short_drama_prop"
- TableNameBackgroundMusic = "short_drama_background_music"
- TableNameUser = "user"
- TableNameAgentProfile = "agent_profile"
- TableNameCustomerProfile = "customer_profile"
- TableNameAgentTier = "agent_tier"
- TableNameAccountTransaction = "account_transaction"
+ TableNameDrama = "short_drama"
+ TableNameCharacter = "short_drama_character"
+ TableNameEpisode = "short_drama_episode"
+ TableNameGenerationTask = "short_drama_generation_task"
+ TableNameModelConfig = "short_drama_model_config"
+ TableNameScene = "short_drama_scene"
+ TableNameProp = "short_drama_prop"
+ TableNameBackgroundMusic = "short_drama_background_music"
+ TableNameUser = "user"
+ TableNameAgentProfile = "agent_profile"
+ TableNameCustomerProfile = "customer_profile"
+ TableNameAgentTier = "agent_tier"
+ TableNameAccountTransaction = "account_transaction"
+ TableNamePaymentOrder = "payment_order"
+ TableNamePaymentChannelTrade = "payment_channel_trade"
+ TableNamePaymentConfig = "payment_config"
)
diff --git a/shortdrama/controller/agent_controller.go b/shortdrama/controller/agent_controller.go
index 5f9e95d..d00f3de 100644
--- a/shortdrama/controller/agent_controller.go
+++ b/shortdrama/controller/agent_controller.go
@@ -7,6 +7,8 @@ import (
"video-factory/shortdrama/model/dto"
"video-factory/shortdrama/model/entity"
"video-factory/shortdrama/service"
+
+ "github.com/gogf/gf/v2/frame/g"
)
type agent struct{}
@@ -18,7 +20,7 @@ type createAgentRes struct {
}
func (c *agent) Create(ctx context.Context, req *dto.CreateAgentReq) (res *createAgentRes, err error) {
- user, err := service.AuthService.CreateAgent(ctx, req.Username, req.Password, req.Name, req.Region, req.TierId)
+ user, err := service.AuthService.CreateAgent(ctx, req.Username, req.Password, req.Phone, req.Name, req.Region, req.TierId)
if err != nil {
return nil, err
}
@@ -35,11 +37,15 @@ func (c *agent) List(ctx context.Context, req *dto.ListAgentReq) (res *dto.ListA
func (c *agent) Update(ctx context.Context, req *dto.UpdateAgentReq) (res *struct{}, err error) {
if req.Name != "" {
- if err := dao.User.UpdateFields(ctx, req.Id, map[string]interface{}{
+ m := g.Map{
"name": req.Name,
"region": req.Region,
"status": req.Status,
- }); err != nil {
+ }
+ if req.Phone != "" {
+ m["phone"] = req.Phone
+ }
+ if err := dao.User.UpdateFields(ctx, req.Id, m); err != nil {
return nil, err
}
}
diff --git a/shortdrama/controller/config_controller.go b/shortdrama/controller/config_controller.go
index cbaac5a..5bbc98e 100644
--- a/shortdrama/controller/config_controller.go
+++ b/shortdrama/controller/config_controller.go
@@ -5,6 +5,7 @@ import (
"fmt"
"video-factory/shortdrama/model/dto"
+ "video-factory/shortdrama/model/entity"
"video-factory/shortdrama/service"
)
@@ -28,3 +29,23 @@ func (c *config) Save(ctx context.Context, req *dto.SaveModelConfigReq) (res *st
}
return nil, service.ConfigService.Save(ctx, req)
}
+
+func (c *config) GetPayment(ctx context.Context, req *dto.GetPaymentConfigReq) (res *dto.GetPaymentConfigRes, err error) {
+ cfg := service.ConfigService.GetPaymentConfig(ctx)
+ if cfg == nil {
+ return &dto.GetPaymentConfigRes{PaymentConfig: &entity.PaymentConfig{}}, nil
+ }
+ return &dto.GetPaymentConfigRes{PaymentConfig: cfg}, nil
+}
+
+func (c *config) SavePayment(ctx context.Context, req *dto.SavePaymentConfigReq) (res *struct{}, err error) {
+ return nil, service.ConfigService.SavePaymentConfig(ctx, &entity.PaymentConfig{
+ WechatAppId: req.WechatAppId,
+ WechatMchId: req.WechatMchId,
+ WechatApiKey: req.WechatApiKey,
+ WechatAppSecret: req.WechatAppSecret,
+ AlipayAppId: req.AlipayAppId,
+ AlipayPrivateKey: req.AlipayPrivateKey,
+ AlipayPublicKey: req.AlipayPublicKey,
+ })
+}
diff --git a/shortdrama/controller/customer_controller.go b/shortdrama/controller/customer_controller.go
index 7f71aaf..5b135ea 100644
--- a/shortdrama/controller/customer_controller.go
+++ b/shortdrama/controller/customer_controller.go
@@ -74,3 +74,20 @@ func (c *customer) Detail(ctx context.Context, req *struct{ Id int64 }) (res *dt
balance := service.AuthService.GetBalance(ctx, req.Id)
return &dto.CustomerDetail{User: user, Profile: profile, Balance: balance}, nil
}
+
+func (c *customer) Update(ctx context.Context, req *dto.UpdateCustomerReq) (res *struct{}, err error) {
+ m := g.Map{}
+ if req.Phone != "" {
+ m["phone"] = req.Phone
+ }
+ if req.Name != "" {
+ m["name"] = req.Name
+ }
+ if req.Region != "" {
+ m["region"] = req.Region
+ }
+ if len(m) == 0 {
+ return nil, nil
+ }
+ return nil, dao.User.UpdateFields(ctx, req.Id, m)
+}
diff --git a/shortdrama/controller/payment_controller.go b/shortdrama/controller/payment_controller.go
new file mode 100644
index 0000000..542290c
--- /dev/null
+++ b/shortdrama/controller/payment_controller.go
@@ -0,0 +1,57 @@
+package controller
+
+import (
+ "context"
+ "video-factory/shortdrama/middleware"
+ "video-factory/shortdrama/model/dto"
+ "video-factory/shortdrama/service"
+
+ "github.com/gogf/gf/v2/frame/g"
+)
+
+type payment struct{}
+
+var Payment = new(payment)
+
+func (c *payment) Prepay(ctx context.Context, req *dto.PrepayReq) (res *dto.PrepayRes, err error) {
+ r := g.RequestFromCtx(ctx)
+ userId := middleware.GetUserId(r)
+
+ order, codeUrl, prepayJson, redirectUrl, err := service.PaymentService.Prepay(ctx, userId, req.Amount, req.Channel)
+ if err != nil {
+ return nil, err
+ }
+ return &dto.PrepayRes{
+ OrderNo: order.OrderNo,
+ Channel: order.Channel,
+ CodeUrl: codeUrl,
+ PrepayJson: prepayJson,
+ RedirectUrl: redirectUrl,
+ }, nil
+}
+
+func (c *payment) Status(ctx context.Context, req *dto.PaymentStatusReq) (res *dto.PaymentStatusRes, err error) {
+ status, amount, err := service.PaymentService.GetStatus(ctx, req.OrderNo)
+ if err != nil {
+ return nil, err
+ }
+ return &dto.PaymentStatusRes{OrderNo: req.OrderNo, Status: status, Amount: amount}, nil
+}
+
+func (c *payment) NotifyWechat(ctx context.Context, req *struct{}) (res *struct{}, err error) {
+ r := g.RequestFromCtx(ctx)
+ body := r.GetBody()
+ _ = service.PaymentService.HandleNotify(ctx, "wechat", body)
+ r.Response.WriteString("")
+ r.Exit()
+ return nil, nil
+}
+
+func (c *payment) NotifyAlipay(ctx context.Context, req *struct{}) (res *struct{}, err error) {
+ r := g.RequestFromCtx(ctx)
+ body := r.GetBody()
+ _ = service.PaymentService.HandleNotify(ctx, "alipay", body)
+ r.Response.WriteString("success")
+ r.Exit()
+ return nil, nil
+}
diff --git a/shortdrama/controller/transaction_controller.go b/shortdrama/controller/transaction_controller.go
index 3fca904..73cabf9 100644
--- a/shortdrama/controller/transaction_controller.go
+++ b/shortdrama/controller/transaction_controller.go
@@ -2,6 +2,7 @@ package controller
import (
"context"
+ "fmt"
"video-factory/shortdrama/dao"
"video-factory/shortdrama/middleware"
@@ -29,7 +30,5 @@ func (c *transaction) List(ctx context.Context, req *dto.ListTransactionReq) (re
}
func (c *transaction) Recharge(ctx context.Context, req *dto.RechargeReq) (res *struct{}, err error) {
- r := g.RequestFromCtx(ctx)
- createdBy := middleware.GetRole(r) + ":" + r.GetCtxVar("userId").String()
- return nil, service.AuthService.Recharge(ctx, req.UserId, req.Amount, createdBy)
+ return nil, service.AuthService.Recharge(ctx, req.UserId, req.Amount, "", fmt.Sprintf("后台充值 %.2f 元", float64(req.Amount)/100))
}
diff --git a/shortdrama/dao/account_transaction_dao.go b/shortdrama/dao/account_transaction_dao.go
index 13bf69d..4ead0ef 100644
--- a/shortdrama/dao/account_transaction_dao.go
+++ b/shortdrama/dao/account_transaction_dao.go
@@ -29,6 +29,7 @@ func init() {
g.Log().Warningf(ctx, "创建 account_transaction 表失败: %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) {
diff --git a/shortdrama/dao/model_config_dao.go b/shortdrama/dao/model_config_dao.go
index 16a005f..727e3f6 100644
--- a/shortdrama/dao/model_config_dao.go
+++ b/shortdrama/dao/model_config_dao.go
@@ -48,6 +48,7 @@ func init() {
"max_ref_audio_count", "max_ref_audio_file_size", "max_ref_audio_duration", "ref_audio_formats",
"max_ref_image_file_size", "ref_image_formats",
"video_query_url",
+ "payment_config",
} {
if _, err := g.DB().Exec(ctx, `ALTER TABLE `+public.TableNameModelConfig+` DROP COLUMN `+col); err != nil {
g.Log().Debugf(ctx, "删除孤儿列 %s 失败(可能已删除): %v", col, err)
diff --git a/shortdrama/dao/payment_channel_trade_dao.go b/shortdrama/dao/payment_channel_trade_dao.go
new file mode 100644
index 0000000..86942be
--- /dev/null
+++ b/shortdrama/dao/payment_channel_trade_dao.go
@@ -0,0 +1,53 @@
+package dao
+
+import (
+ "context"
+ "video-factory/shortdrama/consts/public"
+ "video-factory/shortdrama/model/entity"
+
+ "github.com/gogf/gf/v2/frame/g"
+)
+
+var PaymentChannelTrade = &paymentChannelTradeDao{}
+
+type paymentChannelTradeDao struct{}
+
+func init() {
+ ctx := context.Background()
+ _, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+public.TableNamePaymentChannelTrade+` (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ order_id INTEGER NOT NULL DEFAULT 0,
+ channel TEXT NOT NULL DEFAULT '',
+ prepay_id TEXT NOT NULL DEFAULT '',
+ code_url TEXT NOT NULL DEFAULT '',
+ trade_no TEXT NOT NULL DEFAULT '',
+ channel_response TEXT NOT NULL DEFAULT '{}',
+ created_at DATETIME
+ )`)
+ if err != nil {
+ g.Log().Warningf(ctx, "创建 payment_channel_trade 表失败: %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) {
+ r, err := g.DB().Exec(ctx,
+ "INSERT INTO "+public.TableNamePaymentChannelTrade+" (order_id, channel, prepay_id, code_url, trade_no, channel_response) VALUES (?, ?, ?, ?, ?, ?)",
+ data.OrderId, data.Channel, data.PrepayId, data.CodeUrl, data.TradeNo, data.ChannelResponse)
+ if err != nil {
+ return 0, err
+ }
+ return r.LastInsertId()
+}
+
+func (d *paymentChannelTradeDao) GetByOrderId(ctx context.Context, orderId int64) (*entity.PaymentChannelTrade, error) {
+ var t entity.PaymentChannelTrade
+ err := g.DB().Model(public.TableNamePaymentChannelTrade).Ctx(ctx).Where("order_id", orderId).Scan(&t)
+ if err != nil {
+ return nil, err
+ }
+ if t.Id == 0 {
+ return nil, nil
+ }
+ return &t, nil
+}
diff --git a/shortdrama/dao/payment_config_dao.go b/shortdrama/dao/payment_config_dao.go
new file mode 100644
index 0000000..434482c
--- /dev/null
+++ b/shortdrama/dao/payment_config_dao.go
@@ -0,0 +1,63 @@
+package dao
+
+import (
+ "context"
+ "video-factory/shortdrama/consts/public"
+ "video-factory/shortdrama/model/entity"
+
+ "github.com/gogf/gf/v2/frame/g"
+ "github.com/gogf/gf/v2/util/gconv"
+)
+
+var PaymentConfigDao = &paymentConfigDao{}
+
+type paymentConfigDao struct{}
+
+func init() {
+ ctx := context.Background()
+ if _, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+public.TableNamePaymentConfig+` (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ wechat_app_id TEXT NOT NULL DEFAULT '',
+ wechat_mch_id TEXT NOT NULL DEFAULT '',
+ wechat_api_key TEXT NOT NULL DEFAULT '',
+ wechat_app_secret TEXT NOT NULL DEFAULT '',
+ alipay_app_id TEXT NOT NULL DEFAULT '',
+ alipay_private_key TEXT NOT NULL DEFAULT '',
+ alipay_public_key TEXT NOT NULL DEFAULT '',
+ created_at DATETIME,
+ updated_at DATETIME
+ )`); err != nil {
+ g.Log().Warningf(ctx, "创建支付配置表失败: %v", err)
+ }
+}
+
+func (d *paymentConfigDao) GetFirst(ctx context.Context) (res *entity.PaymentConfig, err error) {
+ r, err := g.DB().Model(public.TableNamePaymentConfig).Ctx(ctx).OrderAsc("id").Limit(1).One()
+ if err != nil {
+ return nil, err
+ }
+ if r == nil {
+ return nil, nil
+ }
+ res = new(entity.PaymentConfig)
+ err = r.Struct(&res)
+ return
+}
+
+func (d *paymentConfigDao) Save(ctx context.Context, data *entity.PaymentConfig) error {
+ existing, err := d.GetFirst(ctx)
+ if err != nil {
+ return err
+ }
+ if existing != nil {
+ data.Id = existing.Id
+ _, err = g.DB().Model(public.TableNamePaymentConfig).Ctx(ctx).Data(data).Where("id", existing.Id).Update()
+ return err
+ }
+ m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}})
+ delete(m, "id")
+ delete(m, "created_at")
+ delete(m, "updated_at")
+ _, err = g.DB().Model(public.TableNamePaymentConfig).Ctx(ctx).Data(m).Insert()
+ return err
+}
diff --git a/shortdrama/dao/payment_order_dao.go b/shortdrama/dao/payment_order_dao.go
new file mode 100644
index 0000000..60d010f
--- /dev/null
+++ b/shortdrama/dao/payment_order_dao.go
@@ -0,0 +1,72 @@
+package dao
+
+import (
+ "context"
+ "video-factory/shortdrama/consts/public"
+ "video-factory/shortdrama/model/entity"
+
+ "github.com/gogf/gf/v2/frame/g"
+)
+
+var PaymentOrder = &paymentOrderDao{}
+
+type paymentOrderDao struct{}
+
+func init() {
+ ctx := context.Background()
+ _, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+public.TableNamePaymentOrder+` (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ order_no TEXT NOT NULL UNIQUE,
+ user_id INTEGER NOT NULL DEFAULT 0,
+ amount INTEGER NOT NULL DEFAULT 0,
+ channel TEXT NOT NULL DEFAULT '',
+ channel_type TEXT NOT NULL DEFAULT '',
+ status TEXT NOT NULL DEFAULT 'pending',
+ subject TEXT NOT NULL DEFAULT '',
+ paid_at DATETIME,
+ notify_raw TEXT NOT NULL DEFAULT '{}',
+ notify_count INTEGER NOT NULL DEFAULT 0,
+ created_at DATETIME,
+ updated_at DATETIME
+ )`)
+ if err != nil {
+ g.Log().Warningf(ctx, "创建 payment_order 表失败: %v", err)
+ }
+ _, _ = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_po_user ON "+public.TableNamePaymentOrder+"(user_id)")
+}
+
+func (d *paymentOrderDao) Insert(ctx context.Context, data *entity.PaymentOrder) (int64, error) {
+ r, err := g.DB().Exec(ctx,
+ "INSERT INTO "+public.TableNamePaymentOrder+" (order_no, user_id, amount, channel, channel_type, status, subject) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ data.OrderNo, data.UserId, data.Amount, data.Channel, data.ChannelType, data.Status, data.Subject)
+ if err != nil {
+ return 0, err
+ }
+ return r.LastInsertId()
+}
+
+func (d *paymentOrderDao) GetByOrderNo(ctx context.Context, orderNo string) (*entity.PaymentOrder, error) {
+ var p entity.PaymentOrder
+ err := g.DB().Model(public.TableNamePaymentOrder).Ctx(ctx).Where("order_no", orderNo).Scan(&p)
+ if err != nil {
+ return nil, err
+ }
+ if p.Id == 0 {
+ return nil, nil
+ }
+ return &p, nil
+}
+
+func (d *paymentOrderDao) UpdateSuccess(ctx context.Context, id int64, notifyRaw string) error {
+ _, err := g.DB().Exec(ctx,
+ "UPDATE "+public.TableNamePaymentOrder+" SET status='success', paid_at=CURRENT_TIMESTAMP, notify_raw=?, notify_count=notify_count+1, updated_at=CURRENT_TIMESTAMP WHERE id=?",
+ notifyRaw, id)
+ return err
+}
+
+func (d *paymentOrderDao) UpdateFail(ctx context.Context, id int64, notifyRaw string) error {
+ _, err := g.DB().Exec(ctx,
+ "UPDATE "+public.TableNamePaymentOrder+" SET status='failed', notify_raw=?, notify_count=notify_count+1, updated_at=CURRENT_TIMESTAMP WHERE id=?",
+ notifyRaw, id)
+ return err
+}
diff --git a/shortdrama/model/dto/agent_dto.go b/shortdrama/model/dto/agent_dto.go
index bcc6bc3..e606c82 100644
--- a/shortdrama/model/dto/agent_dto.go
+++ b/shortdrama/model/dto/agent_dto.go
@@ -15,14 +15,15 @@ type ListAgentRes struct {
}
type UpdateAgentReq struct {
- Id int64 `json:"id"`
- Name string `json:"name"`
- Region string `json:"region"`
- TierId int64 `json:"tier_id"`
+ Id int64 `v:"required" json:"id"`
+ Phone string `json:"phone"`
+ Name string `v:"required" json:"name"`
+ Region string `v:"required" json:"region"`
+ TierId int64 `v:"required" json:"tier_id"`
Status int `json:"status"`
}
type RenewAgentReq struct {
- AgentId int64 `json:"agent_id"`
- TierId int64 `json:"tier_id"`
+ AgentId int64 `v:"required" json:"agent_id"`
+ TierId int64 `v:"required" json:"tier_id"`
}
diff --git a/shortdrama/model/dto/auth_dto.go b/shortdrama/model/dto/auth_dto.go
index 73a28c0..4890181 100644
--- a/shortdrama/model/dto/auth_dto.go
+++ b/shortdrama/model/dto/auth_dto.go
@@ -20,17 +20,18 @@ type LoginUser struct {
}
type CreateAgentReq struct {
- Username string `json:"username"`
- Password string `json:"password"`
- Name string `json:"name"`
- Region string `json:"region"`
- TierId int64 `json:"tier_id"`
+ Username string `v:"required" json:"username"`
+ Password string `v:"required" json:"password"`
+ Phone string `v:"required" json:"phone"`
+ Name string `v:"required" json:"name"`
+ Region string `v:"required" json:"region"`
+ TierId int64 `v:"required" json:"tier_id"`
}
type CreateCustomerReq struct {
- Phone string `json:"phone"`
- Name string `json:"name"`
- Region string `json:"region"`
+ Phone string `v:"required" json:"phone"`
+ Name string `v:"required" json:"name"`
+ Region string `v:"required" json:"region"`
AgentId int64 `json:"agent_id"`
}
diff --git a/shortdrama/model/dto/config_dto.go b/shortdrama/model/dto/config_dto.go
index 6e46972..615d5fe 100644
--- a/shortdrama/model/dto/config_dto.go
+++ b/shortdrama/model/dto/config_dto.go
@@ -37,3 +37,22 @@ type SaveModelConfigReq struct {
PricePerSecond int `json:"pricePerSecond" dc:"每秒价格(分)"`
VideoTaskCallbackUrl string `json:"videoTaskCallbackUrl" dc:"视频生成任务回调地址"`
}
+
+type GetPaymentConfigReq struct {
+ g.Meta `path:"/payment" method:"get" tags:"支付配置" summary:"获取支付配置"`
+}
+
+type GetPaymentConfigRes struct {
+ *entity.PaymentConfig
+}
+
+type SavePaymentConfigReq struct {
+ g.Meta `path:"/payment" method:"post" tags:"支付配置" summary:"保存支付配置"`
+ WechatAppId string `json:"wechatAppId"`
+ WechatMchId string `json:"wechatMchId"`
+ WechatApiKey string `json:"wechatApiKey"`
+ WechatAppSecret string `json:"wechatAppSecret"`
+ AlipayAppId string `json:"alipayAppId"`
+ AlipayPrivateKey string `json:"alipayPrivateKey"`
+ AlipayPublicKey string `json:"alipayPublicKey"`
+}
diff --git a/shortdrama/model/dto/customer_dto.go b/shortdrama/model/dto/customer_dto.go
index da628d6..9c43e98 100644
--- a/shortdrama/model/dto/customer_dto.go
+++ b/shortdrama/model/dto/customer_dto.go
@@ -31,3 +31,10 @@ type CustomerDetail struct {
Profile *entity.CustomerProfile `json:"profile"`
Balance int64 `json:"balance"`
}
+
+type UpdateCustomerReq struct {
+ Id int64 `v:"required" json:"id"`
+ Phone string `json:"phone"`
+ Name string `json:"name"`
+ Region string `json:"region"`
+}
diff --git a/shortdrama/model/dto/payment_dto.go b/shortdrama/model/dto/payment_dto.go
new file mode 100644
index 0000000..8987887
--- /dev/null
+++ b/shortdrama/model/dto/payment_dto.go
@@ -0,0 +1,24 @@
+package dto
+
+type PrepayReq struct {
+ Amount int64 `v:"required|min:1" json:"amount"` // 金额(分)
+ Channel string `v:"required|in:wechat,alipay" json:"channel"` // wechat / alipay
+}
+
+type PrepayRes struct {
+ OrderNo string `json:"order_no"`
+ Channel string `json:"channel"`
+ CodeUrl string `json:"code_url,omitempty"` // PC扫码二维码内容
+ PrepayJson string `json:"prepay_json,omitempty"` // 手机端调起支付参数(JSON)
+ RedirectUrl string `json:"redirect_url,omitempty"` // Alipay WAP跳转URL
+}
+
+type PaymentStatusReq struct {
+ OrderNo string `json:"order_no"`
+}
+
+type PaymentStatusRes struct {
+ OrderNo string `json:"order_no"`
+ Status string `json:"status"`
+ Amount int64 `json:"amount"`
+}
diff --git a/shortdrama/model/entity/account_transaction.go b/shortdrama/model/entity/account_transaction.go
index 8906a5e..24cb62c 100644
--- a/shortdrama/model/entity/account_transaction.go
+++ b/shortdrama/model/entity/account_transaction.go
@@ -4,12 +4,13 @@ import "github.com/gogf/gf/v2/os/gtime"
type AccountTransaction struct {
Id int64 `orm:"id" json:"id"`
- UserId int64 `orm:"user_id" json:"user_id"` // FK → user.id (role=customer)
- Type string `orm:"type" json:"type"` // recharge / deduct / refund
- Amount int64 `orm:"amount" json:"amount"` // 正数(分)
+ UserId int64 `orm:"user_id" json:"user_id"`
+ Type string `orm:"type" json:"type"` // recharge / deduct / refund
+ Amount int64 `orm:"amount" json:"amount"`
BalanceBefore int64 `orm:"balance_before" json:"balance_before"`
BalanceAfter int64 `orm:"balance_after" json:"balance_after"`
+ OrderNo string `orm:"order_no" json:"order_no"`
Remark string `orm:"remark" json:"remark"`
- CreatedBy string `orm:"created_by" json:"created_by"` // admin / agent:{id} / system
+ CreatedBy string `orm:"created_by" json:"created_by"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
}
diff --git a/shortdrama/model/entity/payment_channel_trade.go b/shortdrama/model/entity/payment_channel_trade.go
new file mode 100644
index 0000000..f4597dc
--- /dev/null
+++ b/shortdrama/model/entity/payment_channel_trade.go
@@ -0,0 +1,14 @@
+package entity
+
+import "github.com/gogf/gf/v2/os/gtime"
+
+type PaymentChannelTrade struct {
+ Id int64 `orm:"id" json:"id"`
+ OrderId int64 `orm:"order_id" json:"order_id"`
+ Channel string `orm:"channel" json:"channel"`
+ PrepayId string `orm:"prepay_id" json:"prepay_id"`
+ CodeUrl string `orm:"code_url" json:"code_url"`
+ TradeNo string `orm:"trade_no" json:"trade_no"`
+ ChannelResponse string `orm:"channel_response" json:"channel_response"`
+ CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
+}
diff --git a/shortdrama/model/entity/payment_config.go b/shortdrama/model/entity/payment_config.go
new file mode 100644
index 0000000..cff145b
--- /dev/null
+++ b/shortdrama/model/entity/payment_config.go
@@ -0,0 +1,18 @@
+package entity
+
+import (
+ "github.com/gogf/gf/v2/os/gtime"
+)
+
+type PaymentConfig struct {
+ Id int64 `orm:"id" json:"id"`
+ WechatAppId string `orm:"wechat_app_id" json:"wechatAppId"`
+ WechatMchId string `orm:"wechat_mch_id" json:"wechatMchId"`
+ WechatApiKey string `orm:"wechat_api_key" json:"wechatApiKey"`
+ WechatAppSecret string `orm:"wechat_app_secret" json:"wechatAppSecret"`
+ AlipayAppId string `orm:"alipay_app_id" json:"alipayAppId"`
+ AlipayPrivateKey string `orm:"alipay_private_key" json:"alipayPrivateKey"`
+ AlipayPublicKey string `orm:"alipay_public_key" json:"alipayPublicKey"`
+ CreatedAt *gtime.Time `orm:"created_at" json:"createdAt"`
+ UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt"`
+}
diff --git a/shortdrama/model/entity/payment_order.go b/shortdrama/model/entity/payment_order.go
new file mode 100644
index 0000000..c166c69
--- /dev/null
+++ b/shortdrama/model/entity/payment_order.go
@@ -0,0 +1,19 @@
+package entity
+
+import "github.com/gogf/gf/v2/os/gtime"
+
+type PaymentOrder struct {
+ Id int64 `orm:"id" json:"id"`
+ OrderNo string `orm:"order_no" json:"order_no"`
+ UserId int64 `orm:"user_id" json:"user_id"`
+ Amount int64 `orm:"amount" json:"amount"`
+ Channel string `orm:"channel" json:"channel"`
+ ChannelType string `orm:"channel_type" json:"channel_type"`
+ Status string `orm:"status" json:"status"`
+ Subject string `orm:"subject" json:"subject"`
+ PaidAt *gtime.Time `orm:"paid_at" json:"paid_at"`
+ NotifyRaw string `orm:"notify_raw" json:"notify_raw"`
+ NotifyCount int `orm:"notify_count" json:"notify_count"`
+ CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
+ UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"`
+}
diff --git a/shortdrama/service/auth_service.go b/shortdrama/service/auth_service.go
index 230a6cb..48947ed 100644
--- a/shortdrama/service/auth_service.go
+++ b/shortdrama/service/auth_service.go
@@ -97,7 +97,7 @@ func (s *authService) GetBalance(ctx context.Context, userId int64) int64 {
}
// CreateAgent 创建代理商
-func (s *authService) CreateAgent(ctx context.Context, username, password, name, region string, tierId int64) (*entity.User, error) {
+func (s *authService) CreateAgent(ctx context.Context, username, password, phone, name, region string, tierId int64) (*entity.User, error) {
tier, err := dao.AgentTier.GetOne(ctx, tierId)
if err != nil || tier == nil {
return nil, errors.New("代理商等级不存在")
@@ -268,7 +268,7 @@ func (s *authService) DeductBalance(ctx context.Context, customerId int64, amoun
}
// Recharge 充值
-func (s *authService) Recharge(ctx context.Context, customerId int64, amount int64, createdBy string) error {
+func (s *authService) Recharge(ctx context.Context, customerId int64, amount int64, orderNo, remark string) error {
if amount <= 0 {
return errors.New("充值金额必须大于0")
}
@@ -280,14 +280,19 @@ func (s *authService) Recharge(ctx context.Context, customerId int64, amount int
if err := dao.CustomerProfile.UpdateBalance(ctx, customerId, newBalance); err != nil {
return err
}
+ rk := remark
+ if rk == "" {
+ rk = fmt.Sprintf("充值 %.2f 元", float64(amount)/100)
+ }
_, err = dao.AccountTransaction.Insert(ctx, &entity.AccountTransaction{
UserId: customerId,
Type: "recharge",
Amount: amount,
BalanceBefore: cp.Balance,
BalanceAfter: newBalance,
- Remark: fmt.Sprintf("充值 %.2f 元", float64(amount)/100),
- CreatedBy: createdBy,
+ OrderNo: orderNo,
+ Remark: rk,
+ CreatedBy: "system",
})
return err
}
diff --git a/shortdrama/service/config_service.go b/shortdrama/service/config_service.go
index bc0a3b8..8dae3fa 100644
--- a/shortdrama/service/config_service.go
+++ b/shortdrama/service/config_service.go
@@ -78,6 +78,20 @@ func (s *configService) Save(ctx context.Context, req *dto.SaveModelConfigReq) e
return nil
}
+// GetPaymentConfig 获取支付配置
+func (s *configService) GetPaymentConfig(ctx context.Context) *entity.PaymentConfig {
+ cfg, err := dao.PaymentConfigDao.GetFirst(ctx)
+ if err != nil || cfg == nil {
+ return nil
+ }
+ return cfg
+}
+
+// SavePaymentConfig 保存支付配置
+func (s *configService) SavePaymentConfig(ctx context.Context, cfg *entity.PaymentConfig) error {
+ return dao.PaymentConfigDao.Save(ctx, cfg)
+}
+
// syncModelDurationFromAPI 查询模型API,获取模型支持的参数和时长范围
func syncModelDurationFromAPI(ctx context.Context, cfg *entity.ModelConfig) {
if cfg.VideoApiKey == "" || cfg.VideoBaseUrl == "" || cfg.VideoModelName == "" {
@@ -194,6 +208,8 @@ func inferChatTemperature(modelName string) float64 {
return 0.7
case strings.Contains(modelName, "deepseek"):
return 0.7
+ case strings.Contains(modelName, "doubao") || strings.Contains(modelName, "豆包"):
+ return 0.95
case strings.Contains(modelName, "glm"):
return 0.8
case strings.Contains(modelName, "ernie"):
diff --git a/shortdrama/service/payment_service.go b/shortdrama/service/payment_service.go
new file mode 100644
index 0000000..cb92d7b
--- /dev/null
+++ b/shortdrama/service/payment_service.go
@@ -0,0 +1,420 @@
+package service
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+ "time"
+
+ "video-factory/shortdrama/dao"
+ "video-factory/shortdrama/model/entity"
+)
+
+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() PaymentConfig {
+ cfg := ConfigService.GetPaymentConfig(context.Background())
+ if cfg == nil {
+ return PaymentConfig{}
+ }
+ return PaymentConfig{
+ Wechat: WechatConfig{
+ AppId: cfg.WechatAppId,
+ MchId: cfg.WechatMchId,
+ ApiKey: cfg.WechatApiKey,
+ AppSecret: cfg.WechatAppSecret,
+ },
+ Alipay: AlipayConfig{
+ AppId: cfg.AlipayAppId,
+ PrivateKey: cfg.AlipayPrivateKey,
+ PublicKey: cfg.AlipayPublicKey,
+ },
+ }
+}
+
+// Prepay 创建支付订单并调起渠道
+func (s *paymentService) Prepay(ctx context.Context, userId int64, amount int64, channel string) (*entity.PaymentOrder, string, string, string, error) {
+ if amount < 1 {
+ return nil, "", "", "", errors.New("金额必须大于0")
+ }
+ if channel != "wechat" && channel != "alipay" {
+ return nil, "", "", "", errors.New("不支持的支付渠道")
+ }
+
+ chanType := "native"
+ if channel == "wechat" {
+ chanType = "native"
+ } else {
+ chanType = "precreate"
+ }
+
+ orderNo := s.generateOrderNo()
+
+ order := &entity.PaymentOrder{
+ OrderNo: orderNo,
+ UserId: userId,
+ Amount: amount,
+ Channel: channel,
+ ChannelType: chanType,
+ Status: "pending",
+ Subject: fmt.Sprintf("账户充值 %.2f 元", float64(amount)/100),
+ }
+ id, err := dao.PaymentOrder.Insert(ctx, order)
+ if err != nil {
+ return nil, "", "", "", err
+ }
+ order.Id = id
+
+ pc := s.GetPaymentConfig()
+ var codeUrl, prepayJson, redirectUrl string
+
+ switch channel {
+ case "wechat":
+ codeUrl, prepayJson, err = s.callWechat(ctx, order, pc.Wechat)
+ case "alipay":
+ codeUrl, redirectUrl, err = s.callAlipay(ctx, order, pc.Alipay)
+ }
+ if err != nil {
+ _ = dao.PaymentOrder.UpdateFail(ctx, id, fmt.Sprintf(`{"error":"%s"}`, err.Error()))
+ return nil, "", "", "", err
+ }
+
+ _, _ = dao.PaymentChannelTrade.Insert(ctx, &entity.PaymentChannelTrade{
+ OrderId: id,
+ Channel: channel,
+ CodeUrl: codeUrl,
+ PrepayId: prepayJson,
+ ChannelResponse: "{}",
+ })
+
+ return order, codeUrl, prepayJson, redirectUrl, nil
+}
+
+// HandleNotify 处理渠道回调
+func (s *paymentService) HandleNotify(ctx context.Context, channel string, body []byte) error {
+ pc := s.GetPaymentConfig()
+ var orderNo, tradeNo string
+
+ switch channel {
+ case "wechat":
+ var err error
+ orderNo, tradeNo, err = s.verifyWechatNotify(body, pc.Wechat)
+ if err != nil {
+ return err
+ }
+ case "alipay":
+ var err error
+ orderNo, tradeNo, err = s.verifyAlipayNotify(body, pc.Alipay)
+ if err != nil {
+ return err
+ }
+ default:
+ return errors.New("未知渠道")
+ }
+
+ order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
+ if err != nil || order == nil {
+ return errors.New("订单不存在")
+ }
+ if order.Status == "success" {
+ return nil // 防止重复回调
+ }
+
+ rawStr := string(body)
+ if err := dao.PaymentOrder.UpdateSuccess(ctx, order.Id, rawStr); err != nil {
+ return err
+ }
+
+ if _, err := dao.PaymentChannelTrade.Insert(ctx, &entity.PaymentChannelTrade{
+ OrderId: order.Id,
+ Channel: channel,
+ TradeNo: tradeNo,
+ ChannelResponse: rawStr,
+ }); err != nil {
+ return err
+ }
+
+ return AuthService.Recharge(ctx, order.UserId, order.Amount, orderNo, fmt.Sprintf("支付充值 %.2f 元", float64(order.Amount)/100))
+}
+
+// GetStatus 查询订单状态
+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 order.Status, order.Amount, nil
+}
+
+func (s *paymentService) generateOrderNo() string {
+ now := time.Now()
+ r := rand.Intn(10000)
+ return fmt.Sprintf("PAY%s%04d", now.Format("20060102150405"), r)
+}
+
+// ==================== 微信支付 ====================
+
+func (s *paymentService) callWechat(ctx context.Context, order *entity.PaymentOrder, cfg WechatConfig) (codeUrl, prepayJson string, err error) {
+ if cfg.AppId == "" || cfg.MchId == "" || cfg.ApiKey == "" {
+ return "", "", errors.New("微信支付未配置")
+ }
+ nonceStr := s.randomStr(32)
+ params := map[string]string{
+ "appid": cfg.AppId,
+ "mch_id": cfg.MchId,
+ "nonce_str": nonceStr,
+ "body": order.Subject,
+ "out_trade_no": order.OrderNo,
+ "total_fee": fmt.Sprintf("%d", order.Amount),
+ "spbill_create_ip": "127.0.0.1",
+ "notify_url": "", // 需要实际外网可访问地址
+ "trade_type": "NATIVE",
+ }
+ sign := s.wechatSign(params, cfg.ApiKey)
+ params["sign"] = sign
+
+ 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)
+ }
+ 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 result["code_url"], "", nil
+}
+
+// verifyWechatNotify 验证微信回调签名,返回 (orderNo, transactionId)
+func (s *paymentService) verifyWechatNotify(body []byte, cfg WechatConfig) (string, string, error) {
+ result := s.xmlToMap(string(body))
+ if result["return_code"] != "SUCCESS" {
+ return "", "", errors.New("微信回调失败")
+ }
+ sign := s.wechatSign(result, cfg.ApiKey)
+ if sign != result["sign"] {
+ return "", "", errors.New("微信回调签名验证失败")
+ }
+ return result["out_trade_no"], result["transaction_id"], nil
+}
+
+func (s *paymentService) wechatSign(params map[string]string, apiKey string) string {
+ keys := make([]string, 0, len(params))
+ for k := range params {
+ if k == "sign" {
+ continue
+ }
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var pairs []string
+ for _, k := range keys {
+ v := params[k]
+ if v != "" {
+ pairs = append(pairs, k+"="+v)
+ }
+ }
+ pairs = append(pairs, "key="+apiKey)
+ raw := strings.Join(pairs, "&")
+ h := md5.Sum([]byte(raw))
+ return strings.ToUpper(hex.EncodeToString(h[:]))
+}
+
+// ==================== 支付宝支付 ====================
+
+func (s *paymentService) callAlipay(ctx context.Context, order *entity.PaymentOrder, cfg AlipayConfig) (codeUrl, redirectUrl string, err error) {
+ if cfg.AppId == "" || cfg.PrivateKey == "" {
+ return "", "", errors.New("支付宝支付未配置")
+ }
+
+ bizContent := fmt.Sprintf(`{"out_trade_no":"%s","total_amount":"%.2f","subject":"%s"}`, order.OrderNo, float64(order.Amount)/100, order.Subject)
+
+ params := url.Values{}
+ params.Set("app_id", cfg.AppId)
+ params.Set("method", "alipay.trade.precreate")
+ params.Set("format", "JSON")
+ params.Set("charset", "utf-8")
+ params.Set("sign_type", "RSA2")
+ params.Set("timestamp", time.Now().Format("2006-01-02 15:04:05"))
+ params.Set("version", "1.0")
+ params.Set("biz_content", bizContent)
+ params.Set("notify_url", "")
+
+ sign := s.alipaySign(params, cfg.PrivateKey)
+ params.Set("sign", sign)
+
+ resp, err := http.PostForm("https://openapi.alipay.com/gateway.do", params)
+ if err != nil {
+ return "", "", fmt.Errorf("调用支付宝下单失败: %w", err)
+ }
+ defer resp.Body.Close()
+ b, _ := io.ReadAll(resp.Body)
+
+ var result struct {
+ Response struct {
+ Code string `json:"code"`
+ Msg string `json:"msg"`
+ QrCode string `json:"qr_code"`
+ SubMsg string `json:"sub_msg"`
+ } `json:"alipay_trade_precreate_response"`
+ Sign string `json:"sign"`
+ }
+ if err := json.Unmarshal(b, &result); err != nil {
+ return "", "", errors.New("支付宝返回解析失败")
+ }
+ if result.Response.Code != "10000" {
+ return "", "", fmt.Errorf("支付宝下单失败: %s", result.Response.SubMsg)
+ }
+ return result.Response.QrCode, "", nil
+}
+
+func (s *paymentService) verifyAlipayNotify(body []byte, cfg AlipayConfig) (string, string, error) {
+ vals, err := url.ParseQuery(string(body))
+ if err != nil {
+ return "", "", errors.New("支付宝回调解析失败")
+ }
+ if vals.Get("trade_status") != "TRADE_SUCCESS" {
+ return "", "", errors.New("支付宝回调状态非成功")
+ }
+ sign := s.alipaySign(vals, cfg.PublicKey)
+ if sign != vals.Get("sign") {
+ return "", "", errors.New("支付宝回调签名验证失败")
+ }
+ return vals.Get("out_trade_no"), vals.Get("trade_no"), nil
+}
+
+func (s *paymentService) alipaySign(params url.Values, privateKey string) string {
+ keys := make([]string, 0, len(params))
+ for k := range params {
+ if k == "sign" || k == "sign_type" {
+ continue
+ }
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var pairs []string
+ for _, k := range keys {
+ v := params.Get(k)
+ if v != "" {
+ pairs = append(pairs, k+"="+v)
+ }
+ }
+ raw := strings.Join(pairs, "&")
+ h := md5.Sum([]byte(raw + privateKey))
+ return hex.EncodeToString(h[:])
+}
+
+// ==================== 工具方法 ====================
+
+func (s *paymentService) randomStr(n int) string {
+ letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letters[rand.Intn(len(letters))]
+ }
+ return string(b)
+}
+
+func (s *paymentService) mapToXML(m map[string]string) string {
+ var xml string
+ xml += ""
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ xml += fmt.Sprintf("<%s>%s>", k, m[k], k)
+ }
+ xml += ""
+ return xml
+}
+
+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 = ""
+ }
+ 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)
+ }
+ }
+ return result
+}
+
+// InitDefaultPaymentConfig 初始化默认支付配置(空配置)
+func InitDefaultPaymentConfig() PaymentConfig {
+ return PaymentConfig{
+ Wechat: WechatConfig{},
+ Alipay: AlipayConfig{},
+ }
+}