This commit is contained in:
2026-07-13 13:40:00 +08:00
parent eeca1a4c5b
commit 83f0acd8eb
25 changed files with 880 additions and 41 deletions
+1
View File
@@ -19,6 +19,7 @@ func main() {
controller.Agent,
controller.Customer,
controller.Transaction,
controller.Payment,
})
// ==================== 静态文件服务(workspace 目录) ====================
BIN
View File
Binary file not shown.
+16 -13
View File
@@ -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"
)
+9 -3
View File
@@ -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
}
}
@@ -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,
})
}
@@ -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)
}
@@ -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("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>")
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
}
@@ -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))
}
@@ -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) {
+1
View File
@@ -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)
@@ -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
}
+63
View File
@@ -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
}
+72
View File
@@ -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
}
+7 -6
View File
@@ -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"`
}
+9 -8
View File
@@ -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"`
}
+19
View File
@@ -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"`
}
+7
View File
@@ -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"`
}
+24
View File
@@ -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"`
}
@@ -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"`
}
@@ -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"`
}
+18
View File
@@ -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"`
}
+19
View File
@@ -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"`
}
+9 -4
View File
@@ -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
}
+16
View File
@@ -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"):
+420
View File
@@ -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 += "<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><![CDATA[%s]]></%s>", k, m[k], k)
}
xml += "</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{},
}
}