diff --git a/past-go/README.md b/past-go/README.md index 86ba095..2503bf6 100644 --- a/past-go/README.md +++ b/past-go/README.md @@ -23,7 +23,7 @@ 本地配置包含 AppSecret,已加入 Git 忽略;新环境可复制 `config.dev.example.yaml` 后填写。 管理端地址:`http://localhost:8080/backend/admin/`。 -小程序 `weapp/config/env.js` 根据微信运行版本选择地址:开发版为 `http://127.0.0.1:8080/api`,体验版/正式版为线上地址。 +小程序在 `weapp/config/env.js` 中手动配置 `API_BASE`。本地调试填写本机或局域网地址;上传体验版、正式版前改为 `https://we.sundynix.cn/api`。 开发工具需关闭合法域名校验(项目已设置);真机调试将 dev 地址改为电脑的局域网 IP。 小程序 AppID 为 `wxb463820bf36dd5d6`,AppSecret 仅存放在后端本地配置。 dev 继续使用当前 `past-go` 数据库与 MinIO bucket,并非独立测试数据。 diff --git a/past-go/internal/handler/api_postgres_test.go b/past-go/internal/handler/api_postgres_test.go index e259554..df34da5 100644 --- a/past-go/internal/handler/api_postgres_test.go +++ b/past-go/internal/handler/api_postgres_test.go @@ -284,35 +284,12 @@ func runPostgresAPI(t *testing.T, stringsMode bool) { if err := db.ORM.Model(&model.User{}).Where("id=?", aID).Update("storage_quota_mb", 0).Error; err != nil { t.Fatal(err) } - personalDiary := id(post("diary_add", aToken, M{"content": "Personal diary", "image_file_ids": []string{personalImage}}), "id") - get("diary_detail", aToken, M{"id": personalDiary}) - request("diary_detail", "GET", otherToken, M{"id": personalDiary}, 404) - personalMemory := id(post("timeline_add", aToken, M{"title": "Personal memory", "event_date": today(), "bg_image_file_id": personalImage}), "id") - get("timeline_detail", aToken, M{"id": personalMemory}) - request("timeline_detail", "GET", otherToken, M{"id": personalMemory}, 404) - // Personal albums work before binding and remain scoped to the creator. - personalAlbum := id(post("album_add", aToken, M{"title": "Personal album"}), "id") - personalFile := id(uploadFile("upload", aToken, "file", nil), "file_id") - personalPhoto := id(post("photo_add", aToken, M{"album_id": personalAlbum, "file_id": personalFile}), "id") - uploadFile("set_album_cover", aToken, "cover", M{"album_id": personalAlbum}) - uploadFile("photo_upload", aToken, "photos", M{"album_id": personalAlbum}) - post("photo_update", aToken, M{"id": personalPhoto, "taken_at": today(), "location": "Personal place", "activity": "Personal photo"}) - post("album_update", aToken, M{"id": personalAlbum, "title": "Personal edited"}) - if got := get("photos", aToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 { - t.Fatalf("personal photos missing: %v", got) - } - if got := get("photos", otherToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 0 { - t.Fatalf("personal photos leaked: %v", got) - } - if got := get("album_list", otherToken, M{"page": 1, "page_size": 20}); store.ToInt(got["total"]) != 0 { - t.Fatalf("personal albums leaked: %v", got) - } - request("photo_add", "POST", otherToken, M{"album_id": personalAlbum, "file_id": personalFile}, 404) - request("photo_update", "POST", otherToken, M{"id": personalPhoto, "taken_at": "", "location": "", "activity": ""}, 404) - request("album_delete", "POST", otherToken, M{"id": personalAlbum}, 404) - var personal model.Album - if err := db.ORM.First(&personal, "id=?", personalAlbum).Error; err != nil || personal.CoupleId != nil { - t.Fatalf("personal album relation: %+v %v", personal, err) + // Every record endpoint rejects an authenticated but unbound account. + for _, action := range []string{"diary_list", "diary_add", "timeline_add", "album_add", "photos", "photo_add", "photo_upload", "set_album_cover", "songs_add", "checkin_add", "smoking_card_visible", "period_daily_save", "pet_adopt"} { + denied := request(action, "POST", aToken, M{}, 403) + if store.ToInt(denied["code"]) != 10002 { + t.Fatalf("missing binding code for %s: %v", action, denied) + } } link := post("generate_invite_link", aToken, nil) _ = link @@ -326,8 +303,34 @@ func runPostgresAPI(t *testing.T, stringsMode bool) { t.Fatalf("binding mismatch: %+v %v", user, err) } } + personalDiary := id(post("diary_add", aToken, M{"content": "Personal diary", "image_file_ids": []string{personalImage}}), "id") + get("diary_detail", aToken, M{"id": personalDiary}) + request("diary_detail", "GET", otherToken, M{"id": personalDiary}, 403) + personalMemory := id(post("timeline_add", aToken, M{"title": "Personal memory", "event_date": today(), "bg_image_file_id": personalImage}), "id") + get("timeline_detail", aToken, M{"id": personalMemory}) + request("timeline_detail", "GET", otherToken, M{"id": personalMemory}, 403) + // Bound users can use albums and all associated file operations. + personalAlbum := id(post("album_add", aToken, M{"title": "Personal album"}), "id") + personalFile := id(uploadFile("upload", aToken, "file", nil), "file_id") + personalPhoto := id(post("photo_add", aToken, M{"album_id": personalAlbum, "file_id": personalFile}), "id") + uploadFile("set_album_cover", aToken, "cover", M{"album_id": personalAlbum}) + uploadFile("photo_upload", aToken, "photos", M{"album_id": personalAlbum}) + post("photo_update", aToken, M{"id": personalPhoto, "taken_at": today(), "location": "Personal place", "activity": "Personal photo"}) + post("album_update", aToken, M{"id": personalAlbum, "title": "Personal edited"}) + if got := get("photos", aToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 { + t.Fatalf("personal photos missing: %v", got) + } + request("photos", "GET", otherToken, M{"album_id": personalAlbum}, 403) + request("album_list", "GET", otherToken, nil, 403) + request("photo_add", "POST", otherToken, M{"album_id": personalAlbum, "file_id": personalFile}, 403) + request("photo_update", "POST", otherToken, M{"id": personalPhoto}, 403) + request("album_delete", "POST", otherToken, M{"id": personalAlbum}, 403) + var personal model.Album + if err := db.ORM.First(&personal, "id=?", personalAlbum).Error; err != nil || personal.CoupleId == nil || *personal.CoupleId != cid { + t.Fatalf("personal album relation: %+v %v", personal, err) + } if got := get("photos", bToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 { - t.Fatalf("partner cannot view pre-binding photos: %v", got) + t.Fatalf("partner cannot view shared photos: %v", got) } post("photo_update", bToken, M{"id": personalPhoto, "taken_at": today(), "location": "Shared now", "activity": "Partner edit"}) request("account_deletion_preview", "POST", aToken, nil, 409) @@ -462,7 +465,7 @@ func runPostgresAPI(t *testing.T, stringsMode bool) { } request(action, "GET", otherToken, args, 0) } - request("diary_update", "POST", otherToken, M{"id": diaryID, "title": "intrusion", "content": "bad"}, 404) + request("diary_update", "POST", otherToken, M{"id": diaryID, "title": "intrusion", "content": "bad"}, 403) detail := get("diary_detail", aToken, M{"id": diaryID}) if strings.Contains(fmt.Sprint(detail), "intrusion") { t.Fatal("foreign account overwrote diary") @@ -614,11 +617,7 @@ func runPostgresAPI(t *testing.T, stringsMode bool) { if n := get("announcement", aToken, nil); n["enabled"] != false || n["content"] != "" || n["revision"] == notice["revision"] { t.Fatalf("announcement disable failed: %v", n) } - post("smoking_card_visible", aToken, M{"visible": 0}) - if me := get("check_auth", aToken, nil)["user"].(M); store.ToInt(me["smokingCardVisible"]) != 0 { - t.Fatalf("preference not persisted: %v", me) - } - post("smoking_card_visible", aToken, M{"visible": 1}) + request("smoking_card_visible", "POST", aToken, M{"visible": 0}, 403) post("admin_announcement", "admin", M{"mode": "always", "title": "Always", "content": "Every visit"}) if n := get("announcement", aToken, nil); n["mode"] != "always" || n["enabled"] != true { t.Fatalf("always mode failed: %v", n) @@ -650,14 +649,32 @@ func runPostgresAPI(t *testing.T, stringsMode bool) { if uniqueUpload.Error != "" { t.Fatal(uniqueUpload.Error) } - ownedAlbum := id(post("album_add", deleteToken, M{"title": "Delete personal"}), "id") - post("photo_add", deleteToken, M{"album_id": ownedAlbum, "file_id": ownFile}) - post("photo_add", deleteToken, M{"album_id": ownedAlbum, "file_id": uniqueUpload.FileId}) - deletedDiary := id(post("diary_add", deleteToken, M{"content": "Delete this diary", "image_file_ids": []string{ownFile}}), "id") - post("diary_delete", deleteToken, M{"id": deletedDiary}) + // Seed historical personal content directly: new unbound writes are forbidden, + // but account deletion must still remove records created under the old policy. + legacyAlbum := model.Album{Title: "Delete personal", CreatedBy: &otherID} + if err := db.ORM.Create(&legacyAlbum).Error; err != nil { + t.Fatal(err) + } + for _, fid := range []string{ownFile, uniqueUpload.FileId} { + photo := model.Photo{AlbumId: legacyAlbum.Id, FileId: &fid, Path: "/api/files/" + fid, CreatedBy: &otherID} + if err := db.ORM.Create(&photo).Error; err != nil { + t.Fatal(err) + } + } + legacyDiary := model.Diary{Title: "Legacy diary", Content: "Delete this diary", DiaryDate: today(), CreatedBy: &otherID} + if err := db.ORM.Create(&legacyDiary).Error; err != nil { + t.Fatal(err) + } + deletedDiary := legacyDiary.Id + if err := db.ORM.Delete(&legacyDiary).Error; err != nil { + t.Fatal(err) + } request("account_delete", "POST", deleteToken, M{}, 400) preview := post("account_deletion_preview", deleteToken, nil) - post("album_add", deleteToken, M{"title": "Added after preview"}) + addedLegacy := model.Album{Title: "Added after preview", CreatedBy: &otherID} + if err := db.ORM.Create(&addedLegacy).Error; err != nil { + t.Fatal(err) + } confirm := M{"confirmed": true, "confirmation_text": "注销并删除全部数据", "confirmation_token": preview["confirmation_token"]} request("account_delete", "POST", deleteToken, confirm, 409) preview = post("account_deletion_preview", deleteToken, nil) diff --git a/past-go/internal/httpapi/record_access.go b/past-go/internal/httpapi/record_access.go new file mode 100644 index 0000000..e07667c --- /dev/null +++ b/past-go/internal/httpapi/record_access.go @@ -0,0 +1,23 @@ +package httpapi + +import "strings" + +// 记录功能仅供已绑定情侣的用户使用;资料、头像、存储套餐与邀请独立开放。 +func recordAction(action string) bool { + for _, prefix := range []string{"diary_", "timeline_", "album_", "photo_", "songs_", "checkin_", "smoking_", "pet_", "period_"} { + if strings.HasPrefix(action, prefix) { + return true + } + } + return action == "photos" || action == "set_album_cover" +} + +func (c *Ctx) requireRecordBinding() error { + if !recordAction(c.Action) { + return nil + } + if c.CoupleID() == "" { + return c.ErrCode(403, "请先绑定情侣,再使用记录功能", 10002) + } + return nil +} diff --git a/past-go/internal/httpapi/record_access_test.go b/past-go/internal/httpapi/record_access_test.go new file mode 100644 index 0000000..cce613b --- /dev/null +++ b/past-go/internal/httpapi/record_access_test.go @@ -0,0 +1,32 @@ +package httpapi + +import ( + "github.com/gin-gonic/gin" + "net/http/httptest" + "testing" +) + +func TestRecordBindingAccess(t *testing.T) { + for _, action := range []string{"diary_list", "diary_add", "diary_comment_add", "timeline_detail", "album_add", "photos", "photo_upload", "set_album_cover", "songs_list", "checkin_do", "smoking_card_visible", "pet_adopt", "period_daily_save"} { + t.Run(action, func(t *testing.T) { + for _, cid := range []string{"", "bound-couple"} { + out := httptest.NewRecorder() + g, _ := gin.CreateTestContext(out) + c := &Ctx{G: g, Action: action, coupleResolved: true, coupleID: cid} + err := c.requireRecordBinding() + if cid == "" { + if !IsResponded(err) || out.Code != 403 { + t.Fatalf("unbound allowed: %v %d", err, out.Code) + } + } else if err != nil { + t.Fatal(err) + } + } + }) + } + for _, action := range []string{"upload", "upload_avatar", "update_profile", "couple_info", "invite_partner", "storage_info", "account_delete", "announcement", "admin_contents"} { + if recordAction(action) { + t.Fatalf("unrelated feature blocked: %s", action) + } + } +} diff --git a/past-go/internal/httpapi/router.go b/past-go/internal/httpapi/router.go index c5de59e..26c1c1b 100644 --- a/past-go/internal/httpapi/router.go +++ b/past-go/internal/httpapi/router.go @@ -241,6 +241,10 @@ func (r *Router) serveAPI(g *gin.Context) { return } + if err := c.requireRecordBinding(); err != nil { + return + } + if c.DB != nil { c.DB = c.DB.WithActor(c.uid, adminActions[action] && action != "admin_login") } diff --git a/weapp/components/app-tabbar/app-tabbar.js b/weapp/components/app-tabbar/app-tabbar.js deleted file mode 100644 index 46b50c8..0000000 --- a/weapp/components/app-tabbar/app-tabbar.js +++ /dev/null @@ -1,20 +0,0 @@ -const { tabForPage, paths } = require('../../utils/navigation'); -Component({ - data: { active: '' }, - lifetimes: { attached: function() { this.syncActive(); } }, - pageLifetimes: { show: function() { this.syncActive(); } }, - methods: { - syncActive: function() { - var pages = getCurrentPages(); - var current = pages[pages.length - 1]; - this.setData({ active: tabForPage(current && current.route) }); - }, - navigate: function(e) { - var tab = e.currentTarget.dataset.tab; - if (!paths[tab]) return; - var pages = getCurrentPages(); - // 当前主页面重复点击不重新加载。 - if (pages.length && pages[pages.length - 1].route === paths[tab]) return; - wx.switchTab({ url: '/' + paths[tab] }); - } -} }); diff --git a/weapp/components/app-tabbar/app-tabbar.json b/weapp/components/app-tabbar/app-tabbar.json deleted file mode 100644 index a8456c1..0000000 --- a/weapp/components/app-tabbar/app-tabbar.json +++ /dev/null @@ -1 +0,0 @@ -{"component":true} \ No newline at end of file diff --git a/weapp/components/app-tabbar/app-tabbar.wxml b/weapp/components/app-tabbar/app-tabbar.wxml deleted file mode 100644 index b698cc1..0000000 --- a/weapp/components/app-tabbar/app-tabbar.wxml +++ /dev/null @@ -1,11 +0,0 @@ - - - 我们 - - - 记录 - - - 我的 - - diff --git a/weapp/components/app-tabbar/app-tabbar.wxss b/weapp/components/app-tabbar/app-tabbar.wxss deleted file mode 100644 index b3ea91e..0000000 --- a/weapp/components/app-tabbar/app-tabbar.wxss +++ /dev/null @@ -1,43 +0,0 @@ -.tabs { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 900; - display: flex; - align-items: center; - background: #FFF0F5; - border-top: 1rpx solid #F3DCE5; - padding: 10rpx 20rpx calc(12rpx + env(safe-area-inset-bottom)); - box-sizing: border-box; -} -.tab { - flex: 1; - min-width: 0; - min-height: 92rpx; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4rpx; - padding: 4rpx 0; - color: #8B827B; -} -.tab-icon { - display: flex; - align-items: center; - justify-content: center; - width: 88rpx; - height: 54rpx; - border-radius: 32rpx; - background: transparent; - transition: background-color .18s ease; -} -.tab-icon image { width: 48rpx; height: 48rpx; } -.tab-label { font-size: 23rpx; line-height: 1.4; letter-spacing: 1rpx; } -.active { color: #B74768; } -.active .tab-icon { background: #FADCE7; } -.active .tab-label { font-weight: 600; } -.tab-pressed .tab-icon { background: #FCE5ED; } - -@import '../../styles/typography.wxss'; diff --git a/weapp/components/smoking-card/smoking-card.js b/weapp/components/smoking-card/smoking-card.js index 68ac40f..8581972 100644 --- a/weapp/components/smoking-card/smoking-card.js +++ b/weapp/components/smoking-card/smoking-card.js @@ -8,6 +8,7 @@ Component({ lifetimes: { attached(){this.load();} }, pageLifetimes: { show(){this.load();} }, methods: { + openDisplaySettings(){wx.navigateTo({url:'/pages/settings/settings'});}, async load(force) { if(this._loading) return this._loading; if(force)cache.invalidate(['smokingInfo']); diff --git a/weapp/components/smoking-card/smoking-card.wxml b/weapp/components/smoking-card/smoking-card.wxml index 71d0647..c47365b 100644 --- a/weapp/components/smoking-card/smoking-card.wxml +++ b/weapp/components/smoking-card/smoking-card.wxml @@ -1,4 +1,5 @@ + 可在「我的 → 更多」关闭此事件 › 默认事件 · 按支记录抽烟记录明细 › 我的记录TA的记录 正在加载记录… diff --git a/weapp/components/smoking-card/smoking-card.wxss b/weapp/components/smoking-card/smoking-card.wxss index d6c00ef..d827cf1 100644 --- a/weapp/components/smoking-card/smoking-card.wxss +++ b/weapp/components/smoking-card/smoking-card.wxss @@ -16,3 +16,5 @@ .smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6} .smoke-ledger-footer{text-align:center;padding-top:20rpx} + +.smoke-setting-hint{display:block;text-align:right;color:#A39386;font-size:20rpx;line-height:1.6;margin-bottom:12rpx;} diff --git a/weapp/config/env.js b/weapp/config/env.js index 32408ab..87ab3b7 100644 --- a/weapp/config/env.js +++ b/weapp/config/env.js @@ -1,13 +1,7 @@ -// 开发工具使用本机服务;真机调试时把 dev.apiBase 改为电脑的局域网 IP。 -const environments = { - dev: { apiBase: 'http://127.0.0.1:8080/api' }, - trial: { apiBase: 'https://we.sundynix.cn/api' }, - release: { apiBase: 'https://we.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 }; +// 上传前将此地址改为正式 API 地址。 +//const API_BASE = 'http://192.168.100.3:8080/api'; + +const API_BASE = 'https://we.sundynix.cn/api'; + + +module.exports = { API_BASE }; diff --git a/weapp/custom-tab-bar/index.js b/weapp/custom-tab-bar/index.js index 2d0676b..582a6f5 100644 --- a/weapp/custom-tab-bar/index.js +++ b/weapp/custom-tab-bar/index.js @@ -1,3 +1,4 @@ +const { requireRecordAccess } = require('../utils/record-access'); const { tryLoginRenewal } = require('../utils/subscribe'); const { tabForPage, paths } = require('../utils/navigation'); Component({ @@ -15,6 +16,7 @@ Component({ tryLoginRenewal(); var tab = e.currentTarget.dataset.tab; if (!paths[tab]) return; + if (tab === 'records' && !requireRecordAccess()) return; var pages = getCurrentPages(); // 当前主页面重复点击不重新加载。 if (pages.length && pages[pages.length - 1].route === paths[tab]) return; diff --git a/weapp/pages/album/album.js b/weapp/pages/album/album.js index cdac8a8..8601df8 100644 --- a/weapp/pages/album/album.js +++ b/weapp/pages/album/album.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { uploadReference } = require('../../utils/file-media'); const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination'); @@ -8,7 +9,7 @@ const { getLayout } = require('../../utils/layout'); // pages/album/album.js - 日记式分组相册 const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request'); const { guardStorageSpace } = require('../../utils/storage-guard'); -Page(withDataCache(withListPagination({ +Page(withRecordAccess(withDataCache(withListPagination({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, @@ -534,4 +535,4 @@ Page(withDataCache(withListPagination({ goToAlbums: function() { wx.navigateBack(); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } -}, 'loadData'), {"loadData": ["photos", "albumList"]})); +}, 'loadData'), {"loadData": ["photos", "albumList"]}))); diff --git a/weapp/pages/album/album.wxml b/weapp/pages/album/album.wxml index f75d232..02b0640 100644 --- a/weapp/pages/album/album.wxml +++ b/weapp/pages/album/album.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/albums/albums.js b/weapp/pages/albums/albums.js index d04ee70..e8dd6a9 100644 --- a/weapp/pages/albums/albums.js +++ b/weapp/pages/albums/albums.js @@ -1,10 +1,11 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); // pages/albums/albums.js const { api, getImageUrl } = require('../../utils/request'); -Page(withDataCache(withListPagination({ +Page(withRecordAccess(withDataCache(withListPagination({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { @@ -177,4 +178,4 @@ Page(withDataCache(withListPagination({ goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goBack: function() { wx.navigateBack(); }, goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } - }, 'loadAlbums'), {"loadAlbums": ["albumList"]})); + }, 'loadAlbums'), {"loadAlbums": ["albumList"]}))); diff --git a/weapp/pages/albums/albums.wxml b/weapp/pages/albums/albums.wxml index 1cf2d2c..e955b96 100644 --- a/weapp/pages/albums/albums.wxml +++ b/weapp/pages/albums/albums.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/checkin-detail/checkin-detail.js b/weapp/pages/checkin-detail/checkin-detail.js index 047febf..0d7f918 100644 --- a/weapp/pages/checkin-detail/checkin-detail.js +++ b/weapp/pages/checkin-detail/checkin-detail.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { uploadReference } = require('../../utils/file-media'); const { withDataCache } = require('../../utils/page-cache'); @@ -7,7 +8,7 @@ const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/re const { createSwipeBack } = require('../../utils/util'); const { guardStorageSpace } = require('../../utils/storage-guard'); const COOLDOWN = 5000; -Page(withDataCache({ +Page(withRecordAccess(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, item: null, logs: [], logsPage: 1, logsTotal: 0, logsHasMore: false, logsLoading: false, logsError: false, freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' }, @@ -223,4 +224,4 @@ Page(withDataCache({ goBack: function() { wx.navigateBack(); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } -}, {"loadDetail": ["checkinDetail", "coupleInfo"]})); +}, {"loadDetail": ["checkinDetail", "coupleInfo"]}))); diff --git a/weapp/pages/checkin-detail/checkin-detail.wxml b/weapp/pages/checkin-detail/checkin-detail.wxml index ff3a07a..fbad738 100644 --- a/weapp/pages/checkin-detail/checkin-detail.wxml +++ b/weapp/pages/checkin-detail/checkin-detail.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/diary/diary.js b/weapp/pages/diary/diary.js index c3fb094..2c59a3f 100644 --- a/weapp/pages/diary/diary.js +++ b/weapp/pages/diary/diary.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); @@ -6,7 +7,7 @@ const { getLayout } = require('../../utils/layout'); const { api, getImageUrl } = require('../../utils/request'); const { diaryEntry } = require('../../utils/journal'); const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos'); -Page(withDataCache(withListPagination({ +Page(withRecordAccess(withDataCache(withListPagination({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { @@ -166,4 +167,4 @@ Page(withDataCache(withListPagination({ goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }, goBack: function() { wx.navigateBack(); }, goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); } -}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]})); +}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]}))); diff --git a/weapp/pages/diary/diary.wxml b/weapp/pages/diary/diary.wxml index 198675f..3827025 100644 --- a/weapp/pages/diary/diary.wxml +++ b/weapp/pages/diary/diary.wxml @@ -1,6 +1,6 @@ - + LIFE IN WORDS生活里的每一页 diff --git a/weapp/pages/diary_detail/diary_detail.js b/weapp/pages/diary_detail/diary_detail.js index 8a3fa04..15b48d7 100644 --- a/weapp/pages/diary_detail/diary_detail.js +++ b/weapp/pages/diary_detail/diary_detail.js @@ -1,9 +1,10 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); const { api, getImageUrl } = require('../../utils/request'); -Page(withDataCache({ +Page(withRecordAccess(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, noop: function() {}, onResize: function() { this.setData(getLayout()); }, @@ -279,4 +280,4 @@ Page(withDataCache({ goBack: function() { wx.navigateBack(); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); } -}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]})); +}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]}))); diff --git a/weapp/pages/diary_detail/diary_detail.wxml b/weapp/pages/diary_detail/diary_detail.wxml index cf68119..366b678 100644 --- a/weapp/pages/diary_detail/diary_detail.wxml +++ b/weapp/pages/diary_detail/diary_detail.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/diary_edit/diary_edit.js b/weapp/pages/diary_edit/diary_edit.js index 12ab46d..87cfe05 100644 --- a/weapp/pages/diary_edit/diary_edit.js +++ b/weapp/pages/diary_edit/diary_edit.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { uploadReference } = require('../../utils/file-media'); const { getLayout } = require('../../utils/layout'); @@ -7,7 +8,7 @@ const { guardStorageSpace } = require('../../utils/storage-guard'); var MOOD_OPTIONS = ['😊', '🥰', '😢', '😤', '😴', '🤩', '🥺', '😎', '🤔', '😭', '😋', '😌']; var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️', '🌤️', '🌥️', '🌈', '🌪️', '🌫️', '🌙']; -Page({ +Page(withRecordAccess({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { @@ -165,4 +166,4 @@ Page({ goBack: function() { wx.navigateBack(); } -}); +})); diff --git a/weapp/pages/diary_edit/diary_edit.wxml b/weapp/pages/diary_edit/diary_edit.wxml index ac925c0..34dfa27 100644 --- a/weapp/pages/diary_edit/diary_edit.wxml +++ b/weapp/pages/diary_edit/diary_edit.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/discover/discover.js b/weapp/pages/discover/discover.js index 9955c2e..f9bfece 100644 --- a/weapp/pages/discover/discover.js +++ b/weapp/pages/discover/discover.js @@ -1,3 +1,4 @@ +const { guardRecordEntries, withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, totalOf } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); @@ -7,7 +8,7 @@ const { api } = require('../../utils/request'); const { diaryEntry, albumEntry, memoryEntry } = require('../../utils/journal'); const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos'); -Page(withDataCache({ +Page(withRecordAccess(guardRecordEntries(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { @@ -100,4 +101,4 @@ Page(withDataCache({ this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/period/period' }); } -}, {"loadStats": ["timelineList", "albumList", "diaryList"]})); +}, {"loadStats": ["timelineList", "albumList", "diaryList"]}), ['showPublish', '_publish', 'uploadPhotos', 'openJournalEntry', 'openAlbum', 'goToDiaryFromDiscover', 'goToTimeline', 'goToAlbum', 'goToSongs', 'goToCheckin', 'goToPet', 'goToPeriod', 'selectFilter']))); diff --git a/weapp/pages/discover/discover.wxml b/weapp/pages/discover/discover.wxml index 8605490..310865f 100644 --- a/weapp/pages/discover/discover.wxml +++ b/weapp/pages/discover/discover.wxml @@ -1,6 +1,6 @@ -PAGES OF US我们的生活手账 +PAGES OF US我们的生活手账 一起的日常 歌曲打卡宠物姨姨 diff --git a/weapp/pages/index/index.js b/weapp/pages/index/index.js index b55d291..29eda38 100644 --- a/weapp/pages/index/index.js +++ b/weapp/pages/index/index.js @@ -1,3 +1,4 @@ +const { guardRecordEntries } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const announcementSession = require('../../utils/announcement-session'); const { itemsOf, totalOf } = require('../../utils/list-pagination'); @@ -10,11 +11,12 @@ const { api, getImageUrl } = require('../../utils/request'); const { diaryEntry } = require('../../utils/journal'); const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos'); -Page(withDataCache({ +Page(guardRecordEntries(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { showAnnouncement:false, announcement:null, + greetingDate:'', greetingText:'你好', greetingNote:'把今天的小美好,也留在这里。', userId: null, coupleId: null, @@ -46,6 +48,7 @@ Page(withDataCache({ }, onLoad: function() { + this.updateGreeting(); this.setData(getLayout()); this.setData({ userId: wx.getStorageSync('userId'), @@ -57,6 +60,7 @@ Page(withDataCache({ }, onShow: function() { + this.updateGreeting(); this._announcementActive=true; this.loadAnnouncement(); resetScrollOnTabShow(this, 'index'); @@ -78,6 +82,18 @@ Page(withDataCache({ this.fetchIpLocation(); }, + updateGreeting: function() { + const now = new Date(), hour = now.getHours(); + const weekdays = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']; + let text, note; + if (hour < 5) { text='夜深了'; note='记下心里的小事,也别忘了早点休息。'; } + else if (hour < 11) { text='早上好'; note='新的一天,也有值得珍藏的小美好。'; } + else if (hour < 14) { text='中午好'; note='好好吃饭,慢慢收藏生活里的甜。'; } + else if (hour < 18) { text='下午好'; note='让忙碌暂停一会儿,记下此刻的心情。'; } + else { text='晚上好'; note='今天的故事,想留在哪一页?'; } + this.setData({greetingDate:(now.getMonth()+1)+'月'+now.getDate()+'日 '+weekdays[now.getDay()],greetingText:text,greetingNote:note}); + }, + onHide: function() { this._announcementActive=false; }, onUnload: function() { this._announcementActive=false; }, loadAnnouncement: function() { @@ -241,6 +257,10 @@ Page(withDataCache({ }, loadStats: async function() { + if (!this.data.coupleId) { + this.setData({recentDiary:[],recentAlbums:[],upcomingMemory:null,statsLoading:false,statsError:false,stats:{timeline:0,album:0,diary:0,songs:0,checkin:0,pet:0}}); + return; + } this.setData({ statsLoading: !this._hasCachedData, statsError: false }); try { const [timeline, album, diary, songs, checkin, pet] = await Promise.all([ @@ -368,4 +388,4 @@ Page(withDataCache({ imageUrl: '/images/share.png' }; } -}, {"loadData": ["coupleInfo", "timelineList", "albumList", "diaryList", "songsList", "checkinList", "petInfo", "getBgConfig"]})); +}, {"loadData": ["coupleInfo", "timelineList", "albumList", "diaryList", "songsList", "checkinList", "petInfo", "getBgConfig"]}), ['goToDiary', 'goToTimeline', 'goToAlbum', 'goToSongs', 'goToCheckin', 'goToPet', 'goToPeriod', 'openJournalEntry', 'writeDiary', 'openAlbumPreview', 'openRecentDiary', 'showMore'])); diff --git a/weapp/pages/index/index.wxml b/weapp/pages/index/index.wxml index 2ba0b2c..a9024f0 100644 --- a/weapp/pages/index/index.wxml +++ b/weapp/pages/index/index.wxml @@ -5,6 +5,11 @@ OUR LITTLE WORLD让所有的爱有迹可循 + + {{greetingDate}} + {{greetingText}},{{partner1Name && partner1Name !== '?' ? partner1Name : '亲爱的'}} + {{greetingNote}} + diff --git a/weapp/pages/index/index.wxss b/weapp/pages/index/index.wxss index 843fc6e..da610ee 100644 --- a/weapp/pages/index/index.wxss +++ b/weapp/pages/index/index.wxss @@ -57,3 +57,11 @@ .home-has-background .home-couple-panel { padding:24rpx; border:1rpx solid rgba(233,221,208,.65); border-radius:24rpx; background:rgba(255,253,248,.82); } .notice-overlay {position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;background:rgba(49,36,42,.46);padding:48rpx;box-sizing:border-box;}.notice-dialog{width:100%;max-width:640rpx;background:#FFFCF8;border-radius:36rpx;padding:44rpx;box-sizing:border-box;}.notice-label{display:block;color:#9B7B83;font-size:22rpx;}.notice-title{display:block;font-size:36rpx;color:#64424D;margin:16rpx 0 28rpx;}.notice-content{max-height:50vh;white-space:pre-wrap;font-size:28rpx;line-height:1.8;color:#5F5557;}.notice-dismiss{margin-top:32rpx;background:#667E75;color:#fff;border-radius:20rpx;font-size:28rpx;} + +/* 首页问候沿用手账字体与柔和玫瑰色,随正文一起滚动。 */ +.home-greeting { margin:0 0 48rpx; padding:0 4rpx; } +.home-greeting-date { display:block; color:#9C747F; font-size:24rpx; line-height:1.6; letter-spacing:1rpx; } +.home-greeting-title { display:block; color:#A44E6B; font-size:44rpx; line-height:1.45; margin-top:8rpx; overflow-wrap:break-word; word-break:break-all; } +.home-greeting-note { display:block; color:#948179; font-size:25rpx; line-height:1.7; margin-top:10rpx; } +.home-has-background .home-greeting { background:rgba(255,253,248,.86); border-radius:24rpx; padding:24rpx 28rpx; } +@media (max-width:350px) { .home-greeting-title { font-size:38rpx; } } diff --git a/weapp/pages/lovewords/lovewords.js b/weapp/pages/lovewords/lovewords.js index b14d2ce..38a4c18 100644 --- a/weapp/pages/lovewords/lovewords.js +++ b/weapp/pages/lovewords/lovewords.js @@ -1,10 +1,11 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); // pages/lovewords/lovewords.js - 一起听过的歌 const { api } = require('../../utils/request'); -Page(withDataCache(withListPagination({ +Page(withRecordAccess(withDataCache(withListPagination({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, @@ -126,4 +127,4 @@ Page(withDataCache(withListPagination({ goBack: function() { wx.navigateBack(); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } -}, 'loadData'), {"loadData": ["songsList"]})); +}, 'loadData'), {"loadData": ["songsList"]}))); diff --git a/weapp/pages/lovewords/lovewords.wxml b/weapp/pages/lovewords/lovewords.wxml index 8c2cfee..1c694e7 100644 --- a/weapp/pages/lovewords/lovewords.wxml +++ b/weapp/pages/lovewords/lovewords.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/period/period.js b/weapp/pages/period/period.js index 3a95bdf..5ca948a 100644 --- a/weapp/pages/period/period.js +++ b/weapp/pages/period/period.js @@ -1,10 +1,11 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { getLayout } = require('../../utils/layout'); // pages/period/period.js - 姨姨(美柚风格日历模式) const { api } = require('../../utils/request'); -Page({ +Page(withRecordAccess({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { @@ -1680,4 +1681,4 @@ Page({ goBack: function() { wx.navigateBack(); }, goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); }, goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); } -}); +})); diff --git a/weapp/pages/period/period.wxml b/weapp/pages/period/period.wxml index 47a287f..cb3a8cf 100644 --- a/weapp/pages/period/period.wxml +++ b/weapp/pages/period/period.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/pet/achievements.js b/weapp/pages/pet/achievements.js index acca8da..012cd7d 100644 --- a/weapp/pages/pet/achievements.js +++ b/weapp/pages/pet/achievements.js @@ -1,10 +1,11 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); // pages/pet/achievements.js const { api } = require('../../utils/request'); -Page(withDataCache({ +Page(withRecordAccess(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, @@ -63,4 +64,4 @@ Page(withDataCache({ goBack: function() { wx.navigateBack(); } -}, {"loadAchievements": ["petAchievements"]})); +}, {"loadAchievements": ["petAchievements"]}))); diff --git a/weapp/pages/pet/achievements.wxml b/weapp/pages/pet/achievements.wxml index 64a32a6..274ab73 100644 --- a/weapp/pages/pet/achievements.wxml +++ b/weapp/pages/pet/achievements.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/pet/pet.js b/weapp/pages/pet/pet.js index e1b5360..89babce 100644 --- a/weapp/pages/pet/pet.js +++ b/weapp/pages/pet/pet.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); @@ -20,7 +21,7 @@ const BREED_EMOJIS = { const STAGE_NAMES = ['', '幼犬期', '少年期', '青年期', '成年期', '终极期']; -Page(withDataCache({ +Page(withRecordAccess(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, needsBinding: false, @@ -324,4 +325,4 @@ Page(withDataCache({ }, stopPropagation: function() {} -}, {"loadPetInfo": ["coupleInfo", "petInfo"]})); +}, {"loadPetInfo": ["coupleInfo", "petInfo"]}))); diff --git a/weapp/pages/pet/pet.wxml b/weapp/pages/pet/pet.wxml index 28b4a1f..ad6f797 100644 --- a/weapp/pages/pet/pet.wxml +++ b/weapp/pages/pet/pet.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/profile/profile.js b/weapp/pages/profile/profile.js index 35f7e03..e005e7e 100644 --- a/weapp/pages/profile/profile.js +++ b/weapp/pages/profile/profile.js @@ -1,3 +1,4 @@ +const { requireRecordAccess, hasRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { uploadReference } = require('../../utils/file-media'); const { itemsOf, totalOf } = require('../../utils/list-pagination'); @@ -163,7 +164,8 @@ Page(withDataCache({ this._editSwipe = null; const point = e.changedTouches && e.changedTouches[0]; if (!start || !point) return; - const dx = start.x - point.clientX, dy = Math.abs(start.y - point.clientY); + // 从左往右滑动返回;纵向滚动、反向滑动与输入框操作不处理。 + const dx = point.clientX - start.x, dy = Math.abs(start.y - point.clientY); if (dx < 80 || dx <= dy * 1.5) return; if (this.data.showEditPartnerModal && (this.data.partnerSaving || this.data.partnerAvatarUploading)) return; if (this.data.showEditProfileModal) this.hideEditProfileModal(); @@ -1087,11 +1089,16 @@ Page(withDataCache({ } }, goStorage: function() { + if (!requireRecordAccess()) return; this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/storage/storage' }); }, loadProfileStats: async function() { + if (!hasRecordAccess()) { + this.setData({statisticItems:this.data.statisticItems.map(item=>({...item,count:item.key==='pet'?'未领养':0})),statsLoading:false,statsError:false}); + return; + } this.setData({statsLoading:true,statsError:false}); const keys=['timeline','album','diary','songs','checkin','pet']; const results=await Promise.allSettled([ @@ -1115,12 +1122,13 @@ Page(withDataCache({ const routes={timeline:'timeline',album:'albums',diary:'diary',songs:'lovewords',checkin:'punch',pet:'pet',period:'period',settings:'settings'}; const route=routes[e.currentTarget.dataset.key]; if(!route)return; + if(!requireRecordAccess())return; this._savePosBeforeLeave(); wx.navigateTo({url:'/pages/'+route+'/'+route}); }, // 页面跳转 manageRelationship: function() { wx.navigateTo({ url: '/pages/unbind/unbind' }); }, - goToSettings: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/settings/settings' }); }, + goToSettings: function() { if (!requireRecordAccess()) return; 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"], "loadStorageInfo": ["storageInfo"]})); diff --git a/weapp/pages/publish/publish.js b/weapp/pages/publish/publish.js index a549b46..0c5577a 100644 --- a/weapp/pages/publish/publish.js +++ b/weapp/pages/publish/publish.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { uploadReference } = require('../../utils/file-media'); const { getLayout } = require('../../utils/layout'); @@ -17,7 +18,7 @@ var TYPE_CONFIG = { checkin: { title: '添加打卡', btn: '保存打卡', api: 'checkinAdd' } }; -Page({ +Page(withRecordAccess({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { @@ -479,4 +480,4 @@ Page({ }, goBack: function() { wx.navigateBack(); } -}); +})); diff --git a/weapp/pages/publish/publish.wxml b/weapp/pages/publish/publish.wxml index aaff646..9406976 100644 --- a/weapp/pages/publish/publish.wxml +++ b/weapp/pages/publish/publish.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/punch/punch.js b/weapp/pages/punch/punch.js index 8f99f9d..3b3525c 100644 --- a/weapp/pages/punch/punch.js +++ b/weapp/pages/punch/punch.js @@ -1,14 +1,15 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); // pages/punch/punch.js - 情侣打卡 const { api, getImageUrl } = require('../../utils/request'); -Page(withDataCache(withListPagination({ +Page(withRecordAccess(withDataCache(withListPagination({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loadError: false, - checkins: [], smokingVisible: false, smokingKnown: false, smokingSaving: false, smokingError: false, + checkins: [], smokingVisible: true, smokingKnown: false, smokingError: false, coupleInfo: null, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA', @@ -28,14 +29,7 @@ Page(withDataCache(withListPagination({ try { const auth=await api.checkAuth(); this.setData({smokingVisible:Number(auth.user.smoking_card_visible)!==0, smokingKnown:true, smokingError:false}); } catch(e) { this.setData({smokingError:true}); } }, - toggleSmoking: async function(e) { - if (this.data.smokingSaving || !this.data.smokingKnown) return; - const previous=this.data.smokingVisible; - this.setData({smokingSaving:true}); - try { const result=await api.smokingCardVisible({visible:e.detail.value?1:0}); this.setData({smokingVisible:Number(result.card_visible)===1}); } - catch(error) { this.setData({smokingVisible:previous}); wx.showToast({title:error.error||'保存失败,请重试',icon:'none'}); } - finally { this.setData({smokingSaving:false}); } - }, + onSmokingUpdated: function() { this.loadData(); }, onPullDownRefresh: async function() { @@ -134,4 +128,4 @@ Page(withDataCache(withListPagination({ goBack: function() { wx.navigateBack(); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } -}, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"], "loadSmokingPreference":["checkAuth"], "loadCoupleInfo":["coupleInfo"]})); +}, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"], "loadSmokingPreference":["checkAuth"], "loadCoupleInfo":["coupleInfo"]}))); diff --git a/weapp/pages/punch/punch.wxml b/weapp/pages/punch/punch.wxml index 5c875ee..3fab37e 100644 --- a/weapp/pages/punch/punch.wxml +++ b/weapp/pages/punch/punch.wxml @@ -1,4 +1,4 @@ - + @@ -25,7 +25,6 @@ - 抽烟记录显示设置保存到账号,重新安装后仍生效重试加载中 diff --git a/weapp/pages/punch/punch.wxss b/weapp/pages/punch/punch.wxss index aeb6378..033ff4c 100644 --- a/weapp/pages/punch/punch.wxss +++ b/weapp/pages/punch/punch.wxss @@ -13,5 +13,3 @@ @import '../../styles/button-theme.wxss'; @import '../../styles/typography.wxss'; - -.smoking-preference { display:flex; align-items:center; justify-content:space-between; gap:20rpx; margin:26rpx 0; padding:24rpx; background:#F2F7F4; border-radius:24rpx; } diff --git a/weapp/pages/settings/settings.js b/weapp/pages/settings/settings.js index 2b01351..9de5226 100644 --- a/weapp/pages/settings/settings.js +++ b/weapp/pages/settings/settings.js @@ -6,6 +6,7 @@ const { guardStorageSpace } = require('../../utils/storage-guard'); Page({ onResize: function() { this.setData(getLayout()); }, data: { + smokingVisible:true, smokingKnown:false, smokingSaving:false, smokingError:false, showBgModal: false, bgPreview: '', bgFilePath: '', @@ -41,6 +42,7 @@ Page({ }, onShow: async function() { + this.loadSmokingPreference(); this.setData({ bindingKnown: false, bindingError:false }); try { var info = await api.coupleInfo(); @@ -59,6 +61,19 @@ Page({ } }, + loadSmokingPreference: async function() { + try { const auth=await api.checkAuth(); this.setData({smokingVisible:Number(auth.user.smoking_card_visible)!==0,smokingKnown:true,smokingError:false}); } + catch(error) { this.setData({smokingError:true}); } + }, + toggleSmoking: async function(e) { + if (this.data.smokingSaving || !this.data.smokingKnown) return; + const previous=this.data.smokingVisible; + this.setData({smokingSaving:true}); + try { const result=await api.smokingCardVisible({visible:e.detail.value?1:0}); this.setData({smokingVisible:Number(result.card_visible)===1}); } + catch(error) { this.setData({smokingVisible:previous}); wx.showToast({title:error.error||'保存失败,请重试',icon:'none'}); } + finally { this.setData({smokingSaving:false}); } + }, + goBind: function() { getApp().globalData.openProfileBind = true; wx.switchTab({ url: '/pages/profile/profile' }); diff --git a/weapp/pages/settings/settings.wxml b/weapp/pages/settings/settings.wxml index c5e8dcb..8cab8fc 100644 --- a/weapp/pages/settings/settings.wxml +++ b/weapp/pages/settings/settings.wxml @@ -24,6 +24,12 @@ › + + 抽烟记录默认开启,关闭后不在打卡页显示,已有记录保留 + + 重试加载中 + + 关系与账号 注销账号{{hasPartner ? '请先完成情侣解绑,再注销账号' : '确认后删除账号及全部个人数据'}}› diff --git a/weapp/pages/smoking-detail/smoking-detail.js b/weapp/pages/smoking-detail/smoking-detail.js index 8546da6..6360286 100644 --- a/weapp/pages/smoking-detail/smoking-detail.js +++ b/weapp/pages/smoking-detail/smoking-detail.js @@ -1,10 +1,11 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { getLayout } = require('../../utils/layout'); -Page({ +Page(withRecordAccess({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, data:{}, onLoad(){this.setData(getLayout());}, onResize(){this.setData(getLayout());}, loadMore(){const card=this.selectComponent('#smoking');if(card)return card.loadMoreLogs();}, async onPullDownRefresh(){try{const card=this.selectComponent('#smoking');if(card)await card.load(true);}finally{wx.stopPullDownRefresh();}} -}); +})); diff --git a/weapp/pages/smoking-detail/smoking-detail.wxml b/weapp/pages/smoking-detail/smoking-detail.wxml index 26b8b81..a2a518d 100644 --- a/weapp/pages/smoking-detail/smoking-detail.wxml +++ b/weapp/pages/smoking-detail/smoking-detail.wxml @@ -1 +1 @@ - + diff --git a/weapp/pages/storage/storage.js b/weapp/pages/storage/storage.js index 45ec95e..af41623 100644 --- a/weapp/pages/storage/storage.js +++ b/weapp/pages/storage/storage.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { withDataCache } = require('../../utils/page-cache'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { getLayout } = require('../../utils/layout'); @@ -10,7 +11,7 @@ function formatBytes(bytes) { return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'; } -Page(withDataCache({ +Page(withRecordAccess(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function () { this.setData(getLayout()); }, @@ -160,4 +161,4 @@ Page(withDataCache({ } }, -}, {loadInfo:['storageInfo']})); +}, {loadInfo:['storageInfo']}))); diff --git a/weapp/pages/storage/storage.wxml b/weapp/pages/storage/storage.wxml index c837505..872b2f5 100644 --- a/weapp/pages/storage/storage.wxml +++ b/weapp/pages/storage/storage.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/timeline-detail/timeline-detail.js b/weapp/pages/timeline-detail/timeline-detail.js index 4b9d232..f9f487c 100644 --- a/weapp/pages/timeline-detail/timeline-detail.js +++ b/weapp/pages/timeline-detail/timeline-detail.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { withDataCache } = require('../../utils/page-cache'); const { getLayout } = require('../../utils/layout'); @@ -59,7 +60,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) { return new Date(eventDate); } -Page(withDataCache({ +Page(withRecordAccess(withDataCache({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, @@ -207,4 +208,4 @@ Page(withDataCache({ goBack: function() { wx.navigateBack(); }, goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } -}, {"loadDetail": ["timelineDetail", "coupleInfo"]})); +}, {"loadDetail": ["timelineDetail", "coupleInfo"]}))); diff --git a/weapp/pages/timeline-detail/timeline-detail.wxml b/weapp/pages/timeline-detail/timeline-detail.wxml index 2e58d9f..326179e 100644 --- a/weapp/pages/timeline-detail/timeline-detail.wxml +++ b/weapp/pages/timeline-detail/timeline-detail.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/pages/timeline/timeline.js b/weapp/pages/timeline/timeline.js index bda4ec4..1a0d2d7 100644 --- a/weapp/pages/timeline/timeline.js +++ b/weapp/pages/timeline/timeline.js @@ -1,3 +1,4 @@ +const { withRecordAccess } = require('../../utils/record-access'); const { tryLoginRenewal } = require('../../utils/subscribe'); const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination'); const { withDataCache } = require('../../utils/page-cache'); @@ -58,7 +59,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) { return new Date(eventDate); } -Page(withDataCache(withListPagination({ +Page(withRecordAccess(withDataCache(withListPagination({ onLoginSubscribeTap: function() { return tryLoginRenewal(); }, onResize: function() { this.setData(getLayout()); }, data: { loading: true, loadError: false, @@ -194,4 +195,4 @@ Page(withDataCache(withListPagination({ goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }, goBack: function() { wx.navigateBack(); }, goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); } -}, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]})); +}, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]}))); diff --git a/weapp/pages/timeline/timeline.wxml b/weapp/pages/timeline/timeline.wxml index 72058e4..aa3fec3 100644 --- a/weapp/pages/timeline/timeline.wxml +++ b/weapp/pages/timeline/timeline.wxml @@ -1,4 +1,4 @@ - + diff --git a/weapp/tests/account-preferences.test.cjs b/weapp/tests/account-preferences.test.cjs index 0f57cf7..aa59349 100644 --- a/weapp/tests/account-preferences.test.cjs +++ b/weapp/tests/account-preferences.test.cjs @@ -3,11 +3,11 @@ function page(name,api,wxExtra={},app={}){let definition;const storage=new Map([ const flush=async()=>{for(let i=0;i<30;i++)await Promise.resolve();}; test('smoking preference is restored from server on a newly created page, even without local storage',async()=>{ let visible=0;const api={checkAuth:async()=>({user:{smoking_card_visible:visible}}),smokingCardVisible:async d=>{visible=d.visible;return {card_visible:visible};}}; - const first=page('punch/punch.js',api);await first.p.loadSmokingPreference();assert.equal(first.p.data.smokingVisible,false);await first.p.toggleSmoking({detail:{value:true}});assert.equal(visible,1); + const first=page('settings/settings.js',api);await first.p.loadSmokingPreference();assert.equal(first.p.data.smokingVisible,false);await first.p.toggleSmoking({detail:{value:true}});assert.equal(visible,1); const installed=page('punch/punch.js',api);await installed.p.loadSmokingPreference();assert.equal(installed.p.data.smokingVisible,true);assert.equal(installed.storage.has('smokingVisible'),false); }); test('failed smoking preference save retains the last server state',async()=>{ - const e=page('punch/punch.js',{checkAuth:async()=>({user:{smoking_card_visible:0}}),smokingCardVisible:async()=>{throw {error:'offline'};}});await e.p.loadSmokingPreference();await e.p.toggleSmoking({detail:{value:true}});assert.equal(e.p.data.smokingVisible,false);assert.equal(e.p.data.smokingSaving,false); + const e=page('settings/settings.js',{checkAuth:async()=>({user:{smoking_card_visible:0}}),smokingCardVisible:async()=>{throw {error:'offline'};}});await e.p.loadSmokingPreference();await e.p.toggleSmoking({detail:{value:true}});assert.equal(e.p.data.smokingVisible,false);assert.equal(e.p.data.smokingSaving,false); }); test('announcement is dismissible per revision and server disable immediately hides it',async()=>{ let data={enabled:true,title:'公告',content:'Hello',revision:'v1'};const e=page('index/index.js',{announcement:async()=>data});await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);e.p.closeAnnouncement();await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);data={...data,content:'Updated',revision:'v2'};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);data={...data,enabled:false};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false); diff --git a/weapp/tests/env.test.cjs b/weapp/tests/env.test.cjs index ea0b51e..6cc861b 100644 --- a/weapp/tests/env.test.cjs +++ b/weapp/tests/env.test.cjs @@ -2,14 +2,14 @@ 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', () => { +test('requests and file previews use the manually configured API base in every runtime', () => { 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 configured = 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://we.sundynix.cn/api'); + assert.deepEqual(Object.keys(configured),['API_BASE']); + assert.equal(API_BASE,require('../config/env').API_BASE); assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238'); } }); diff --git a/weapp/tests/file-media.test.cjs b/weapp/tests/file-media.test.cjs index c6940d0..6a2a09d 100644 --- a/weapp/tests/file-media.test.cjs +++ b/weapp/tests/file-media.test.cjs @@ -33,7 +33,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://we.sundynix.cn/api/files/'+id); + assert.equal(c.getImageUrl(result.path),c.API_BASE+'/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 () => { @@ -56,5 +56,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://we.sundynix.cn/api/files/' + id); + assert.equal(c.getImageUrl(id), c.API_BASE+'/files/' + id); }); diff --git a/weapp/tests/journal.test.cjs b/weapp/tests/journal.test.cjs index 7c69438..4ead296 100644 --- a/weapp/tests/journal.test.cjs +++ b/weapp/tests/journal.test.cjs @@ -15,6 +15,7 @@ function records(api) { Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(), wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)}, require(name) { + if(name.endsWith('/record-access'))return {guardRecordEntries:p=>p,withRecordAccess:p=>p}; if(name.endsWith('/list-pagination'))return require('../utils/list-pagination'); if(name.endsWith('/page-cache'))return {withDataCache:p=>p}; if(name.endsWith('/request'))return {api}; diff --git a/weapp/tests/navigation.test.cjs b/weapp/tests/navigation.test.cjs index e0ce211..591e3c4 100644 --- a/weapp/tests/navigation.test.cjs +++ b/weapp/tests/navigation.test.cjs @@ -25,7 +25,7 @@ test('native custom tabs retain the three main pages; pages never mount a duplic test('component selects the main page, hides on child routes and avoids reloading its current tab',()=>{ let definition,route='pages/profile/profile',pageData={};const navigations=[]; vm.runInNewContext(fs.readFileSync(path.join(root,'custom-tab-bar/index.js'),'utf8'),{ - Component:c=>definition=c,require:name=>name.includes('subscribe')?{tryLoginRenewal(){}}:({tabForPage,paths}),getCurrentPages:()=>[{route,data:pageData}],wx:{switchTab:o=>{navigations.push(o.url);o.complete();},reLaunch(){assert.fail('normal tab switching must retain pages');}} + Component:c=>definition=c,require:name=>name.includes('record-access')?{requireRecordAccess:()=>true}:name.includes('subscribe')?{tryLoginRenewal(){}}:({tabForPage,paths}),getCurrentPages:()=>[{route,data:pageData}],wx:{switchTab:o=>{navigations.push(o.url);o.complete();},reLaunch(){assert.fail('normal tab switching must retain pages');}} }); const component={...definition.methods,data:{...definition.data},setData(v){Object.assign(this.data,v);}}; definition.lifetimes.attached.call(component);assert.equal(component.data.active,'profile'); diff --git a/weapp/tests/permissions.test.cjs b/weapp/tests/permissions.test.cjs index 373e94e..de1ca42 100644 --- a/weapp/tests/permissions.test.cjs +++ b/weapp/tests/permissions.test.cjs @@ -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://we.sundynix.cn/api/files/6083e119-6b26-441b-8b70-44d224366238'); + assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),require('../config/env').API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238'); }); test('settings and profile consent retain couple cache and lead into cooling',async()=>{ diff --git a/weapp/tests/profile-swipe.test.cjs b/weapp/tests/profile-swipe.test.cjs index 12a0954..0141f4a 100644 --- a/weapp/tests/profile-swipe.test.cjs +++ b/weapp/tests/profile-swipe.test.cjs @@ -7,13 +7,17 @@ function setup(partner=false){ } const start=(x,y,ignore=false)=>({touches:[{clientX:x,clientY:y}],target:{dataset:{swipeIgnore:ignore}}}); const end=(x,y)=>({changedTouches:[{clientX:x,clientY:y}]}); -test('left swipe closes either editor once, restores tabs and never navigates away from profile',()=>{ - for(const partner of [false,true]){const {p,calls}=setup(partner);p.onEditTouchStart(start(280,300));p.onEditTouchEnd(end(100,310));p.onEditTouchEnd(end(100,310));assert.equal(p.data.showEditProfileModal,false);assert.equal(p.data.showEditPartnerModal,false);assert.deepEqual(calls,[false,'keyboard']);} +test('leftward swipe keeps either editor open',()=>{ + for(const partner of [false,true]){const {p,calls}=setup(partner);p.onEditTouchStart(start(280,300));p.onEditTouchEnd(end(100,310));assert.equal(partner?p.data.showEditPartnerModal:p.data.showEditProfileModal,true);assert.deepEqual(calls,[]);} }); -test('vertical scrolling, short or rightward gestures and text selection keep the editor open',()=>{ - for(const [s,e] of [[start(280,300),end(240,305)],[start(280,300),end(160,500)],[start(100,300),end(280,310)],[start(280,300,true),end(100,310)]]){const {p,calls}=setup();p.onEditTouchStart(s);p.onEditTouchEnd(e);assert.equal(p.data.showEditProfileModal,true);assert.equal(calls.length,0);} +test('vertical scrolling, short gestures and text selection keep the editor open',()=>{ + for(const [s,e] of [[start(40,300),end(80,305)],[start(40,300),end(160,500)],[start(40,300,true),end(180,310)]]){const {p,calls}=setup();p.onEditTouchStart(s);p.onEditTouchEnd(e);assert.equal(p.data.showEditProfileModal,true);assert.equal(calls.length,0);} }); test('cancelled touches and partner saving never dismiss the editor',()=>{ - const {p}=setup();p.onEditTouchStart(start(280,300));p.onEditTouchCancel();p.onEditTouchEnd(end(100,300));assert.equal(p.data.showEditProfileModal,true); - const e=setup(true);e.p.data.partnerSaving=true;e.p.onEditTouchStart(start(280,300));e.p.onEditTouchEnd(end(100,300));assert.equal(e.p.data.showEditPartnerModal,true); + const {p}=setup();p.onEditTouchStart(start(12,300));p.onEditTouchCancel();p.onEditTouchEnd(end(160,300));assert.equal(p.data.showEditProfileModal,true); + const e=setup(true);e.p.data.partnerSaving=true;e.p.onEditTouchStart(start(12,300));e.p.onEditTouchEnd(end(160,300));assert.equal(e.p.data.showEditPartnerModal,true); +}); + +test('right swipe closes either editor exactly once without navigating the tab page',()=>{ + for(const partner of [false,true]){const {p,calls}=setup(partner);p.onEditTouchStart(start(12,300));p.onEditTouchEnd(end(160,310));p.onEditTouchEnd(end(160,310));assert.equal(p.data.showEditProfileModal,false);assert.equal(p.data.showEditPartnerModal,false);assert.deepEqual(calls,[false,'keyboard']);} }); diff --git a/weapp/tests/record-access.test.cjs b/weapp/tests/record-access.test.cjs new file mode 100644 index 0000000..c1b0781 --- /dev/null +++ b/weapp/tests/record-access.test.cjs @@ -0,0 +1,56 @@ +const test=require('node:test'), assert=require('node:assert/strict'), path=require('node:path'); +const createLoader=require('./load-module.cjs'); +const root=path.join(__dirname,'..'); +function env(cid='') { + const modals=[],routes=[],storage={coupleId:cid};let page,component; + const app={globalData:{},ensureAuth:async()=>{}}; + const wx={getStorageSync:k=>storage[k],removeStorageSync:k=>delete storage[k],showModal:o=>modals.push(o),switchTab:o=>routes.push(o.url),navigateTo:o=>routes.push(o.url),showToast(){}}; + const load=createLoader({wx,console,Date,Promise,setTimeout,clearTimeout,setInterval,clearInterval,getApp:()=>app,Page:p=>page=p,Component:c=>component=c,getCurrentPages:()=>[{route:'pages/index/index',data:{}}]}, {'../../utils/request':{api:{},getImageUrl:v=>v},'../../utils/page-cache':{withDataCache:p=>p}}); + return {wx,modals,routes,storage,app,load,page:()=>({...page,data:{...page.data},setData(v){Object.assign(this.data,v)}}),component:()=>component}; +} +test('unbound entry modal is shared and confirmation opens the existing invitation panel',()=>{ + const e=env(),guard=e.load(path.join(root,'utils/record-access.js')); + assert.equal(guard.requireRecordAccess(),false);guard.requireRecordAccess();assert.equal(e.modals.length,1); + const modal=e.modals[0];assert.equal(modal.confirmText,'立即邀请');modal.success({confirm:true});modal.complete(); + assert.equal(e.app.globalData.openProfileBind,true);assert.deepEqual(e.routes,['/pages/profile/profile']); + e.storage.coupleId='couple';assert.equal(guard.requireRecordAccess(),true); +}); +test('homepage record links and profile feature shortcuts reject unbound users before navigation',()=>{ + for(const [file,methods] of [['index/index',['showMore','writeDiary','goToAlbum','goToCheckin']],['profile/profile',['openStatistic']]]) { + for(const method of methods){const e=env();e.load(path.join(root,'pages',file+'.js'));const p=e.page();p[method]({currentTarget:{dataset:{key:'diary'}}});assert.equal(e.routes.length,0);assert.equal(e.modals.length,1,method);} + } +}); +test('record tab rejects unbound navigation and allows bound navigation',()=>{ + for(const cid of ['', 'couple']){const e=env(cid);e.load(path.join(root,'custom-tab-bar/index.js'));const c=e.component();const p={...c.methods,setData(){}};p.navigate({currentTarget:{dataset:{tab:'records'}}});assert.equal(e.routes.length,cid?1:0);assert.equal(e.modals.length,cid?0:1);} +}); +test('all protected direct links avoid loading records when unbound and cancellation returns home',async()=>{ + const pages=['discover/discover','timeline/timeline','punch/punch','diary/diary','albums/albums','album/album','lovewords/lovewords','publish/publish','checkin-detail/checkin-detail','timeline-detail/timeline-detail','period/period','diary_detail/diary_detail','diary_edit/diary_edit','pet/pet','pet/achievements','smoking-detail/smoking-detail','storage/storage']; + for(const file of pages){const e=env();e.load(path.join(root,'pages',file+'.js'));const p=e.page();await p.onLoad({});await p.onShow();assert.equal(p.data.recordAccessBlocked,true,file);assert.equal(e.modals.length,1,file);e.modals[0].success({cancel:true});e.modals[0].complete();assert.deepEqual(e.routes,['/pages/index/index']);} +}); +test('cold direct entry waits for login before deciding binding and loading the page',async()=>{ + const e=env();let loaded=0,shown=0,finish; + e.app.ensureAuth=()=>new Promise(resolve=>finish=()=>{e.storage.coupleId='bound';resolve();}); + const guard=e.load(path.join(root,'utils/record-access.js')); + const p=guard.withRecordAccess({onLoad(){loaded++;},onShow(){shown++;},setData(v){this.data={...this.data,...v};}}); + const load=p.onLoad(),show=p.onShow();assert.equal(loaded,0);finish();await Promise.all([load,show]);assert.equal(loaded,1);assert.equal(shown,1);assert.equal(p.data.recordAccessBlocked,false);assert.equal(e.modals.length,0); + e.storage.coupleId='';await p.onShow();assert.equal(shown,1);assert.equal(e.modals.length,1); +}); + +test('authoritative backend denial clears stale binding and offers invitation',async()=>{ + const e=env('stale-couple'); + e.wx.request=o=>o.success({statusCode:403,data:{error:'请先绑定情侣,再使用记录功能',code:10002}}); + const {api}=e.load(path.join(root,'utils/request.js')); + await assert.rejects(api.diaryAdd({content:'blocked'}),error=>error.code===10002); + assert.equal(e.storage.coupleId,undefined);assert.equal(e.modals.length,1);assert.equal(e.routes.length,0); +}); + +test('more and storage entries require binding while bound navigation stays available',()=>{ + for(const cid of ['', 'couple']) { + for(const method of ['openStatistic','goStorage','goToSettings']) { + const e=env(cid);e.load(path.join(root,'pages/profile/profile.js'));const p=e.page(); + p[method]({currentTarget:{dataset:{key:'settings'}}}); + assert.equal(e.modals.length,cid?0:1,method);assert.equal(e.routes.length,cid?1:0,method); + if(cid) assert.equal(e.routes[0],method==='goStorage'?'/pages/storage/storage':'/pages/settings/settings'); + } + } +}); diff --git a/weapp/tests/request.test.cjs b/weapp/tests/request.test.cjs index 0ece809..630cf70 100644 --- a/weapp/tests/request.test.cjs +++ b/weapp/tests/request.test.cjs @@ -12,9 +12,9 @@ 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://we.sundynix.cn/api/diaryList?page=2&pageSize=20'); + assert.equal(calls[0].url,require('../config/env').API_BASE+'/diaryList?page=2&pageSize=20'); assert.equal(calls[0].header.Authorization,'Bearer test-token'); - assert.equal(calls[1].url,'https://we.sundynix.cn/api/periodVisibleSet'); + assert.equal(calls[1].url,require('../config/env').API_BASE+'/periodVisibleSet'); assert.equal(calls[1].data.periodVisible,0); }); test('multipart upload retains action and token when Android omits headers',async()=>{ @@ -38,7 +38,7 @@ test('failed homepage load ends the recent-record spinner',async()=>{ let definition; const api={coupleInfo:async()=>{throw { _statusCode:404 };}}; vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{ - Page(p){definition=p;},require(name){return name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};}, + Page(p){definition=p;},require(name){return name.endsWith('/record-access')?{guardRecordEntries:p=>p}:name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};}, wx:{getStorageSync:()=> 'test-token'}, console:{error(){}}, Date, setTimeout }); const page={...definition,data:{...definition.data},setData(data){Object.assign(this.data,data);}}; @@ -50,7 +50,7 @@ test('unbound newcomer loads homepage records without requesting couple-only pet let definition;let petCalls=0; const api={coupleInfo:async()=>({not_bound:true,partner1:{nickname:'新用户'}}),timelineList:async()=>[],albumList:async()=>[],diaryList:async()=>({items:[],total:0}),songsList:async()=>[],checkinList:async()=>({items:[]}),petInfo:async()=>{petCalls++;throw Error('请先绑定情侣');}}; vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{ - Page(p){definition=p;},require(name){return name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:name.endsWith('/journal')?{diaryEntry:v=>v}:{};}, + Page(p){definition=p;},require(name){return name.endsWith('/record-access')?{guardRecordEntries:p=>p}:name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:name.endsWith('/journal')?{diaryEntry:v=>v}:{};}, wx:{getStorageSync:()=>1,removeStorageSync(){}},console,Date,setTimeout }); const page={...definition,data:{...definition.data,coupleId:99},setData(data){Object.assign(this.data,data);}}; @@ -68,7 +68,7 @@ test('smoking visibility page uses the real request adapter and restores the per else if(o.url.endsWith('/smokingCardVisible')){visible=o.data.visible;o.success({statusCode:200,data:{success:true,cardVisible:visible}});} else throw new Error('Unexpected request: '+o.url); }}; - require('./load-module.cjs')({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/page-cache':{withDataCache:p=>p}})(path.join(__dirname,'../pages/punch/punch.js')); + require('./load-module.cjs')({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/page-cache':{withDataCache:p=>p}})(path.join(__dirname,'../pages/settings/settings.js')); const makePage=()=>({...definition,data:JSON.parse(JSON.stringify(definition.data)),setData(p){Object.assign(this.data,p);}}); const page=makePage();await page.loadSmokingPreference();assert.equal(page.data.smokingVisible,true); await page.toggleSmoking({detail:{value:false}});assert.equal(visible,0);assert.equal(page.data.smokingVisible,false); diff --git a/weapp/tests/upload-quota.test.cjs b/weapp/tests/upload-quota.test.cjs index b327f4c..a712e40 100644 --- a/weapp/tests/upload-quota.test.cjs +++ b/weapp/tests/upload-quota.test.cjs @@ -17,7 +17,7 @@ function setup({preflightQuota=false,uploadQuota=false,networkRetry=false,metada const options={url:'https://we.sundynix.cn/api/upload',filePath:'original.jpg'}; test('preflight checks the compressed bytes before any upload',async()=>{ const c=setup();await c.uploadFileCompat(options); - assert.equal(c.checks[0].url,'https://we.sundynix.cn/api/uploadPreflight'); + assert.equal(c.checks[0].url,c.API_BASE+'/uploadPreflight'); assert.equal(c.checks[0].data.size,100);assert.equal(c.checks[0].data.md5,'a'.repeat(32)); assert.equal(c.uploads[0].filePath,'compressed.jpg'); }); diff --git a/weapp/utils/record-access.js b/weapp/utils/record-access.js new file mode 100644 index 0000000..786e6b7 --- /dev/null +++ b/weapp/utils/record-access.js @@ -0,0 +1,60 @@ +// 所有记录入口使用同一绑定检查,个人资料、头像与邀请不受影响。 +let showing = false; +function hasRecordAccess() { const id = wx.getStorageSync('coupleId'); return !!id && String(id) !== '0'; } +function invitePartner() { + getApp().globalData.openProfileBind = true; + wx.switchTab({url:'/pages/profile/profile'}); +} +function requireRecordAccess(onCancel) { + if (hasRecordAccess()) return true; + if (showing) return false; + showing = true; + wx.showModal({ + title:'邀请另一半,开启共同记录', + content:'绑定情侣后,就可以一起写日记、收藏照片、记录纪念日和使用其他记录功能。邀请TA加入,开始你们的生活手账吧。', + confirmText:'立即邀请', cancelText:'暂时不了', confirmColor:'#B74768', + success(result) { if (result.confirm) invitePartner(); else if (onCancel) onCancel(); }, + fail() { if (onCancel) onCancel(); }, + complete() { showing = false; } + }); + return false; +} +function guardRecordEntries(definition, names) { + names.forEach(name => { + const original = definition[name]; + if (original) definition[name] = function(...args) { + if (!requireRecordAccess()) return; + return original.apply(this, args); + }; + }); + return definition; +} +// 分享链接、历史页面栈等直接进入二级页面时同样拦截,不加载数据。 +function withRecordAccess(definition) { + function enter(page, original, args) { + const allowed = hasRecordAccess(); + page.setData({recordAccessBlocked:!allowed}); + if (!allowed) { + requireRecordAccess(() => wx.switchTab({url:'/pages/index/index'})); + return; + } + return original && original.apply(page, args); + } + const load = definition.onLoad, show = definition.onShow, change = definition.onDataChanged; + definition.onLoad = function(...args) { + this.setData({recordAccessBlocked:true}); + const app = typeof getApp === 'function' && getApp(); + this._recordEntryReady = Promise.resolve(app && app.ensureAuth ? app.ensureAuth() : null) + .then(() => enter(this, load, args)).catch(error => { + console.warn('检查记录权限失败', error); + wx.showToast({title:'登录状态获取失败,请重新进入',icon:'none'}); + }); + return this._recordEntryReady; + }; + definition.onShow = function(...args) { + return Promise.resolve(this._recordEntryReady).then(() => enter(this, show, args)); + }; + definition.onDataChanged = function(...args) { return enter(this, change, args); }; + return definition; +} +module.exports = {hasRecordAccess, requireRecordAccess, invitePartner, guardRecordEntries, withRecordAccess}; diff --git a/weapp/utils/request.js b/weapp/utils/request.js index 84d3aec..c9aee27 100644 --- a/weapp/utils/request.js +++ b/weapp/utils/request.js @@ -64,6 +64,17 @@ function formatUploadError(data) { return msg; } +function maybeRedirectBinding(data) { + if (!data || Number(data.code) !== 10002) return false; + if (!data._bindingPromptShown) { + data._bindingPromptShown = true; + wx.removeStorageSync('coupleId'); + dataCache.clear(); + require('./record-access').requireRecordAccess(); + } + return true; +} + // 存储配额错误码(与后端一致),上传超限时引导用户付费。 var STORAGE_QUOTA_EXCEEDED = 10001; function isStorageQuotaError(data) { @@ -170,6 +181,7 @@ function sendRequest(action, data = null, method = 'POST', options = {}) { } if (!options.silent) { try { wx.hideLoading(); } catch(e) {} } if (res.statusCode >= 400 || (res.data && res.data.error)) { + maybeRedirectBinding(res.data); reject({ ...fromWire(res.data), _statusCode: res.statusCode }); } else { const result = fromWire(res.data); @@ -213,11 +225,11 @@ const api = { ); wx.hideLoading(); if (data && !data.error && (data.path || data.success)) { dataCache.mutation('uploadAvatar', data); return data; } - if (!maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' }); + if (!maybeRedirectBinding(data) && !maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' }); throw data || { error: '上传失败' }; } catch(e) { wx.hideLoading(); - if (!maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' }); + if (!maybeRedirectBinding(e) && !maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' }); throw e; } }, @@ -233,11 +245,11 @@ const api = { ); wx.hideLoading(); if (data && !data.error && (data.path || data.success)) { dataCache.mutation('uploadAvatar', data); return data; } - if (!maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' }); + if (!maybeRedirectBinding(data) && !maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' }); throw data || { error: '上传失败' }; } catch(e) { wx.hideLoading(); - if (!maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' }); + if (!maybeRedirectBinding(e) && !maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' }); throw e; } }, @@ -280,11 +292,11 @@ const api = { }); wx.hideLoading(); if (data && !data.error && (data.success || data.path || data.uploaded)) { dataCache.mutation('photoUpload', data); return data; } - if (!maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' }); + if (!maybeRedirectBinding(data) && !maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' }); throw data || { error: '上传失败' }; } catch(e) { wx.hideLoading(); - if (!maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' }); + if (!maybeRedirectBinding(e) && !maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' }); throw e; } }, @@ -433,7 +445,7 @@ async function uploadFileCompat(options) { success: (res) => { var data = parseUploadResponse(res.data); if (data) { - if (maybeRedirectStorage(data)) { reject(data); return; } + if (maybeRedirectBinding(data) || maybeRedirectStorage(data)) { reject(data); return; } if (res.statusCode < 400 && !data.error && (data.success || data.path || data.uploaded)) { dataCache.mutation(actionMatch ? decodeURIComponent(actionMatch[1] || actionMatch[2]) : '', data); }