136 lines
4.8 KiB
JavaScript
136 lines
4.8 KiB
JavaScript
/**
|
||
* 阿里云 OSS PostObject 直传(对齐 xk-admin oss-upload.ts)
|
||
*
|
||
* 运维:微信小程序 uploadFile 合法域名需包含 OSS host
|
||
* 例如 https://xiaokang88.oss-cn-hangzhou.aliyuncs.com
|
||
*/
|
||
import { getBusinessOssSignatureApi, unwrapOssSignature, registerBusinessOssFile } from '@/api/upload.js';
|
||
import { checkImage } from '@/common/js/content-security.js';
|
||
|
||
/**
|
||
* @param {string} filePath 本地临时文件路径
|
||
* @returns {string} xk_upload_YYYYMMDD/uuid_timestamp.ext
|
||
*/
|
||
export function generateObjectName(filePath) {
|
||
const today = new Date();
|
||
const year = today.getFullYear();
|
||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||
const day = String(today.getDate()).padStart(2, '0');
|
||
const dateStr = `${year}${month}${day}`;
|
||
|
||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||
const r = (Math.random() * 16) | 0;
|
||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||
return v.toString(16);
|
||
});
|
||
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
const pathStr = String(filePath || '');
|
||
const lastDotIndex = pathStr.lastIndexOf('.');
|
||
const extension = lastDotIndex > 0 ? pathStr.substring(lastDotIndex + 1).toLowerCase() : 'jpg';
|
||
|
||
return `xk_upload_${dateStr}/${uuid}_${timestamp}.${extension}`;
|
||
}
|
||
|
||
function basenameFromPath(filePath) {
|
||
const pathStr = String(filePath || '');
|
||
const parts = pathStr.split(/[/\\]/);
|
||
return parts[parts.length - 1] || '';
|
||
}
|
||
|
||
/**
|
||
* 根据文件路径/文件名推断 Content-Type
|
||
* 小程序本地临时路径通常没有 MIME,必须按扩展名推断,否则 OSS 会落成 octet-stream
|
||
* 与后端 Policy 的 starts-with $Content-Type 配套:表单必须带该字段
|
||
*/
|
||
function resolveUploadContentType(filePath, fileName) {
|
||
const name = String(fileName || filePath || '').toLowerCase();
|
||
if (name.endsWith('.pdf')) return 'application/pdf';
|
||
if (name.endsWith('.png')) return 'image/png';
|
||
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) return 'image/jpeg';
|
||
if (name.endsWith('.gif')) return 'image/gif';
|
||
if (name.endsWith('.webp')) return 'image/webp';
|
||
if (name.endsWith('.bmp')) return 'image/bmp';
|
||
// 医生端直传以图片为主,缺省按 jpeg,避免浏览器当附件下载
|
||
return 'image/jpeg';
|
||
}
|
||
|
||
/**
|
||
* @returns {Promise<{ accessKeyId, policy, signature, host, bucket, key, expire }>}
|
||
*/
|
||
export async function getOssSignatureFromBackend() {
|
||
const res = await getBusinessOssSignatureApi();
|
||
const signature = unwrapOssSignature(res);
|
||
if (!signature) {
|
||
throw new Error('获取 OSS 签名失败');
|
||
}
|
||
return signature;
|
||
}
|
||
|
||
/**
|
||
* @param {{ filePath: string, onProgress?: (percent: number) => void }} options
|
||
* @returns {Promise<{ url: string, objectName: string }>}
|
||
*/
|
||
export async function uploadToOss(options) {
|
||
const { filePath, onProgress, fileSize = 0, fileName = '' } = options || {};
|
||
if (!filePath) {
|
||
throw new Error('文件路径为空');
|
||
}
|
||
|
||
await checkImage(filePath, 1);
|
||
|
||
const signature = await getOssSignatureFromBackend();
|
||
const objectName = generateObjectName(filePath);
|
||
|
||
// Content-Type / Content-Disposition 必须与后端 PostPolicy 条件一致,否则 OSS 报 AccessDenied
|
||
// (starts-with 空前缀要求字段存在;inline 便于浏览器预览而非附件下载)
|
||
const formData = {
|
||
key: objectName,
|
||
policy: signature.policy,
|
||
OSSAccessKeyId: signature.accessKeyId,
|
||
signature: signature.signature,
|
||
success_action_status: '200',
|
||
'x-oss-object-acl': 'public-read',
|
||
'Content-Type': resolveUploadContentType(filePath, fileName),
|
||
'Content-Disposition': 'inline',
|
||
};
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const uploadTask = uni.uploadFile({
|
||
url: signature.host,
|
||
filePath,
|
||
name: 'file',
|
||
formData,
|
||
success: (uploadRes) => {
|
||
const code = uploadRes.statusCode;
|
||
if (code >= 200 && code < 300 || code === 204) {
|
||
const host = String(signature.host).replace(/\/$/, '');
|
||
const url = `${host}/${objectName}`;
|
||
const displayName = fileName || basenameFromPath(filePath);
|
||
registerBusinessOssFile(url, fileSize, displayName).catch((err) => {
|
||
console.warn('OSS 文件登记失败:', err);
|
||
});
|
||
resolve({
|
||
url,
|
||
objectName,
|
||
});
|
||
return;
|
||
}
|
||
reject(new Error(`OSS 上传失败,HTTP ${code}`));
|
||
},
|
||
fail: (err) => {
|
||
reject(err instanceof Error ? err : new Error((err && err.errMsg) || 'OSS 上传失败'));
|
||
},
|
||
});
|
||
|
||
if (onProgress && uploadTask && typeof uploadTask.onProgressUpdate === 'function') {
|
||
uploadTask.onProgressUpdate((res) => {
|
||
if (res.totalBytesExpectedToSend > 0) {
|
||
const percent = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100);
|
||
onProgress(percent);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|