Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
344 lines
11 KiB
Go
344 lines
11 KiB
Go
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
|
||
}
|