@@ -0,0 +1,214 @@
|
||||
package txnmod
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"91porn-server/models"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const ledgerQueryTimeout = 5 * time.Second
|
||||
const maxLedgerWindow int64 = 100000
|
||||
|
||||
var ledgerSort = bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
|
||||
// Fund records have no global time index. Only a concrete user's ledger can
|
||||
// include them without introducing a collection scan into the admin listing.
|
||||
func hasLedgerUID(filter bson.M) bool {
|
||||
switch uid := filter["uid"].(type) {
|
||||
case uint64:
|
||||
return uid > 0
|
||||
case int64:
|
||||
return uid > 0
|
||||
case int:
|
||||
return uid > 0
|
||||
case int32:
|
||||
return uid > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Normalize at read time: no historical transaction or wallet is rewritten.
|
||||
// The indexable user/time match precedes projection and derived-field filters.
|
||||
func fundLedgerPipeline(filter bson.M, ordered bool) []bson.M {
|
||||
if !hasLedgerUID(filter) {
|
||||
return nil
|
||||
}
|
||||
if kind, ok := filter["tranType"].(string); ok && kind != AiGirlfriendTransferIn.Key() && kind != AiGirlfriendTransferOut.Key() {
|
||||
return nil
|
||||
}
|
||||
match := bson.M{"uid": filter["uid"], "category": 0, "fundType": bson.M{"$in": []int{1, 2}}}
|
||||
if period, ok := filter["createdAt"]; ok {
|
||||
match["createdAt"] = period
|
||||
}
|
||||
in := bson.M{"$eq": bson.A{"$fundType", 1}}
|
||||
amount := bson.M{"$cond": bson.A{in, bson.M{"$multiply": bson.A{"$amount", -1}}, "$amount"}}
|
||||
kind := bson.M{"$cond": bson.A{in, AiGirlfriendTransferIn.Key(), AiGirlfriendTransferOut.Key()}}
|
||||
pipeline := []bson.M{{"$match": match}}
|
||||
if ordered {
|
||||
pipeline = append(pipeline, bson.M{"$sort": ledgerSort})
|
||||
}
|
||||
pipeline = append(pipeline,
|
||||
bson.M{"$project": bson.M{
|
||||
"_id": 1, "uid": 1, "createdAt": 1,
|
||||
"purchaseOrder": "$_id", "amount": amount,
|
||||
"actualAmount": bson.M{"$toDouble": amount},
|
||||
"integral": bson.M{"$literal": 0},
|
||||
"tranType": kind,
|
||||
"tranTypeInt": bson.M{"$cond": bson.A{in, AiGirlfriendTransferIn, AiGirlfriendTransferOut}},
|
||||
"desc": bson.M{"$ifNull": bson.A{"$desc", kind}},
|
||||
"realAmount": bson.M{"$toDecimal": "$balance"},
|
||||
}},
|
||||
bson.M{"$match": filter},
|
||||
)
|
||||
return pipeline
|
||||
}
|
||||
|
||||
func mergeLedgerRows(ordinary, transfers []*TransactionLog, skip, limit int64) []*TransactionLog {
|
||||
rows := append(ordinary, transfers...)
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
if !rows[i].CreatedAt.Equal(rows[j].CreatedAt) {
|
||||
return rows[i].CreatedAt.After(rows[j].CreatedAt)
|
||||
}
|
||||
return bytes.Compare(rows[i].ID[:], rows[j].ID[:]) > 0
|
||||
})
|
||||
if skip >= int64(len(rows)) {
|
||||
return []*TransactionLog{}
|
||||
}
|
||||
end := int64(len(rows))
|
||||
if limit > 0 && skip+limit < end {
|
||||
end = skip + limit
|
||||
}
|
||||
return rows[skip:end]
|
||||
}
|
||||
|
||||
// Read only the prefix needed from each source, then paginate the merged order.
|
||||
// An explicit window and server timeout bound old monthly and export requests.
|
||||
func findLedgerRows(filter bson.M, skip, limit int64) ([]*TransactionLog, error) {
|
||||
if skip < 0 || limit < 0 || skip > maxLedgerWindow || limit > maxLedgerWindow || (limit > 0 && skip > maxLedgerWindow-limit) {
|
||||
return nil, errors.New("ledger query window is too large; narrow the time range")
|
||||
}
|
||||
window := skip + limit
|
||||
if limit == 0 {
|
||||
window = maxLedgerWindow + 1
|
||||
}
|
||||
rows := []*TransactionLog{}
|
||||
pipeline := fundLedgerPipeline(filter, true)
|
||||
opt := options.Find().SetSort(ledgerSort).SetMaxTime(ledgerQueryTimeout)
|
||||
if !hasLedgerUID(filter) {
|
||||
opt.SetSort(bson.D{{Key: "createdAt", Value: -1}})
|
||||
}
|
||||
if pipeline == nil && limit > 0 {
|
||||
opt.SetSkip(skip).SetLimit(limit)
|
||||
} else {
|
||||
opt.SetLimit(window)
|
||||
}
|
||||
if err := coll(nil).Find(&rows, filter, opt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pipeline == nil && limit > 0 {
|
||||
return rows, nil
|
||||
}
|
||||
transfers := []*TransactionLog{}
|
||||
if pipeline != nil {
|
||||
pipeline = append(pipeline, bson.M{"$limit": window})
|
||||
if err := mdb.Coll(models.FundTransferLog).Aggregate(&transfers, pipeline, options.Aggregate().SetMaxTime(ledgerQueryTimeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if limit == 0 && int64(len(rows)+len(transfers)) > maxLedgerWindow {
|
||||
return nil, errors.New("ledger query window is too large; narrow the time range")
|
||||
}
|
||||
return mergeLedgerRows(rows, transfers, skip, limit), nil
|
||||
}
|
||||
|
||||
func countLedgerRows(filter bson.M) (int64, error) {
|
||||
count, err := coll(nil).Count(filter, options.Count().SetMaxTime(ledgerQueryTimeout))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
pipeline := fundLedgerPipeline(filter, false)
|
||||
if pipeline == nil {
|
||||
return count, nil
|
||||
}
|
||||
var result []struct {
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
pipeline = append(pipeline, bson.M{"$count": "count"})
|
||||
if err := mdb.Coll(models.FundTransferLog).Aggregate(&result, pipeline, options.Aggregate().SetMaxTime(ledgerQueryTimeout)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(result) > 0 {
|
||||
count += result[0].Count
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func getLedgerCoinLogs(page, size uint64, filter bson.M) ([]*TransactionLog, uint64, int64, error) {
|
||||
if size == 0 || size > uint64(maxLedgerWindow) {
|
||||
return nil, 0, 0, errors.New("invalid ledger page size")
|
||||
}
|
||||
total, err := countLedgerRows(filter)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
pages := (uint64(total) + size - 1) / size
|
||||
if pages == 0 {
|
||||
return []*TransactionLog{}, 0, 0, nil
|
||||
}
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if page > pages {
|
||||
page = pages
|
||||
}
|
||||
if page-1 > uint64(maxLedgerWindow)/size {
|
||||
return nil, pages, total, errors.New("ledger query window is too large; narrow the time range")
|
||||
}
|
||||
rows, err := findLedgerRows(filter, int64((page-1)*size), int64(size))
|
||||
return rows, pages, total, err
|
||||
}
|
||||
|
||||
func FindLedgerByTime(uid uint64, start, end time.Time) ([]*TransactionLog, error) {
|
||||
return findLedgerRows(bson.M{"uid": uid, "createdAt": bson.M{"$gte": start, "$lt": end}}, 0, 0)
|
||||
}
|
||||
|
||||
func LastLedgerTime(uid uint64, end time.Time) (TransactionLog, error) {
|
||||
rows, err := findLedgerRows(bson.M{"uid": uid, "createdAt": bson.M{"$lt": end}}, 0, 1)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return TransactionLog{}, err
|
||||
}
|
||||
return *rows[0], nil
|
||||
}
|
||||
|
||||
// These are wallet inflow/outflow totals, not recharge, creator or agent revenue.
|
||||
func sumLedgerAmount(uid uint64, start, end time.Time, income bool) (float64, error) {
|
||||
op := "$lt"
|
||||
if income {
|
||||
op = "$gt"
|
||||
}
|
||||
filter := bson.M{"uid": uid, "createdAt": bson.M{"$gte": start, "$lt": end}, "actualAmount": bson.M{op: 0}}
|
||||
group := bson.M{"$group": bson.M{"_id": nil, "total": bson.M{"$sum": "$actualAmount"}}}
|
||||
pipelines := [][]bson.M{{{"$match": filter}, group}, append(fundLedgerPipeline(filter, false), group)}
|
||||
var total float64
|
||||
for i, name := range []string{table, models.FundTransferLog} {
|
||||
var result []struct {
|
||||
Total float64 `bson:"total"`
|
||||
}
|
||||
if i == 1 && !hasLedgerUID(filter) {
|
||||
continue
|
||||
}
|
||||
if err := mdb.Coll(name).Aggregate(&result, pipelines[i], options.Aggregate().SetMaxTime(ledgerQueryTimeout)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(result) > 0 {
|
||||
total += result[0].Total
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
Reference in New Issue
Block a user