Files
wxserver/utils/migrate.js
T
yuming e9c7330f31
部署到群晖 / deploy (push) Successful in 57s
数据自愈迁移 + 后端同步幂等,版本号 v2.1.7 → v2.1.8
后端(server/src/index.js)——两处同步幂等:

1. deleteAnniversary 删除不存在的记录时返回 success:true。
   原先返回 false,被前端 sync.js:34 当作失败重新入队,那条删除会永远留在
   pending_sync_queue 里每次启动重试且永远清不掉。

2. updateAnniversary / updatePerson 在记录不存在时退化为插入(upsert)。
   同一类问题:本地才是主真相源,若某条记录当初的 add 没同步成功,
   之后所有 update 都会失败并无限重试。

前端 —— 新增 utils/migrate.js,在 app.js onLaunch 执行:

3. 补齐老数据缺失的农历字段。早期 lunar_birthday 类型只记公历日期,
   没有 lunarMonth/lunarDay,这类记录会被安全降级成按公历算——不崩,但每年日期是错的。
   用它存的公历日期反算回农历补齐。

4. 为孤儿纪念日重建人员。部分纪念日的 personId 指向已不存在的人(历史上删人没级联干净),
   首页按 persons 遍历所以隐形,日历页按 anniversaries 遍历会显示成「未知」。
   用记录自带的 personName 重建,并沿用原 personId,纪念日无需改动。

迁移放在客户端而非后端跑 SQL:本地 wx.Storage 是主真相源,改服务端会被客户端同步覆盖。
每次启动都跑而非记版本号:函数是纯检测式的,无坏数据时零写入零请求,
还能顺带覆盖「从云端拉回坏数据」的情况。顺便把原本是死代码的 initData() 替换掉。

⚠️ 本次后端有改动,需要重新部署;部署顺序应为先后端、后小程序。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 15:55:44 +08:00

126 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 数据自愈迁移
*
* Why 放在客户端而不是后端跑一次 SQL:
* 本项目「本地 wx.Storage 是主真相源,云端只是备份」。在服务端改数据,客户端下次同步
* 又会把本地的坏数据推上去覆盖掉。只有在每台设备上修,才能真正修干净。
*
* Why 每次启动都跑而不是用版本号只跑一次:
* 本函数是纯检测式的——没有坏数据时不写库、不发请求、零副作用,成本只是遍历一遍数组
* (量级是个位数到几十条)。每次都跑还能顺带覆盖「从云端拉回来的坏数据」这种情况,
* 比记版本号更稳。
*/
const storage = require('./storage')
const sync = require('./sync')
const lunar = require('./lunar')
const REBUILT_PERSON_REMARK = '自动恢复的联系人'
const FALLBACK_PERSON_NAME = '未命名'
/**
* 迁移一:补齐老数据缺失的农历字段
*
* 背景:早期版本有独立的 lunar_birthday 类型,只记公历日期,没有 lunarMonth/lunarDay。
* 后来改成用 isLunar 标志决定农历与否,这批老数据就变成了「声称是农历、却没有农历字段」。
* 这种记录会被日期计算安全降级成按公历算——不会崩,但每年的日期是错的。
* 用它存着的公历日期反算回农历,即可补齐。
*
* @returns {Array} 被修复的纪念日(已带上新字段)
*/
function _patchMissingLunarFields(anniversaries) {
const patched = []
const next = anniversaries.map(a => {
if (!a.isLunar) return a
if (a.lunarMonth && a.lunarDay) return a
// 没有公历基准就无从反算,保持原样(不制造假数据)
if (!a.solarYear || !a.solarMonth || !a.solarDay) return a
const ld = lunar.solarToLunar(new Date(a.solarYear, a.solarMonth - 1, a.solarDay))
const fixed = {
...a,
lunarYear: ld.year,
lunarMonth: ld.month,
lunarDay: ld.day,
isLeapMonth: ld.isLeap,
updateTime: Date.now()
}
patched.push(fixed)
return fixed
})
return { next, patched }
}
/**
* 迁移二:为孤儿纪念日重建人员
*
* 背景:有些纪念日的 personId 指向已不存在的人员(历史上删人没级联干净)。
* 首页按 persons 遍历,所以这些记录是隐形的;日历页按 anniversaries 遍历,会显示成「未知」。
*
* 做法:用纪念日自带的 personName 重建人员,并且**沿用原来的 personId**
* 这样纪念日记录一个字段都不用改,风险最小。
*
* @returns {Array} 新建出来的人员
*/
function _rebuildMissingPersons(persons, anniversaries) {
const known = new Set(persons.map(p => p.id))
const missing = new Map() // personId -> name
for (const a of anniversaries) {
if (!a.personId || known.has(a.personId)) continue
const name = (a.personName || '').trim()
const recorded = missing.get(a.personId)
// 同一个失踪 personId 可能对应多条纪念日,优先采用非空的姓名
if (!recorded || (recorded === FALLBACK_PERSON_NAME && name)) {
missing.set(a.personId, name || FALLBACK_PERSON_NAME)
}
}
if (missing.size === 0) return []
const now = Date.now()
return Array.from(missing.entries()).map(([id, name]) => ({
id,
name,
nickname: '',
avatar: '',
remark: REBUILT_PERSON_REMARK,
createTime: now,
updateTime: now
}))
}
/**
* 执行迁移。幂等,可重复调用。
* 任何异常都吞掉,绝不能因为迁移失败导致小程序起不来。
* @returns {{ lunarPatched: Number, personsRebuilt: Number }|null}
*/
function run() {
try {
const anniversaries = storage.getAnniversaries()
const { next, patched } = _patchMissingLunarFields(anniversaries)
if (patched.length > 0) {
storage.saveAnniversaries(next)
// 逐条推送更新;失败会自动入队,下次启动 flush
patched.forEach(a => sync.syncOrEnqueue({ kind: 'anniversary', action: 'update', data: a }))
}
const persons = storage.getPersons()
const rebuilt = _rebuildMissingPersons(persons, next)
if (rebuilt.length > 0) {
storage.savePersons(persons.concat(rebuilt))
rebuilt.forEach(p => sync.syncOrEnqueue({ kind: 'person', action: 'add', data: p }))
}
if (patched.length > 0 || rebuilt.length > 0) {
console.log(`[migrate] 补齐农历字段 ${patched.length} 条,重建人员 ${rebuilt.length}`)
}
return { lunarPatched: patched.length, personsRebuilt: rebuilt.length }
} catch (e) {
console.error('[migrate] 迁移失败,已跳过(不影响启动)', e)
return null
}
}
module.exports = { run }