Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package srv_viporder
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VipOrderReq struct {
|
||||
pubdef.Page
|
||||
UserId *int64 `json:"userId"` // 用户ID
|
||||
OrderType *vipordermod.OrderType `json:"orderType"` // 订单类型:1会员 2金币
|
||||
Status *pubdef.VipOrderStatus `json:"status"` // 订单状态
|
||||
TradeNo *string `json:"tradeNo"` // 订单号
|
||||
DistrictCode string `json:"districtCode"` // 渠道码
|
||||
StartTime *string `json:"startTime"` // 创建时间-开始
|
||||
EndTime *string `json:"endTime"` // 创建时间-结束
|
||||
}
|
||||
|
||||
type VipOrderResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List vipordermod.VipOrderList `json:"list"`
|
||||
}
|
||||
|
||||
type SyncDistrictCodeReq struct {
|
||||
MaxSize int64 `json:"maxSize"` // 本次最多扫描的空渠道码订单数,默认500,最大2000
|
||||
}
|
||||
|
||||
type SyncDistrictCodeResp struct {
|
||||
Scanned int64 `json:"scanned"` // 本次扫描的空渠道码订单数
|
||||
OrderUpdated int64 `json:"orderUpdated"` // 成功回填的订单数
|
||||
UserUpdated int64 `json:"userUpdated"` // 成功回填的用户数
|
||||
UserSkipped int64 `json:"userSkipped"` // 用户本身没有渠道码
|
||||
HasMore bool `json:"hasMore"` // 是否还有未处理的历史订单
|
||||
}
|
||||
|
||||
type timeLayout = string
|
||||
|
||||
const layout timeLayout = "2006-01-02 15:04:05"
|
||||
|
||||
func parseTime(s string) (time.Time, bool) {
|
||||
t, err := time.ParseInLocation(layout, s, time.Local)
|
||||
return t, err == nil
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package srv_viporder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetVipOrders 分页查询会员购买记录(固定 OrderType=会员)
|
||||
func GetVipOrders(ctx context.Context, req *VipOrderReq) (*VipOrderResp, *srvdef.SrvErr) {
|
||||
orderType := vipordermod.OrderTypeVip
|
||||
reqCopy := *req
|
||||
reqCopy.OrderType = &orderType
|
||||
return GetOrders(ctx, &reqCopy)
|
||||
}
|
||||
|
||||
// GetOrders 分页查询充值订单,支持按 OrderType 筛选(不传则查全部类型)
|
||||
func GetOrders(ctx context.Context, req *VipOrderReq) (*VipOrderResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.OrderType != nil {
|
||||
filter[vipordermod.FN.VipOrder.OrderType] = *req.OrderType
|
||||
}
|
||||
if req.UserId != nil && *req.UserId > 0 {
|
||||
filter[vipordermod.FN.VipOrder.UserId] = *req.UserId
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[vipordermod.FN.VipOrder.Status] = *req.Status
|
||||
}
|
||||
if req.TradeNo != nil && *req.TradeNo != "" {
|
||||
filter[vipordermod.FN.VipOrder.TradeNo] = *req.TradeNo
|
||||
}
|
||||
if districtCode := strings.TrimSpace(req.DistrictCode); districtCode != "" {
|
||||
filter[vipordermod.FN.VipOrder.DistrictCode] = districtCode
|
||||
}
|
||||
|
||||
timeFilter := bson.M{}
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if t, ok := parseTime(*req.StartTime); ok {
|
||||
timeFilter["$gte"] = t
|
||||
}
|
||||
}
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if t, ok := parseTime(*req.EndTime); ok {
|
||||
timeFilter["$lte"] = t
|
||||
}
|
||||
}
|
||||
if len(timeFilter) > 0 {
|
||||
filter[vipordermod.FN.VipOrder.CreatedAt] = timeFilter
|
||||
}
|
||||
|
||||
total, serr := vipordermod.CountVipOrder(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp := &VipOrderResp{Total: total}
|
||||
if total == 0 {
|
||||
resp.List = vipordermod.VipOrderList{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
opt := options.Find().
|
||||
SetSort(bson.D{{Key: vipordermod.FN.VipOrder.CreatedAt, Value: -1}}).
|
||||
SetSkip((req.PageNum - 1) * req.PageSize).
|
||||
SetLimit(req.PageSize)
|
||||
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
const (
|
||||
syncDistrictDefaultSize = int64(500)
|
||||
syncDistrictMaxSize = int64(2000)
|
||||
syncDistrictBatchSize = int64(200)
|
||||
)
|
||||
|
||||
// SyncDistrictCode 将历史订单中空的渠道码,按用户当前 districtCode 回填。
|
||||
func SyncDistrictCode(ctx context.Context, req *SyncDistrictCodeReq) (*SyncDistrictCodeResp, *srvdef.SrvErr) {
|
||||
maxSize := syncDistrictDefaultSize
|
||||
if req != nil && req.MaxSize > 0 {
|
||||
maxSize = req.MaxSize
|
||||
}
|
||||
if maxSize > syncDistrictMaxSize {
|
||||
maxSize = syncDistrictMaxSize
|
||||
}
|
||||
|
||||
resp := &SyncDistrictCodeResp{}
|
||||
lastId := int64(0)
|
||||
updatedUsers := make(map[int64]struct{})
|
||||
skippedUsers := make(map[int64]struct{})
|
||||
|
||||
for resp.Scanned < maxSize {
|
||||
limit := syncDistrictBatchSize
|
||||
if remain := maxSize - resp.Scanned; remain < limit {
|
||||
limit = remain
|
||||
}
|
||||
filter := emptyDistrictSyncFilter(lastId, skippedUsers)
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, options.Find().
|
||||
SetProjection(bson.D{
|
||||
{Key: vipordermod.FN.VipOrder.Id, Value: 1},
|
||||
{Key: vipordermod.FN.VipOrder.UserId, Value: 1},
|
||||
}).
|
||||
SetSort(bson.D{{Key: vipordermod.FN.VipOrder.Id, Value: 1}}).
|
||||
SetLimit(limit))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
resp.Scanned += int64(len(list))
|
||||
lastId = list[len(list)-1].Id
|
||||
if int64(len(list)) == limit && resp.Scanned >= maxSize {
|
||||
resp.HasMore = true
|
||||
}
|
||||
|
||||
userIds := make([]int64, 0, len(list))
|
||||
seen := make(map[int64]struct{}, len(list))
|
||||
for _, order := range list {
|
||||
if order == nil || order.UserId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[order.UserId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[order.UserId] = struct{}{}
|
||||
userIds = append(userIds, order.UserId)
|
||||
}
|
||||
if len(userIds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
users, serr := usermod.FindManyUser(ctx, bson.M{
|
||||
usermod.FN.User.Id: bson.M{"$in": userIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: usermod.FN.User.Id, Value: 1},
|
||||
{Key: usermod.FN.User.DistrictCode, Value: 1},
|
||||
}))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
userMap := make(map[int64]*usermod.User, len(users))
|
||||
for _, user := range users {
|
||||
if user != nil {
|
||||
userMap[user.Id] = user
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, userId := range userIds {
|
||||
user := userMap[userId]
|
||||
code := ""
|
||||
if user != nil {
|
||||
code = strings.TrimSpace(user.DistrictCode)
|
||||
}
|
||||
if code == "" {
|
||||
skippedUsers[userId] = struct{}{}
|
||||
continue
|
||||
}
|
||||
modified, serr := vipordermod.UpdateManyVipOrder(ctx,
|
||||
mergeEmptyDistrictFilter(bson.M{vipordermod.FN.VipOrder.UserId: userId}),
|
||||
bson.M{"$set": bson.M{
|
||||
vipordermod.FN.VipOrder.DistrictCode: code,
|
||||
vipordermod.FN.VipOrder.UpdatedAt: now,
|
||||
}},
|
||||
)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if modified > 0 {
|
||||
updatedUsers[userId] = struct{}{}
|
||||
resp.OrderUpdated += modified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.UserUpdated = int64(len(updatedUsers))
|
||||
resp.UserSkipped = int64(len(skippedUsers))
|
||||
if !resp.HasMore {
|
||||
remain, serr := vipordermod.FindManyVipOrder(ctx, emptyDistrictSyncFilter(lastId, skippedUsers),
|
||||
options.Find().SetProjection(bson.D{{Key: vipordermod.FN.VipOrder.Id, Value: 1}}).SetLimit(1))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp.HasMore = len(remain) > 0
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func emptyDistrictSyncFilter(lastId int64, skippedUsers map[int64]struct{}) bson.M {
|
||||
conds := []bson.M{vipordermod.EmptyDistrictFilter()}
|
||||
if lastId > 0 {
|
||||
conds = append(conds, bson.M{vipordermod.FN.VipOrder.Id: bson.M{"$gt": lastId}})
|
||||
}
|
||||
if skipIds := mapKeys(skippedUsers); len(skipIds) > 0 {
|
||||
conds = append(conds, bson.M{vipordermod.FN.VipOrder.UserId: bson.M{"$nin": skipIds}})
|
||||
}
|
||||
if len(conds) == 1 {
|
||||
return conds[0]
|
||||
}
|
||||
return bson.M{"$and": conds}
|
||||
}
|
||||
|
||||
func mergeEmptyDistrictFilter(extra bson.M) bson.M {
|
||||
filter := vipordermod.EmptyDistrictFilter()
|
||||
if len(extra) == 0 {
|
||||
return filter
|
||||
}
|
||||
conds := []bson.M{filter}
|
||||
for key, value := range extra {
|
||||
conds = append(conds, bson.M{key: value})
|
||||
}
|
||||
return bson.M{"$and": conds}
|
||||
}
|
||||
|
||||
func mapKeys(m map[int64]struct{}) []int64 {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]int64, 0, len(m))
|
||||
for id := range m {
|
||||
keys = append(keys, id)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package srv_viporder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// RechargeSummaryReq 充值汇总请求,时间按北京时间解析。
|
||||
type RechargeSummaryReq struct {
|
||||
StartTime string `form:"startTime" json:"startTime"` // 开始时间,北京时间:2006-01-02 / 2006-01-02 15:04:05 / RFC3339
|
||||
EndTime string `form:"endTime" json:"endTime"` // 结束时间,北京时间:2006-01-02 / 2006-01-02 15:04:05 / RFC3339
|
||||
}
|
||||
|
||||
// RechargeSummaryResp 充值汇总结果,金额单位为分。
|
||||
type RechargeSummaryResp struct {
|
||||
StartTime string `json:"startTime"` // 实际统计开始时间(北京时间)
|
||||
EndTime string `json:"endTime"` // 实际统计结束时间(北京时间)
|
||||
OrderCount int64 `json:"orderCount"` // 成功订单数
|
||||
TotalAmount int64 `json:"totalAmount"` // 成功订单金额合计(分)
|
||||
VipOrderCount int64 `json:"vipOrderCount"` // 会员成功订单数
|
||||
VipAmount int64 `json:"vipAmount"` // 会员成功订单金额(分)
|
||||
CoinOrderCount int64 `json:"coinOrderCount"` // 金币成功订单数
|
||||
CoinAmount int64 `json:"coinAmount"` // 金币成功订单金额(分)
|
||||
UserCount int64 `json:"userCount"` // 成功充值用户数(去重)
|
||||
}
|
||||
|
||||
type rechargeSummaryAgg struct {
|
||||
OrderCount int64 `bson:"orderCount"`
|
||||
TotalAmount int64 `bson:"totalAmount"`
|
||||
VipOrderCount int64 `bson:"vipOrderCount"`
|
||||
VipAmount int64 `bson:"vipAmount"`
|
||||
CoinOrderCount int64 `bson:"coinOrderCount"`
|
||||
CoinAmount int64 `bson:"coinAmount"`
|
||||
UserCount int64 `bson:"userCount"`
|
||||
}
|
||||
|
||||
// GetRechargeSummary 按支付完成时间汇总成功充值订单。
|
||||
func GetRechargeSummary(ctx context.Context, req *RechargeSummaryReq) (*RechargeSummaryResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空")
|
||||
}
|
||||
start, end, err := parseBeijingTimeRange(req.StartTime, req.EndTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &RechargeSummaryResp{
|
||||
StartTime: start.In(utils.LocBJ).Format(pubdef.TimeISO),
|
||||
EndTime: end.In(utils.LocBJ).Format(pubdef.TimeISO),
|
||||
}
|
||||
list := make([]rechargeSummaryAgg, 0)
|
||||
if serr := vipordermod.AggregateVipOrder(ctx, &list, rechargeSummaryPipeline(start, end)); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
item := list[0]
|
||||
resp.OrderCount = item.OrderCount
|
||||
resp.TotalAmount = item.TotalAmount
|
||||
resp.VipOrderCount = item.VipOrderCount
|
||||
resp.VipAmount = item.VipAmount
|
||||
resp.CoinOrderCount = item.CoinOrderCount
|
||||
resp.CoinAmount = item.CoinAmount
|
||||
resp.UserCount = item.UserCount
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func rechargeSummaryPipeline(start, end time.Time) []bson.M {
|
||||
vipType := bson.M{"$eq": bson.A{bson.M{"$ifNull": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeVip}}, vipordermod.OrderTypeVip}}
|
||||
return []bson.M{
|
||||
{
|
||||
"$match": bson.M{
|
||||
vipordermod.FN.VipOrder.Status: pubdef.VipOrderSuccess,
|
||||
vipordermod.FN.VipOrder.FinishedAt: bson.M{
|
||||
"$gte": start,
|
||||
"$lte": end,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$group": bson.M{
|
||||
"_id": nil,
|
||||
"orderCount": bson.M{"$sum": 1},
|
||||
"totalAmount": bson.M{"$sum": "$" + vipordermod.FN.VipOrder.Amount},
|
||||
"vipOrderCount": bson.M{"$sum": bson.M{"$cond": bson.A{vipType, 1, 0}}},
|
||||
"vipAmount": bson.M{"$sum": bson.M{"$cond": bson.A{
|
||||
vipType,
|
||||
"$" + vipordermod.FN.VipOrder.Amount,
|
||||
0,
|
||||
}}},
|
||||
"coinOrderCount": bson.M{"$sum": bson.M{"$cond": bson.A{
|
||||
bson.M{"$eq": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeCoin}},
|
||||
1,
|
||||
0,
|
||||
}}},
|
||||
"coinAmount": bson.M{"$sum": bson.M{"$cond": bson.A{
|
||||
bson.M{"$eq": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeCoin}},
|
||||
"$" + vipordermod.FN.VipOrder.Amount,
|
||||
0,
|
||||
}}},
|
||||
"userIds": bson.M{"$addToSet": "$" + vipordermod.FN.VipOrder.UserId},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$project": bson.M{
|
||||
"_id": 0,
|
||||
"orderCount": 1,
|
||||
"totalAmount": 1,
|
||||
"vipOrderCount": 1,
|
||||
"vipAmount": 1,
|
||||
"coinOrderCount": 1,
|
||||
"coinAmount": 1,
|
||||
"userCount": bson.M{"$size": "$userIds"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func parseBeijingTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) {
|
||||
start, ok := parseBeijingTime(startRaw, true)
|
||||
if !ok {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间无效")
|
||||
}
|
||||
end, ok := parseBeijingTime(endRaw, false)
|
||||
if !ok {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间无效")
|
||||
}
|
||||
if end.Before(start) {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间不能早于开始时间")
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func parseBeijingTime(raw string, isStart bool) (time.Time, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
layouts := []string{
|
||||
pubdef.TimeISO,
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
"2006-01-02 15:04:05 -0700 MST",
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, utils.LocBJ); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
if t, err := time.ParseInLocation(pubdef.DateISO, raw, utils.LocBJ); err == nil {
|
||||
if isStart {
|
||||
return utils.BeginOfTime(t), true
|
||||
}
|
||||
return utils.EndOfTime(t), true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
Reference in New Issue
Block a user