Files
rootandClaude Opus 4.7 79ac89b458 Initial commit
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-11-07 10:47:18 +08:00

344 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package srv_rechargepackage
import (
"context"
"fmt"
"strings"
"time"
"videoweb/pub/common/srvdef"
"videoweb/pub/models/rechargepackagemod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
defaultPageNum int64 = 1
defaultPageSize int64 = 10
maxPageSize int64 = 100
)
// ListRechargePackage 分页查询购买套餐配置。
func ListRechargePackage(ctx context.Context, req *RechargePackageListReq) (*RechargePackageListResp, *srvdef.SrvErr) {
if req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
filter := buildListFilter(req)
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
total, err := rechargepackagemod.CountRechargePackage(ctx, filter)
if err != nil {
return nil, err
}
resp := &RechargePackageListResp{
Total: total,
List: make([]*RechargePackageItem, 0),
}
if total == 0 {
return resp, nil
}
list, err := rechargepackagemod.FindManyRechargePackage(ctx, filter, options.Find().
SetSort(bson.D{
{Key: rechargepackagemod.FN.RechargePackage.PackageType, Value: 1},
{Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
{Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
}).
SetSkip((pageNum-1)*pageSize).
SetLimit(pageSize))
if err != nil {
return nil, err
}
for _, item := range list {
resp.List = append(resp.List, rechargePackageToWebItem(item))
}
return resp, nil
}
// ListVipPackageOptions 查询启用的会员畅享套餐选项,不分页。
func ListVipPackageOptions(ctx context.Context) ([]*VipPackageOption, *srvdef.SrvErr) {
list, err := rechargepackagemod.FindManyRechargePackage(ctx, bson.M{
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
}, options.Find().
SetProjection(bson.D{
{Key: rechargepackagemod.FN.RechargePackage.Id, Value: 1},
{Key: rechargepackagemod.FN.RechargePackage.Name, Value: 1},
}).
SetSort(bson.D{
{Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
{Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
}))
if err != nil {
return nil, err
}
result := make([]*VipPackageOption, 0, len(list))
for _, item := range list {
if item == nil {
continue
}
result = append(result, &VipPackageOption{
Id: item.Id,
Name: item.Name,
})
}
return result, nil
}
// AddRechargePackage 新增购买套餐配置。
func AddRechargePackage(ctx context.Context, req *RechargePackageAddReq) *srvdef.SrvErr {
if req == nil {
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
doc := &rechargepackagemod.RechargePackage{
PackageType: req.PackageType,
Name: strings.TrimSpace(req.Name),
Price: req.Price,
OriginalPrice: req.OriginalPrice,
DurationDays: req.DurationDays,
BaseCoin: req.BaseCoin,
GiftCoin: req.GiftCoin,
Sort: req.Sort,
Status: req.Status,
Remark: strings.TrimSpace(req.Remark),
}
if doc.Status == 0 {
doc.Status = rechargepackagemod.StatusEnabled
}
normalizePackageFields(doc)
if err := validatePackage(doc); err != nil {
return err
}
doc.SubTitle = buildSubTitle(doc)
return rechargepackagemod.InsertOneRechargePackage(ctx, doc)
}
// UpdateRechargePackage 更新购买套餐配置。
func UpdateRechargePackage(ctx context.Context, req *RechargePackageUpdateReq) *srvdef.SrvErr {
if req == nil || req.Id <= 0 {
return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
}
exist, err := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{rechargepackagemod.FN.RechargePackage.Id: req.Id})
if err != nil {
return err
}
doc := *exist
applyUpdate(&doc, req)
normalizePackageFields(&doc)
if err := validatePackage(&doc); err != nil {
return err
}
doc.SubTitle = buildSubTitle(&doc)
now := time.Now()
setDoc := bson.M{
rechargepackagemod.FN.RechargePackage.PackageType: doc.PackageType,
rechargepackagemod.FN.RechargePackage.Name: doc.Name,
rechargepackagemod.FN.RechargePackage.SubTitle: doc.SubTitle,
rechargepackagemod.FN.RechargePackage.Price: doc.Price,
rechargepackagemod.FN.RechargePackage.OriginalPrice: doc.OriginalPrice,
rechargepackagemod.FN.RechargePackage.DurationDays: doc.DurationDays,
rechargepackagemod.FN.RechargePackage.BaseCoin: doc.BaseCoin,
rechargepackagemod.FN.RechargePackage.GiftCoin: doc.GiftCoin,
rechargepackagemod.FN.RechargePackage.Sort: doc.Sort,
rechargepackagemod.FN.RechargePackage.Status: doc.Status,
rechargepackagemod.FN.RechargePackage.Remark: doc.Remark,
rechargepackagemod.FN.RechargePackage.UpdatedAt: now,
}
return rechargepackagemod.UpdateOneRechargePackage(ctx,
bson.M{rechargepackagemod.FN.RechargePackage.Id: req.Id},
bson.M{"$set": setDoc},
)
}
// DeleteRechargePackage 删除购买套餐配置。
func DeleteRechargePackage(ctx context.Context, id int64) *srvdef.SrvErr {
if id <= 0 {
return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
}
return rechargepackagemod.DeleteOneRechargePackage(ctx, bson.M{rechargepackagemod.FN.RechargePackage.Id: id})
}
// SetRechargePackageStatus 单/批量设置购买套餐启停状态。
func SetRechargePackageStatus(ctx context.Context, req *RechargePackageStatusSetReq) *srvdef.SrvErr {
if req == nil {
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
ids := dedupePositiveInt64Ids(req.Ids)
if len(ids) == 0 {
return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
}
if !isValidStatus(req.Status) {
return srvdef.New(srvdef.ErrParamError, "状态无效", "")
}
return rechargepackagemod.UpdateManyRechargePackage(ctx,
bson.M{rechargepackagemod.FN.RechargePackage.Id: bson.M{"$in": ids}},
bson.M{"$set": bson.M{
rechargepackagemod.FN.RechargePackage.Status: req.Status,
rechargepackagemod.FN.RechargePackage.UpdatedAt: time.Now(),
}},
)
}
func buildListFilter(req *RechargePackageListReq) bson.M {
filter := bson.M{}
if req.Id != nil {
filter[rechargepackagemod.FN.RechargePackage.Id] = *req.Id
}
if req.PackageType != nil {
filter[rechargepackagemod.FN.RechargePackage.PackageType] = *req.PackageType
}
if req.Status != nil {
filter[rechargepackagemod.FN.RechargePackage.Status] = *req.Status
}
if strings.TrimSpace(req.Name) != "" {
filter[rechargepackagemod.FN.RechargePackage.Name] = bson.M{
"$regex": primitive.Regex{Pattern: strings.TrimSpace(req.Name), Options: "i"},
}
}
return filter
}
// normalizePage 统一分页兜底,限制 pageSize 防止后台误传过大导致慢查询。
func normalizePage(pageNum, pageSize int64) (int64, int64) {
if pageNum <= 0 {
pageNum = defaultPageNum
}
if pageSize <= 0 {
pageSize = defaultPageSize
}
if pageSize > maxPageSize {
pageSize = maxPageSize
}
return pageNum, pageSize
}
// normalizePackageFields 清理不同套餐类型不生效的字段。
// 会员套餐不使用金币字段;金币套餐不使用会员有效期字段。
func normalizePackageFields(doc *rechargepackagemod.RechargePackage) {
switch doc.PackageType {
case rechargepackagemod.PackageTypeVip:
doc.BaseCoin = 0
doc.GiftCoin = 0
case rechargepackagemod.PackageTypeCoin:
doc.DurationDays = 0
}
}
// validatePackage 校验购买套餐配置和套餐类型字段约束。
func validatePackage(doc *rechargepackagemod.RechargePackage) *srvdef.SrvErr {
if !isValidPackageType(doc.PackageType) {
return srvdef.New(srvdef.ErrParamError, "套餐类型无效", "")
}
if strings.TrimSpace(doc.Name) == "" {
return srvdef.New(srvdef.ErrParamError, "套餐标题不能为空", "")
}
if doc.Price <= 0 {
return srvdef.New(srvdef.ErrParamError, "套餐价格必须大于0", "")
}
if !isValidStatus(doc.Status) {
return srvdef.New(srvdef.ErrParamError, "状态无效", "")
}
if doc.PackageType == rechargepackagemod.PackageTypeVip && doc.DurationDays <= 0 {
return srvdef.New(srvdef.ErrParamError, "会员套餐有效天数必须大于0", "")
}
if doc.PackageType == rechargepackagemod.PackageTypeCoin && doc.BaseCoin+doc.GiftCoin <= 0 {
return srvdef.New(srvdef.ErrParamError, "金币套餐到账金币数必须大于0", "")
}
return nil
}
func isValidPackageType(packageType rechargepackagemod.PackageType) bool {
return packageType == rechargepackagemod.PackageTypeVip || packageType == rechargepackagemod.PackageTypeCoin
}
func isValidStatus(status rechargepackagemod.Status) bool {
return status == rechargepackagemod.StatusEnabled || status == rechargepackagemod.StatusDisabled
}
// applyUpdate 将更新请求合并到现有文档副本,再统一做类型归一化和校验。
func applyUpdate(doc *rechargepackagemod.RechargePackage, req *RechargePackageUpdateReq) {
if req.PackageType != nil {
doc.PackageType = *req.PackageType
}
if req.Name != nil {
doc.Name = strings.TrimSpace(*req.Name)
}
if req.Price != nil {
doc.Price = *req.Price
}
if req.OriginalPrice != nil {
doc.OriginalPrice = *req.OriginalPrice
}
if req.DurationDays != nil {
doc.DurationDays = *req.DurationDays
}
if req.BaseCoin != nil {
doc.BaseCoin = *req.BaseCoin
}
if req.GiftCoin != nil {
doc.GiftCoin = *req.GiftCoin
}
if req.Sort != nil {
doc.Sort = *req.Sort
}
if req.Status != nil {
doc.Status = *req.Status
}
if req.Remark != nil {
doc.Remark = strings.TrimSpace(*req.Remark)
}
}
// buildSubTitle 根据套餐类型和核心配置生成展示副标题,不由后台请求手动传入。
func buildSubTitle(doc *rechargepackagemod.RechargePackage) string {
switch doc.PackageType {
case rechargepackagemod.PackageTypeVip:
return fmt.Sprintf("%d × 24 小时会员", doc.DurationDays)
case rechargepackagemod.PackageTypeCoin:
if doc.GiftCoin > 0 {
return fmt.Sprintf("基础%d + 赠送%d", doc.BaseCoin, doc.GiftCoin)
}
return fmt.Sprintf("基础%d", doc.BaseCoin)
default:
return ""
}
}
// rechargePackageToWebItem 转换后台列表展示项,并补充派生字段。
func rechargePackageToWebItem(item *rechargepackagemod.RechargePackage) *RechargePackageItem {
return &RechargePackageItem{
Id: item.Id,
PackageType: item.PackageType,
Name: item.Name,
SubTitle: item.SubTitle,
Price: item.Price,
OriginalPrice: item.OriginalPrice,
DurationDays: item.DurationDays,
DurationHours: item.DurationDays * 24,
BaseCoin: item.BaseCoin,
GiftCoin: item.GiftCoin,
TotalCoin: item.BaseCoin + item.GiftCoin,
Sort: item.Sort,
Status: item.Status,
Remark: item.Remark,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}
// dedupePositiveInt64Ids 去重并过滤非法ID,批量操作只处理正整数ID。
func dedupePositiveInt64Ids(ids []int64) []int64 {
result := make([]int64, 0, len(ids))
seen := make(map[int64]struct{}, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}