Files
huangguo_server/models/v/txnmod/fund_ledger.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

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
}