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>", 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{}, + } +}