刘光辉
6 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({ post: vi.fn(), get: vi.fn(), print: vi.fn(), open: vi.fn(), emit: vi.fn(), preview: vi.fn() }));
vi.mock('#/api/request', () => ({ defHttp: { post: mocks.post, get: mocks.get } }));
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { openFormModal: mocks.open, previewDmsFile: mocks.preview, getEmitter: () => ({ emit: mocks.emit }) } }));
vi.mock('../dmsHiprint', () => ({ printManagedDmsDocument: mocks.print }));
import { openDmsReprint, submitDmsReprint, sendDmsReprint, resumeDmsReprint, cancelDmsReprint, refreshDmsReprintLock, REPRINT_LOCK_EVENT } from '../dmsReprint';
import { configureReprintModal, reprintAuditSign, DMS_REPRINT_MODEL_ID } from '../dmsReprintMeasure';
let attempt: any;
let data: any;
let submit: any;
let context: any;
const receiptStore = new Map<string, string>();
beforeEach(() => {
  vi.resetAllMocks();
  receiptStore.clear();
  vi.stubGlobal('localStorage', {
    getItem: (k: string) => receiptStore.get(k),
    setItem: (k: string, v: string) => receiptStore.set(k, v),
    removeItem: (k: string) => receiptStore.delete(k),
  });
  attempt = {
    id: 'request',
    tenantId: '0',
    userId: 'actor',
    printRecordId: 'record',
    fileVersionId: 'version',
    reason: 'original reason',
    bizSign: 'original-sign',
    createdAt: 1700000000000,
    state: 'OPEN',
    numbersJson: JSON.stringify([{ serialNumber: 'original', countSerialNumber: '20260001' }]),
    copies: 1,
    batchesJson: '[]',
  };
  data = { chongda_fasong_id: 'request', biz_sign: 'new-sign', bupingheng_mingxi: [{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3', yuanyin: 'new reason' }] };
  submit = vi.fn((resolve: any) => {
    attempt.documentId = 'doc';
    resolve({ code: 200, data: { id: 'doc' } });
  });
  context = {
    formData: data,
    setFormData: (k: string, v: any) => {
      data[k] = v;
    },
    formContext: { isEdit: false, submit, closeReprintForm: vi.fn().mockResolvedValue(undefined) },
  };
  mocks.post.mockImplementation(async ({ url }: any) => {
    if (url.endsWith('jieshu-fasong')) attempt.state = 'CLOSED';
    if (url.endsWith('queren-pici')) attempt.state = 'CONFIRMED';
    return { code: 200, data: { ...attempt } };
  });
  mocks.print.mockImplementation(async () => {
    attempt.state = 'CONFIRMED';
  });
  mocks.preview.mockResolvedValue(true);
});
describe('原生重打成功后登记及恢复', () => {
  it('在线配置格式贯穿重打入口、原生表单、预览和实际发送,号码保持原流水', async () => {
    const builder = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: `原件 ${serialNumber} / ${countSerialNumber || '历史无计数号'}` }];
    mocks.get.mockResolvedValue({ code: 200, data: { locked: false, printRecordId: 'record' } });
    await openDmsReprint('record', undefined, builder, ['RECORD', 'REPORT']);
    context.params = mocks.open.mock.calls[0]![0].params;
    expect(context.params.buildCustomBarcode).toBe(builder);
    expect(context.params.watermarkBusinessTypes).toEqual(['RECORD', 'REPORT']);
    await submitDmsReprint(context);
    const preview = mocks.preview.mock.calls[0]![0];
    expect(preview.watermarkType).toBe('print');
    expect(preview.watermarkBusinessTypes).toEqual(['RECORD', 'REPORT']);
    expect(preview.customBarcode).toEqual([{ barValue: 'original', barText: '原件 original / 20260001' }]);
    await preview.reprint.send();
    expect(mocks.print.mock.calls[0]![1].buildCustomBarcode).toBe(builder);
    expect(mocks.print.mock.calls[0]![1].watermarkBusinessTypes).toEqual(['RECORD', 'REPORT']);
    expect(mocks.print.mock.calls[0]![2].numbersJson).toBe(attempt.numbersJson);
  });
  it('共享格式接口失败时保留原重打任务,不打开预览、不发送、不提前登记', async () => {
    const builder = async () => { throw new Error('格式接口不可用'); };
    mocks.get.mockResolvedValue({ code: 200, data: { locked: false, printRecordId: 'record' } });
    await openDmsReprint('record', undefined, builder);
    context.params = mocks.open.mock.calls[0]![0].params;
    const result = await submitDmsReprint(context);
    expect(result.msg).toContain('格式接口不可用');
    expect(mocks.preview).not.toHaveBeenCalled();
    expect(mocks.print).not.toHaveBeenCalled();
    expect(submit).not.toHaveBeenCalled();
  });
  it('历史无计数号重打和已保存登记恢复仍使用在线配置,不补号', async () => {
    const builder = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: countSerialNumber || `历史 ${serialNumber}` }];
    attempt.numbersJson = JSON.stringify([{ serialNumber: 'original' }]);
    attempt.documentId = 'doc';
    mocks.get.mockResolvedValue({ code: 200, data: attempt });
    await resumeDmsReprint('record', 'request', undefined, builder);
    const preview = mocks.preview.mock.calls[0]![0];
    expect(preview.customBarcode).toEqual([{ barValue: 'original', barText: '历史 original' }]);
    await preview.reprint.send();
    expect(mocks.print.mock.calls[0]![1].buildCustomBarcode).toBe(builder);
    expect(mocks.post.mock.calls.some(([req]: any) => req.url.endsWith('/yulan-canshu'))).toBe(false);
  });
  it('占用弹窗及未保存登记恢复均保留格式函数', async () => {
    const builder = ({ serialNumber }: any) => [{ barValue: serialNumber, barText: serialNumber }];
    mocks.get.mockResolvedValueOnce({ code: 200, data: { locked: true } }).mockResolvedValueOnce({ code: 200, data: attempt });
    await openDmsReprint('record', undefined, builder);
    expect(mocks.emit.mock.calls[0]![1].buildCustomBarcode).toBe(builder);
    await resumeDmsReprint('record', 'request', undefined, builder);
    expect(mocks.open.mock.calls[0]![0].params.buildCustomBarcode).toBe(builder);
  });
  it('统一弹窗预填一条固定措施,恢复保持原请求ID', async () => {
    mocks.get.mockResolvedValue({ code: 200, data: attempt });
    await resumeDmsReprint('record', 'request');
    const config = mocks.open.mock.calls[0]![0];
    expect(config.submitMode).toBe('form');
    expect(config.params.requestId).toBe('request');
    expect(config.params.bupingheng_mingxi).toEqual([{ dayin_jilu_id: 'record', cuoshi: 'cuoshi3', yuanyin: 'original reason' }]);
  });
  it('查询未锁定后才打开原生重打表单', async () => {
    mocks.get.mockResolvedValue({ code: 200, data: { locked: false, printRecordId: 'record' } });
    await openDmsReprint('record');
    expect(mocks.get).toHaveBeenCalledWith({ url: expect.stringContaining('/record/lock-status') });
    expect(mocks.open).toHaveBeenCalledTimes(1);
    expect(mocks.emit).not.toHaveBeenCalled();
  });
  it('已有占用只打开状态弹窗,包括其他发起人的占用', async () => {
    const status = { locked: true, printRecordId: 'record', requestId: 'request', owner: false, phase: 'READY' };
    mocks.get.mockResolvedValue({ code: 200, data: status });
    await openDmsReprint('record');
    expect(mocks.open).not.toHaveBeenCalled();
    expect(mocks.emit).toHaveBeenCalledWith(REPRINT_LOCK_EVENT, { status, onConfirm: undefined, buildCustomBarcode: undefined });
  });
  it('刷新状态只补交已保存回执,不打印也不自行登记', async () => {
    attempt.state = 'OPEN';
    attempt.batchesJson = JSON.stringify([{ id: 'batch', state: 'SENDING' }]);
    receiptStore.set('dms-reprint-receipt:0:actor:request:batch', 'token');
    mocks.get.mockResolvedValueOnce({ code: 200, data: attempt }).mockResolvedValueOnce({ code: 200, data: { locked: true, phase: 'REGISTRATION_PENDING' } });
    const result = await refreshDmsReprintLock({ locked: true, printRecordId: 'record', requestId: 'request', owner: true });
    expect(result.phase).toBe('REGISTRATION_PENDING');
    expect(mocks.post).toHaveBeenCalledWith(
      expect.objectContaining({ url: expect.stringContaining('queren-pici'), data: expect.objectContaining({ token: 'token' }) }),
      expect.anything(),
    );
    expect(mocks.print).not.toHaveBeenCalled();
    expect(submit).not.toHaveBeenCalled();
    expect(receiptStore.size).toBe(0);
  });
  it('取消只使用原任务ID,后端拒绝时不打开新表单', async () => {
    mocks.post.mockResolvedValue({ code: 400, msg: '任务已派发,不能取消' });
    await expect(cancelDmsReprint('record', 'request')).rejects.toThrow('不能取消');
    expect(mocks.open).not.toHaveBeenCalled();
    expect(mocks.print).not.toHaveBeenCalled();
  });
  it('原任务已消失时恢复不偷偷创建新任务', async () => {
    mocks.get.mockResolvedValue({ code: 200, data: null });
    await expect(resumeDmsReprint('record', 'request')).rejects.toThrow('状态已变化');
    expect(mocks.open).not.toHaveBeenCalled();
  });
  it('先暂存关闭表单再打开预览,手动发送成功后才原生登记', async () => {
    await expect(submitDmsReprint(context)).resolves.toMatchObject({ code: 200, dmsReprintPrepared: true });
    expect(submit).not.toHaveBeenCalled();
    expect(data.biz_sign).toBe('original-sign');
    expect(data.dengji_ren_id).toBe('actor');
    expect(data.bupingheng_mingxi[0].yuanyin).toBe('original reason');
    expect(context.formContext.closeReprintForm.mock.invocationCallOrder[0]!).toBeLessThan(mocks.preview.mock.invocationCallOrder[0]!);
    expect(mocks.print).not.toHaveBeenCalled();
    expect(mocks.preview.mock.calls[0]![0]).toMatchObject({
      wenjianZhongxinId: 'version',
      printOptions: { copies: 1 },
      customBarcode: [{ barValue: 'original', barText: '打印流水号: original  计数流水号: 20260001' }],
    });
    await mocks.preview.mock.calls[0]![0].reprint.send();
    expect(mocks.print).toHaveBeenCalledTimes(1);
    expect(submit).toHaveBeenCalledTimes(1);
    expect(mocks.print.mock.invocationCallOrder[0]!).toBeLessThan(submit.mock.invocationCallOrder[0]!);
    expect(attempt.state).toBe('CLOSED');
  });
  it('打印成功登记失败保留确认状态,重试只补登记不再派发', async () => {
    submit.mockImplementation((_: any, reject: any) => reject(new Error('save failed')));
    await submitDmsReprint(context);
    const send = mocks.preview.mock.calls[0]![0].reprint.send;
    await expect(send()).rejects.toThrow('save failed');
    expect(attempt.state).toBe('CONFIRMED');
    expect(mocks.post.mock.calls.some(([r]: any[]) => r.url.endsWith('jieshu-fasong'))).toBe(false);
    submit.mockImplementation((resolve: any) => {
      attempt.documentId = 'doc';
      resolve({ code: 200 });
    });
    await send();
    expect(mocks.print).toHaveBeenCalledTimes(1);
    expect(attempt.state).toBe('CLOSED');
  });
  it('发送成功后原生保存响应丢失按唯一关联恢复,不重复创建', async () => {
    submit.mockImplementation((_: any, reject: any) => {
      attempt.documentId = 'doc';
      reject(new Error('network'));
    });
    await submitDmsReprint(context);
    await mocks.preview.mock.calls[0]![0].reprint.send();
    expect(submit).toHaveBeenCalledTimes(1);
    expect(attempt.state).toBe('CLOSED');
  });
  it('原生非200响应不得标记登记完成,保留已发送事实', async () => {
    submit.mockImplementation((resolve: any) => resolve({ code: 400, msg: 'save failed' }));
    await submitDmsReprint(context);
    await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('save failed');
    expect(attempt.state).toBe('CONFIRMED');
    expect(mocks.emit).not.toHaveBeenCalledWith('DMS_REPRINT_COMPLETED', expect.anything());
  });
  it('预览打开失败保留技术任务,不生成不平衡登记', async () => {
    mocks.preview.mockRejectedValue(new Error('preview failed'));
    await expect(submitDmsReprint(context)).resolves.toMatchObject({ code: 200, dmsReprintPrepared: true, msg: expect.stringContaining('预览打开失败') });
    expect(submit).not.toHaveBeenCalled();
    expect(mocks.print).not.toHaveBeenCalled();
    expect(attempt.state).toBe('OPEN');
  });
  it('未发送就取消只结束技术任务,不创建登记', async () => {
    await submitDmsReprint(context);
    mocks.post.mockImplementation(async () => {
      attempt.state = 'CANCELLED';
      return { code: 200, data: attempt };
    });
    await cancelDmsReprint('record', 'request');
    await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('已取消');
    expect(submit).not.toHaveBeenCalled();
    expect(mocks.print).not.toHaveBeenCalled();
  });
  it('已确认任务恢复表单后直接补登记,复用冻结签名且不打开预览', async () => {
    attempt.state = 'CONFIRMED';
    await expect(submitDmsReprint(context)).resolves.toMatchObject({ code: 200, data: { id: 'doc' } });
    expect(data.biz_sign).toBe('original-sign');
    expect(submit).toHaveBeenCalledTimes(1);
    expect(mocks.preview).not.toHaveBeenCalled();
    expect(mocks.print).not.toHaveBeenCalled();
  });
  it('迟到及并发成功回执只创建一次原生登记', async () => {
    let transport: any;
    mocks.print.mockImplementation(async (_: any, __: any, ___: any, t: any) => {
      transport = t;
      throw new Error('timeout');
    });
    await submitDmsReprint(context);
    await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('timeout');
    expect(submit).not.toHaveBeenCalled();
    const b = { id: 'batch', state: 'SENDING' };
    await Promise.all([transport.submitted(attempt, b, 'token'), transport.submitted(attempt, b, 'token')]);
    expect(submit).toHaveBeenCalledTimes(1);
    expect(attempt.state).toBe('CLOSED');
  });
  it('暂存失败不关闭填写窗口,也不保存或打开预览', async () => {
    mocks.post.mockRejectedValue(new Error('prepare failed'));
    await expect(submitDmsReprint(context)).rejects.toThrow('prepare failed');
    expect(context.formContext.closeReprintForm).not.toHaveBeenCalled();
    expect(submit).not.toHaveBeenCalled();
    expect(mocks.preview).not.toHaveBeenCalled();
  });
  it('部分回传或结果未知时不登记,全部成功才登记', async () => {
    let transport: any;
    mocks.print.mockImplementation(async (_: any, __: any, ___: any, t: any) => {
      transport = t;
      throw new Error('unknown');
    });
    await submitDmsReprint(context);
    await expect(mocks.preview.mock.calls[0]![0].reprint.send()).rejects.toThrow('unknown');
    mocks.post.mockImplementation(async () => ({ code: 200, data: { ...attempt } }));
    await transport.submitted(attempt, { id: 'partial', state: 'SENDING' }, 'token');
    expect(submit).not.toHaveBeenCalled();
    attempt.state = 'CONFIRMED';
    mocks.post.mockImplementation(async ({ url }: any) => {
      if (url.endsWith('jieshu-fasong')) attempt.state = 'CLOSED';
      return { code: 200, data: { ...attempt } };
    });
    await transport.submitted(attempt, { id: 'final', state: 'SENDING' }, 'token');
    expect(submit).toHaveBeenCalledTimes(1);
    expect(attempt.state).toBe('CLOSED');
  });
  it('已有登记恢复直接打开原任务预览,不再次签名登记或自动打印', async () => {
    attempt.documentId = 'doc';
    mocks.get.mockResolvedValue({ code: 200, data: attempt });
    await resumeDmsReprint('record', 'request');
    expect(mocks.open).not.toHaveBeenCalled();
    expect(mocks.preview).toHaveBeenCalledTimes(1);
    expect(submit).not.toHaveBeenCalled();
    expect(mocks.print).not.toHaveBeenCalled();
  });
  it('同请求并发提交仅暂存并打开一次预览', async () => {
    const first = submitDmsReprint(context);
    const second = submitDmsReprint(context);
    expect(first).toBe(second);
    await first;
    expect(submit).not.toHaveBeenCalled();
    expect(mocks.preview).toHaveBeenCalledTimes(1);
  });
  it('无已保存登记时即使调用发送也不打印', async () => {
    await expect(sendDmsReprint(attempt)).rejects.toThrow('恢复原生登记上下文');
    expect(mocks.print).not.toHaveBeenCalled();
  });
  it('未知结果不能登记或结束任务', async () => {
    attempt.documentId = 'doc';
    mocks.print.mockRejectedValue(new Error('结果未知'));
    await expect(sendDmsReprint(attempt)).rejects.toThrow('结果未知');
    expect(submit).not.toHaveBeenCalled();
    expect(mocks.post.mock.calls.some(([r]: any[]) => r.url.endsWith('jieshu-fasong'))).toBe(false);
  });
  it('旧流程已有登记的迟到回传复用原单据', async () => {
    attempt.documentId = 'doc';
    let transport: any;
    mocks.print.mockImplementation(async (_: any, __: any, ___: any, t: any) => {
      transport = t;
      throw new Error('timeout');
    });
    await expect(sendDmsReprint(attempt)).rejects.toThrow('timeout');
    const b = { id: 'batch', items: [0], pages: [1], state: 'SENDING' };
    await transport.submitted(attempt, b, 'token');
    expect(mocks.emit).toHaveBeenCalledWith('DMS_REPRINT_COMPLETED', { printRecordId: 'record', requestId: 'request' });
    expect(submit).not.toHaveBeenCalled();
    expect(receiptStore.size).toBe(0);
  });
  it('发送成功但更新执行失败,重试读取确认状态后仅更新执行', async () => {
    attempt.documentId = 'doc';
    mocks.post.mockImplementation(async ({ url }: any) => {
      if (url.endsWith('jieshu-fasong')) throw new Error('finish failed');
      return { code: 200, data: { ...attempt } };
    });
    await expect(sendDmsReprint(attempt)).rejects.toThrow('finish failed');
    expect(attempt.state).toBe('CONFIRMED');
    // 真实传输层对CONFIRMED直接返回;模拟此契约,不派发新批次。
    mocks.print.mockImplementation(async (_: any, __: any, original: any) => {
      expect(original.state).toBe('CONFIRMED');
    });
    mocks.post.mockImplementation(async ({ url }: any) => {
      if (url.endsWith('jieshu-fasong')) attempt.state = 'CLOSED';
      return { code: 200, data: { ...attempt } };
    });
    await sendDmsReprint(attempt);
    expect(attempt.state).toBe('CLOSED');
    expect(submit).not.toHaveBeenCalled();
  });
});
describe('重打弹窗专属配置', () => {
  it('仅目标入口隐藏子表增删按钮并锁恢复原因', () => {
    const form = {
      fields: [
        {
          __vModel__: 'bupingheng_mingxi',
          columnBtnsList: [{ show: true }],
          footerBtnsList: [{ show: true }],
          __config__: { children: [{ __vModel__: 'cuoshi' }, { __vModel__: 'yuanyin' }] },
        },
      ],
    };
    configureReprintModal('other', form, { dmsReprint: true });
    expect(form.fields[0]!.columnBtnsList[0]!.show).toBe(true);
    configureReprintModal(DMS_REPRINT_MODEL_ID, form, { dmsReprint: true, recovering: true });
    expect(form.fields[0]!.columnBtnsList[0]!.show).toBe(false);
    expect(form.fields[0]!.footerBtnsList[0]!.show).toBe(false);
    expect((form.fields[0]!.__config__.children[1] as any).disabled).toBe(true);
  });
  it('仅已冻结任务恢复免再次签名,首次确认仍要求原生签名', () => {
    const form = { confirmBtnConfig: { biz_sign_enabled: true }, fields: [] };
    configureReprintModal(DMS_REPRINT_MODEL_ID, form, { dmsReprint: true });
    expect(form.confirmBtnConfig.biz_sign_enabled).toBe(true);
    configureReprintModal(DMS_REPRINT_MODEL_ID, form, { dmsReprint: true, recovering: true });
    expect(form.confirmBtnConfig.biz_sign_enabled).toBe(false);
  });
  it('恢复使用冻结签名作为原生保存审计关联,其他业务不变', () => {
    expect(reprintAuditSign(DMS_REPRINT_MODEL_ID, data, 'current-sign')).toBe('new-sign');
    expect(reprintAuditSign('other', data, 'current-sign')).toBe('current-sign');
  });
});