153 lines
5.4 KiB
JavaScript
153 lines
5.4 KiB
JavaScript
// 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),
|
||
});
|
||
});
|
||
};
|