feat: add WeChat Pay, Redis logging, storage tiers and personal albums
Replace virtual payments with WeChat Pay API v3 and verify callbacks and order polling. Cache WeChat access tokens in Redis and write structured Zap logs. Support configurable capacity plans across the API, admin and mini program, with purchase snapshots and quota enforcement. Allow personal albums before binding and share them with the current partner. Update deployment configuration and domain routing.
This commit is contained in:
+2
-2
@@ -1,8 +1,8 @@
|
||||
// 开发工具使用本机服务;真机调试时把 dev.apiBase 改为电脑的局域网 IP。
|
||||
const environments = {
|
||||
dev: { apiBase: 'http://127.0.0.1:8080/api' },
|
||||
trial: { apiBase: 'https://past.sundynix.cn/api' },
|
||||
release: { apiBase: 'https://past.sundynix.cn/api' }
|
||||
trial: { apiBase: 'https://we.sundynix.cn/api' },
|
||||
release: { apiBase: 'https://we.sundynix.cn/api' }
|
||||
};
|
||||
let version = 'release';
|
||||
try {
|
||||
|
||||
@@ -138,7 +138,7 @@ Page(withDataCache(withListPagination({
|
||||
if (!ticket) return;
|
||||
try {
|
||||
var self = this;
|
||||
var albumId = this.data.albumId;
|
||||
var albumId = /^-\d+$/.test(String(this.data.albumId)) ? null : this.data.albumId;
|
||||
if (!albumId) {
|
||||
var albums = itemsOf(await api.albumList({page:1,page_size:1}));
|
||||
if (albums && albums.length > 0) {
|
||||
@@ -322,18 +322,11 @@ Page(withDataCache(withListPagination({
|
||||
await requestSubscribeAfterAction('photo');
|
||||
|
||||
try {
|
||||
// 上传前先预热 session,确保认证状态有效
|
||||
// 解决 Android 端首次上传时 session 未初始化的问题
|
||||
try {
|
||||
wx.showLoading({ title: '准备中...', mask: true });
|
||||
await api.checkAuth();
|
||||
wx.hideLoading();
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
console.warn('Session 预热失败(不影响后续上传)', e);
|
||||
var albumId = /^-\d+$/.test(String(this.data.albumId)) ? null : this.data.albumId;
|
||||
if (!albumId) {
|
||||
const albums = itemsOf(await api.albumList({ page: 1, page_size: 1 }));
|
||||
albumId = albums.length ? albums[0].id : null;
|
||||
}
|
||||
|
||||
var albumId = this.data.albumId;
|
||||
if (!albumId) {
|
||||
wx.showLoading({ title: '创建相册...' });
|
||||
var album = await api.albumAdd({ title: '默认相册', description: '我们的甜蜜照片' });
|
||||
@@ -341,6 +334,7 @@ Page(withDataCache(withListPagination({
|
||||
this.setData({ albumId: albumId });
|
||||
wx.hideLoading();
|
||||
}
|
||||
this.setData({ albumId: albumId });
|
||||
var batchId = Date.now().toString();
|
||||
|
||||
// 自动定位:为未填写位置的照片自动获取地理位置(发布后自动显示)
|
||||
|
||||
@@ -1036,7 +1036,7 @@ Page(withDataCache({
|
||||
loadStorageInfo: async function() {
|
||||
try {
|
||||
const d = await api.storageInfo();
|
||||
const freeBytes = d.free_bytes || d.freeBytes || 0;
|
||||
const freeBytes = d.quota_bytes != null ? d.quota_bytes : (d.quotaBytes != null ? d.quotaBytes : (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);
|
||||
@@ -1045,11 +1045,11 @@ Page(withDataCache({
|
||||
const expire = d.vip_expire_at || d.vipExpireAt;
|
||||
this.setData({
|
||||
storageVip: true,
|
||||
storageText: '无限空间',
|
||||
storageText: (d.is_unlimited || d.isUnlimited) ? '无限空间' : formatStorageBytes(freeBytes),
|
||||
storagePercent: 0,
|
||||
storageOverQuota: false,
|
||||
storageOverQuota: !(d.is_unlimited || d.isUnlimited) && usedBytes>freeBytes,
|
||||
storageUsedText: usedText,
|
||||
storageExpireText: expire ? ('有效期至 ' + String(expire).slice(0, 10)) : '已开通无限空间'
|
||||
storageExpireText: expire ? ('有效期至 ' + String(expire).slice(0, 10)) : '已开通容量套餐'
|
||||
});
|
||||
} else {
|
||||
const percent = freeBytes > 0 ? Math.min(100, Math.round(usedBytes / freeBytes * 100)) : 0;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<view wx:if="{{storageVip}}" class="j-storage-card j-storage-vip" bindtap="goStorage">
|
||||
<view class="j-storage-top">
|
||||
<text class="j-storage-label">存储空间</text>
|
||||
<text class="j-storage-val j-vip">👑 无限空间</text>
|
||||
<text class="j-storage-val j-vip">👑 {{storageText}}</text>
|
||||
</view>
|
||||
<view class="j-storage-bottom">
|
||||
<text class="j-storage-hint">已用 {{storageUsedText}}{{storageExpireText ? ' · ' + storageExpireText : ''}}</text>
|
||||
@@ -28,7 +28,7 @@
|
||||
<view class="j-storage-bar"><view class="j-storage-fill" style="width:{{storagePercent}}%;"></view></view>
|
||||
<view class="j-storage-bottom">
|
||||
<text class="j-storage-hint">{{storageOverQuota ? '已超出免费空间,升级后可继续上传' : '已用 ' + storagePercent + '% 免费空间'}}</text>
|
||||
<text class="j-storage-cta">{{storageOverQuota ? '升级无限空间 ›' : '查看 ›'}}</text>
|
||||
<text class="j-storage-cta">{{storageOverQuota ? '扩容 ›' : '查看 ›'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-statistics">
|
||||
|
||||
@@ -21,12 +21,15 @@ Page({
|
||||
percent: 0,
|
||||
overQuota: false,
|
||||
isVip: false,
|
||||
isUnlimited:false,
|
||||
quotaText:"200 MB",
|
||||
vipExpireText: '',
|
||||
plans: [],
|
||||
paying: false,
|
||||
},
|
||||
|
||||
onLoad: function () { this.setData(getLayout()); },
|
||||
onLoad: function () { this._unloaded = false; this.setData(getLayout()); },
|
||||
onUnload: function () { this._unloaded = true; },
|
||||
onShow: function () { this.loadInfo(); },
|
||||
onPullDownRefresh: function () { this.loadInfo().then(function () { wx.stopPullDownRefresh(); }); },
|
||||
|
||||
@@ -34,7 +37,8 @@ Page({
|
||||
var self = this;
|
||||
this.setData({ loading: true, error: false });
|
||||
return api.storageInfo().then(function (d) {
|
||||
var freeBytes = d.free_bytes || d.freeBytes || 0;
|
||||
var freeBytes = d.quota_bytes != null ? d.quota_bytes : (d.quotaBytes != null ? d.quotaBytes : (d.free_bytes || d.freeBytes || 0));
|
||||
var unlimited=!!(d.is_unlimited || d.isUnlimited);
|
||||
var usedBytes = d.used_bytes || d.usedBytes || 0;
|
||||
var percent = freeBytes > 0 ? Math.min(100, Math.round(usedBytes / freeBytes * 100)) : 0;
|
||||
var overQuota = usedBytes > freeBytes;
|
||||
@@ -43,25 +47,30 @@ Page({
|
||||
return {
|
||||
key: p.key,
|
||||
name: p.name,
|
||||
priceText: (cents / 100).toFixed(cents % 100 === 0 ? 0 : 1),
|
||||
unit: p.key === 'year' ? '年' : '月',
|
||||
productId: p.product_id || p.productId,
|
||||
priceCents:cents,
|
||||
version:p.updated_at || p.updatedAt,
|
||||
priceText: (cents / 100).toFixed(2),
|
||||
unit: (p.months || (p.key==='year'?12:1))+'个月',
|
||||
description: (Number(p.quota_mb != null ? p.quota_mb : p.quotaMb || 0)===0 ? '无限空间' : formatBytes(Number(p.quota_mb != null ? p.quota_mb : p.quotaMb)*1024*1024))+' · 有效期'+(p.months || (p.key==='year'?12:1))+'个月',
|
||||
|
||||
};
|
||||
});
|
||||
var vipExpireText = '';
|
||||
if (d.is_vip || d.isVip) {
|
||||
var expire = d.vip_expire_at || d.vipExpireAt ? String(d.vip_expire_at || d.vipExpireAt).slice(0, 10) : '';
|
||||
vipExpireText = expire ? ('有效期至 ' + expire) : '已开通无限空间';
|
||||
vipExpireText = expire ? ('有效期至 ' + expire) : '已开通容量套餐';
|
||||
}
|
||||
self.setData({
|
||||
loading: false,
|
||||
usedBytes: usedBytes,
|
||||
usedText: formatBytes(usedBytes),
|
||||
freeMb: d.free_mb || d.freeMb || 200,
|
||||
freeMb: d.free_mb != null ? d.free_mb : (d.freeMb != null ? d.freeMb : 200),
|
||||
freeText: formatBytes(freeBytes),
|
||||
percent: percent,
|
||||
overQuota: overQuota,
|
||||
isVip: !!(d.is_vip || d.isVip),
|
||||
isUnlimited:unlimited,
|
||||
quotaText:unlimited?"无限空间":formatBytes(freeBytes),
|
||||
vipExpireText: vipExpireText,
|
||||
plans: plans,
|
||||
});
|
||||
@@ -83,14 +92,14 @@ Page({
|
||||
|
||||
wx.showLoading({ title: '下单中...', mask: true });
|
||||
this.setData({ paying: true });
|
||||
api.storageCreateOrder({ plan_key: key }).then(function (order) {
|
||||
api.storageCreateOrder({ plan_key: key, expected_price_cents:plan.priceCents, expected_plan_version:plan.version }).then(function (order) {
|
||||
wx.hideLoading();
|
||||
if (!order || !(order.sign_data || order.signData)) {
|
||||
self.setData({ paying: false });
|
||||
wx.showToast({ title: '下单失败', icon: 'none' });
|
||||
if (order && order.pay_provider === 'wechat' && order.payment) {
|
||||
self.callWechatPay(order);
|
||||
return;
|
||||
}
|
||||
self.callVirtualPay(order);
|
||||
self.setData({ paying: false });
|
||||
wx.showToast({ title: '支付参数不完整,请重新下单', icon: 'none' });
|
||||
}).catch(function (err) {
|
||||
wx.hideLoading();
|
||||
self.setData({ paying: false });
|
||||
@@ -98,37 +107,54 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
callVirtualPay: function (order) {
|
||||
callWechatPay: function (order) {
|
||||
var self = this;
|
||||
if (!wx.requestVirtualPayment) {
|
||||
var payment = order.payment;
|
||||
if (!wx.requestPayment) {
|
||||
this.setData({ paying: false });
|
||||
wx.showModal({
|
||||
title: '版本过低',
|
||||
content: '请升级微信至最新版本后再试。',
|
||||
showCancel: false
|
||||
});
|
||||
wx.showToast({ title: '当前环境不支持微信支付,请使用微信真机', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// wx.requestVirtualPayment 仅接受 signData / paySig / signature / mode 四个参数;
|
||||
// offerId、env、productId、goodsPrice 等均已编码在 signData JSON 字符串内,不可再平铺为顶层参数。
|
||||
wx.requestVirtualPayment({
|
||||
signData: order.sign_data || order.signData,
|
||||
paySig: order.pay_sig || order.paySig,
|
||||
signature: order.signature,
|
||||
mode: order.mode || 'short_series_goods',
|
||||
success: function () {
|
||||
self.setData({ paying: false });
|
||||
wx.showToast({ title: '支付成功', icon: 'success' });
|
||||
setTimeout(function () { self.loadInfo(); }, 1200);
|
||||
},
|
||||
fail: function (err) {
|
||||
self.setData({ paying: false });
|
||||
var msg = '支付未完成';
|
||||
var m = (err && err.errMsg) || '';
|
||||
if (m.indexOf('cancel') >= 0) msg = '已取消支付';
|
||||
else if (m.indexOf('fail') >= 0) msg = '支付失败,请稍后重试';
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
wx.requestPayment({
|
||||
timeStamp: payment.time_stamp,
|
||||
nonceStr: payment.nonce_str,
|
||||
package: payment.package,
|
||||
signType: payment.sign_type,
|
||||
paySign: payment.pay_sign,
|
||||
success: function () { return self.confirmWechatPayment(order.out_trade_no); },
|
||||
fail: function (error) {
|
||||
// Even a cashier error can occur after payment; query the server once.
|
||||
var message = String(error && error.errMsg || '').indexOf('cancel') >= 0 ? '已取消支付' : '支付未完成';
|
||||
return self.confirmWechatPayment(order.out_trade_no, message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
confirmWechatPayment: async function (number, failureMessage) {
|
||||
if (this._confirming) return;
|
||||
this._confirming = true;
|
||||
try {
|
||||
for (var attempt = 0; attempt < (failureMessage ? 1 : 5); attempt++) {
|
||||
var result = await api.storageOrderStatus(number);
|
||||
if (this._unloaded) return;
|
||||
if (result.status === 'success') {
|
||||
wx.showToast({ title: '支付成功,会员已开通', icon: 'success' });
|
||||
await this.loadInfo();
|
||||
return;
|
||||
}
|
||||
if (result.status === 'closed') {
|
||||
wx.showToast({ title: '订单已关闭,请重新下单', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!failureMessage && attempt < 4) await new Promise(function (resolve) { setTimeout(resolve, 1200); });
|
||||
}
|
||||
if (!this._unloaded) wx.showToast({ title: failureMessage || '支付结果确认中,请稍后刷新', icon: 'none' });
|
||||
} catch (e) {
|
||||
if (!this._unloaded) wx.showToast({ title: '支付结果暂未确认,请稍后刷新', icon: 'none' });
|
||||
} finally {
|
||||
this._confirming = false;
|
||||
if (!this._unloaded) this.setData({ paying: false });
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -9,27 +9,27 @@
|
||||
<view class="storage-card">
|
||||
<view class="storage-card-head">
|
||||
<view class="storage-title">已用空间</view>
|
||||
<view class="storage-vip" wx:if="{{isVip}}">👑 无限空间</view>
|
||||
<view class="storage-vip" wx:if="{{isVip}}">👑 {{quotaText}}</view>
|
||||
</view>
|
||||
<view class="storage-used">
|
||||
<text class="storage-num">{{usedText}}</text>
|
||||
<text class="storage-of" wx:if="{{!isVip}}"> / {{freeText}} 免费</text>
|
||||
<text class="storage-of" wx:if="{{!isUnlimited}}"> / {{quotaText}}</text>
|
||||
</view>
|
||||
<view class="storage-bar" wx:if="{{!isVip}}">
|
||||
<view class="storage-bar" wx:if="{{!isUnlimited}}">
|
||||
<view class="storage-bar-fill" style="width: {{percent}}%;"></view>
|
||||
</view>
|
||||
<view class="storage-hint" wx:if="{{isVip}}">{{vipExpireText}}</view>
|
||||
<view class="storage-hint over" wx:elif="{{overQuota}}">已超出免费空间,升级后即可继续上传</view>
|
||||
<view class="storage-hint" wx:else>已使用 {{percent}}% 免费空间</view>
|
||||
<view class="storage-hint" wx:else>已使用 {{percent}}% 存储空间</view>
|
||||
</view>
|
||||
|
||||
<!-- 套餐 -->
|
||||
<view class="section-heading">升级无限空间</view>
|
||||
<view class="section-heading">选择容量套餐</view>
|
||||
<view class="plan-list">
|
||||
<view class="plan-card" wx:for="{{plans}}" wx:key="key" bindtap="buyPlan" data-key="{{item.key}}">
|
||||
<view class="plan-info">
|
||||
<view class="plan-name">{{item.name}}</view>
|
||||
<view class="plan-desc">无限存储空间,照片视频想存就存</view>
|
||||
<view class="plan-desc">{{item.description}}</view>
|
||||
</view>
|
||||
<view class="plan-price">
|
||||
<text class="plan-amount">¥{{item.priceText}}</text>
|
||||
@@ -38,12 +38,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="storage-hint" wx:if="{{!loading && !plans.length}}">暂无上架套餐</view>
|
||||
<!-- 说明 -->
|
||||
<view class="storage-note">
|
||||
<view class="note-title">📌 说明</view>
|
||||
<view class="note-item">• 默认提供 {{freeMb}}MB 免费空间</view>
|
||||
<view class="note-item">• 开通会员后,照片、视频等不再限制存储空间</view>
|
||||
<view class="note-item">• 会员到期后,已上传内容仍可正常查看</view>
|
||||
<view class="note-item">• 套餐容量为总可用容量,不与免费额度叠加;情侣共享当前有效的最大容量</view>
|
||||
<view class="note-item">• 套餐到期后已上传内容仍可查看,超出可用容量将暂停新增上传</view>
|
||||
</view>
|
||||
|
||||
<page-state wx:if="{{error}}" kind="error" title="加载失败" action="重试" bind:action="loadInfo" />
|
||||
|
||||
@@ -46,3 +46,26 @@ test('blank edit address uses current user location; a supplied address wins',as
|
||||
p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'location'},' '));await p.savePhotoInfo();assert.equal(e.calls[0].location,'当前用户地址');assert.equal(e.calls[0].lat,25);assert.equal(p.data.batches[0].photos[0].location,'当前用户地址');
|
||||
p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'location'},' 手填地址 '));await p.savePhotoInfo();assert.equal(e.calls[1].location,'手填地址');assert.equal(e.calls[1].lat,0);
|
||||
});
|
||||
|
||||
function uploadSetup(auth) {
|
||||
let definition;const calls=[],modals=[],toasts=[];
|
||||
const wx={getStorageSync:()=>'',showToast:v=>toasts.push(v),showModal:v=>modals.push(v),showLoading(){},hideLoading(){},switchTab:v=>calls.push(['switchTab',v]),redirectTo:v=>calls.push(['redirectTo',v]),chooseImage:()=>calls.push(['picker'])};
|
||||
const api={checkAuth:async()=>auth(),storageInfo:async()=>({is_unlimited:true}),albumList:async()=>({items:[]}),albumAdd:async()=>{calls.push(['albumAdd']);return {id:'c224bc8c-0848-442d-abf6-350fab34d669'};},photoAdd:async data=>{calls.push(['photoAdd',data]);return {success:true};}};
|
||||
const load=createLoader({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/request':{api,getImageUrl:v=>v,API_BASE:'https://we.sundynix.cn/api',uploadFileCompat:async()=>{calls.push(['upload']);return {file_id:'6487966a-28ac-41d1-97b6-26f279b11fa9'};}},'../../utils/user-location':{currentUserLocation:async()=>({location:'',lat:0,lng:0})}});
|
||||
load(path.join(__dirname,'../pages/album/album.js'));
|
||||
const page={...definition,data:{...definition.data,albumId:'c224bc8c-0848-442d-abf6-350fab34d669',uploadFilePaths:['photo.jpg'],photoFormList:[{}]},setData(v){Object.assign(this.data,v);},loadData(){calls.push(['reload']);}};
|
||||
return {page,calls,modals,toasts};
|
||||
}
|
||||
test('unbound users upload and save photos in their own UUID album',async()=>{
|
||||
const e=uploadSetup(()=>({logged_in:true,user:{couple_id:null}}));await e.page.uploadAll();
|
||||
assert.equal(e.calls[0][0],'upload');assert.equal(e.calls[1][0],'photoAdd');assert.equal(e.calls[1][1].path,'6487966a-28ac-41d1-97b6-26f279b11fa9');assert.equal(e.calls[2][0],'reload');assert.equal(e.page.data.showUploadModal,false);assert.equal(e.modals.length,0);
|
||||
});
|
||||
test('bound users keep uploading to the existing shared album',async()=>{
|
||||
const e=uploadSetup(()=>({logged_in:true,user:{couple_id:'bound'}}));await e.page.uploadAll();
|
||||
assert.equal(e.calls[1][0],'photoAdd');assert.equal(e.calls[1][1].album_id,'c224bc8c-0848-442d-abf6-350fab34d669');
|
||||
});
|
||||
|
||||
test('stale demo album is replaced with a real personal album before transferring photos',async()=>{
|
||||
const e=uploadSetup(()=>({logged_in:true,user:{couple_id:null}}));e.page.data.albumId=-1;await e.page.uploadAll();
|
||||
assert.equal(e.calls[0][0],'albumAdd');assert.equal(e.calls[1][0],'upload');assert.equal(e.calls[2][0],'photoAdd');assert.equal(e.calls[2][1].album_id,'c224bc8c-0848-442d-abf6-350fab34d669');
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ test('development requests and file previews use local backend, trial and releas
|
||||
const env = load(path.join(__dirname,'../config/env.js'));
|
||||
const {API_BASE,getImageUrl} = load(path.join(__dirname,'../utils/request.js'));
|
||||
assert.equal(env.env, version === 'develop' ? 'dev' : version);
|
||||
assert.equal(API_BASE, version === 'develop' ? 'http://127.0.0.1:8080/api' : 'https://past.sundynix.cn/api');
|
||||
assert.equal(API_BASE, version === 'develop' ? 'http://127.0.0.1:8080/api' : 'https://we.sundynix.cn/api');
|
||||
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ function client(extra = {}) {
|
||||
}
|
||||
test('business requests persist IDs, preserve order and distinguish clear from omitted fields', async () => {
|
||||
const c = client();
|
||||
await c.api.diaryUpdate({id:'diary',images:['https://past.sundynix.cn/api/files/'+id, id2]});
|
||||
await c.api.diaryUpdate({id:'diary',images:['https://we.sundynix.cn/api/files/'+id, id2]});
|
||||
await c.api.albumUpdate({id:'album',cover:''});
|
||||
await c.api.updateProfile({nickname:'test'});
|
||||
await c.api.photoAdd({album_id:'album',path:'/api/files/'+id});
|
||||
@@ -32,7 +32,7 @@ test('duplicate and ID-only uploads keep stable display URLs and file metadata',
|
||||
assert.equal(uploadReference(result),id);
|
||||
assert.equal(result.duplicate,true);
|
||||
assert.equal(result.file.url,'https://minio/raw-object');
|
||||
assert.equal(c.getImageUrl(result.path),'https://past.sundynix.cn/api/files/'+id);
|
||||
assert.equal(c.getImageUrl(result.path),'https://we.sundynix.cn/api/files/'+id);
|
||||
assert.throws(()=>uploadReference({success:true,path:'http://tmp/new.jpg'}),/文件 ID/);
|
||||
});
|
||||
test('diary edit uploads HTTP temporary images, retains existing IDs and stops on upload failure', async () => {
|
||||
@@ -55,5 +55,5 @@ test('media display keeps local assets and temporary paths without a backend dir
|
||||
assert.equal(c.getImageUrl('/images/default-avatar.png'), '/images/default-avatar.png');
|
||||
assert.equal(c.getImageUrl('wxfile://tmp/photo.jpg'), 'wxfile://tmp/photo.jpg');
|
||||
assert.equal(c.getImageUrl('http://tmp/photo.jpg'), 'http://tmp/photo.jpg');
|
||||
assert.equal(c.getImageUrl(id), 'https://past.sundynix.cn/api/files/' + id);
|
||||
assert.equal(c.getImageUrl(id), 'https://we.sundynix.cn/api/files/' + id);
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ test('profile editing hides tabs until close and reserves a fixed save area outs
|
||||
});
|
||||
test('photo memory detail uses light system controls and restores them on exit',()=>{
|
||||
const createLoader=require('./load-module.cjs');let definition;const colors=[];let back=0;
|
||||
const load=createLoader({Page:value=>definition=value,wx:{setNavigationBarColor:value=>colors.push(value),navigateBack:()=>back++},console,Date},{'../../utils/request':{api:{},getImageUrl:value=>'https://past.sundynix.cn/backend/'+value}});
|
||||
const load=createLoader({Page:value=>definition=value,wx:{setNavigationBarColor:value=>colors.push(value),navigateBack:()=>back++},console,Date},{'../../utils/request':{api:{},getImageUrl:value=>'https://we.sundynix.cn/backend/'+value}});
|
||||
load(path.join(root,'pages/timeline-detail/timeline-detail.js'));
|
||||
const page={...definition,data:{...definition.data,item:{bg_image:'uploads/memory.jpg'}},setData(value){Object.assign(this.data,value);}};
|
||||
page.loadBgImage();assert.equal(colors.at(-1).frontColor,'#ffffff');assert.ok(page.data.bgImageUrl.endsWith('memory.jpg'));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const createLoader = require('./load-module.cjs');
|
||||
function setup(statuses) {
|
||||
let definition, cashier, queries = 0, refreshes = 0;
|
||||
const toasts = [];
|
||||
const load = createLoader({Page(p) { definition = p; }, console, Promise, Date,
|
||||
setTimeout(resolve) { resolve(); },
|
||||
wx: { requestPayment(o) { cashier = o; }, showToast(o) { toasts.push(o); } }
|
||||
}, {'../../utils/request': {api: {async storageOrderStatus(number) {
|
||||
assert.equal(number, 'P12345678');
|
||||
const result = statuses[Math.min(queries++, statuses.length - 1)];
|
||||
if (result instanceof Error) throw result;
|
||||
return result;
|
||||
}}}});
|
||||
load(path.join(__dirname,'../pages/storage/storage.js'));
|
||||
const page = {...definition,data:{paying:true},setData(data){Object.assign(this.data,data);},async loadInfo(){refreshes++;}};
|
||||
const order = {out_trade_no:'P12345678',payment:{time_stamp:'123',nonce_str:'nonce',package:'prepay_id=123',sign_type:'RSA',pay_sign:'signed'}};
|
||||
page.callWechatPay(order);
|
||||
return {page,toasts,get cashier(){return cashier;},get queries(){return queries;},get refreshes(){return refreshes;}};
|
||||
}
|
||||
test('JSAPI uses signed fields and only announces membership after server confirmation', async () => {
|
||||
const e = setup([{status:'pending'},{status:'success'}]);
|
||||
assert.equal(e.cashier.timeStamp,'123');
|
||||
assert.equal(e.cashier.nonceStr,'nonce');
|
||||
assert.equal(e.cashier.package,'prepay_id=123');
|
||||
assert.equal(e.cashier.signType,'RSA');
|
||||
assert.equal(e.cashier.paySign,'signed');
|
||||
assert.equal(e.toasts.length,0);
|
||||
await e.cashier.success();
|
||||
assert.equal(e.queries,2);
|
||||
assert.equal(e.refreshes,1);
|
||||
assert.equal(e.toasts[0].title,'支付成功,会员已开通');
|
||||
assert.equal(e.page.data.paying,false);
|
||||
});
|
||||
test('cancelled cashier checks for completed payment once, without granting pending orders',async()=>{
|
||||
const e=setup([{status:'pending'}]);
|
||||
await e.cashier.fail({errMsg:'requestPayment:fail cancel'});
|
||||
assert.equal(e.queries,1);assert.equal(e.refreshes,0);
|
||||
assert.equal(e.toasts[0].title,'已取消支付');assert.equal(e.page.data.paying,false);
|
||||
const paid=setup([{status:'success'}]);
|
||||
await paid.cashier.fail({errMsg:'requestPayment:fail'});
|
||||
assert.equal(paid.refreshes,1);assert.equal(paid.toasts[0].title,'支付成功,会员已开通');
|
||||
});
|
||||
test('unknown payment results and network errors never claim success',async()=>{
|
||||
for(const status of [{status:'pending'},Error('offline')]){
|
||||
const e=setup([status]);await e.cashier.success();
|
||||
assert.equal(e.refreshes,0);assert.equal(e.toasts[0].icon,'none');assert.equal(e.page.data.paying,false);
|
||||
}
|
||||
});
|
||||
@@ -35,7 +35,7 @@ test('server relation changes discard cached data before another read',async()=>
|
||||
await api.diaryList();await api.diaryList();assert.equal(reads,1);
|
||||
await api.checkAuth();assert.equal(storage.get('coupleId'),'22');assert.equal(redirects,1);
|
||||
const fresh=await api.diaryList();assert.equal(reads,2);assert.equal(fresh.items[0].title,'new space');
|
||||
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),'https://past.sundynix.cn/api/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),'https://we.sundynix.cn/api/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||
});
|
||||
|
||||
test('settings and profile consent retain couple cache and lead into cooling',async()=>{
|
||||
|
||||
@@ -12,16 +12,16 @@ test('GET business parameters and POST bodies use clean paths with auth', async
|
||||
const response = await api.diaryList({page:2,page_size:20});
|
||||
assert.equal(response.page_size,20);
|
||||
await api.periodVisibleSet({period_visible:0});
|
||||
assert.equal(calls[0].url,'https://past.sundynix.cn/api/diaryList?page=2&pageSize=20');
|
||||
assert.equal(calls[0].url,'https://we.sundynix.cn/api/diaryList?page=2&pageSize=20');
|
||||
assert.equal(calls[0].header.Authorization,'Bearer test-token');
|
||||
assert.equal(calls[1].url,'https://past.sundynix.cn/api/periodVisibleSet');
|
||||
assert.equal(calls[1].url,'https://we.sundynix.cn/api/periodVisibleSet');
|
||||
assert.equal(calls[1].data.periodVisible,0);
|
||||
});
|
||||
test('multipart upload retains action and token when Android omits headers',async()=>{
|
||||
let upload;
|
||||
const {uploadFileCompat}=load('utils/request.js',{getStorageSync:()=> 'test-token',compressImage(o){o.fail();},uploadFile(o){upload=o;o.success({statusCode:200,data:'{"success":true,"path":"uploads/test.jpg"}'});} });
|
||||
await uploadFileCompat({url:'https://past.sundynix.cn/api/uploadPartnerAvatar',filePath:'test.jpg',name:'file',formData:{partner_id:87}});
|
||||
assert.equal(upload.url,'https://past.sundynix.cn/api/uploadPartnerAvatar?token=test-token');
|
||||
await uploadFileCompat({url:'https://we.sundynix.cn/api/uploadPartnerAvatar',filePath:'test.jpg',name:'file',formData:{partner_id:87}});
|
||||
assert.equal(upload.url,'https://we.sundynix.cn/api/uploadPartnerAvatar?token=test-token');
|
||||
assert.equal(upload.formData.action,'uploadPartnerAvatar');
|
||||
assert.equal(upload.formData.token,'test-token');
|
||||
assert.equal(upload.formData.partnerId,87);
|
||||
|
||||
@@ -5,7 +5,7 @@ const createLoader=require('./load-module.cjs');
|
||||
function setup(action,info) {
|
||||
let definition;const calls=[],toasts=[],events=[];
|
||||
const api={coupleInfo:async()=>({partner1:{id:1,nickname:'本人',avatar:'uploads/me.jpg'},partner2:{id:2,nickname:'伴侣',avatar:'uploads/partner.jpg'}}),smokingAction:async payload=>{calls.push(payload);return action(payload);},smokingInfo:info||(async()=>({stock:5,today_count:0,logs:[],daily:[],available:true,read_only:false}))};
|
||||
const load=createLoader({Component:c=>definition=c,Date,Math,Promise,wx:{getStorageSync:()=>1,showToast:v=>toasts.push(v),showModal:o=>o.success({confirm:true}),navigateTo(){}}},{'../../utils/request':{api,getImageUrl:value=>value?'https://past.sundynix.cn/backend/'+value:''}});
|
||||
const load=createLoader({Component:c=>definition=c,Date,Math,Promise,wx:{getStorageSync:()=>1,showToast:v=>toasts.push(v),showModal:o=>o.success({confirm:true}),navigateTo(){}}},{'../../utils/request':{api,getImageUrl:value=>value?'https://we.sundynix.cn/backend/'+value:''}});
|
||||
load(path.join(__dirname,'../components/smoking-card/smoking-card.js'));
|
||||
const component={...definition.methods,properties:{detailed:false},data:JSON.parse(JSON.stringify(definition.data)),setData(values){Object.assign(this.data,values);},triggerEvent:name=>events.push(name)};
|
||||
component.applyInfo({stock:5,today_count:0,logs:[],daily:[],available:true,read_only:false});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const path=require('node:path');
|
||||
const loader=require('./load-module.cjs');
|
||||
test('finite paid space uses its quota, zero is respected, unlimited is explicit',()=>{
|
||||
const load=loader({wx:{},console},{'./request':{api:{}}});
|
||||
const {quotaExceeded}=load(path.join(__dirname,'../utils/storage-guard.js'));
|
||||
assert.equal(quotaExceeded({isVip:true,isUnlimited:false,quotaBytes:1024,usedBytes:1000},25),true);
|
||||
assert.equal(quotaExceeded({is_vip:true,is_unlimited:false,quota_bytes:1024,used_bytes:1000},24),false);
|
||||
assert.equal(quotaExceeded({isVip:true,isUnlimited:true,quotaBytes:0,usedBytes:9999},100),false);
|
||||
assert.equal(quotaExceeded({quotaBytes:0,freeBytes:200,usedBytes:1},0),true);
|
||||
});
|
||||
test('storage page renders 1-cent plans and custom month/capacity values',async()=>{
|
||||
let definition;
|
||||
const info={freeBytes:200*1024*1024,quotaBytes:1024*1024*1024,usedBytes:512*1024*1024,isVip:true,isUnlimited:false,vipExpireAt:'2026-12-02',plans:[{key:'custom',name:'测试套餐',priceCents:1,quotaMb:1024,months:3}]};
|
||||
const load=loader({Page(p){definition=p},wx:{},console},{'../../utils/request':{api:{storageInfo:async()=>info}}});
|
||||
load(path.join(__dirname,'../pages/storage/storage.js'));
|
||||
const page={...definition,data:{...definition.data},setData(d){Object.assign(this.data,d)}};
|
||||
await page.loadInfo();
|
||||
assert.equal(page.data.plans[0].priceText,'0.01');
|
||||
assert.equal(page.data.plans[0].unit,'3个月');
|
||||
assert.match(page.data.plans[0].description,/1.00 GB/);
|
||||
assert.equal(page.data.percent,50);
|
||||
assert.equal(page.data.isVip,true);
|
||||
assert.equal(page.data.isUnlimited,false);
|
||||
assert.equal(page.data.quotaText,'1.00 GB');
|
||||
});
|
||||
@@ -73,7 +73,7 @@ function maybeRedirectStorage(data) {
|
||||
if (!isStorageQuotaError(data)) return false;
|
||||
wx.showModal({
|
||||
title: '存储空间不足',
|
||||
content: '免费空间已用完,升级为无限空间会员后即可继续上传。',
|
||||
content: '存储空间已用完,请选择更大容量的套餐后继续上传。',
|
||||
confirmText: '去升级',
|
||||
cancelText: '暂不',
|
||||
success: function (res) {
|
||||
@@ -402,6 +402,7 @@ const api = {
|
||||
// 存储空间
|
||||
storageInfo: () => request('storageInfo', null, 'GET'),
|
||||
storageCreateOrder: (data) => request('storageCreateOrder', data),
|
||||
storageOrderStatus: (number) => request('storageOrderStatus', { out_trade_no: number }, 'GET'),
|
||||
|
||||
// 操作记录
|
||||
activityLogList: (data) => request('activityLogList', data, 'GET'),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// 存储空间统一拦截:上传照片/图片前检查剩余空间,超出时在按钮处直接阻止上传。
|
||||
const { api } = require('./request');
|
||||
|
||||
// 判断是否超出配额(会员无限空间,不拦截)
|
||||
// 只有无限容量套餐免配额检查,付费有限套餐同样需要校验。
|
||||
function quotaExceeded(d, extraBytes) {
|
||||
if (!d) return false;
|
||||
const isVip = d.is_vip || d.isVip;
|
||||
if (isVip) return false;
|
||||
const free = d.free_bytes || d.freeBytes || 0;
|
||||
const unlimited = d.is_unlimited || d.isUnlimited;
|
||||
if (unlimited) return false;
|
||||
const free = d.quota_bytes != null ? d.quota_bytes : (d.quotaBytes != null ? d.quotaBytes : (d.free_bytes || d.freeBytes || 0));
|
||||
const used = d.used_bytes || d.usedBytes || 0;
|
||||
return used + (extraBytes || 0) > free;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function quotaExceeded(d, extraBytes) {
|
||||
function showQuotaBlocked() {
|
||||
wx.showModal({
|
||||
title: '存储空间不足',
|
||||
content: '当前存储空间已满,请升级为无限空间会员后再上传照片。',
|
||||
content: '当前存储空间已满,请选择更大容量的套餐后再上传照片。',
|
||||
confirmText: '去升级',
|
||||
cancelText: '取消',
|
||||
success: function (res) {
|
||||
|
||||
Reference in New Issue
Block a user