215 lines
6.8 KiB
Go
215 lines
6.8 KiB
Go
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
|
|
}
|