Initial commit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 11:07:42 +08:00
co-authored by Claude Opus 5.5
commit 7974a42301
633 changed files with 120248 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
package coinser
import (
"context"
"strings"
"time"
"videoweb/app/service/rechargeser/jiahua"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/models/rechargepackagemod"
"videoweb/pub/models/usermod"
"videoweb/pub/models/vipordermod"
"videoweb/pub/mongos"
"videoweb/pub/pubdef"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
// BuyCoinResp 购买金币下单响应
type BuyCoinResp struct {
TradeNo string `json:"tradeNo"`
PayUrl string `json:"payUrl"`
PayUrlMode string `json:"payUrlMode"`
IsOpenNewBrowser bool `json:"isOpenNewBrowser"`
}
// GetUserInfo 获取用户信息
func GetUserInfo(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
}
// CreateCoinOrder 创建金币购买订单(统一写入 vip_order 表,OrderType=2)。
// 防重复:15 秒内同一用户+同一套餐的 pending 订单直接返回原支付链接。
func CreateCoinOrder(ctx context.Context,
userId int64, userName string,
packageId int64, payMode string,
sysType string, devType string, ip string,
districtCode string,
) (*BuyCoinResp, *srvdef.SrvErr) {
pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
rechargepackagemod.FN.RechargePackage.Id: packageId,
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeCoin,
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
})
if serr != nil || pkg == nil {
return nil, srvdef.New(srvdef.ErrNoData, "金币套餐不存在或已下架", "")
}
coinAmount := pkg.BaseCoin + pkg.GiftCoin
if coinAmount <= 0 {
return nil, srvdef.New(srvdef.ErrParamError, "金币套餐到账数量无效", "")
}
now := time.Now()
dupFilter := bson.M{
vipordermod.FN.VipOrder.UserId: userId,
vipordermod.FN.VipOrder.CoinCfgId: packageId,
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
vipordermod.FN.VipOrder.PayMode: payMode,
vipordermod.FN.VipOrder.Status: pubdef.VipOrderPending,
vipordermod.FN.VipOrder.CreatedAt: bson.M{"$gte": now.Add(-15 * time.Second)},
}
existing, _ := vipordermod.FindOneVipOrder(ctx, dupFilter)
if existing != nil && existing.PayUrl != "" {
return &BuyCoinResp{
TradeNo: existing.TradeNo,
PayUrl: existing.PayUrl,
PayUrlMode: existing.PayUrlMode,
IsOpenNewBrowser: true,
}, nil
}
tradeNo := pubdef.GetTrandNoWithUUID()
payInfo, serr := jiahua.SubmitOrder(ctx, userId, userName, tradeNo, pkg.Price, payMode, sysType, devType, ip)
if serr != nil {
return nil, serr
}
order := &vipordermod.VipOrder{
Id: mongos.Lid.VipOrder.Get(),
OrderType: vipordermod.OrderTypeCoin,
UserId: userId,
CoinCfgId: packageId, // 兼容历史字段,存购买套餐ID
Coin: coinAmount,
Amount: pkg.Price,
PayMode: payMode,
TradeNo: tradeNo,
Oid: payInfo.Oid,
Status: pubdef.VipOrderPending,
PayUrl: payInfo.PayUrl,
PayUrlMode: payInfo.PayUrlMode,
PayChannel: payInfo.PayChannel,
PayRate: payInfo.PayRate,
IPAddr: ip,
SysType: sysType,
DistrictCode: strings.TrimSpace(districtCode),
CreatedAt: now,
UpdatedAt: now,
}
if serr = vipordermod.InsertOneVipOrder(ctx, order); serr != nil {
log.Error("coinser:CreateCoinOrder InsertOneVipOrder failed",
log.Any("tradeNo", tradeNo), log.E(serr))
return nil, serr
}
return &BuyCoinResp{
TradeNo: tradeNo,
PayUrl: payInfo.PayUrl,
PayUrlMode: payInfo.PayUrlMode,
IsOpenNewBrowser: true,
}, nil
}
// GetCoinOrderHistory 查询用户金币购买记录
func GetCoinOrderHistory(ctx context.Context, userId int64, page, pageSize int64) (vipordermod.VipOrderList, int64, *srvdef.SrvErr) {
filter := bson.M{
vipordermod.FN.VipOrder.UserId: userId,
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
}
total, serr := vipordermod.CountVipOrder(ctx, filter)
if serr != nil {
return nil, 0, serr
}
skip := (page - 1) * pageSize
opts := options.Find().
SetSort(bson.M{vipordermod.FN.VipOrder.CreatedAt: -1}).
SetSkip(skip).
SetLimit(pageSize)
list, serr := vipordermod.FindManyVipOrder(ctx, filter, opts)
return list, total, serr
}
+146
View File
@@ -0,0 +1,146 @@
package coinser
import (
"context"
"time"
"videoweb/app/api/proto"
"videoweb/pub/common/response"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/utils"
"videoweb/pub/models/cointransmod"
"videoweb/pub/pubdef"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
// coinTransDefaultPageSize 金币余额明细默认每页数量
coinTransDefaultPageSize int64 = 10
// coinTransMaxPageSize 金币余额明细最大每页数量,避免单次请求拉取过多流水
coinTransMaxPageSize int64 = 100
)
var (
// coinTransListProjection 余额明细只读取列表展示需要的字段,降低Mongo读取成本
coinTransListProjection = bson.D{
{Key: cointransmod.FN.CoinTrans.Id, Value: 1},
{Key: cointransmod.FN.CoinTrans.FlowDir, Value: 1},
{Key: cointransmod.FN.CoinTrans.TranType, Value: 1},
{Key: cointransmod.FN.CoinTrans.Amount, Value: 1},
{Key: cointransmod.FN.CoinTrans.BalanceBefore, Value: 1},
{Key: cointransmod.FN.CoinTrans.BalanceAfter, Value: 1},
{Key: cointransmod.FN.CoinTrans.OrderNo, Value: 1},
{Key: cointransmod.FN.CoinTrans.RelatedId, Value: 1},
{Key: cointransmod.FN.CoinTrans.Remark, Value: 1},
{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: 1},
}
// coinTransListSort 按交易时间倒序展示余额明细
coinTransListSort = bson.D{
{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1},
}
// coinTransListHint 匹配 coin_trans 的 userId+createdAt 复合索引
coinTransListHint = bson.D{
{Key: cointransmod.FN.CoinTrans.UserId, Value: 1},
{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1},
}
)
// GetCoinTransList 查询当前用户金币余额明细
//
// 只查询当前登录用户的流水,按交易时间倒序分页,pageSize 最大限制为100
func GetCoinTransList(ctx context.Context, userId int64, page, pageSize int64) (*response.BasePageResp, *srvdef.SrvErr) {
if userId <= 0 {
return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
}
page, pageSize = normalizeCoinTransPage(page, pageSize)
filter := bson.M{
cointransmod.FN.CoinTrans.UserId: userId,
}
total, err := cointransmod.CountCoinTrans(ctx, filter)
if err != nil {
return nil, err
}
if total <= 0 {
return &response.BasePageResp{
TotalCount: 0,
PageSize: int(pageSize),
PageNum: int(page),
List: make([]*proto.CoinTransListItemResp, 0),
}, nil
}
list, err1 := cointransmod.FindManyCoinTrans(ctx, filter, options.Find().
SetProjection(coinTransListProjection).
SetSort(coinTransListSort).
SetHint(coinTransListHint).
SetSkip((page-1)*pageSize).
SetLimit(pageSize))
if err1 != nil {
return nil, err1
}
resp := make([]*proto.CoinTransListItemResp, 0, len(list))
for _, item := range list {
resp = append(resp, coinTransToListItemResp(item))
}
return &response.BasePageResp{
TotalCount: total,
PageSize: int(pageSize),
PageNum: int(page),
List: resp,
}, nil
}
// normalizeCoinTransPage 规范化金币流水分页参数
//
// page 小于等于0时按第1页处理;pageSize 未传时使用默认值,超过上限时截断
func normalizeCoinTransPage(page, pageSize int64) (int64, int64) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = coinTransDefaultPageSize
}
if pageSize > coinTransMaxPageSize {
pageSize = coinTransMaxPageSize
}
return page, pageSize
}
// coinTransToListItemResp 转换金币流水响应项
//
// amount 保持数据库中的正数金额,changeAmount 根据收支方向转换为带正负号的变动金额
func coinTransToListItemResp(item *cointransmod.CoinTrans) *proto.CoinTransListItemResp {
title := item.Remark
if title == "" {
title = item.TranType.Desc()
}
changeAmount := item.Amount
if item.FlowDir == cointransmod.FlowOut {
changeAmount = -item.Amount
}
return &proto.CoinTransListItemResp{
Id: item.Id,
Title: title,
FlowDir: item.FlowDir,
FlowDirName: item.FlowDir.Desc(),
TranType: item.TranType,
TranTypeName: item.TranType.Desc(),
Amount: item.Amount,
ChangeAmount: changeAmount,
BalanceBefore: item.BalanceBefore,
BalanceAfter: item.BalanceAfter,
OrderNo: item.OrderNo,
RelatedId: item.RelatedId,
Remark: item.Remark,
CreatedAt: formatCoinTransTime(item.CreatedAt),
}
}
func formatCoinTransTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.In(utils.LocBJ).Format(pubdef.TimeISO)
}