fix(cloud-rise): allow index setup with duplicate player ids
This commit is contained in:
parent
035a688cfe
commit
4c9bf507a7
|
|
@ -116,7 +116,7 @@ cloudRise/
|
||||||
|
|
||||||
1. 按本次发布清单发布新增云函数及对应 YAML,更新 `login`。相对 main,`userCoin` 和 `jungleTreasure` 没有变化,无需重复发布。
|
1. 按本次发布清单发布新增云函数及对应 YAML,更新 `login`。相对 main,`userCoin` 和 `jungleTreasure` 没有变化,无需重复发布。
|
||||||
2. 设置服务端环境变量 `ADMIN_TOKEN`,仅用于运营配置,不下发客户端。
|
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 毫秒。
|
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 必须在未来;版本标识同一活动内唯一,重复同内容可安全重试,不能覆盖已有版本。
|
5. POST `cloudRise/admin`:`{ "action": "save", "adminToken": "<运营密钥>", "configVersion": "cloudRiseTestV1", "enabled": true, "config": { ... } }`。startsAt 必须在未来;版本标识同一活动内唯一,重复同内容可安全重试,不能覆盖已有版本。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,14 @@ export default async function (ctx: FunctionContext) {
|
||||||
}
|
}
|
||||||
if (body.action === "setup_indexes") {
|
if (body.action === "setup_indexes") {
|
||||||
await mongo.collection("users").createIndex({ taskTime: 1 });
|
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 ConfigStore.setupIndexes();
|
||||||
await mongo.collection("cloudRiseSamples").createIndex({ "samples.stage": 1, "samples.start_level": 1, "samples.success_num": 1 });
|
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 }, {
|
await mongo.collection("cloudRiseSamples").createIndex({ uid: 1, periodId: 1 }, {
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,21 @@ globalThis.__cloudRiseCloud = { mongo: { db: { collection(name) { return {
|
||||||
}
|
}
|
||||||
return { matchedCount: count, modifiedCount: count };
|
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 {
|
globalThis.__cloudRiseCloud.database = () => ({ collection(name) { return { where(query) { return {
|
||||||
async getOne() { return { data: await globalThis.__cloudRiseCloud.mongo.db.collection(name).findOne(query) }; },
|
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; }
|
} 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 () => {
|
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 { createRequire } = await import('node:module');
|
||||||
const { runInNewContext } = await import('node:vm');
|
const { runInNewContext } = await import('node:vm');
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,12 @@
|
||||||
{ "action": "setup_indexes", "adminToken": "<ADMIN_TOKEN 的值>" }
|
{ "action": "setup_indexes", "adminToken": "<ADMIN_TOKEN 的值>" }
|
||||||
```
|
```
|
||||||
|
|
||||||
该操作会调用以下 9 项索引创建;只有返回 `code: 1` 才算完成。
|
该操作会准备以下 8 项索引;只有返回 `code: 1` 才算完成。已有可用的 `users.onlyId` 索引直接复用。
|
||||||
|
|
||||||
| 集合 | 索引 | 附加约束 |
|
| 集合 | 索引 | 附加约束 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `users` | `{ taskTime: 1 }` | 匹配资料池查询使用,不是启用每日任务 |
|
| `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, configVersion: 1 }` | unique;部分条件 `recordType: "version"`;名称 `activity_config_version` |
|
||||||
| `activityConfigs` | `{ activityId: 1, recordType: 1, status: 1, effectiveFrom: -1, publishedAt: -1 }` | 普通索引 |
|
| `activityConfigs` | `{ activityId: 1, recordType: 1, status: 1, effectiveFrom: -1, publishedAt: -1 }` | 普通索引 |
|
||||||
| `activityConfigs` | `{ activityId: 1, "config.periodId": 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` |
|
| `cloudRiseSamples` | `{ uid: 1, periodId: 1 }` | unique;部分条件 `schemaVersion: 2`;名称 `cloudRiseSamples_uid_period` |
|
||||||
| `cloudRiseRuns` | `{ uid: 1, periodId: 1 }` | 普通索引 |
|
| `cloudRiseRuns` | `{ uid: 1, periodId: 1 }` | 普通索引 |
|
||||||
|
|
||||||
上线前检查 `users.onlyId` 数字值没有重复,否则唯一索引会失败;不要为使索引通过而直接删除用户。配置、活动状态和样本由服务端维护,不给客户端直写权限。
|
生产已有重复 `users.onlyId`,索引初始化暂不强制该字段唯一,不删除用户、不重分配编号,也不移除已有唯一索引。之前因 `onlyId` 重复失败的环境,更新 `cloudRise/admin` 后重试原 `setup_indexes` 请求即可。此变更只解除索引初始化阻塞,账号定位对 `onlyId` 唯一性的依赖仍待后续修复。配置、活动状态和样本由服务端维护,不给客户端直写权限。
|
||||||
|
|
||||||
main 中不存在百人赛模块,按本次首次上线场景无需运行配置迁移、旧样本迁移或历史归档精简。如果生产曾有 main 之外的人工部署记录,应先核对存量再决定,不能直接运行同时处理黄金矿工的 `activityConfig/migrate.cjs`。
|
main 中不存在百人赛模块,按本次首次上线场景无需运行配置迁移、旧样本迁移或历史归档精简。如果生产曾有 main 之外的人工部署记录,应先核对存量再决定,不能直接运行同时处理黄金矿工的 `activityConfig/migrate.cjs`。
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user