feat: migrate to PostgreSQL GORM and MinIO file storage
Use UUID models and soft deletion, persist file relationships with deduplicated monthly MinIO objects, and add admin file management. Update mini-program media handling and add local dev configuration.
This commit is contained in:
@@ -4,7 +4,7 @@ const cache = require('../../utils/data-cache');
|
||||
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
|
||||
Component({
|
||||
properties: { detailed: Boolean },
|
||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], periodStats:[], logsPage:1, logsTotal:0, logsHasMore:false, logsLoadingMore:false, logsMoreError:false, ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:"", lastLabel:'还没有记录', daily:[], logs:[], periodStats:[], logsPage:1, logsTotal:0, logsHasMore:false, logsLoadingMore:false, logsMoreError:false, ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||
lifetimes: { attached(){this.load();} },
|
||||
pageLifetimes: { show(){this.load();} },
|
||||
methods: {
|
||||
@@ -30,8 +30,8 @@ Component({
|
||||
money:info[period.key+'_purchase_cents'] == null ? '—' : (Number(info[period.key+'_purchase_cents'])/100).toFixed(2)
|
||||
}));
|
||||
const profile=this._profile||{};
|
||||
const ownerId=Number(info.user_id || this.data.viewUserId || wx.getStorageSync('userId'));
|
||||
const owner=[profile.partner1,profile.partner2].find(user=>user && Number(user.id)===ownerId)||{};
|
||||
const ownerId=String(info.user_id || this.data.viewUserId || wx.getStorageSync('userId') || '');
|
||||
const owner=[profile.partner1,profile.partner2].find(user=>user && String(user.id)===ownerId)||{};
|
||||
const ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
|
||||
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||'');
|
||||
this.setData({info,logs,daily,periodStats,logsPage:Number(info.logs_page)||1,logsTotal:Number(info.logs_total)||logs.length,logsHasMore:info.logs_has_more===true,logsLoadingMore:false,logsMoreError:false,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
||||
@@ -50,7 +50,7 @@ Component({
|
||||
avatarFailed(){this.setData({ownerAvatar:''});},
|
||||
retry(){this.load(true);},
|
||||
openDetail(){if(!this.properties.detailed)wx.navigateTo({url:'/pages/smoking-detail/smoking-detail'});},
|
||||
async switchOwner(e){if(this.data.saving)return;this._logsGeneration=(this._logsGeneration||0)+1;this.setData({viewUserId:Number(e.currentTarget.dataset.user)||0,loading:true,info:null});if(this._loading)await this._loading;await this.load();},
|
||||
async switchOwner(e){if(this.data.saving)return;this._logsGeneration=(this._logsGeneration||0)+1;this.setData({viewUserId:String(e.currentTarget.dataset.user || ""),loading:true,info:null});if(this._loading)await this._loading;await this.load();},
|
||||
replenish(){if(this.data.saving)return;this.setData({sheet:true,mode:'purchase',quantity:'1',packSize:'20',amount:'',note:''});},
|
||||
mode(e){if(this.data.saving)return;const mode=e.currentTarget.dataset.mode;this.setData({mode,quantity:mode==='adjust'?String(this.data.info.stock):'1',amount:'',note:''});},
|
||||
input(e){if(!this.data.saving)this.setData({[e.currentTarget.dataset.field]:e.detail.value});},
|
||||
@@ -74,7 +74,7 @@ Component({
|
||||
if(amount>100000000)return wx.showToast({title:'金额超出范围',icon:'none'});
|
||||
return this.act({kind:mode,quantity,amount_cents:amount,note:this.data.note.trim()});
|
||||
},
|
||||
undo(e){if(this.data.saving)return;wx.showModal({title:'撤销这条记录?',content:'恢复该操作前的库存变化和抽烟次数。赠出一支仅扣库存,撤销会加回一支;“收到并抽一支”会同时撤销收到和抽烟。',success:res=>{if(res.confirm)this.act({kind:'undo',original_id:Number(e.currentTarget.dataset.id),quantity:0});}});},
|
||||
undo(e){if(this.data.saving)return;wx.showModal({title:'撤销这条记录?',content:'恢复该操作前的库存变化和抽烟次数。赠出一支仅扣库存,撤销会加回一支;“收到并抽一支”会同时撤销收到和抽烟。',success:res=>{if(res.confirm)this.act({kind:'undo',original_id:String(e.currentTarget.dataset.id),quantity:0});}});},
|
||||
async act(payload) {
|
||||
if(this.data.saving||!this.data.info||this.data.info.read_only||!this.data.info.available)return;
|
||||
const signature=JSON.stringify(payload);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// 开发工具使用本机服务;真机调试时把 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' }
|
||||
};
|
||||
let version = 'release';
|
||||
try {
|
||||
const account = wx.getAccountInfoSync();
|
||||
version = account.miniProgram.envVersion || 'release';
|
||||
} catch (e) {}
|
||||
const env = version === 'develop' ? 'dev' : version === 'trial' ? 'trial' : 'release';
|
||||
module.exports = { env, API_BASE: environments[env].apiBase };
|
||||
@@ -1,3 +1,4 @@
|
||||
const { uploadReference } = require('../../utils/file-media');
|
||||
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||
const { currentUserLocation } = require('../../utils/user-location');
|
||||
const { photoForm, metadataRows } = require('../../utils/photo-metadata');
|
||||
@@ -168,7 +169,7 @@ Page(withDataCache(withListPagination({
|
||||
(photos || []).forEach(function(p) {
|
||||
var batchId = p.batch_id || p.created_at || 'default';
|
||||
if (!batchMap[batchId]) {
|
||||
var cid = parseInt(p.created_by) || 0;
|
||||
var cid = String(p.created_by) || 0;
|
||||
batchMap[batchId] = {
|
||||
batch_id: batchId,
|
||||
creator_name: nameMap[cid] || defaultName,
|
||||
@@ -358,7 +359,7 @@ Page(withDataCache(withListPagination({
|
||||
formData: { sub_dir: 'photos' }
|
||||
});
|
||||
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
|
||||
var uploadedPath = uploadRes && (uploadRes.path || uploadRes.url || '');
|
||||
var uploadedPath = uploadReference(uploadRes);
|
||||
if (uploadedPath) {
|
||||
await api.photoAdd({
|
||||
album_id: String(albumId),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { uploadReference } = require('../../utils/file-media');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/checkin-detail/checkin-detail.js
|
||||
@@ -14,9 +15,9 @@ Page(withDataCache({
|
||||
this.setData(getLayout());
|
||||
var id = options.id;
|
||||
if (id) {
|
||||
this.setData({ detailId: parseInt(id) });
|
||||
this.setData({ detailId: String(id) });
|
||||
this.loadCoupleInfo();
|
||||
this.loadDetail(parseInt(id));
|
||||
this.loadDetail(String(id));
|
||||
}
|
||||
var self = this;
|
||||
var swipe = createSwipeBack(function() { wx.navigateBack(); });
|
||||
@@ -191,13 +192,9 @@ Page(withDataCache({
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'checkins' }
|
||||
});
|
||||
photoPath = upResult.path || '';
|
||||
photoPath = uploadReference(upResult);
|
||||
} catch(e) {
|
||||
// 内容安全违规:抛出错误中断打卡并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
photoPath = '';
|
||||
throw e;
|
||||
}
|
||||
wx.hideLoading();
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ Page(withDataCache({
|
||||
this.setData(getLayout());
|
||||
var diaryId = options.id;
|
||||
if (!diaryId) { wx.showToast({ title: '参数错误', icon: 'none' }); return; }
|
||||
this.setData({ diaryId: parseInt(diaryId), myUid: wx.getStorageSync('userId') || 0 });
|
||||
this.setData({ diaryId: String(diaryId), myUid: wx.getStorageSync('userId') || 0 });
|
||||
this.loadDiary();
|
||||
this.loadComments();
|
||||
},
|
||||
@@ -90,7 +90,7 @@ Page(withDataCache({
|
||||
var result = await api.diaryCommentList({diary_id:this.data.diaryId,page:page,page_size:20});
|
||||
if (generation !== this._commentsGeneration) return;
|
||||
var added = itemsOf(result).map(function(comment) {
|
||||
return Object.assign({}, comment, {_time:String(comment.created_at || comment.create_time || '').slice(0,16).replace('T',' ')});
|
||||
return Object.assign({}, comment, {avatar_url:getImageUrl(comment.avatar_url || comment.avatar || ''),_time:String(comment.created_at || comment.create_time || '').slice(0,16).replace('T',' ')});
|
||||
});
|
||||
this.setData({comments:page===1?added:mergeUnique(this.data.comments,added),commentPage:page,commentsTotal:totalOf(result),hasMoreComments:!Array.isArray(result)&&(result.has_more != null?!!result.has_more:page*20<totalOf(result))});
|
||||
} catch(e) {
|
||||
@@ -186,6 +186,7 @@ Page(withDataCache({
|
||||
this._commentsFirst = false;
|
||||
this.setData({commentsLoadingMore:false});
|
||||
var newComment = res.comment || {};
|
||||
newComment.avatar_url = getImageUrl(newComment.avatar_url || newComment.avatar || "");
|
||||
if (newComment.created_at) {
|
||||
newComment._time = newComment.created_at.substring(0, 16).replace('T', ' ');
|
||||
} else if (newComment.create_time) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { uploadReference } = require('../../utils/file-media');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
const { api, API_BASE, getImageUrl, uploadFileCompat } = require('../../utils/request');
|
||||
const { guardStorageSpace } = require('../../utils/storage-guard');
|
||||
@@ -24,7 +25,7 @@ Page({
|
||||
wx.showToast({ title: '参数错误', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ diaryId: parseInt(id) });
|
||||
this.setData({ diaryId: String(id) });
|
||||
this.loadDiary();
|
||||
},
|
||||
|
||||
@@ -50,7 +51,7 @@ Page({
|
||||
'form.lng': data.lng || 0,
|
||||
'form.location': data.location || '',
|
||||
tempImages: tempImages,
|
||||
existingImages: imgs,
|
||||
existingImages: Array.isArray(data.image_file_ids) && data.image_file_ids.length === imgs.length ? data.image_file_ids : imgs,
|
||||
contentLength: content.length
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -110,10 +111,10 @@ Page({
|
||||
var existing = this.data.existingImages || [];
|
||||
var temps = this.data.tempImages || [];
|
||||
|
||||
// 判断哪些是新上传的图片(tempFilePaths 以 http 开头的是已有图片,否则是新选的)
|
||||
// 已有图片通过原始引用识别;开发工具的本地图片也可能以 http 开头。
|
||||
for (var i = 0; i < temps.length; i++) {
|
||||
var p = temps[i];
|
||||
if (p.indexOf('http') === 0) {
|
||||
if (existing[i] && getImageUrl(existing[i]) === p) {
|
||||
// 已有图片,用原始路径
|
||||
uploadPaths.push(existing[i] || '');
|
||||
} else {
|
||||
@@ -149,42 +150,16 @@ Page({
|
||||
},
|
||||
|
||||
_uploadImage: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
path = retryData && (retryData.path || retryData.url || '');
|
||||
} catch(retryErr) {
|
||||
console.error('图片上传重试失败', retryErr);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('图片上传失败', e);
|
||||
return '';
|
||||
}
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
return uploadReference(data);
|
||||
},
|
||||
|
||||
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { uploadReference } = require('../../utils/file-media');
|
||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
@@ -359,7 +360,7 @@ Page(withDataCache({
|
||||
hasPartner: true,
|
||||
partnerName: partner.nickname || partner.username || 'TA',
|
||||
partnerDisplayId: partner.user_id || '',
|
||||
partnerId: Number(partner.id),
|
||||
partnerId: String(partner.id),
|
||||
partnerDetail: details.join(' · ') || 'TA的信息',
|
||||
partnerAvatarUrl: getImageUrl(partner.avatar),
|
||||
partnerInitial: (partner.nickname || partner.username || '?')[0],
|
||||
@@ -511,7 +512,7 @@ Page(withDataCache({
|
||||
const data = await api.uploadAvatar(filePath);
|
||||
self.setData({
|
||||
editAvatarUrl: getImageUrl(data.path),
|
||||
editAvatarPath: data.path
|
||||
editAvatarPath: uploadReference(data)
|
||||
});
|
||||
wx.showToast({ title: '头像上传成功 ✨', icon: 'success' });
|
||||
} catch (e) {
|
||||
@@ -663,7 +664,7 @@ Page(withDataCache({
|
||||
self.setData({partnerAvatarUploading:true});
|
||||
try {
|
||||
const uploaded = await api.uploadPartnerAvatar(res.tempFilePaths[0],partnerId);
|
||||
self.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploaded.path});
|
||||
self.setData({editPartnerAvatarUrl:getImageUrl(uploaded.path),editPartnerAvatarPath:uploadReference(uploaded)});
|
||||
} catch(error) { wx.showToast({title:error.error || '头像上传失败',icon:'none'}); }
|
||||
finally { self.setData({partnerAvatarUploading:false}); }
|
||||
}});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { uploadReference } = require('../../utils/file-media');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/publish/publish.js - 统一发布页(增强版编辑器)
|
||||
const { api, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||
@@ -50,7 +51,7 @@ Page({
|
||||
onLoad: function(options) {
|
||||
this.setData(getLayout());
|
||||
var type = options.type || 'timeline';
|
||||
var editId = options.edit ? parseInt(options.edit) : 0;
|
||||
var editId = options.edit ? String(options.edit) : "";
|
||||
var cfg = TYPE_CONFIG[type] || TYPE_CONFIG['timeline'];
|
||||
var titleText = editId ? '编辑事件' : cfg.title;
|
||||
var btnText = editId ? '保存修改' : cfg.btn;
|
||||
@@ -173,7 +174,7 @@ Page({
|
||||
repeat_unit: item.repeat_unit || ''
|
||||
};
|
||||
// 加载背景图
|
||||
var bgPath = item.bg_image || '';
|
||||
var bgPath = item.bg_image_file_id || item.bg_image || '';
|
||||
var bgUrl = bgPath ? require('../../utils/request').getImageUrl(bgPath) : '';
|
||||
var repeatOptionIdx = 0;
|
||||
if (form.repeat_interval > 0 && form.repeat_unit) {
|
||||
@@ -384,9 +385,8 @@ Page({
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'timeline_bg' }
|
||||
});
|
||||
bgImagePath = uploadRes && (uploadRes.path || uploadRes.url || '');
|
||||
} catch(e) {}
|
||||
wx.hideLoading();
|
||||
bgImagePath = uploadReference(uploadRes);
|
||||
} finally { wx.hideLoading(); }
|
||||
}
|
||||
var result;
|
||||
if (editId) {
|
||||
@@ -451,93 +451,22 @@ Page({
|
||||
},
|
||||
|
||||
_uploadAlbumCover: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'albums' }
|
||||
});
|
||||
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
console.error('封面上传返回无路径', JSON.stringify(data), '错误码:', data && data.code);
|
||||
// 尝试重新上传一次
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'albums' }
|
||||
});
|
||||
var retryPath = retryData && (retryData.path || retryData.url || '');
|
||||
if (retryPath) {
|
||||
console.log('封面上传重试成功');
|
||||
return retryPath;
|
||||
}
|
||||
} catch(retryErr) {
|
||||
console.error('封面上传重试也失败', retryErr, '错误码:', retryErr && retryErr.code);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('封面上传失败', e, '错误码:', e && e.code);
|
||||
return '';
|
||||
}
|
||||
return this._uploadMedia(filePath, 'albums');
|
||||
},
|
||||
|
||||
_uploadDiaryImage: async function(filePath) {
|
||||
try {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
// 兼容不同响应格式:{path:'...'} 或 {success:true, path:'...'} 或 {url:'...'}
|
||||
var path = data && (data.path || data.url || '');
|
||||
if (!path) {
|
||||
// 内容安全检测失败:抛出错误提示用户
|
||||
if (data && data.error) {
|
||||
throw data;
|
||||
}
|
||||
console.error('日记图片上传返回无路径', JSON.stringify(data), '错误码:', data && data.code);
|
||||
// 尝试重新上传一次
|
||||
try {
|
||||
var retryData = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: 'diaries' }
|
||||
});
|
||||
var retryPath = retryData && (retryData.path || retryData.url || '');
|
||||
if (retryPath) {
|
||||
console.log('日记图片重试上传成功');
|
||||
return retryPath;
|
||||
}
|
||||
} catch(retryErr) {
|
||||
console.error('日记图片重试上传也失败', retryErr, '错误码:', retryErr && retryErr.code);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch(e) {
|
||||
// 内容安全违规错误:直接抛出,中断发布并提示用户
|
||||
if (e && e.error) {
|
||||
throw e;
|
||||
}
|
||||
console.error('日记图片上传失败', e, '错误码:', e && e.code);
|
||||
return '';
|
||||
}
|
||||
return this._uploadMedia(filePath, 'diaries');
|
||||
},
|
||||
|
||||
_uploadMedia: async function(filePath, subDir) {
|
||||
var data = await uploadFileCompat({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: { sub_dir: subDir }
|
||||
});
|
||||
return uploadReference(data);
|
||||
},
|
||||
// ===== 背景图 =====
|
||||
chooseBgImage: function() {
|
||||
var self = this;
|
||||
|
||||
@@ -74,8 +74,8 @@ Page(withDataCache({
|
||||
this.setData(getLayout());
|
||||
var id = options.id;
|
||||
if (id) {
|
||||
this.setData({ detailId: parseInt(id) });
|
||||
this.loadDetail(parseInt(id));
|
||||
this.setData({ detailId: String(id) });
|
||||
this.loadDetail(String(id));
|
||||
}
|
||||
var self = this;
|
||||
var swipe = createSwipeBack(function() { wx.navigateBack(); });
|
||||
@@ -108,7 +108,7 @@ Page(withDataCache({
|
||||
try {
|
||||
var item = await api.timelineDetail(id);
|
||||
var myUid = wx.getStorageSync('userId');
|
||||
var cid = parseInt(item.created_by) || 0;
|
||||
var cid = String(item.created_by) || 0;
|
||||
|
||||
// 获取情侣信息
|
||||
try {
|
||||
|
||||
@@ -101,7 +101,7 @@ Page(withDataCache(withListPagination({
|
||||
|
||||
var today = new Date(); today.setHours(0,0,0,0);
|
||||
var items = itemsOf(list).map(function(item, idx) {
|
||||
var cid = parseInt(item.created_by) || 0;
|
||||
var cid = String(item.created_by) || 0;
|
||||
item.creator_name = nameMap[cid] || (cid == myUid ? '我' : 'TA');
|
||||
item.creator_avatar = avatarMap[cid] || '';
|
||||
item.creator_initial = (item.creator_name || '?')[0].toUpperCase();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compileType": "miniprogram",
|
||||
"appid": "wxdc40d0c51cf8fe1e",
|
||||
"appid": "wxb463820bf36dd5d6",
|
||||
"projectname": "couple-space",
|
||||
"description": "情侣空间小程序",
|
||||
"setting": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const loader = require('./load-module.cjs');
|
||||
test('development requests and file previews use local backend, trial and release use production', () => {
|
||||
for (const version of ['develop','trial','release']) {
|
||||
const wx = {getAccountInfoSync:()=>({miniProgram:{envVersion:version}})};
|
||||
const load = loader({wx,console});
|
||||
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(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const createLoader = require('./load-module.cjs');
|
||||
const {uploadReference} = require('../utils/file-media');
|
||||
const id = '6083e119-6b26-441b-8b70-44d224366238';
|
||||
const id2 = '7083e119-6b26-441b-8b70-44d224366238';
|
||||
function client(extra = {}) {
|
||||
const calls = [];
|
||||
const load = createLoader({console, Promise, Date, wx: {
|
||||
getStorageSync: () => 'token', hideLoading() {},
|
||||
request(o) { calls.push(o); o.success({statusCode:200, data:{success:true}}); }, ...extra
|
||||
}});
|
||||
return {...load(path.join(__dirname, '../utils/request.js')), calls};
|
||||
}
|
||||
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.albumUpdate({id:'album',cover:''});
|
||||
await c.api.updateProfile({nickname:'test'});
|
||||
await c.api.photoAdd({album_id:'album',path:'/api/files/'+id});
|
||||
await c.api.checkinDo({id:'checkin',photo:id});
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(c.calls[0].data)), {id:'diary',imageFileIds:[id,id2]});
|
||||
assert.equal(c.calls[1].data.coverFileId,'');
|
||||
assert.equal('avatarFileId' in c.calls[2].data,false);
|
||||
assert.equal(c.calls[3].data.fileId,id);
|
||||
assert.equal(c.calls[4].data.photoFileId,id);
|
||||
});
|
||||
test('duplicate and ID-only uploads keep stable display URLs and file metadata', async () => {
|
||||
const c = client({compressImage(o){o.fail();},uploadFile(o){o.success({statusCode:200,data:JSON.stringify({success:true,duplicate:true,file:{id,url:'https://minio/raw-object'}})});}});
|
||||
const result = await c.uploadFileCompat({url:c.API_BASE+'/upload',filePath:'http://tmp/new.jpg'});
|
||||
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.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 () => {
|
||||
let definition, saved, uploads=[];
|
||||
const c = client();
|
||||
const load = createLoader({console, Date, Promise, setTimeout(){}, Page(p){definition=p;}, wx:{showLoading(){},hideLoading(){},showToast(){}}}, {
|
||||
'../../utils/request':{getImageUrl:c.getImageUrl,api:{async diaryUpdate(data){saved=data;}}}
|
||||
});
|
||||
load(path.join(__dirname,'../pages/diary_edit/diary_edit.js'));
|
||||
const p = {...definition, data:{loading:false,form:{content:'text'},existingImages:[id],tempImages:[c.getImageUrl(id),'http://tmp/new.jpg']},setData(v){Object.assign(this.data,v);},async _uploadImage(v){uploads.push(v);return id2;}};
|
||||
await p.submit();
|
||||
assert.deepEqual(uploads,['http://tmp/new.jpg']);
|
||||
assert.deepEqual(Array.from(saved.images),[id,id2]);
|
||||
saved=null;p._uploadImage=async()=>{throw Error('network');};
|
||||
await p.submit();assert.equal(saved,null);
|
||||
});
|
||||
|
||||
test('media display keeps local assets and temporary paths without a backend directory prefix', () => {
|
||||
const c = client();
|
||||
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);
|
||||
});
|
||||
@@ -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('/backend/uploads/a.jpg?media_sig=abc'),'https://past.sundynix.cn/backend/uploads/a.jpg?media_sig=abc');
|
||||
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),'https://past.sundynix.cn/api/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||
});
|
||||
|
||||
test('settings and profile consent retain couple cache and lead into cooling',async()=>{
|
||||
|
||||
@@ -25,7 +25,7 @@ test('purchase converts packs to pieces and cents; invalid quantities never subm
|
||||
e.component.setData({quantity:'1.5'});await e.component.submit();assert.equal(e.calls.length,1);
|
||||
});
|
||||
test('undo includes the original record and fills a seven-day chart with zero days',async()=>{
|
||||
const e=setup(async()=>result());e.component.undo({currentTarget:{dataset:{id:17}}});await Promise.resolve();await Promise.resolve();assert.equal(e.calls[0].kind,'undo');assert.equal(e.calls[0].original_id,17);assert.equal(e.component.data.daily.length,7);
|
||||
const e=setup(async()=>result());e.component.undo({currentTarget:{dataset:{id:'6083e119-6b26-441b-8b70-44d224366238'}}});await Promise.resolve();await Promise.resolve();assert.equal(e.calls[0].kind,'undo');assert.equal(e.calls[0].original_id,'6083e119-6b26-441b-8b70-44d224366238');assert.equal(e.component.data.daily.length,7);
|
||||
});
|
||||
|
||||
test('string zero is active while one is undone, and ledger identity follows the selected account',async()=>{
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// File IDs are persisted; stable API URLs are used for display only.
|
||||
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
||||
function fileId(value) {
|
||||
if (value && typeof value === 'object') return fileId(value.file_id || value.fileId || (value.file && value.file.id) || value.id || value.path || value.url);
|
||||
if (typeof value !== 'string') return '';
|
||||
if (new RegExp('^' + UUID + '$', 'i').test(value)) return value;
|
||||
var match = value.match(new RegExp('^(?:https?://[^/]+)?/api/files/(' + UUID + ')(?:[?#].*)?$', 'i'));
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
function uploadReference(value) {
|
||||
if (!value || value.error) throw value || new Error('上传失败');
|
||||
var id = fileId(value);
|
||||
if (!id) throw new Error('上传响应缺少文件 ID,请重试');
|
||||
return id;
|
||||
}
|
||||
function normalizeUploadResult(value) {
|
||||
if (!value || typeof value !== 'object' || value.error) return value;
|
||||
var id = fileId(value);
|
||||
if (!id) return value;
|
||||
return Object.assign({}, value, { file_id: id, path: '/api/files/' + id, url: '/api/files/' + id });
|
||||
}
|
||||
function filePayload(action, data) {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
var mappings = {
|
||||
updateProfile: { avatar: 'avatar_file_id' }, updatePartnerProfile: { avatar: 'avatar_file_id' },
|
||||
albumAdd: { cover: 'cover_file_id' }, albumUpdate: { cover: 'cover_file_id' },
|
||||
photoAdd: { path: 'file_id' }, diaryAdd: { images: 'image_file_ids' }, diaryUpdate: { images: 'image_file_ids' },
|
||||
timelineAdd: { bg_image: 'bg_image_file_id', images: 'image_file_ids' },
|
||||
timelineUpdate: { bg_image: 'bg_image_file_id', images: 'image_file_ids' }, checkinDo: { photo: 'photo_file_id' }
|
||||
};
|
||||
var out = Object.assign({}, data);
|
||||
Object.keys(mappings[action] || {}).forEach(function(key) {
|
||||
if (!Object.prototype.hasOwnProperty.call(out, key)) return;
|
||||
var target = mappings[action][key];
|
||||
var value = out[key];
|
||||
// Legacy references remain compatible; all new uploads supply UUIDs.
|
||||
var convert = function(v) { return fileId(v) || v; };
|
||||
if (!Object.prototype.hasOwnProperty.call(out, target)) out[target] = Array.isArray(value) ? value.map(convert) : convert(value);
|
||||
delete out[key];
|
||||
});
|
||||
return out;
|
||||
}
|
||||
module.exports = { fileId, uploadReference, normalizeUploadResult, filePayload };
|
||||
+15
-14
@@ -1,22 +1,23 @@
|
||||
const { fileId, normalizeUploadResult, filePayload } = require('./file-media');
|
||||
const { camelName, toWire, fromWire } = require('./api-naming');
|
||||
const dataCache = require('./data-cache');
|
||||
/**
|
||||
* 网络请求工具 - 对接后端 API
|
||||
*/
|
||||
const API_BASE = 'https://past.sundynix.cn/api';
|
||||
const MEDIA_BASE = 'https://past.sundynix.cn/backend/';
|
||||
const { API_BASE } = require('../config/env');
|
||||
|
||||
/**
|
||||
* 将相对路径转为完整URL(供小程序端显示图片使用)
|
||||
* @param {string} path - 相对路径,如 "uploads/avatars/xxx.jpg"
|
||||
* @returns {string} 完整URL,如 "http://192.168.2.103/uploads/avatars/xxx.jpg"
|
||||
* 将文件 UUID 或 /api/files/{id} 转成完整访问地址。
|
||||
* 完整 URL 和小程序本地图片路径直接使用。
|
||||
*/
|
||||
function getImageUrl(path) {
|
||||
if (!path) return '';
|
||||
if (typeof path === 'object') path = fileId(path) || path.path || path.url || '';
|
||||
if (typeof path !== 'string') return '';
|
||||
if (path.startsWith('/api/files/')) return API_BASE.replace(/\/api$/, '') + path;
|
||||
if (fileId(path) === path) return API_BASE + '/files/' + path;
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||||
if (path.startsWith('/backend/')) return MEDIA_BASE + path.substring('/backend/'.length);
|
||||
if (path.startsWith('/')) return MEDIA_BASE + path.substring(1);
|
||||
return MEDIA_BASE + path;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,23 +27,23 @@ function getImageUrl(path) {
|
||||
*/
|
||||
function parseUploadResponse(raw) {
|
||||
if (!raw) return null;
|
||||
if (typeof raw === 'object') return fromWire(raw);
|
||||
if (typeof raw === 'object') return normalizeUploadResult(fromWire(raw));
|
||||
var str = String(raw).trim();
|
||||
// 去除 BOM 和不可见字符
|
||||
str = str.replace(/^\uFEFF/, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
||||
try {
|
||||
return fromWire(JSON.parse(str));
|
||||
return normalizeUploadResult(fromWire(JSON.parse(str)));
|
||||
} catch(e) {
|
||||
// 尝试提取 JSON 片段(Android 端可能返回带额外字符的响应)
|
||||
var match = str.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
try { return fromWire(JSON.parse(match[0])); } catch(e2) {}
|
||||
try { return normalizeUploadResult(fromWire(JSON.parse(match[0]))); } catch(e2) {}
|
||||
}
|
||||
// 最后尝试:查找 "success" 或 "path" 关键词,手动构建响应对象
|
||||
if (str.indexOf('success') > -1 || str.indexOf('path') > -1) {
|
||||
var pathMatch = str.match(/"path"\s*:\s*"([^"]+)"/);
|
||||
if (pathMatch) {
|
||||
return { success: true, path: pathMatch[1] };
|
||||
return normalizeUploadResult({ success: true, path: pathMatch[1] });
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -129,7 +130,7 @@ function sendRequest(action, data = null, method = 'POST') {
|
||||
}
|
||||
|
||||
let url = API_BASE + '/' + encodeURIComponent(camelName(action));
|
||||
let requestData = toWire(data);
|
||||
let requestData = toWire(filePayload(camelName(action), data));
|
||||
|
||||
if (method === 'GET' && data) {
|
||||
const params = [];
|
||||
@@ -385,7 +386,7 @@ const api = {
|
||||
songsDelete: (data) => request('songsDelete', data),
|
||||
|
||||
// 打卡
|
||||
smokingInfo: (userId, data = {}) => request("smokingInfo", { page: 1, page_size: 20, ...data, user_id: userId || 0 }, "GET"),
|
||||
smokingInfo: (userId, data = {}) => request("smokingInfo", { page: 1, page_size: 20, ...data, user_id: userId || "" }, "GET"),
|
||||
smokingAction: (data) => request("smokingAction", data),
|
||||
checkinList: (data = {}) => request('checkinList', { page: 1, page_size: 20, ...data }, 'GET'),
|
||||
checkinAdd: (data) => request('checkinAdd', data),
|
||||
|
||||
Reference in New Issue
Block a user