const DB_COLLECTION = 'plank_data' const ENV_ID = 'cloudbase-d1g56kl2q8f4f7d8a' let _db = null let _pushTimer = null let _enabled = false let _docId = null // cache doc id after first successful push let _openid = null // cache openid from first successful add, used to // re-locate our doc when _docId goes stale // (e.g. user manually deleted the doc in the console) const getDb = () => { if (!_enabled) return null if (!_db) { try { _db = wx.cloud.database({ env: ENV_ID }) } catch (e) { _enabled = false } } return _db } const init = () => { try { wx.cloud.init({ env: ENV_ID, traceUser: true }) _enabled = true } catch (e) { _enabled = false } } const pullAll = async () => { const db = getDb() if (!db) return null try { // Always need the openid to safely locate OUR doc under the custom // security rule. Cache it on first call so subsequent pulls are fast. if (!_openid) { _openid = await _fetchOpenid() } if (!_openid) return null // cloud function call failed; bail // Locate our doc by _openid, not by limit(1) which can pick someone // else's doc when the custom rule is in effect. const mine = await db.collection(DB_COLLECTION) .where({ _openid: _openid }) .limit(1) .get() if (mine && mine.data && mine.data.length > 0) { _docId = mine.data[0]._id return mine.data[0] } // No doc for us — that's a fresh install on a new device with no // history, or the user cleared their cloud data. Either way, nothing // to restore. _docId = null return null } catch (e) { return null } } // One-shot helper: call the getOpenid cloud function and return the // openid string. Returns null on any failure (function not deployed, // network error, etc.) — pullAll then bails cleanly. const _fetchOpenid = async () => { try { const res = await wx.cloud.callFunction({ name: 'getOpenid' }) if (res && res.result && res.result.openid) return res.result.openid } catch (e) { /* function not deployed, etc. */ } return null } const pushAll = () => { if (!_enabled) { console.log('[cloud] pushAll skipped (not enabled)'); return } if (_pushTimer) clearTimeout(_pushTimer) console.log('[cloud] pushAll scheduled') _pushTimer = setTimeout(() => _doPush(), 2000) } const _doPush = async () => { console.log('[cloud] _doPush starting...') const db = getDb() if (!db) { console.log('[cloud] _doPush aborted (no db)'); return } try { const storage = require('./storage') const themeMod = require('./theme') const data = { records: storage.getRecords(), settings: storage.getSettings(), streak: storage.getStreak(), customPlans: storage.getCustomPlans(), profile: storage.getProfile(), themeId: themeMod.getCurrentTheme().id, updatedAt: db.serverDate() } console.log('[cloud] _doPush records keys:', Object.keys(data.records || {})) // Use cached doc id if available if (_docId) { try { await db.collection(DB_COLLECTION).doc(_docId).update({ data }) console.log('[cloud] _doPush update ok') return } catch (e) { // Most commonly: doc was deleted out from under us (user cleared // the collection in the console). The custom write rule evaluates // `doc._openid == auth.openid` → `undefined == openid` → false, // so this surfaces as -502003 rather than "not found". Either way, // fall through and re-locate the doc via cached _openid (or add). console.log('[cloud] _doPush cached _docId stale, clearing:', e.message) _docId = null } } // Fallback: locate OUR doc by _openid (cached from a previous add) and // update it. If we have no cached openid yet (first ever push) just add. if (_openid) { const existing = await db.collection(DB_COLLECTION) .where({ _openid: _openid }) .limit(1) .get() if (existing && existing.data && existing.data.length > 0) { _docId = existing.data[0]._id await db.collection(DB_COLLECTION).doc(_docId).update({ data }) console.log('[cloud] _doPush update ok (re-located by _openid)') return } } // First push ever, or our doc was deleted and no other doc exists. const res = await db.collection(DB_COLLECTION).add({ data }) if (res && res._id) _docId = res._id if (res && res._openid) _openid = res._openid console.log('[cloud] _doPush add ok, docId=', _docId) } catch (e) { console.log('[cloud] _doPush error:', e.message || e) } } const clearAll = async () => { const db = getDb() if (!db) return // Ensure _openid is cached before trying to delete — without it we // can't reliably locate the user's doc (add() only returns _id). if (!_openid) { _openid = await _fetchOpenid() } if (!_openid) { console.log('[cloud] clearAll: cannot fetch openid, giving up') return } // Always locate by _openid (never by cached _docId alone) so we're // guaranteed to target OUR doc under the custom permission rule. const mine = await db.collection(DB_COLLECTION) .where({ _openid: _openid }) .limit(1) .get() if (mine && mine.data && mine.data.length > 0) { const myDocId = mine.data[0]._id try { await db.collection(DB_COLLECTION).doc(myDocId).remove() console.log('[cloud] clearAll: removed doc', myDocId) } catch (e) { console.log('[cloud] clearAll: remove failed:', e.message || e) } } else { console.log('[cloud] clearAll: no doc found for openid') } // Always invalidate cached ids so the next _doPush starts fresh // instead of trying to update a now-deleted or foreign doc. _docId = null } module.exports = { init, pullAll, pushAll, clearAll, get enabled() { return _enabled } }