feat: 完成现有小程序接口的全部迁移至新后端

This commit is contained in:
R524809
2026-10-19 13:17:51 +08:00
parent 8827110f2f
commit d30cf25890
148 changed files with 13287 additions and 34 deletions
+4
View File
@@ -105,3 +105,7 @@ pnpm check
本地备份预演:62 个文件,共 3,568,778 字节;其中题型预览 51、头像 7、字体 1、素材 1、首页配置 1、demo 1。该数量为本地预演结果,不代表已上传。
实现依据:[七牛官方 Node.js SDK](https://developer.qiniu.com/kodo/sdk/nodejs)、[七牛上传策略](https://developer.qiniu.com/kodo/manual/put-policy)。
## 回填服务端资产
上传报告确认成功后,根目录执行 `pnpm assets:map --report /绝对路径/报告.json` 预演,再添加 `--apply` 写入本机 doodle_dev。该独立回填步骤会重新核对七牛对象与备份摘要,事务更新已有题型预览资产,保留资源 ID 并记录审计;不处理头像归属、首页 JSON 嵌套链接或其他资源绑定。配置必须与 server 的域名、前缀及空间一致,详见根 README。
@@ -0,0 +1,100 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { test } from 'node:test';
import { buildAssetMappings } from '../../../apps/server/scripts/asset-mapping.mjs';
const hash = (value) => createHash('sha256').update(value).digest('hex');
function fixture() {
const sourceRef = 'cloud://legacy.env/assets/previews/math/a.jpg';
const asset = {
id: 'keep-original-id',
sourceRef,
sourceRefHash: hash(sourceRef),
purpose: 'preview',
visibility: 'public',
ownerUserId: null,
status: 'pending',
};
const file = {
relativePath: 'assets/previews/math/a.jpg',
key: 'doodle-assets/assets/previews/math/a.jpg',
bucket: 'test-public',
visibility: 'public',
mimeType: 'image/jpeg',
size: 10,
sha256: hash('local'),
etag: 'etag',
status: 'uploaded',
verifiedBy: 'qiniu-etag-and-size',
};
return {
asset,
file,
report: { schemaVersion: 1, mode: 'apply', ok: true, files: [file] },
config: { publicBucket: 'test-public', prefix: 'doodle-assets/' },
};
}
test('资产回填:保留 ID、完整 key 与原始来源;重复映射幂等', () => {
const { asset, file, report, config } = fixture();
const [mapped] = buildAssetMappings(report, [asset], config);
assert.equal(mapped.asset.id, asset.id);
assert.equal(mapped.data.objectKey, file.key);
assert.equal(
mapped.data.objectRefHash,
hash(JSON.stringify(['qiniu', file.bucket, file.key])),
);
assert.equal(mapped.data.status, 'ready');
assert.equal(mapped.unchanged, false);
assert.equal(
buildAssetMappings(report, [{ ...asset, ...mapped.data }], config)[0]
.unchanged,
true,
);
assert.throws(
() =>
buildAssetMappings(
report,
[{ ...asset, ...mapped.data, objectKey: 'edited-key' }],
config,
),
/READY_ASSET_CONFLICT/,
);
});
test('资产回填:预演报告、未核对对象、缺失映射和来源不一致整批拒绝', () => {
const { asset, file, report, config } = fixture();
assert.throws(
() =>
buildAssetMappings({ ...report, mode: 'dry-run' }, [asset], config),
/INVALID_SYNC_REPORT/,
);
for (const changed of [
{ status: 'planned' },
{ bucket: 'another-bucket' },
{ key: 'wrong-prefix/a.jpg' },
{ verifiedBy: null },
{ sha256: 'bad' },
])
assert.throws(
() =>
buildAssetMappings(
{ ...report, files: [{ ...file, ...changed }] },
[asset],
config,
),
/INVALID_REPORT_FILE/,
);
assert.throws(
() =>
buildAssetMappings(
report,
[{ ...asset, sourceRefHash: 'bad' }],
config,
),
/INVALID_SOURCE_REFERENCE/,
);
assert.throws(
() => buildAssetMappings({ ...report, files: [] }, [asset], config),
/ASSET_MAPPING_MISSING_OR_INELIGIBLE/,
);
});