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:
2026-11-01 11:48:45 +08:00
parent 0530523750
commit 4934a69581
94 changed files with 4686 additions and 2147 deletions
+3 -2
View File
@@ -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),
+5 -8
View File
@@ -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();
}
+3 -2
View File
@@ -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) {
+13 -38
View File
@@ -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();
}
+4 -3
View File
@@ -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}); }
}});
+16 -87
View File
@@ -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 {
+1 -1
View File
@@ -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();