Files
wechat-clock-mini/utils/api.js
王宏豪 a76696c37b feat(api): 修改oss
- 替换开发环境API基础URL为本地服务地址
- 实现签名URL本地缓存机制,解决微信按URL缓存图片导致重复下载问题
- 添加缓存清理功能,包括过期清理和按需删除特定缓存项
- 集成阿里云OSS上传功能,支持直传减少服务器带宽占用
- 优化头像地址处理逻辑,统一使用签名缓存避免地址频繁变化
- 移除过时的OSS上传示例文件oss-upload.js
- 更新课程页面实现跳转第三方小程序功能
- 调整打卡记录图片地址获取方式,统一使用新的签名缓存逻辑
- 优化图片上传流程,增加压缩处理和批量上传支持
- 完善认证模块中的缓存清理逻辑,切换账号时清除相关图片缓存
- 添加后端环境变量配置模板和代理文档说明
2026-10-10 10:26:59 +08:00

277 lines
9.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.
// 根据小程序版本自动选择后端地址
function _getBaseUrls() {
let envType = 'release';
try {
const accountInfo = wx.getAccountInfoSync();
envType = accountInfo.miniProgram.envVersion || 'release';
} catch (e) {
// 低版本基础库不支持时默认线上环境
envType = 'release';
}
switch (envType) {
case 'develop':
// 开发版:本地服务
return {
baseUrl: 'https://test.muguaclub.cn/clock',
//baseUrl: 'http://localhost:8000',
staticUrl: 'https://test.muguaclub.cn',
h5Url: 'https://test.muguaclub.cn'
};
case 'trial':
// 体验版:测试环境
return {
baseUrl: 'https://test.muguaclub.cn/clock',
staticUrl: 'https://test.muguaclub.cn',
h5Url: 'https://test.muguaclub.cn'
};
case 'release':
default:
// 线上版:正式环境
return {
baseUrl: 'https://m.muguaclub.cn/clock',
staticUrl: 'https://m.muguaclub.cn',
h5Url: 'https://m.muguaclub.cn'
};
}
}
const _urls = _getBaseUrls();
const BASE_URL = _urls.baseUrl;
// ⚠️ 图片**不再**用它拼地址了:图片只在 OSS 上,展示地址一律由后端签发
// (attachment_urls / avatar_url)。保留它是因为它和 H5_BASE_URL 同属"部署域名"描述。
const STATIC_BASE_URL = _urls.staticUrl;
const H5_BASE_URL = _urls.h5Url;
let redirectingProfile = false;
function goProfile() {
if (redirectingProfile) {
return;
}
const pages = getCurrentPages();
const current = pages.length ? pages[pages.length - 1] : null;
const route = current && current.route;
if (route === 'pages/profile/profile') {
return;
}
redirectingProfile = true;
wx.switchTab({
url: '/pages/profile/profile',
complete() {
setTimeout(() => {
redirectingProfile = false;
}, 1200);
}
});
}
function request(options) {
const { url, method = 'GET', data = {}, auth = true } = options;
const isIdempotent = method === 'GET' || method === 'DELETE';
let retried = false;
return new Promise((resolve, reject) => {
const doRequest = () => {
const header = { 'Content-Type': 'application/json' };
const token = wx.getStorageSync('token');
if (auth && token) {
header['Authorization'] = 'Bearer ' + token;
}
wx.request({
url: BASE_URL + url,
method,
data,
header,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data);
} else if (res.statusCode === 401) {
wx.removeStorageSync('token');
if (auth) {
goProfile();
}
reject(res.data || { error: '未登录' });
} else {
reject(res.data || { error: '请求失败' });
}
},
fail(err) {
// 冷启动时首个请求可能因网络栈未就绪而失败,空闲时对幂等请求自动重试一次
const msg = (err && err.errMsg) || '';
const domainError = /url not in domain list|domain not/i.test(msg);
if (!retried && isIdempotent && !domainError) {
retried = true;
setTimeout(doRequest, 800);
return;
}
reject(err);
}
});
};
doRequest();
});
}
// ── 签名 URL 本地缓存 ─────────────────────────────────────────────────────
// 为什么值得缓存:微信 <image> 是**按 URL** 缓存图片的。后端每次现签的 URL 里
// Expires 都不同(= 现在 + 有效期),URL 一变,微信就当新图**重新下载整个文件**。
// 把 URL 留在本地复用,同一张图在窗口期内才是同一个地址,图片缓存才真正生效。
//
// 判断失效**不需要后端额外给字段**:签名 URL 自带绝对过期的 `Expires` 参数,
// 前端解析它就够了。
const SIGN_CACHE_KEY = 'oss_signed_urls_v1';
const SIGN_CACHE_MAX = 400; // 条数上限,超出时丢最早过期的
const SIGN_CACHE_SKEW = 3600; // 提前 1 小时判过期,留出时钟偏差与请求耗时
let _signCache = null;
function _loadSignCache() {
if (_signCache) return _signCache;
let raw = null;
try {
raw = wx.getStorageSync(SIGN_CACHE_KEY);
} catch (e) {
raw = null;
}
_signCache = raw && typeof raw === 'object' ? raw : {};
return _signCache;
}
function _saveSignCache() {
try {
wx.setStorageSync(SIGN_CACHE_KEY, _signCache || {});
} catch (e) {
// 写失败(配额满等)不影响展示,只是下次还得重新签
}
}
/** 签名 URL 里的绝对过期时间戳(秒);解析不到返回 0(当作不可用)。 */
function _expiresAt(url) {
const m = /[?&]Expires=(\d+)/.exec(String(url || ''));
return m ? Number(m[1]) : 0;
}
function _isFresh(url) {
const exp = _expiresAt(url);
return !!exp && Date.now() / 1000 + SIGN_CACHE_SKEW < exp;
}
function _pruneSignCache(cache) {
const keys = Object.keys(cache);
if (keys.length <= SIGN_CACHE_MAX) return;
keys.sort((a, b) => _expiresAt(cache[a]) - _expiresAt(cache[b]));
keys.slice(0, keys.length - SIGN_CACHE_MAX).forEach((k) => { delete cache[k]; });
}
/**
* 取一个相对路径的展示地址。
* @param {string} relPath 规范相对路径(attachments/…);外链原样返回
* @param {string} freshUrl 后端本次随记录返回的签名 URL(可能为空串)
*
* 顺序:**缓存未过期就用缓存**(这是 URL 能稳定的关键),否则用 freshUrl 并写回缓存;
* 两者都不能用则返回空串 —— 调用方当作「这张图不显示」。**没有静态地址可退**:
* 图片只在 OSS 上,nginx 那条路已经拆掉了。
*/
function signedOrFresh(relPath, freshUrl) {
if (!relPath) return '';
if (/^https?:\/\//i.test(relPath)) return relPath;
const cache = _loadSignCache();
const hit = cache[relPath];
if (hit && _isFresh(hit)) return hit;
const fresh = String(freshUrl || '');
if (_isFresh(fresh)) {
cache[relPath] = fresh;
_pruneSignCache(cache);
_saveSignCache();
return fresh;
}
// ⚠️ 「解析不出过期时间」≠「这个 URL 不能用」。后端**刚签出来**的地址必然有效,
// 只是格式我们不认识(例如签名版本切到 V4 后参数名会变成 x-oss-expires)。
// 所以这里必须仍然用它 —— 直接退回静态地址,在 OSS 化之后就是**裂图**。
if (fresh) return fresh;
// 缓存已过期、后端这次也没给签名:清掉过期项,交给调用方降级
if (hit) {
delete cache[relPath];
_saveSignCache();
}
return '';
}
/**
* 删掉某一条缓存(不动其它)。
*
* 两个**必须**调用它的时机,漏了都会出现"显示旧图":
* · 换了头像:key 没变、内容变了。不清掉就会一直沿用旧 URL,
* 而微信是按 URL 缓存图片的 → 用户看到旧头像。
* · 删了附件:之后若重传同名文件,会命中已删对象的旧地址。
*
* 清掉之后,本函数不需要自己写新值 —— 下一次 `signedOrFresh` 拿到后端给的新签名
* 会自然写回去。
*/
function forgetSignedUrl(relPath) {
const key = String(relPath || '').trim();
if (!key) return;
const cache = _loadSignCache();
if (Object.prototype.hasOwnProperty.call(cache, key)) {
delete cache[key];
_saveSignCache();
}
}
/** 退出登录 / 换账号时调用,避免把上一个人的图片地址留给下一个人。 */
function clearSignedCache() {
_signCache = {};
try {
wx.removeStorageSync(SIGN_CACHE_KEY);
} catch (e) {
// ignore
}
}
/**
* 取一条打卡记录的附件展示地址。
*
* 后端并排给两个字段,**长度与顺序严格一一对应**:
* attachments 规范相对路径。会被原样提交回 /plan/checkin,所以**不能拿来展示**,
* 否则一保存就把带签名的地址写进数据库了。
* attachment_urls OSS 签名 URL,展示专用;OSS 未启用时全是空串。
*
* 取值顺序:本地签名缓存 → 后端这次的签名 → 外链原样 → **空串**。
* 空串表示那张图不显示。**不再退回 nginx 静态地址**:图片只在 OSS 上,静态地址必然
* 404,退过去只会多出一张裂图,反而把「OSS 没配对」这个真问题掩盖掉。
*/
function attachmentThumbs(record) {
const rec = record || {};
const canon = rec.attachments || [];
const urls = rec.attachment_urls || [];
// signedOrFresh 内部先查本地缓存(URL 稳定 → 微信图片缓存才命中),再兜后端这次的
return canon.map((path, i) => signedOrFresh(path, urls[i]));
}
/**
* 头像展示地址。
*
* 缓存与打卡附件共用同一套:缓存 key 用的是库里的原值,所以新旧两种历史格式各自成键,
* 但都指向同一个 OSS 对象:
* avatars/{uid}/avatar.jpg 新格式(等于对象 key)
* /clock/static/avatars/{uid}/avatar.jpg 旧格式,库里仍有存量值
* 微信头像(https://wx.qlogo.cn/...)是外链,原样返回。
* 都取不到就是空串,**不退回 nginx 静态地址**(理由同 attachmentThumbs)。
*/
function avatarUrl(payload) {
const data = payload || {};
const path = data.wechat_avatar || '';
if (/^https?:\/\//i.test(path)) return path;
return signedOrFresh(path, data.avatar_url);
}
module.exports = {
request, attachmentThumbs, avatarUrl, forgetSignedUrl, clearSignedCache,
BASE_URL, STATIC_BASE_URL, H5_BASE_URL
};