feat: 存储空间管理、虚拟支付接入与容量守卫

- 新增存储空间管理:会员套餐配置、用户容量/到期管理、后台支付记录查询
- 接入个人主体小程序虚拟支付:下单/发货推送/兜底查单(query_order)
- 修复 adminStorageUsers 等接口鉴权、out_trade_no 长度、前端 requestVirtualPayment 参数
- 前端上传前容量预检,超容引导升级无限空间
- 我的页会员卡片展示「无限空间」与已用容量/到期时间
- 后台「存储空间管理」新增支付记录标签页
This commit is contained in:
CodeBuddy
2026-10-30 23:45:39 +08:00
parent b7c6224e54
commit bf99e52937
41 changed files with 2387 additions and 108 deletions
+85 -29
View File
@@ -5,12 +5,21 @@ const { getLayout } = require('../../utils/layout');
const { api, getImageUrl } = require('../../utils/request');
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
const { requestSubscribe } = require('../../utils/subscribe');
const { guardStorageSpace } = require('../../utils/storage-guard');
function formatStorageBytes(bytes) {
bytes = bytes || 0;
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
}
Page(withDataCache({
onResize: function() { this.setData(getLayout()); },
data: {
scrollTop: 0,
statsLoading:false, statsError:false,
storageText:'—', storagePercent:0, storageOverQuota:false, storageVip:false, storageExpireText:'',
statisticItems:[
{key:'timeline',label:'记忆日',icon:'stat-timeline',count:'—',color:'pink'},
{key:'album',label:'相册',icon:'stat-album',count:'—',color:'yellow'},
@@ -191,6 +200,7 @@ Page(withDataCache({
}
this.loadData();
this.loadProfileStats();
this.loadStorageInfo();
if (!cid && this.data.showInvitePanel && !this._pollTimer) this.startPollCoupleStatus();
// 如果已绑定情侣,关闭邀请面板
if (cid && this.data.showInvitePanel) {
@@ -214,7 +224,7 @@ Page(withDataCache({
},
onPullDownRefresh: async function() {
await Promise.all([this.loadData(),this.loadProfileStats()]);
await Promise.all([this.loadData(),this.loadProfileStats(),this.loadStorageInfo()]);
wx.stopPullDownRefresh();
},
@@ -486,25 +496,29 @@ Page(withDataCache({
},
chooseAvatar: function() {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const filePath = res.tempFiles[0].path;
this.setData({ editAvatarUrl: filePath });
// 上传头像
try {
const data = await api.uploadAvatar(filePath);
this.setData({
editAvatarUrl: getImageUrl(data.path),
editAvatarPath: data.path
});
wx.showToast({ title: '头像上传成功 ✨', icon: 'success' });
} catch (e) {
wx.showToast({ title: e.error || '上传失败', icon: 'none' });
const self = this;
guardStorageSpace(0).then(function(ok) {
if (!ok) return;
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const filePath = res.tempFiles[0].path;
self.setData({ editAvatarUrl: filePath });
// 上传头像
try {
const data = await api.uploadAvatar(filePath);
self.setData({
editAvatarUrl: getImageUrl(data.path),
editAvatarPath: data.path
});
wx.showToast({ title: '头像上传成功 ✨', icon: 'success' });
} catch (e) {
wx.showToast({ title: e.error || '上传失败', icon: 'none' });
}
}
}
});
});
},
@@ -640,16 +654,20 @@ Page(withDataCache({
},
choosePartnerAvatar: function() {
if (this.data.partnerSaving || this.data.partnerAvatarUploading || !this._editPartnerId) return;
const self = this;
const partnerId = this._editPartnerId;
wx.chooseImage({count:1,sizeType:['compressed'],sourceType:['album','camera'],success:async res=>{
if (!this.data.showEditPartnerModal || partnerId !== this._editPartnerId) return;
this.setData({partnerAvatarUploading:true});
try {
const uploaded = await api.uploadPartnerAvatar(res.tempFilePaths[0],partnerId);
this.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploaded.path});
} catch(error) { wx.showToast({title:error.error || '头像上传失败',icon:'none'}); }
finally { this.setData({partnerAvatarUploading:false}); }
}});
guardStorageSpace(0).then(function(ok) {
if (!ok) return;
wx.chooseImage({count:1,sizeType:['compressed'],sourceType:['album','camera'],success:async res=>{
if (!self.data.showEditPartnerModal || partnerId !== self._editPartnerId) return;
self.setData({partnerAvatarUploading:true});
try {
const uploaded = await api.uploadPartnerAvatar(res.tempFilePaths[0],partnerId);
self.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploaded.path});
} catch(error) { wx.showToast({title:error.error || '头像上传失败',icon:'none'}); }
finally { self.setData({partnerAvatarUploading:false}); }
}});
});
},
savePartnerProfile: async function() {
const d = this.data;
@@ -1014,6 +1032,44 @@ Page(withDataCache({
this.setData({ showShareBindModal: false });
},
loadStorageInfo: async function() {
try {
const d = await api.storageInfo();
const freeBytes = d.free_bytes || d.freeBytes || 0;
const usedBytes = d.used_bytes || d.usedBytes || 0;
const isVip = !!(d.is_vip || d.isVip);
const usedText = formatStorageBytes(usedBytes);
if (isVip) {
// 会员:展示「无限空间」+ 已用容量 + 到期时间,不再提示空间已满。
const expire = d.vip_expire_at || d.vipExpireAt;
this.setData({
storageVip: true,
storageText: '无限空间',
storagePercent: 0,
storageOverQuota: false,
storageUsedText: usedText,
storageExpireText: expire ? ('有效期至 ' + String(expire).slice(0, 10)) : '已开通无限空间'
});
} else {
const percent = freeBytes > 0 ? Math.min(100, Math.round(usedBytes / freeBytes * 100)) : 0;
this.setData({
storageVip: false,
storageText: usedText,
storagePercent: percent,
storageOverQuota: usedBytes > freeBytes,
storageUsedText: usedText,
storageExpireText: ''
});
}
} catch (e) {
console.error('加载存储信息失败', e);
}
},
goStorage: function() {
this._savePosBeforeLeave();
wx.navigateTo({ url: '/pages/storage/storage' });
},
loadProfileStats: async function() {
this.setData({statsLoading:true,statsError:false});
const keys=['timeline','album','diary','songs','checkin','pet'];
@@ -1046,4 +1102,4 @@ Page(withDataCache({
manageRelationship: function() { wx.navigateTo({ url: '/pages/unbind/unbind' }); },
goToSettings: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/settings/settings' }); },
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"]}));
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"], "loadStorageInfo": ["storageInfo"]}));