feat: 新增配送合同附件安全上传功能
- 增加合同专用 PDF 上传、鉴权预览和失败清理接口 - 支持草稿附件替换移除、并发保护和启用完整性校验 - 修复循环模板引用导致文件选择器无法打开的问题 - 补充专项测试、中文项目文档和操作日志
This commit is contained in:
74
frontend/platform_admin/src/api/contract-attachment.ts
Normal file
74
frontend/platform_admin/src/api/contract-attachment.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 功能:配送合同 PDF 附件的上传、失败清理与鉴权预览客户端。
|
||||
* 版本:v1.0.0
|
||||
*/
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
const platformApiBaseURL =
|
||||
import.meta.env.VITE_API_BASE_URL ||
|
||||
'http://localhost:12426/heqi/platform/v1';
|
||||
|
||||
export type ContractAttachmentMetadata = {
|
||||
has_file: boolean;
|
||||
available: boolean;
|
||||
requires_reupload: boolean;
|
||||
display_name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type ContractAttachmentUploadReply = {
|
||||
receipt: string;
|
||||
cleanup_token: string;
|
||||
display_name: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
type ApiEnvelope<T> = { code?: number; message?: string; details?: T };
|
||||
|
||||
/** 返回当前登录凭证请求头。 */
|
||||
function authorizationHeaders(): Record<string, string> {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: token } : {};
|
||||
}
|
||||
|
||||
/** 上传经过前端预检的单个 PDF,服务端仍会验证真实内容。 */
|
||||
async function upload(file: File): Promise<ContractAttachmentUploadReply> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const response = await fetch(
|
||||
`${platformApiBaseURL}/gasorder_contract/attachment/upload`,
|
||||
{ method: 'POST', headers: authorizationHeaders(), body: form },
|
||||
);
|
||||
const payload = (await response.json()) as ApiEnvelope<ContractAttachmentUploadReply>;
|
||||
if (!response.ok || payload.code !== 0 || !payload.details) {
|
||||
throw new Error(payload.message || '合同附件上传失败');
|
||||
}
|
||||
return payload.details;
|
||||
}
|
||||
|
||||
/** 使用受签名保护的清理凭证删除尚未绑定的临时文件。 */
|
||||
async function cleanup(cleanupToken: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${platformApiBaseURL}/gasorder_contract/attachment/cleanup`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authorizationHeaders() },
|
||||
body: JSON.stringify({ cleanup_token: cleanupToken }),
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error('临时合同附件清理失败');
|
||||
const payload = (await response.json()) as ApiEnvelope<{ deleted: boolean }>;
|
||||
if (payload.code !== 0) throw new Error(payload.message || '临时合同附件清理失败');
|
||||
}
|
||||
|
||||
/** 获取鉴权 PDF Blob;内部存储 URI 始终不会暴露给浏览器。 */
|
||||
async function load(identity: string): Promise<Blob> {
|
||||
const response = await fetch(
|
||||
`${platformApiBaseURL}/gasorder_contract/${encodeURIComponent(identity)}/attachment`,
|
||||
{ headers: authorizationHeaders() },
|
||||
);
|
||||
if (!response.ok) throw new Error('合同附件不可用,请重新上传');
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
export const contractAttachmentApi = { upload, cleanup, load };
|
||||
@@ -22,7 +22,8 @@ export type ResourceFieldType =
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'textarea'
|
||||
| 'select';
|
||||
| 'select'
|
||||
| 'contract-file';
|
||||
|
||||
/** 关联下拉的父子联动配置;未配置的资源继续保持独立选择。 */
|
||||
export type ResourceRelationLinkage = {
|
||||
@@ -458,10 +459,12 @@ export const resources: ResourceUiDefinition[] = [
|
||||
filterKey: 'gas_basic_identities', backfillParent: true,
|
||||
},
|
||||
}),
|
||||
f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'),
|
||||
f('title', { required: true }), f('terms'),
|
||||
f('file_uri', { label: '合同附件', type: 'contract-file' }),
|
||||
f('default_delivery_fee'),
|
||||
f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at'),
|
||||
], 'list', [
|
||||
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } },
|
||||
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0, 13] } },
|
||||
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12] } },
|
||||
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
|
||||
], { canCreate: true, canEdit: true }),
|
||||
|
||||
Reference in New Issue
Block a user