init: 初始化 web_hgdjb_server 项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 10:47:32 +08:00
co-authored by Claude Opus 4.7
commit f13d33af4d
707 changed files with 147705 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
package srv_rechargepackage
import (
"time"
"videoweb/pub/models/rechargepackagemod"
"videoweb/pub/pubdef"
)
// RechargePackageListReq Web后台购买套餐分页列表请求
type RechargePackageListReq struct {
pubdef.Page
Id *int64 `json:"id"` // 套餐ID
PackageType *rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
Status *rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
Name string `json:"name"` // 展示标题,模糊搜索
}
// RechargePackageListResp Web后台购买套餐分页列表响应
type RechargePackageListResp struct {
Total int64 `json:"total"` // 总数量
List []*RechargePackageItem `json:"list"` // 当前页列表
}
// VipPackageOption 会员畅享下拉列表项。
type VipPackageOption struct {
Id int64 `json:"id"` // 套餐ID
Name string `json:"name"` // 套餐名称
}
// RechargePackageItem Web后台购买套餐列表项
type RechargePackageItem struct {
Id int64 `json:"id"` // 套餐ID
PackageType rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
Name string `json:"name"` // 展示标题
SubTitle string `json:"subTitle"` // 展示副标题
Price int64 `json:"price"` // 售价,单位分
OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
DurationDays int64 `json:"durationDays"` // 会员有效天数
DurationHours int64 `json:"durationHours"` // 会员有效小时数,由durationDays计算
BaseCoin int64 `json:"baseCoin"` // 基础金币数
GiftCoin int64 `json:"giftCoin"` // 赠送金币数
TotalCoin int64 `json:"totalCoin"` // 到账总金币数,由baseCoin+giftCoin计算
Sort int32 `json:"sort"` // 排序,越小越靠前
Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
Remark string `json:"remark"` // 后台备注
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
}
// RechargePackageAddReq Web后台新增购买套餐请求
type RechargePackageAddReq struct {
PackageType rechargepackagemod.PackageType `json:"packageType" binding:"required"` // 套餐类型:1-会员畅享 2-金币充值
Name string `json:"name" binding:"required"` // 展示标题
Price int64 `json:"price" binding:"required"` // 售价,单位分
OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
DurationDays int64 `json:"durationDays"` // 会员有效天数,金币套餐传0
BaseCoin int64 `json:"baseCoin"` // 基础金币数,会员套餐传0
GiftCoin int64 `json:"giftCoin"` // 赠送金币数
Sort int32 `json:"sort"` // 排序,越小越靠前
Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用,默认1
Remark string `json:"remark"` // 后台备注
}
// RechargePackageUpdateReq Web后台更新购买套餐请求
type RechargePackageUpdateReq struct {
Id int64 `json:"id" binding:"required"` // 套餐ID
PackageType *rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
Name *string `json:"name"` // 展示标题
Price *int64 `json:"price"` // 售价,单位分
OriginalPrice *int64 `json:"originalPrice"` // 原价,单位分
DurationDays *int64 `json:"durationDays"` // 会员有效天数
BaseCoin *int64 `json:"baseCoin"` // 基础金币数
GiftCoin *int64 `json:"giftCoin"` // 赠送金币数
Sort *int32 `json:"sort"` // 排序
Status *rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
Remark *string `json:"remark"` // 后台备注
}
// RechargePackageDelReq Web后台删除购买套餐请求。
type RechargePackageDelReq struct {
Id int64 `json:"id" binding:"required"` // 套餐ID
}
// RechargePackageStatusSetReq Web后台单/批量设置购买套餐状态请求。
type RechargePackageStatusSetReq struct {
Ids []int64 `json:"ids"` // 套餐ID集合
Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
}
+343
View File
@@ -0,0 +1,343 @@
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
}