更新修复一个显示异常
This commit is contained in:
parent
36e6adb803
commit
0d12143572
|
|
@ -770,7 +770,7 @@ export class GameConfig {
|
|||
vibrateOpen: true, //震动
|
||||
coinnum: 0, //每局的金币数
|
||||
paid_user: false, //是否是付费用户
|
||||
version: "1.0.74", //版本号
|
||||
version: "1.0.75", //版本号
|
||||
shushu_DistinctId: "", //数数访客ID
|
||||
shushu_AccountId: "", //数数账号ID
|
||||
uid: "", //用户和后端唯一id
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -27,14 +27,14 @@
|
|||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 85
|
||||
"__id__": 84
|
||||
},
|
||||
{
|
||||
"__id__": 86
|
||||
"__id__": 85
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 87
|
||||
"__id__": 86
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -254,7 +254,7 @@
|
|||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": {
|
||||
"__id__": 84
|
||||
"__id__": 83
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -734,28 +734,28 @@
|
|||
"__id__": 33
|
||||
},
|
||||
{
|
||||
"__id__": 39
|
||||
"__id__": 38
|
||||
},
|
||||
{
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
},
|
||||
{
|
||||
"__id__": 66
|
||||
"__id__": 65
|
||||
},
|
||||
{
|
||||
"__id__": 69
|
||||
"__id__": 68
|
||||
},
|
||||
{
|
||||
"__id__": 77
|
||||
"__id__": 76
|
||||
},
|
||||
{
|
||||
"__id__": 80
|
||||
"__id__": 79
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": {
|
||||
"__id__": 83
|
||||
"__id__": 82
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -1198,8 +1198,8 @@
|
|||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
49.16,
|
||||
-10,
|
||||
41.986,
|
||||
0.667,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
|
@ -1292,13 +1292,10 @@
|
|||
},
|
||||
{
|
||||
"__id__": 35
|
||||
},
|
||||
{
|
||||
"__id__": 36
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 38
|
||||
"__id__": 37
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -1379,22 +1376,6 @@
|
|||
"_atlas": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "6624fhcuF5DF67gzSViWhRV",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 33
|
||||
},
|
||||
"_enabled": true,
|
||||
"heath": null,
|
||||
"heatht": null,
|
||||
"switchNode": null,
|
||||
"timeNode": null,
|
||||
"switchButtons": [],
|
||||
"coin": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Button",
|
||||
"_name": "",
|
||||
|
|
@ -1409,7 +1390,7 @@
|
|||
"zoomScale": 1.2,
|
||||
"clickEvents": [
|
||||
{
|
||||
"__id__": 37
|
||||
"__id__": 36
|
||||
}
|
||||
],
|
||||
"_N$interactable": true,
|
||||
|
|
@ -1499,14 +1480,14 @@
|
|||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 40
|
||||
"__id__": 39
|
||||
},
|
||||
{
|
||||
"__id__": 41
|
||||
"__id__": 40
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 43
|
||||
"__id__": 42
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -1560,7 +1541,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 39
|
||||
"__id__": 38
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -1592,7 +1573,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 39
|
||||
"__id__": 38
|
||||
},
|
||||
"_enabled": true,
|
||||
"_normalMaterial": null,
|
||||
|
|
@ -1601,7 +1582,7 @@
|
|||
"zoomScale": 1.1,
|
||||
"clickEvents": [
|
||||
{
|
||||
"__id__": 42
|
||||
"__id__": 41
|
||||
}
|
||||
],
|
||||
"_N$interactable": true,
|
||||
|
|
@ -1657,7 +1638,7 @@
|
|||
"hoverSprite": null,
|
||||
"_N$disabledSprite": null,
|
||||
"_N$target": {
|
||||
"__id__": 39
|
||||
"__id__": 38
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
|
|
@ -1691,23 +1672,23 @@
|
|||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 45
|
||||
"__id__": 44
|
||||
},
|
||||
{
|
||||
"__id__": 59
|
||||
"__id__": 58
|
||||
}
|
||||
],
|
||||
"_active": false,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 62
|
||||
"__id__": 61
|
||||
},
|
||||
{
|
||||
"__id__": 63
|
||||
"__id__": 62
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 65
|
||||
"__id__": 64
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -1761,26 +1742,26 @@
|
|||
"_name": "coinh",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 46
|
||||
"__id__": 45
|
||||
},
|
||||
{
|
||||
"__id__": 49
|
||||
"__id__": 48
|
||||
},
|
||||
{
|
||||
"__id__": 52
|
||||
"__id__": 51
|
||||
},
|
||||
{
|
||||
"__id__": 55
|
||||
"__id__": 54
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": {
|
||||
"__id__": 58
|
||||
"__id__": 57
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -1804,15 +1785,15 @@
|
|||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
183.311,
|
||||
207.103,
|
||||
5.847,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0.8,
|
||||
0.8,
|
||||
1
|
||||
]
|
||||
},
|
||||
|
|
@ -1834,17 +1815,17 @@
|
|||
"_name": "button_1",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 45
|
||||
"__id__": 44
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 47
|
||||
"__id__": 46
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 48
|
||||
"__id__": 47
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -1898,7 +1879,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 46
|
||||
"__id__": 45
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -1943,17 +1924,17 @@
|
|||
"_name": "button_0",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 45
|
||||
"__id__": 44
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 50
|
||||
"__id__": 49
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 51
|
||||
"__id__": 50
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2007,7 +1988,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 49
|
||||
"__id__": 48
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2052,17 +2033,17 @@
|
|||
"_name": "button_0",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 45
|
||||
"__id__": 44
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 53
|
||||
"__id__": 52
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 54
|
||||
"__id__": 53
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2116,7 +2097,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 52
|
||||
"__id__": 51
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2161,17 +2142,17 @@
|
|||
"_name": "button_0",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 45
|
||||
"__id__": 44
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 56
|
||||
"__id__": 55
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 57
|
||||
"__id__": 56
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2225,7 +2206,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 55
|
||||
"__id__": 54
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2281,17 +2262,17 @@
|
|||
"_name": "tilibuman",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 60
|
||||
"__id__": 59
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 61
|
||||
"__id__": 60
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2345,7 +2326,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 59
|
||||
"__id__": 58
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2390,7 +2371,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2424,7 +2405,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
},
|
||||
"_enabled": true,
|
||||
"_normalMaterial": null,
|
||||
|
|
@ -2433,7 +2414,7 @@
|
|||
"zoomScale": 1.1,
|
||||
"clickEvents": [
|
||||
{
|
||||
"__id__": 64
|
||||
"__id__": 63
|
||||
}
|
||||
],
|
||||
"_N$interactable": true,
|
||||
|
|
@ -2489,7 +2470,7 @@
|
|||
"hoverSprite": null,
|
||||
"_N$disabledSprite": null,
|
||||
"_N$target": {
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
|
|
@ -2525,11 +2506,11 @@
|
|||
"_active": false,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 67
|
||||
"__id__": 66
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 68
|
||||
"__id__": 67
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2583,7 +2564,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 66
|
||||
"__id__": 65
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2632,20 +2613,20 @@
|
|||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 70
|
||||
"__id__": 69
|
||||
},
|
||||
{
|
||||
"__id__": 73
|
||||
"__id__": 72
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 75
|
||||
"__id__": 74
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 76
|
||||
"__id__": 75
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2699,17 +2680,17 @@
|
|||
"_name": "tili_x",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 69
|
||||
"__id__": 68
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 71
|
||||
"__id__": 70
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 72
|
||||
"__id__": 71
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2763,7 +2744,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 70
|
||||
"__id__": 69
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2808,13 +2789,13 @@
|
|||
"_name": "huifuyidian",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 69
|
||||
"__id__": 68
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [],
|
||||
"_prefab": {
|
||||
"__id__": 74
|
||||
"__id__": 73
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2879,7 +2860,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 69
|
||||
"__id__": 68
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -2930,11 +2911,11 @@
|
|||
"_active": false,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 78
|
||||
"__id__": 77
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 79
|
||||
"__id__": 78
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -2988,7 +2969,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 77
|
||||
"__id__": 76
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -3039,11 +3020,11 @@
|
|||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 81
|
||||
"__id__": 80
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 82
|
||||
"__id__": 81
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
|
|
@ -3097,7 +3078,7 @@
|
|||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 80
|
||||
"__id__": 79
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
|
|
@ -3202,25 +3183,25 @@
|
|||
},
|
||||
"switchNode": [
|
||||
{
|
||||
"__id__": 66
|
||||
"__id__": 65
|
||||
},
|
||||
{
|
||||
"__id__": 69
|
||||
"__id__": 68
|
||||
}
|
||||
],
|
||||
"timeNode": {
|
||||
"__id__": 73
|
||||
"__id__": 72
|
||||
},
|
||||
"switchButtons": [
|
||||
{
|
||||
"__id__": 39
|
||||
"__id__": 38
|
||||
},
|
||||
{
|
||||
"__id__": 44
|
||||
"__id__": 43
|
||||
}
|
||||
],
|
||||
"coin": {
|
||||
"__id__": 45
|
||||
"__id__": 44
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import cloud from '@lafjs/cloud';
|
||||
import { buildPaymentProfile, paymentVipLevel, PAYMENT_PROFILE_RULE } from '@/paymentProfile';
|
||||
import { buildPaymentProfile, paymentVipLevel } from '@/paymentProfile';
|
||||
import type { ProfileOrder } from '@/paymentProfile';
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
|
@ -10,7 +10,7 @@ const userKey = (id: any) => `${typeof id}:${String(id)}`;
|
|||
|
||||
function emptyStats(asOf: number) {
|
||||
return {
|
||||
version: 8,
|
||||
version: 9,
|
||||
currency: 'CNY',
|
||||
unit: 'fen',
|
||||
asOf,
|
||||
|
|
@ -61,6 +61,7 @@ export default async function () {
|
|||
let invalidProfiles = 0;
|
||||
let scannedUsers = 0;
|
||||
let skippedNeverPaidUsers = 0;
|
||||
let removedNeverPaidStats = 0;
|
||||
|
||||
while (true) {
|
||||
const candidates: any[] = await users.find({
|
||||
|
|
@ -70,30 +71,44 @@ export default async function () {
|
|||
// Advance on the identity page, including pages consisting entirely of stable VIP0 users.
|
||||
afterId = candidates[candidates.length - 1]._id;
|
||||
scannedUsers += candidates.length;
|
||||
const previous: any[] = await rechargeStatsCollection.find({ _id: { $in: candidates.map(user => user._id) } },
|
||||
{ projection: { _id: 1, openid: 1, has_ever_paid: 1, paid_status: 1,
|
||||
data_valid: 1, version: 1, asOf: 1, updatedAt: 1, profile_rule_version: 1, maintenance_identity: 1 } }).toArray();
|
||||
const previousById = new Map(previous.map(row => [userKey(row._id), row]));
|
||||
// VIP0 is a user attribute, not a cached zero-valued statistics document.
|
||||
const batch = candidates.filter(user => {
|
||||
const saved = previousById.get(userKey(user._id));
|
||||
// Only a verified, initialized never-payer can be cached. A missing profile,
|
||||
// ambiguous identity or changed account must go through the order check below.
|
||||
const stableNeverPaid = user.pay_user === false && saved?.version === 8
|
||||
&& saved.data_valid === true && saved.paid_status === 'never' && saved.has_ever_paid === false
|
||||
&& user.vip_level === 0 && user.vip_profile?.data_valid === true
|
||||
&& user.vip_profile.as_of === saved.asOf && user.vip_profile.updated_at === saved.updatedAt
|
||||
&& user.vip_profile.rule_version === PAYMENT_PROFILE_RULE.version
|
||||
&& saved.profile_rule_version === PAYMENT_PROFILE_RULE.version
|
||||
&& saved.openid === user.openid && saved.maintenance_identity?.pay_user === false
|
||||
&& saved.maintenance_identity.register_time === timestamp(user.register_time);
|
||||
const vip = user.vip_profile;
|
||||
const stableNeverPaid = user.pay_user === false && user.vip_level === 0
|
||||
&& vip?.maintenance_version === 9 && vip.data_valid === true && vip.has_ever_paid === false
|
||||
&& vip.identity?.openid === user.openid && vip.identity?.pay_user === false
|
||||
&& vip.identity?.register_time === timestamp(user.register_time);
|
||||
if (stableNeverPaid) skippedNeverPaidUsers++;
|
||||
return !stableNeverPaid;
|
||||
});
|
||||
if (!batch.length) continue;
|
||||
// Read legacy evidence only for users actually needing maintenance/migration.
|
||||
const previous: any[] = await rechargeStatsCollection.find({ _id: { $in: batch.map(user => user._id) } },
|
||||
{
|
||||
projection: {
|
||||
_id: 1, openid: 1, has_ever_paid: 1, paid_status: 1,
|
||||
data_valid: 1, amountTotal: 1, orderCount: 1, maintenance_identity: 1
|
||||
}
|
||||
}).toArray();
|
||||
const previousById = new Map(previous.map(row => [userKey(row._id), row]));
|
||||
const previouslyPaid = new Set([
|
||||
...previous.filter(row => row.has_ever_paid === true),
|
||||
...previous.filter(row => row.has_ever_paid === true || row.paid_status === 'paid' || row.amountTotal > 0 || row.orderCount > 0),
|
||||
...batch.filter(user => user.vip_level > 0 || user.vip_profile?.has_ever_paid === true),
|
||||
].map(row => userKey(row._id)));
|
||||
const directNeverPaid = new Set(batch.filter(user => {
|
||||
const saved = previousById.get(userKey(user._id));
|
||||
const identity = user.vip_profile?.identity;
|
||||
const identityMatches = !identity || (identity.openid === user.openid
|
||||
&& identity.pay_user === user.pay_user && identity.register_time === timestamp(user.register_time));
|
||||
// Trust the explicit server-owned false flag, never a missing flag/profile.
|
||||
// Existing conflicts and account changes still require an order check.
|
||||
return user.pay_user === false && !previouslyPaid.has(userKey(user._id))
|
||||
&& typeof user.openid === 'string' && !!user.openid.trim()
|
||||
&& user.vip_profile?.data_valid !== false && identityMatches
|
||||
&& (!saved || (saved.data_valid === true && saved.paid_status === 'never'
|
||||
&& saved.openid === user.openid && saved.maintenance_identity?.pay_user === false
|
||||
&& saved.maintenance_identity?.register_time === timestamp(user.register_time)));
|
||||
}).map(user => userKey(user._id)));
|
||||
|
||||
const summaries = new Map<string, {
|
||||
stats: ReturnType<typeof emptyStats>;
|
||||
|
|
@ -106,9 +121,12 @@ export default async function () {
|
|||
futureOrders: number;
|
||||
}>();
|
||||
for (const user of batch) {
|
||||
if (directNeverPaid.has(userKey(user._id))) continue;
|
||||
if (typeof user.openid === 'string' && user.openid.trim() && !summaries.has(user.openid)) {
|
||||
summaries.set(user.openid, { stats: emptyStats(asOf), seen: new Map(), fen15d: 0, fen30d: 0, fenTotal: 0,
|
||||
recent: [], lastPaidAt: null, futureOrders: 0 });
|
||||
summaries.set(user.openid, {
|
||||
stats: emptyStats(asOf), seen: new Map(), fen15d: 0, fen30d: 0, fenTotal: 0,
|
||||
recent: [], lastPaidAt: null, futureOrders: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,9 +136,13 @@ export default async function () {
|
|||
state: { $in: [1, 2] },
|
||||
paymentAppEnv: { $ne: 'test' },
|
||||
outTradeNo: { $not: /^wct_/ },
|
||||
}, { projection: { openid: 1, outTradeNo: 1, goodsPrice: 1, itemCount: 1, chargeTime: 1, time: 1,
|
||||
itemid: 1, 'goldMiner.configVersion': 1, 'goldMiner.paidAt': 1, 'goldMiner.confirmedAt': 1,
|
||||
rewardVersion: 1, activityType: 1 } })
|
||||
}, {
|
||||
projection: {
|
||||
openid: 1, outTradeNo: 1, goodsPrice: 1, itemCount: 1, chargeTime: 1, time: 1,
|
||||
itemid: 1, 'goldMiner.configVersion': 1, 'goldMiner.paidAt': 1, 'goldMiner.confirmedAt': 1,
|
||||
rewardVersion: 1, activityType: 1
|
||||
}
|
||||
})
|
||||
.sort({ openid: 1, outTradeNo: 1, _id: 1 });
|
||||
try {
|
||||
for await (const order of orders) {
|
||||
|
|
@ -158,9 +180,11 @@ export default async function () {
|
|||
if (paidAt > asOf - 15 * DAY) summary.fen15d += amount;
|
||||
if (paidAt > asOf - 30 * DAY) {
|
||||
summary.fen30d += amount;
|
||||
summary.recent.push({ amountFen: amount, paidAt, sku: String(order.itemid ?? ''),
|
||||
summary.recent.push({
|
||||
amountFen: amount, paidAt, sku: String(order.itemid ?? ''),
|
||||
rewardVersion: order.rewardVersion ?? order.goldMiner?.configVersion,
|
||||
activity: order.activityType ?? (order.goldMiner ? 'coin' : undefined) });
|
||||
activity: order.activityType ?? (order.goldMiner ? 'coin' : undefined)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -174,10 +198,15 @@ export default async function () {
|
|||
summary.stats.amount30d = summary.fen30d;
|
||||
summary.stats.amountTotal = summary.fenTotal;
|
||||
}
|
||||
const result = await rechargeStatsCollection.bulkWrite(batch.map(user => {
|
||||
const rechargeStats = summaries.get(user.openid)?.stats ?? { ...emptyStats(asOf), missingOpenid: true };
|
||||
const records = batch.map(user => {
|
||||
const rechargeStats = summaries.get(user.openid)?.stats
|
||||
?? { ...emptyStats(asOf), missingOpenid: !directNeverPaid.has(userKey(user._id)) };
|
||||
const summary = summaries.get(user.openid);
|
||||
const profile = buildPaymentProfile({
|
||||
const profile = directNeverPaid.has(userKey(user._id)) ? {
|
||||
vip_level: 0, data_valid: true, paid_status: 'never', has_ever_paid: false,
|
||||
invalid_reasons: [], profile_rule_version: null, profile_calculated_as_of: updatedAt,
|
||||
rfm: null,
|
||||
} : buildPaymentProfile({
|
||||
cutoff: asOf + 1, registerTime: timestamp(user.register_time), payUser: user.pay_user,
|
||||
missingOpenid: rechargeStats.missingOpenid, previousPaid: previouslyPaid.has(userKey(user._id)),
|
||||
confirmedOrders: rechargeStats.orderCount, futureOrders: summary?.futureOrders ?? 0,
|
||||
|
|
@ -189,107 +218,189 @@ export default async function () {
|
|||
if (profile.data_valid) validProfiles++; else invalidProfiles++;
|
||||
// Persist calculation inputs/results here; the public VIP attribute belongs only to users.
|
||||
const { vip_level: _vipLevel, ...features } = profile;
|
||||
const record = { _id: user._id, openid: user.openid ?? null, ...rechargeStats, ...features, updatedAt,
|
||||
maintenance_identity: { pay_user: user.pay_user ?? null, register_time: timestamp(user.register_time) } };
|
||||
const record = {
|
||||
_id: user._id, openid: user.openid ?? null, ...rechargeStats, ...features, updatedAt,
|
||||
maintenance_identity: { pay_user: user.pay_user ?? null, register_time: timestamp(user.register_time) }
|
||||
};
|
||||
return record;
|
||||
});
|
||||
const freeRecords = records.filter(record => record.data_valid && record.paid_status === 'never');
|
||||
const statsRecords = records.filter(record => !record.data_valid || record.paid_status !== 'never');
|
||||
const result = statsRecords.length ? await rechargeStatsCollection.bulkWrite(statsRecords.map(record => {
|
||||
// Preserve the complete last calculated feature tuple atomically on bad input.
|
||||
// Keep current error/identity metadata and its attempted rule version visible.
|
||||
const heldFields = Object.fromEntries([
|
||||
'profile_rule_version', 'profile_calculated_as_of', 'rfm', 'spending', 'ticket_factors',
|
||||
].map(key => [key, { $ifNull: [`$${key}`, null] }]));
|
||||
let replacement: any = profile.data_valid ? { $literal: record }
|
||||
let replacement: any = record.data_valid ? { $literal: record }
|
||||
: { $mergeObjects: [{ $literal: record }, heldFields] };
|
||||
// A concurrent earlier run may establish paid identity after our batch read.
|
||||
// Do not erase it or publish VIP0 over it, even on structurally valid free-user input.
|
||||
if (profile.has_ever_paid !== true) replacement = { $cond: [
|
||||
{ $eq: ['$has_ever_paid', true] },
|
||||
{ $mergeObjects: [{ $literal: { ...record, has_ever_paid: true, paid_status: 'unknown',
|
||||
data_valid: false, invalid_reasons: [...profile.invalid_reasons, 'paid_identity_changed_during_calculation'] } }, heldFields] },
|
||||
replacement,
|
||||
] };
|
||||
return { updateOne: {
|
||||
// Match only the unique user ID, so a newer snapshot cannot cause an upsert ID conflict.
|
||||
// Apply the freshness check atomically inside the update, including same-day reruns.
|
||||
filter: { _id: user._id },
|
||||
update: [{ $replaceWith: { $cond: [
|
||||
{ $and: [
|
||||
{ $lte: [{ $ifNull: ['$asOf', 0] }, asOf] },
|
||||
{ $lte: [{ $ifNull: ['$updatedAt', 0] }, updatedAt] },
|
||||
] },
|
||||
if (record.has_ever_paid !== true) replacement = {
|
||||
$cond: [
|
||||
{ $eq: ['$has_ever_paid', true] },
|
||||
{
|
||||
$mergeObjects: [{
|
||||
$literal: {
|
||||
...record, has_ever_paid: true, paid_status: 'unknown',
|
||||
data_valid: false, invalid_reasons: [...record.invalid_reasons, 'paid_identity_changed_during_calculation']
|
||||
}
|
||||
}, heldFields]
|
||||
},
|
||||
replacement,
|
||||
'$$ROOT',
|
||||
] } }],
|
||||
upsert: true,
|
||||
} };
|
||||
}));
|
||||
]
|
||||
};
|
||||
return {
|
||||
updateOne: {
|
||||
// Match only the unique user ID, so a newer snapshot cannot cause an upsert ID conflict.
|
||||
// Apply the freshness check atomically inside the update, including same-day reruns.
|
||||
filter: { _id: record._id },
|
||||
update: [{
|
||||
$replaceWith: {
|
||||
$cond: [
|
||||
{
|
||||
$and: [
|
||||
{ $lte: [{ $ifNull: ['$asOf', 0] }, asOf] },
|
||||
{ $lte: [{ $ifNull: ['$updatedAt', 0] }, updatedAt] },
|
||||
]
|
||||
},
|
||||
replacement,
|
||||
'$$ROOT',
|
||||
]
|
||||
}
|
||||
}],
|
||||
upsert: true,
|
||||
}
|
||||
};
|
||||
})) : { modifiedCount: 0, upsertedCount: 0 };
|
||||
// Read back the committed tuple: a concurrent task may have won the freshness check,
|
||||
// or invalid input may have retained a newer good tuple. Never publish the attempted result.
|
||||
const committed: any[] = await rechargeStatsCollection.find({ _id: { $in: batch.map(user => user._id) } },
|
||||
{ projection: { _id: 1, openid: 1, asOf: 1, updatedAt: 1, data_valid: 1, paid_status: 1,
|
||||
has_ever_paid: 1, maintenance_identity: 1, profile_rule_version: 1,
|
||||
profile_calculated_as_of: 1, rfm: 1 } }).toArray();
|
||||
const persisted: any[] = await rechargeStatsCollection.find({ _id: { $in: batch.map(user => user._id) } },
|
||||
{
|
||||
projection: {
|
||||
_id: 1, openid: 1, asOf: 1, updatedAt: 1, data_valid: 1, paid_status: 1,
|
||||
has_ever_paid: 1, amountTotal: 1, orderCount: 1, maintenance_identity: 1, profile_rule_version: 1,
|
||||
profile_calculated_as_of: 1, rfm: 1
|
||||
}
|
||||
}).toArray();
|
||||
const persistedById = new Map(persisted.map(row => [userKey(row._id), row]));
|
||||
const freeById = new Map(freeRecords.map(row => [userKey(row._id), row]));
|
||||
const committed: any[] = batch.map(user => {
|
||||
const saved = persistedById.get(userKey(user._id));
|
||||
const free = freeById.get(userKey(user._id));
|
||||
// A paid/newer snapshot committed during calculation wins over initialization.
|
||||
if (!free || saved?.has_ever_paid === true || saved?.paid_status === 'paid'
|
||||
|| saved?.amountTotal > 0 || saved?.orderCount > 0
|
||||
|| saved?.updatedAt > updatedAt || saved?.asOf > asOf) return saved;
|
||||
return free;
|
||||
}).filter(Boolean);
|
||||
// Remove only zero-valued, explicitly verified legacy free-user records. Do this
|
||||
// before marking migration complete on users so a failed cleanup can be retried.
|
||||
const cleanupIds = committed.filter(row => row.data_valid && row.paid_status === 'never').map(row => row._id);
|
||||
if (cleanupIds.length) {
|
||||
const cleanup = await rechargeStatsCollection.deleteMany({
|
||||
_id: { $in: cleanupIds },
|
||||
has_ever_paid: { $ne: true }, paid_status: { $ne: 'paid' }, amountTotal: 0, orderCount: 0,
|
||||
asOf: { $lte: asOf }, updatedAt: { $lte: updatedAt }
|
||||
});
|
||||
removedNeverPaidStats += cleanup.deletedCount;
|
||||
}
|
||||
const batchById = new Map(batch.map(user => [userKey(user._id), user]));
|
||||
const vipResult = committed.length ? await users.bulkWrite(committed.map(saved => {
|
||||
const source = batchById.get(userKey(saved._id))!;
|
||||
// The retained RFM tuple also permits first-time migration/retry without the legacy
|
||||
// userRechargeStats.vip_level field, even if this attempt has invalid order inputs.
|
||||
const score = saved.rfm?.score;
|
||||
const calculatedLevel = Number.isFinite(score) && saved.profile_calculated_as_of != null
|
||||
const calculatedLevel = saved.data_valid && saved.paid_status === 'never' ? 0 : Number.isFinite(score) && saved.profile_calculated_as_of != null
|
||||
&& saved.rfm.last_effective_paid_at !== undefined
|
||||
? paymentVipLevel(saved.rfm.last_effective_paid_at !== null, score)
|
||||
: null;
|
||||
const identity = { openid: saved.openid ?? null, pay_user: saved.maintenance_identity?.pay_user ?? null,
|
||||
register_time: saved.maintenance_identity?.register_time ?? null };
|
||||
const currentIdentity = { $and: [
|
||||
{ $eq: [{ $ifNull: ['$openid', null] }, { $literal: identity.openid ?? null }] },
|
||||
{ $eq: [{ $ifNull: ['$pay_user', null] }, { $literal: identity.pay_user ?? null }] },
|
||||
{ $eq: [{ $ifNull: ['$register_time', null] }, { $literal: source.register_time ?? null }] },
|
||||
{ $literal: timestamp(source.register_time) === identity.register_time },
|
||||
] };
|
||||
const valid = { $and: [
|
||||
{ $literal: saved.data_valid === true && calculatedLevel !== null }, currentIdentity,
|
||||
// Do not turn a concurrently established paid identity back into a never-payer.
|
||||
{ $or: [
|
||||
{ $literal: saved.has_ever_paid === true },
|
||||
{ $and: [{ $ne: ['$vip_profile.has_ever_paid', true] },
|
||||
{ $lte: [{ $ifNull: ['$vip_level', 0] }, 0] }] },
|
||||
] },
|
||||
] };
|
||||
const identity = {
|
||||
openid: saved.openid ?? null, pay_user: saved.maintenance_identity?.pay_user ?? null,
|
||||
register_time: saved.maintenance_identity?.register_time ?? null
|
||||
};
|
||||
const currentIdentity = {
|
||||
$and: [
|
||||
{ $eq: [{ $ifNull: ['$openid', null] }, { $literal: identity.openid ?? null }] },
|
||||
{ $eq: [{ $ifNull: ['$pay_user', null] }, { $literal: identity.pay_user ?? null }] },
|
||||
{ $eq: [{ $ifNull: ['$register_time', null] }, { $literal: source.register_time ?? null }] },
|
||||
{ $literal: timestamp(source.register_time) === identity.register_time },
|
||||
]
|
||||
};
|
||||
const valid = {
|
||||
$and: [
|
||||
{ $literal: saved.data_valid === true && calculatedLevel !== null }, currentIdentity,
|
||||
// Do not turn a concurrently established paid identity back into a never-payer.
|
||||
{
|
||||
$or: [
|
||||
{ $literal: saved.has_ever_paid === true },
|
||||
{
|
||||
$and: [{ $ne: ['$vip_profile.has_ever_paid', true] },
|
||||
{ $lte: [{ $ifNull: ['$vip_level', 0] }, 0] }]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
};
|
||||
const oldLevel = { $ifNull: ['$vip_level', { $literal: calculatedLevel }] };
|
||||
const heldMetadata = (userField: string, value: any) => ({ $cond: [
|
||||
{ $ne: [{ $ifNull: ['$vip_level', null] }, null] },
|
||||
{ $ifNull: [`$vip_profile.${userField}`, null] },
|
||||
{ $literal: calculatedLevel === null ? null : value ?? null },
|
||||
] });
|
||||
const heldMetadata = (userField: string, value: any) => ({
|
||||
$cond: [
|
||||
{ $ne: [{ $ifNull: ['$vip_level', null] }, null] },
|
||||
{ $ifNull: [`$vip_profile.${userField}`, null] },
|
||||
{ $literal: calculatedLevel === null ? null : value ?? null },
|
||||
]
|
||||
});
|
||||
const metadata = {
|
||||
maintenance_version: { $literal: 9 },
|
||||
as_of: { $literal: saved.asOf }, updated_at: { $literal: saved.updatedAt },
|
||||
data_valid: valid, identity: { $literal: identity },
|
||||
has_ever_paid: { $or: [{ $eq: ['$vip_profile.has_ever_paid', true] },
|
||||
{ $gt: [{ $ifNull: ['$vip_level', 0] }, 0] }, { $literal: saved.has_ever_paid === true }] },
|
||||
calculated_as_of: { $cond: [valid, { $literal: saved.profile_calculated_as_of ?? null },
|
||||
heldMetadata('calculated_as_of', saved.profile_calculated_as_of)] },
|
||||
rule_version: { $cond: [valid, { $literal: saved.profile_rule_version ?? null },
|
||||
heldMetadata('rule_version', saved.profile_rule_version)] },
|
||||
has_ever_paid: {
|
||||
$or: [{ $eq: ['$vip_profile.has_ever_paid', true] },
|
||||
{ $gt: [{ $ifNull: ['$vip_level', 0] }, 0] }, { $literal: saved.has_ever_paid === true }]
|
||||
},
|
||||
calculated_as_of: {
|
||||
$cond: [valid, { $literal: saved.profile_calculated_as_of ?? null },
|
||||
heldMetadata('calculated_as_of', saved.profile_calculated_as_of)]
|
||||
},
|
||||
rule_version: {
|
||||
$cond: [valid, { $literal: saved.profile_rule_version ?? null },
|
||||
heldMetadata('rule_version', saved.profile_rule_version)]
|
||||
},
|
||||
};
|
||||
return {
|
||||
updateOne: {
|
||||
filter: { _id: saved._id }, upsert: false,
|
||||
// Merge only the two owned attributes; concurrent gameplay/account changes survive.
|
||||
update: [{
|
||||
$replaceWith: {
|
||||
$cond: [
|
||||
{
|
||||
$and: [
|
||||
{ $lte: [{ $ifNull: ['$vip_profile.as_of', 0] }, saved.asOf] },
|
||||
{ $lte: [{ $ifNull: ['$vip_profile.updated_at', 0] }, saved.updatedAt] },
|
||||
]
|
||||
},
|
||||
{
|
||||
$mergeObjects: ['$$ROOT', {
|
||||
vip_level: { $cond: [valid, { $literal: calculatedLevel }, oldLevel] },
|
||||
vip_profile: metadata
|
||||
}]
|
||||
}, '$$ROOT',
|
||||
]
|
||||
}
|
||||
}],
|
||||
}
|
||||
};
|
||||
return { updateOne: {
|
||||
filter: { _id: saved._id }, upsert: false,
|
||||
// Merge only the two owned attributes; concurrent gameplay/account changes survive.
|
||||
update: [{ $replaceWith: { $cond: [
|
||||
{ $and: [
|
||||
{ $lte: [{ $ifNull: ['$vip_profile.as_of', 0] }, saved.asOf] },
|
||||
{ $lte: [{ $ifNull: ['$vip_profile.updated_at', 0] }, saved.updatedAt] },
|
||||
] },
|
||||
{ $mergeObjects: ['$$ROOT', { vip_level: { $cond: [valid, { $literal: calculatedLevel }, oldLevel] },
|
||||
vip_profile: metadata }] }, '$$ROOT',
|
||||
] } }],
|
||||
} };
|
||||
})) : { modifiedCount: 0 };
|
||||
processedUsers += batch.length;
|
||||
updatedUsers += result.modifiedCount + result.upsertedCount;
|
||||
updatedVipUsers += vipResult.modifiedCount;
|
||||
}
|
||||
|
||||
const result = { asOf, updatedAt, scannedUsers, skippedNeverPaidUsers,
|
||||
processedUsers, updatedUsers, updatedVipUsers, validProfiles, invalidProfiles };
|
||||
const result = {
|
||||
asOf, updatedAt, scannedUsers, skippedNeverPaidUsers,
|
||||
processedUsers, updatedUsers, updatedVipUsers, removedNeverPaidStats, validProfiles, invalidProfiles
|
||||
};
|
||||
console.log('rechargeStats completed', result);
|
||||
return { code: 1, data: result, msg: '用户付费画像维护完成' };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user