- 替换开发环境API基础URL为本地服务地址 - 实现签名URL本地缓存机制,解决微信按URL缓存图片导致重复下载问题 - 添加缓存清理功能,包括过期清理和按需删除特定缓存项 - 集成阿里云OSS上传功能,支持直传减少服务器带宽占用 - 优化头像地址处理逻辑,统一使用签名缓存避免地址频繁变化 - 移除过时的OSS上传示例文件oss-upload.js - 更新课程页面实现跳转第三方小程序功能 - 调整打卡记录图片地址获取方式,统一使用新的签名缓存逻辑 - 优化图片上传流程,增加压缩处理和批量上传支持 - 完善认证模块中的缓存清理逻辑,切换账号时清除相关图片缓存 - 添加后端环境变量配置模板和代理文档说明
277 lines
9.5 KiB
JavaScript
277 lines
9.5 KiB
JavaScript
// 根据小程序版本自动选择后端地址
|
||
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
|
||
};
|