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 }