fix(cloud-rise): allow index setup with duplicate player ids

This commit is contained in:
guanchao 2026-09-24 20:13:29 +08:00
parent 035a688cfe
commit 4c9bf507a7
4 changed files with 59 additions and 6 deletions

View File

@ -116,7 +116,7 @@ cloudRise/
1. 按本次发布清单发布新增云函数及对应 YAML,更新 `login`。相对 main,`userCoin` 和 `jungleTreasure` 没有变化,无需重复发布。
2. 设置服务端环境变量 `ADMIN_TOKEN`,仅用于运营配置,不下发客户端。
3. 对 `cloudRise/admin` 发 POST:`{ "action": "setup_indexes", "adminToken": "<运营密钥>" }`。
3. 对 `cloudRise/admin` 发 POST:`{ "action": "setup_indexes", "adminToken": "<运营密钥>" }`。`users.onlyId` 缺少索引时创建普通索引,已有可用普通或唯一索引则保留,允许历史重复编号下完成初始化;其他唯一约束不变。此操作不修复重复编号及其账号定位影响。
4. 参考 `cloudRise-config.example.json` 设置 `periodId/startsAt/endsAt/durationHours/unlockLevel/pools`。时间均为 Unix 毫秒。
5. POST `cloudRise/admin`:`{ "action": "save", "adminToken": "<运营密钥>", "configVersion": "cloudRiseTestV1", "enabled": true, "config": { ... } }`。startsAt 必须在未来;版本标识同一活动内唯一,重复同内容可安全重试,不能覆盖已有版本。

View File

@ -27,7 +27,14 @@ export default async function (ctx: FunctionContext) {
}
if (body.action === "setup_indexes") {
await mongo.collection("users").createIndex({ taskTime: 1 });
await mongo.collection("users").createIndex({ onlyId: 1 }, { unique: true, partialFilterExpression: { onlyId: { $type: "number" } } });
// Legacy users can share onlyId. Prepare lookup indexes without repairing accounts
// or weakening an existing unique index in environments where it already succeeded.
const userIndexes = await mongo.collection("users").listIndexes().toArray();
const hasOnlyIdIndex = userIndexes.some((index: any) =>
Object.keys(index.key).length === 1 && index.key.onlyId === 1 && !index.hidden
&& (!index.partialFilterExpression
|| JSON.stringify(index.partialFilterExpression) === JSON.stringify({ onlyId: { $type: "number" } })));
if (!hasOnlyIdIndex) await mongo.collection("users").createIndex({ onlyId: 1 });
await ConfigStore.setupIndexes();
await mongo.collection("cloudRiseSamples").createIndex({ "samples.stage": 1, "samples.start_level": 1, "samples.success_num": 1 });
await mongo.collection("cloudRiseSamples").createIndex({ uid: 1, periodId: 1 }, {

View File

@ -212,7 +212,21 @@ globalThis.__cloudRiseCloud = { mongo: { db: { collection(name) { return {
}
return { matchedCount: count, modifiedCount: count };
},
async createIndex(spec, options) { indexes.push({ name, spec, options }); return 'test_index'; },
listIndexes() { return { async toArray() {
return indexes.filter(i => i.name === name).map(i => ({ key: i.spec, ...i.options }));
} }; },
async createIndex(spec, options) {
if (name === 'users' && spec.onlyId === 1 && options?.unique) {
const ids = [...db.users.values()].map(user => user.onlyId).filter(id => typeof id === 'number');
if (new Set(ids).size !== ids.length) throw Object.assign(new Error('duplicate onlyId'), { code: 11000 });
}
const existing = indexes.find(i => i.name === name && isDeepStrictEqual(i.spec, spec));
if (existing) {
if (!isDeepStrictEqual(existing.options, options)) throw new Error('IndexOptionsConflict');
return 'test_index';
}
indexes.push({ name, spec, options }); return 'test_index';
},
}; } } } };
globalThis.__cloudRiseCloud.database = () => ({ collection(name) { return { where(query) { return {
async getOne() { return { data: await globalThis.__cloudRiseCloud.mongo.db.collection(name).findOne(query) }; },
@ -393,6 +407,38 @@ test('admin requires configured secret, validates periods, and preserves enrolle
} finally { delete process.env.ADMIN_TOKEN; }
});
test('index setup tolerates duplicate onlyId, prepares all indexes and can be retried', async () => {
setup(); process.env.ADMIN_TOKEN = 'test-only';
db.users.set('duplicate', { _id: 'duplicate', onlyId: 1001, token: 'other-token' });
const before = structuredClone([...db.users]);
const request = { body: { action: 'setup_indexes', adminToken: 'test-only' } };
try {
assert.equal((await adminApi(request)).code, 1);
assert.equal(indexes.length, 8);
const lookup = indexes.find(i => i.name === 'users' && i.spec.onlyId === 1);
assert(lookup && !lookup.options?.unique);
assert(indexes.some(i => i.name === 'activityConfigs' && i.options?.unique));
assert(indexes.some(i => i.name === 'cloudRiseSamples' && i.options?.unique));
assert(indexes.some(i => i.name === 'cloudRiseRuns'));
assert.equal((await adminApi(request)).code, 1);
assert.equal(indexes.length, 8);
assert.deepEqual([...db.users], before);
} finally { delete process.env.ADMIN_TOKEN; }
});
test('index setup preserves existing ordinary and unique onlyId indexes', async () => {
for (const options of [undefined, { unique: true },
{ unique: true, partialFilterExpression: { onlyId: { $type: 'number' } } }]) {
setup(); process.env.ADMIN_TOKEN = 'test-only';
indexes.push({ name: 'users', spec: { onlyId: 1 }, options });
try {
assert.equal((await adminApi({ body: { action: 'setup_indexes', adminToken: 'test-only' } })).code, 1);
assert.equal(indexes.length, 8);
assert.deepEqual(indexes.find(i => i.name === 'users' && i.spec.onlyId === 1).options, options);
} finally { delete process.env.ADMIN_TOKEN; }
}
});
test('real client service talks to real activity API: manual stages, client awards, and offline win recovery', async () => {
const { createRequire } = await import('node:module');
const { runInNewContext } = await import('node:vm');

View File

@ -56,12 +56,12 @@
{ "action": "setup_indexes", "adminToken": "<ADMIN_TOKEN 的值>" }
```
该操作会调用以下 9 项索引创建;只有返回 `code: 1` 才算完成。
该操作会准备以下 8 项索引;只有返回 `code: 1` 才算完成。已有可用的 `users.onlyId` 索引直接复用。
| 集合 | 索引 | 附加约束 |
| --- | --- | --- |
| `users` | `{ taskTime: 1 }` | 匹配资料池查询使用,不是启用每日任务 |
| `users` | `{ onlyId: 1 }` | unique;部分条件 `onlyId: { $type: "number" }` |
| `users` | `{ onlyId: 1 }` | 缺失时创建普通索引;已有普通或覆盖数字编号的唯一索引则保留 |
| `activityConfigs` | `{ activityId: 1, configVersion: 1 }` | unique;部分条件 `recordType: "version"`;名称 `activity_config_version` |
| `activityConfigs` | `{ activityId: 1, recordType: 1, status: 1, effectiveFrom: -1, publishedAt: -1 }` | 普通索引 |
| `activityConfigs` | `{ activityId: 1, "config.periodId": 1, publishedAt: -1 }` | 普通索引 |
@ -69,7 +69,7 @@
| `cloudRiseSamples` | `{ uid: 1, periodId: 1 }` | unique;部分条件 `schemaVersion: 2`;名称 `cloudRiseSamples_uid_period` |
| `cloudRiseRuns` | `{ uid: 1, periodId: 1 }` | 普通索引 |
上线前检查 `users.onlyId` 数字值没有重复,否则唯一索引会失败;不要为使索引通过而直接删除用户。配置、活动状态和样本由服务端维护,不给客户端直写权限。
生产已有重复 `users.onlyId`,索引初始化暂不强制该字段唯一,不删除用户、不重分配编号,也不移除已有唯一索引。之前因 `onlyId` 重复失败的环境,更新 `cloudRise/admin` 后重试原 `setup_indexes` 请求即可。此变更只解除索引初始化阻塞,账号定位对 `onlyId` 唯一性的依赖仍待后续修复。配置、活动状态和样本由服务端维护,不给客户端直写权限。
main 中不存在百人赛模块,按本次首次上线场景无需运行配置迁移、旧样本迁移或历史归档精简。如果生产曾有 main 之外的人工部署记录,应先核对存量再决定,不能直接运行同时处理黄金矿工的 `activityConfig/migrate.cjs`。