Files
wechat-clock-mini/utils/oss-upload.js
王宏豪 ee3abed69f feat(app): 添加课程页面并优化打卡和计划功能
- 新增 pages/course/course 页面,集成 web-view 显示线上课程列表
- 在 app.json 中注册课程页面并添加底部 tabbar 导航
- 优化打卡页面,添加学号显示和改进成员列表布局
- 重构计划页面,添加内容详情弹层和优化加载逻辑
- 添加阿里云 OSS 上传工具模块用于文件上传功能
- 统一姓名和学号在同一行显示,改善 UI 体验
- 修复多个页面中的绑定事件和样式问题
2026-09-20 16:53:36 +08:00

195 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 阿里云 OSS 上传封装(demo)
*
* 提供两种模式,二选一即可:
* · uploadDirect() 模式B:向后端取签名 → 小程序直接传 OSS(省服务器带宽,推荐)
* · uploadViaServer() 模式A:文件先传后端,由后端转存 OSS(改动最小,兼容现有流程)
*
* 依赖:utils/api.js 里的 request / BASE_URL
*
* ⚠️ 用模式B之前,必须先把 OSS 域名加进微信后台「上传域名(uploadFile 合法域名)」,
* 否则真机上会直接 fail(errMsg: url not in domain list)。
*/
const api = require('./api');
const SIGN_API = '/api/v1/oss-demo/signature';
const CONFIRM_API = '/api/v1/oss-demo/confirm';
const SERVER_UPLOAD_API = '/api/v1/oss-demo/upload';
/** 支持的扩展名(与后端白名单保持一致) */
const ALLOWED_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
/** 单文件上限,需与后端 OSS_MAX_SIZE 一致,前端只是提前拦截省流量 */
const MAX_FILE_SIZE = 10 * 1024 * 1024;
function extOf(path) {
const m = /\.([a-zA-Z0-9]+)(?:\?.*)?$/.exec(path || '');
return m ? m[1].toLowerCase() : '';
}
function baseNameOf(path) {
const noQuery = String(path || '').split('?')[0];
const parts = noQuery.split('/');
return parts[parts.length - 1] || 'upload.jpg';
}
/**
* 前端先做一次类型/大小拦截,避免无谓的上传流量。
* @param {string} filePath
* @param {number} [size] wx.chooseMedia 返回的 size(字节),不传则跳过体积校验
*/
function checkFile(filePath, size) {
const ext = extOf(filePath);
if (ALLOWED_EXT.indexOf(ext) === -1) {
return { ok: false, msg: '仅支持 jpg/png/gif/webp/bmp 图片' };
}
if (size && size > MAX_FILE_SIZE) {
return { ok: false, msg: '单张图片不能超过 ' + Math.round(MAX_FILE_SIZE / 1024 / 1024) + 'MB' };
}
return { ok: true };
}
/**
* 模式B:取签名 → 直传 OSS → 回执后端
* @param {string} filePath wx.chooseMedia 返回的 tempFilePath
* @param {object} options { planType, periodKey, filename, onProgress }
* @returns {Promise<{key:string,url:string,signedUrl:string}>}
*/
function uploadDirect(filePath, options) {
const opts = options || {};
const filename = opts.filename || baseNameOf(filePath);
const guard = checkFile(filePath);
if (!guard.ok) return Promise.reject({ error: guard.msg });
const query = [
'plan_type=' + encodeURIComponent(opts.planType || ''),
'period_key=' + encodeURIComponent(opts.periodKey || ''),
'filename=' + encodeURIComponent(filename)
].join('&');
return api.request({ url: SIGN_API + '?' + query }).then((sign) => {
if (!sign || !sign.success) {
return Promise.reject({ error: (sign && sign.error) || '获取上传签名失败' });
}
// 注意:formData 的字段名由 OSS 约定,不能改名;
// `key` 决定对象落到哪个路径;`file` 必须是最后一个表单域(原生实现已保证)。
const formData = Object.assign({}, sign.form_data, {
key: sign.key,
'x-oss-content-type': sign.content_type || 'image/jpeg'
});
return new Promise((resolve, reject) => {
const task = wx.uploadFile({
// 直传的地址是 OSS Bucket 域名,不是自家后端
url: sign.host,
filePath,
name: 'file',
formData,
timeout: 60000,
success(res) {
// success_action_status=200 时成功即 200,返回体为空
if (res.statusCode === 200 || res.statusCode === 204) {
resolve({ key: sign.key, host: sign.host });
} else {
reject({ error: '上传 OSS 失败(' + res.statusCode + ')', raw: res.data });
}
},
fail(err) {
const msg = (err && err.errMsg) || '';
if (/url not in domain list|domain not/i.test(msg)) {
reject({ error: 'OSS 域名未加入小程序上传域名白名单' });
return;
}
reject({ error: msg || '上传失败' });
}
});
if (opts.onProgress && task && task.onProgressUpdate) {
task.onProgressUpdate((p) => opts.onProgress(p.progress));
}
});
}).then((uploaded) => {
// 直传时后端不知情,必须回执一次:后端会校验对象确实存在,并写业务表
return api.request({
url: CONFIRM_API,
method: 'POST',
data: { key: uploaded.key, size: 0 }
}).then((res) => ({
key: uploaded.key,
url: (res && res.url) || '',
signedUrl: (res && res.signed_url) || ''
}));
});
}
/**
* 模式B-批量:顺序上传(串行避免并发把弱网打爆)
* @returns {Promise<Array<{key:string,url:string}>>}
*/
function uploadDirectBatch(filePaths, options) {
const results = [];
return filePaths.reduce((chain, path) => {
return chain.then(() => uploadDirect(path, options)).then((r) => {
results.push(r);
if (options && options.onItemDone) options.onItemDone(r, results.length);
});
}, Promise.resolve()).then(() => results);
}
/**
* 模式A:走自家后端中转(与现有 plan-submit.js 的写法一致)
* 好处是不用配 OSS 域名白名单,改造时把 url 从 /plan/upload 换成本接口即可。
*/
function uploadViaServer(filePath, options) {
const opts = options || {};
const token = wx.getStorageSync('token');
return new Promise((resolve, reject) => {
wx.uploadFile({
url: api.BASE_URL + SERVER_UPLOAD_API,
filePath,
name: 'files',
header: token ? { Authorization: 'Bearer ' + token } : {},
formData: {
plan_type: opts.planType || 'weekly',
period_key: opts.periodKey || ''
},
success(res) {
let data = {};
try {
data = JSON.parse(res.data);
} catch (e) {
reject({ error: '返回格式异常' });
return;
}
if (res.statusCode >= 200 && res.statusCode < 300 && data.success) {
resolve({ key: (data.attachments || [])[0] || '', items: data.items || [] });
} else {
reject({ error: data.detail || data.error || '上传失败' });
}
},
fail: reject
});
});
}
/**
* 私有 Bucket 取临时可读 URL(给 <image src> 用)
* key 必须是 attachments/{userId}/... 形式
*/
function getFileUrl(key) {
return api.request({
url: '/api/v1/oss-demo/file-url?key=' + encodeURIComponent(key)
}).then((res) => (res && res.url) || '');
}
module.exports = {
uploadDirect,
uploadDirectBatch,
uploadViaServer,
getFileUrl,
ALLOWED_EXT
};