|
|
@@ -0,0 +1,167 @@
|
|
|
+/**
|
|
|
+ * 验证「附件上传到 DMS 文件服务」这条链路(`POST {DMS}/file/uploadFile`)。
|
|
|
+ *
|
|
|
+ * 背景(用户 2026-09-23):「图片和文件调用接口存储…然后文件存放的相对路径放到对应的 URL 里」——
|
|
|
+ * 即上传走 DMS,返回的**相对路径**随聊天请求体的 `pictures` / `files` 发给后端。
|
|
|
+ * 此前附件直传厂商(metamaker)OSS,给的是绝对地址。
|
|
|
+ *
|
|
|
+ * **实测契约(2026-09-23,打真实服务)**:
|
|
|
+ * - `multipart/form-data`,**五个字段都必填**:file / type / columnId / contentId / paraName
|
|
|
+ * (少一个 → `{"code":-1,"message":"缺少必传字段"}`)
|
|
|
+ * - `type` 决定存储目录:0 图 → `/static/photo/…`;1音/2视/3 文件 → `/static/file/…`
|
|
|
+ * - 成功返回 `{"code":200,"content":"/static/photo/1889-c_question-<uuid>.png"}`
|
|
|
+ *
|
|
|
+ * 怎么跑(需要 dev server 起着,走同源代理;node 侧不校验 TLS):
|
|
|
+ * 1) npm run dev
|
|
|
+ * 2) npx esbuild harness/tools/_entry-dms.ts --bundle --format=esm \
|
|
|
+ * --outfile=harness/tools/_dms.mjs --alias:@=./src --define:import.meta.env='{}'
|
|
|
+ * 3) node harness/tools/verify-dms-file-upload.mjs
|
|
|
+ *
|
|
|
+ * ⚠️ 「真上传」那一段会往文件服务写一个**极小的测试文件**(名字带 verify_upload_ 前缀,
|
|
|
+ * 体积几十字节);接口没有提供删除能力,所以它会留在那儿 —— 可接受但要知情。
|
|
|
+ */
|
|
|
+
|
|
|
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
|
+
|
|
|
+const base = process.argv[2] || 'https://localhost:8083/dms-api';
|
|
|
+globalThis.VITE_DMS_API = base;
|
|
|
+
|
|
|
+const realFetch = globalThis.fetch;
|
|
|
+let captured = [];
|
|
|
+const stubFetch = (responder) => {
|
|
|
+ globalThis.fetch = async (url, init = {}) => {
|
|
|
+ captured.push({ url: String(url), body: init.body });
|
|
|
+ return responder(String(url), init);
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const {
|
|
|
+ uploadDmsAttachment,
|
|
|
+ buildDmsFileDisplayUrl,
|
|
|
+ DMS_ATTACHMENT_COLUMN_ID,
|
|
|
+ DMS_ATTACHMENT_CONTENT_ID,
|
|
|
+ DMS_ATTACHMENT_PARA_NAME,
|
|
|
+ DMS_ATTACHMENT_TYPE_IMAGE,
|
|
|
+ DMS_ATTACHMENT_TYPE_FILE,
|
|
|
+} = await import('./_dms.mjs');
|
|
|
+
|
|
|
+let pass = 0;
|
|
|
+let fail = 0;
|
|
|
+const check = (name, cond, extra = '') => {
|
|
|
+ if (cond) {
|
|
|
+ pass++;
|
|
|
+ console.log(' ok ' + name);
|
|
|
+ } else {
|
|
|
+ fail++;
|
|
|
+ console.log(' FAIL ' + name + ' ' + extra);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** node 里构造一个假的 File(Node 20+ 有全局 File) */
|
|
|
+const makeFile = (name, type, content = 'verify upload probe') =>
|
|
|
+ new File([content], name, { type });
|
|
|
+
|
|
|
+console.log(`\nDMS 附件上传验证\n 代理地址: ${base}\n`);
|
|
|
+
|
|
|
+console.log('【1】请求形态:五个字段都得带上');
|
|
|
+{
|
|
|
+ captured = [];
|
|
|
+ stubFetch(() =>
|
|
|
+ new Response(JSON.stringify({ code: 200, message: '上传成功!', content: '/static/photo/x.png' }), {
|
|
|
+ status: 200,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ })
|
|
|
+ );
|
|
|
+ const path = await uploadDmsAttachment(makeFile('a.png', 'image/png'), { isImage: true });
|
|
|
+ globalThis.fetch = realFetch;
|
|
|
+
|
|
|
+ const req = captured[0];
|
|
|
+ check('只发一次请求', captured.length === 1, String(captured.length));
|
|
|
+ check('打的是 /file/uploadFile', req?.url.endsWith('/file/uploadFile'), req?.url);
|
|
|
+ check('返回相对路径', path === '/static/photo/x.png', path);
|
|
|
+
|
|
|
+ const form = req?.body;
|
|
|
+ check('body 是 FormData', form instanceof FormData, String(form?.constructor?.name));
|
|
|
+ const keys = form ? [...form.keys()].sort() : [];
|
|
|
+ check(
|
|
|
+ '**五个字段齐全**(少一个后端就返回"缺少必传字段")',
|
|
|
+ JSON.stringify(keys) === JSON.stringify(['columnId', 'contentId', 'file', 'paraName', 'type']),
|
|
|
+ JSON.stringify(keys)
|
|
|
+ );
|
|
|
+ check('type:图片 = 0', form?.get('type') === DMS_ATTACHMENT_TYPE_IMAGE, String(form?.get('type')));
|
|
|
+ check(`columnId = ${DMS_ATTACHMENT_COLUMN_ID}`, form?.get('columnId') === String(DMS_ATTACHMENT_COLUMN_ID));
|
|
|
+ check(`contentId = ${DMS_ATTACHMENT_CONTENT_ID}`, form?.get('contentId') === String(DMS_ATTACHMENT_CONTENT_ID));
|
|
|
+ check(`paraName = ${DMS_ATTACHMENT_PARA_NAME}`, form?.get('paraName') === DMS_ATTACHMENT_PARA_NAME);
|
|
|
+ check('file 就是那个文件', form?.get('file')?.name === 'a.png', String(form?.get('file')?.name));
|
|
|
+}
|
|
|
+
|
|
|
+console.log('\n【2】非图片走 type=3');
|
|
|
+{
|
|
|
+ captured = [];
|
|
|
+ stubFetch(() => new Response(JSON.stringify({ code: 200, content: '/static/file/y.pdf' }), { status: 200 }));
|
|
|
+ await uploadDmsAttachment(makeFile('b.pdf', 'application/pdf'), { isImage: false });
|
|
|
+ globalThis.fetch = realFetch;
|
|
|
+ check('type:非图片 = 3', captured[0]?.body?.get('type') === DMS_ATTACHMENT_TYPE_FILE, String(captured[0]?.body?.get('type')));
|
|
|
+}
|
|
|
+
|
|
|
+console.log('\n【3】失败要抛错(调用方据此把上传项标成 error)');
|
|
|
+{
|
|
|
+ stubFetch(() => new Response(JSON.stringify({ code: -1, message: '缺少必传字段' }), { status: 200 }));
|
|
|
+ let msg = '';
|
|
|
+ try {
|
|
|
+ await uploadDmsAttachment(makeFile('c.png', 'image/png'), { isImage: true });
|
|
|
+ } catch (err) {
|
|
|
+ msg = err.message;
|
|
|
+ }
|
|
|
+ globalThis.fetch = realFetch;
|
|
|
+ check('code != 200 → 抛错并带上后端说法', msg === '缺少必传字段', msg);
|
|
|
+}
|
|
|
+{
|
|
|
+ stubFetch(() => new Response('boom', { status: 500 }));
|
|
|
+ let msg = '';
|
|
|
+ try {
|
|
|
+ await uploadDmsAttachment(makeFile('d.png', 'image/png'), { isImage: true });
|
|
|
+ } catch (err) {
|
|
|
+ msg = err.message;
|
|
|
+ }
|
|
|
+ globalThis.fetch = realFetch;
|
|
|
+ check('HTTP 非 2xx → 抛错', msg.includes('500'), msg);
|
|
|
+}
|
|
|
+
|
|
|
+console.log('\n【4】真上传一次(走代理打真实文件服务)');
|
|
|
+{
|
|
|
+ try {
|
|
|
+ const imgPath = await uploadDmsAttachment(makeFile(`verify_upload_${Date.now()}.png`, 'image/png'), {
|
|
|
+ isImage: true,
|
|
|
+ });
|
|
|
+ check('图片:返回 200 且拿到相对路径', /^\/static\/photo\/1889-c_question-[\w-]+\.png$/.test(imgPath), imgPath);
|
|
|
+
|
|
|
+ const filePath = await uploadDmsAttachment(makeFile(`verify_upload_${Date.now()}.txt`, 'text/plain'), {
|
|
|
+ isImage: false,
|
|
|
+ });
|
|
|
+ check('文件:落在 /static/file/', /^\/static\/file\/1889-c_question-[\w-]+\.txt$/.test(filePath), filePath);
|
|
|
+ check(
|
|
|
+ '路径里带上了 columnId 与 paraName(后端按这三个字段定位)',
|
|
|
+ imgPath.includes('1889-c_question-') && filePath.includes('1889-c_question-'),
|
|
|
+ `${imgPath} | ${filePath}`
|
|
|
+ );
|
|
|
+ console.log(' (测试文件已写入文件服务,名字前缀 verify_upload_,体积几十字节)');
|
|
|
+
|
|
|
+ // 关键:相对路径**取不到**,但拼上代理前缀能取到 —— 缩略图预览靠的就是它
|
|
|
+ const direct = await realFetch(`http://121.43.55.7:10081${imgPath}`).catch(() => null);
|
|
|
+ check('相对路径直接取**不到**(所以不能当 img src)', !direct || direct.status === 404, `HTTP ${direct?.status}`);
|
|
|
+
|
|
|
+ const displayUrl = buildDmsFileDisplayUrl(imgPath);
|
|
|
+ check('展示地址 = 代理前缀 + 相对路径', displayUrl === `${base}${imgPath}`, displayUrl);
|
|
|
+ const served = await realFetch(displayUrl).catch(() => null);
|
|
|
+ check('**展示地址能取到**(缩略图刷新后仍在)', served?.status === 200, `HTTP ${served?.status}`);
|
|
|
+ check('空路径返回空串(不拼出半个地址)', buildDmsFileDisplayUrl('') === '', buildDmsFileDisplayUrl(''));
|
|
|
+ check('已是绝对地址则原样返回', buildDmsFileDisplayUrl('https://a.cn/x.png') === 'https://a.cn/x.png');
|
|
|
+ } catch (err) {
|
|
|
+ check('真上传成功', false, err?.message || String(err));
|
|
|
+ console.log(' ⚠️ dev server 没起、或代理不通会走到这里');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
|
|
|
+process.exit(fail ? 1 : 0);
|