// 云函数 walkManual — 手动补记(docs/05 R7) // 规则:isManual=true、countsForRanking=false(不计排行榜,防刷);可维持连续打卡不断签。 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); const _ = db.command; const DAY = 86400000; function toDateStr(ts, tz = 8) { const d = new Date(ts + tz * 3600 * 1000); const y = d.getUTCFullYear(); const m = String(d.getUTCMonth() + 1).padStart(2, '0'); const day = String(d.getUTCDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function diffDays(a, b) { return Math.round((Date.parse(b + 'T00:00:00Z') - Date.parse(a + 'T00:00:00Z')) / DAY); } // 连续天数:从该狗全部 walkDate 重算(与 walkSave 一致),保证补记任意顺序都能正确续/断签(docs/05 R10) async function computeStreak(openid, dogId) { const res = await db.collection('walks') .where({ _openid: openid, dogIds: dogId }) .field({ walkDate: true }) .orderBy('walkDate', 'desc') .limit(1000) .get(); const dates = Array.from(new Set(res.data.map((w) => w.walkDate).filter(Boolean))).sort(); // 升序 if (!dates.length) return { currentStreak: 0, lastWalkDate: '' }; const lastWalkDate = dates[dates.length - 1]; let streak = 1; for (let i = dates.length - 1; i > 0; i--) { if (diffDays(dates[i - 1], dates[i]) === 1) streak++; else break; } return { currentStreak: streak, lastWalkDate }; } // 补记仅触发连续/累计类成就(不触发分享/场景类) const STAT_ACHIEVEMENTS = [ { key: 'first_walk', name: '首遛', cond: (s) => s.totalWalks >= 1 }, { key: 'week_streak', name: '一周不断', cond: (s) => s.currentStreak >= 7 }, { key: 'month_streak', name: '满月坚持', cond: (s) => s.currentStreak >= 30 }, { key: 'hundred_km', name: '百里同行', cond: (s) => s.totalDistance >= 100 }, ]; async function unlockAchievements(openid, dogId, candidates) { if (!candidates.length) return []; const col = db.collection('dog_achievements'); const existing = await col.where({ dogId }).field({ achievementKey: true }).get(); const have = new Set(existing.data.map((d) => d.achievementKey)); const fresh = candidates.filter((c) => !have.has(c.key)); const now = Date.now(); for (const c of fresh) { await col.add({ data: { _openid: openid, dogId, achievementKey: c.key, unlockedAt: now } }); } return fresh.map((c) => ({ key: c.key, name: c.name })); } exports.main = async (event) => { const { OPENID } = cloud.getWXContext(); const { dogId, startAt, duration } = event; const distance = event.distance != null ? event.distance : null; if (!dogId || !startAt || !duration) { return { ok: false, code: 'INVALID', msg: '缺少必要信息' }; } // 校验归属 const dogRes = await db.collection('dogs').doc(dogId).get().catch(() => null); if (!dogRes || !dogRes.data || dogRes.data._openid !== OPENID) { return { ok: false, code: 'FORBIDDEN', msg: '无权操作' }; } const now = Date.now(); const walkDate = toDateStr(startAt); const addRes = await db.collection('walks').add({ data: { _openid: OPENID, dogIds: [dogId], startAt, endAt: startAt + duration * 1000, duration, distance, dogSteps: null, pooped: false, weather: null, mood: null, note: null, photos: [], source: 'manual', // 来源标识 isManual: true, // 补记标识 countsForRanking: false, // 不计排行榜 walkDate, createdAt: now, }, }); // 更新狗狗统计(维持连续);连续天数按全部历史重算(已含本条补记) const dog = dogRes.data; const s = dog.stats || { totalWalks: 0, totalDistance: 0, totalSteps: 0, currentStreak: 0, lastWalkDate: '' }; const streakInfo = await computeStreak(OPENID, dogId); const newStats = { totalWalks: (s.totalWalks || 0) + 1, totalDistance: +(((s.totalDistance || 0) + (distance || 0)).toFixed(2)), totalSteps: s.totalSteps || 0, earlyWalks: s.earlyWalks || 0, // 补记不触发场景类成就,早遛计数保持不变 currentStreak: streakInfo.currentStreak, lastWalkDate: streakInfo.lastWalkDate, }; await db.collection('dogs').doc(dogId).update({ data: { stats: newStats } }); const satisfied = STAT_ACHIEVEMENTS.filter((a) => a.cond(newStats)).map((a) => ({ key: a.key, name: a.name })); const newAchievements = await unlockAchievements(OPENID, dogId, satisfied); await db.collection('users').where({ _openid: OPENID }).update({ data: { walkCount: _.inc(1) } }); return { ok: true, data: { walkId: addRes._id, newAchievements } }; };