// uniapp-ZHSQ —— OSS 直传客户端 // // 用法: // import { uploadOSS } from '@/utils/uploadOSS' // const { objectKey, url } = await uploadOSS({ filePath: tempFilePath, scene: 'avatar' }) // // objectKey: OSS 上的相对 key,存数据库 // // url: 公开访问 URL(如果走签名 URL 显示,前端再单独处理) // // 支持的 scene 由服务端 wechatOssSceneCfg 控制,目前: // - avatar 用户头像 (jpg/jpeg/png/webp,<=5MB) // - merchant 商家入驻资料 (jpg/jpeg/png/webp/pdf,<=10MB) import { RequsetUrl, aliyunOssUrl } from "@/utils/index.js"; const inferExt = (filePath) => { if (!filePath) return ""; const idx = filePath.lastIndexOf("."); if (idx < 0) return ""; return filePath.substring(idx + 1).toLowerCase(); }; const fetchCredential = (scene, ext, bindId) => { return new Promise((resolve, reject) => { const data = { scene, ext }; if (bindId) data.bind_id = bindId; // 客服聊天场景:按会话分目录 uni.request({ url: RequsetUrl + "/api/v1/wechat/oss/credential", method: "POST", header: { Authorization: uni.getStorageSync("ctoken") || "", "Content-Type": "application/json", }, data, success: (res) => { if (res.statusCode !== 200) return reject(new Error("获取上传凭证失败:" + res.statusCode)); const body = res.data || {}; // 兼容服务端通用响应包装 const data = body.data || body; if (!data || !data.host) return reject(new Error("上传凭证字段缺失")); resolve(data); }, fail: (err) => reject(err), }); }); }; const postToOSS = (filePath, cred) => { return new Promise((resolve, reject) => { uni.uploadFile({ url: cred.host, filePath, name: "file", // 必须是 file,OSS PostObject 协议约定 formData: { key: cred.object_key, OSSAccessKeyId: cred.access_key_id, "x-oss-security-token": cred.security_token, policy: cred.policy, signature: cred.signature, success_action_status: "200", }, success: (res) => { // OSS 200 -> 空 body 视为成功 if (res.statusCode === 200 || res.statusCode === 204) { resolve(); } else { reject(new Error("OSS 上传失败:" + res.statusCode + " " + (res.data || ""))); } }, fail: (err) => reject(err), }); }); }; /** * 直传 OSS。 * @param {Object} opts * @param {string} opts.filePath 本地文件临时路径(uni.chooseImage 拿到的 path) * @param {string} opts.scene 上传场景:avatar / merchant * @param {string} [opts.ext] 扩展名(不含点);不传则从 filePath 推断 * @param {boolean} [opts.showLoading=true] * @returns {Promise<{ objectKey: string, url: string }>} */ export const uploadOSS = async ({ filePath, scene, ext, bindId, showLoading = true }) => { if (!filePath) throw new Error("filePath 不能为空"); if (!scene) throw new Error("scene 不能为空"); const finalExt = (ext || inferExt(filePath)).toLowerCase(); if (!finalExt) throw new Error("无法识别文件扩展名"); if (showLoading) uni.showLoading({ title: "上传中", mask: true }); try { const cred = await fetchCredential(scene, finalExt, bindId); await postToOSS(filePath, cred); return { objectKey: cred.object_key, url: aliyunOssUrl + "/" + cred.object_key, }; } finally { if (showLoading) uni.hideLoading(); } }; /** * 兼容旧版 utils/index.js 中 upload(filename, fn) 的回调式 API。 * 内部走 OSS 直传,回调 res 形状对齐旧版(res.data.path 为以 / 开头的 object key, * 调用方 picUrl + res.data.path 可以直接拼成完整 URL)。 * * @param {string} filePath * @param {Function} fn 接收 { code, msg, data: { path } } * @param {string} [scene='merchant'] */ export const uploadOSSCompat = (filePath, fn, scene = "merchant") => { uploadOSS({ filePath, scene }) .then(({ objectKey }) => { fn && fn({ code: 1, msg: "ok", data: { path: "/" + objectKey } }); }) .catch((err) => { console.error("uploadOSSCompat 失败:", err); uni.showToast({ title: err.message || "上传文件失败", icon: "none" }); }); }; /** * 取私密文件查看 URL(小程序商家本人查看自己上传的证件 / 合同)。 * 服务端会校验 path 必须以 wechat/merchant_private/{当前 user_id}/ 开头。 * * @param {string} objectKey 存数据库的 path(带或不带前导 /) * @returns {Promise<{ url: string, expireSeconds: number }>} */ export const signPrivateView = (objectKey) => { return new Promise((resolve, reject) => { if (!objectKey) return reject(new Error("object_key 不能为空")); const cleanKey = objectKey.replace(/^\/+/, ""); uni.request({ url: RequsetUrl + "/api/v1/wechat/oss/sign-private-view", method: "POST", header: { Authorization: uni.getStorageSync("ctoken") || "", "Content-Type": "application/json", }, data: { object_key: cleanKey }, success: (res) => { if (res.statusCode !== 200) return reject(new Error("签发失败:" + res.statusCode)); const body = res.data || {}; const data = body.data || body; if (!data || !data.url) return reject(new Error("签发结果字段缺失")); resolve({ url: data.url, expireSeconds: data.expire_seconds }); }, fail: (err) => reject(err), }); }); };