- Phase 1: 添加纪念日合并人物创建流程(方案 B) - Phase 2: 农历提醒按 lunarMonth/Day 计算每年公历 - Phase 3: 人员数据同步到后端(新增 /api/person) - Phase 4: 新设备启动从云端恢复数据 - Phase 5: 工具函数收敛 utils/format.js - Phase 6: 同步失败入队 + 启动重试 - Phase 7: 闰月生日完整支持(含 isLeapMonth + UI 警示) - 修复 lunarInfo 数据表错位(替换为权威源 jjonline/calendar.js) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -67,4 +67,9 @@ function anniversary(action, data) {
|
||||
return request({ url: '/api/anniversary', data: { action, data } })
|
||||
}
|
||||
|
||||
module.exports = { request, login, anniversary, BASE_URL }
|
||||
// 人员操作
|
||||
function person(action, data) {
|
||||
return request({ url: '/api/person', data: { action, data } })
|
||||
}
|
||||
|
||||
module.exports = { request, login, anniversary, person, BASE_URL }
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 纪念日相关的展示格式化工具
|
||||
* Why:原本散落在 index / person-detail / add-anniversary 里的转换逻辑,统一到这里避免多份维护
|
||||
*/
|
||||
|
||||
const { TYPE_NAMES, TYPE_ICONS, IMPORTANCE_TEXTS } = require('./constants')
|
||||
|
||||
function getTypeName(type, customTypeName) {
|
||||
if (type === 'other' && customTypeName) return customTypeName
|
||||
return TYPE_NAMES[type] || '纪念日'
|
||||
}
|
||||
|
||||
function getTypeIcon(type) {
|
||||
return TYPE_ICONS[type] || '📅'
|
||||
}
|
||||
|
||||
function getImportanceText(importance) {
|
||||
return IMPORTANCE_TEXTS[importance] || '一般'
|
||||
}
|
||||
|
||||
// 距离 X 天的口语化展示
|
||||
function formatDaysUntil(days) {
|
||||
if (days === 0) return '今天'
|
||||
if (days === 1) return '明天'
|
||||
if (days < 7) return `${days}天后`
|
||||
if (days < 30) return `还有${Math.floor(days / 7)}周`
|
||||
return `还有${Math.floor(days / 30)}个月`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTypeName,
|
||||
getTypeIcon,
|
||||
getImportanceText,
|
||||
formatDaysUntil
|
||||
}
|
||||
+23
-21
@@ -4,28 +4,30 @@
|
||||
*/
|
||||
|
||||
// 农历数据:每条数据记录该年的月份大小和闰月信息
|
||||
// 格式:高4位=闰月大小(0无闰/1闰大月), 中4位=闰月位置, 低12位=12个月大小(1大/0小)
|
||||
// 格式:bit 16=闰月大小(1 大月 / 0 小月),bit 15-4=12 个月大小(1大/0小),bit 3-0=闰月位置(0 无闰)
|
||||
// 数据源:github.com/jjonline/calendar.js(权威,经过修订)
|
||||
const lunarInfo = [
|
||||
0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2, // 1900-1909
|
||||
0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977, // 1910-1919
|
||||
0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970, // 1920-1929
|
||||
0x06566,0x0d4a0,0x0ea50,0x06e95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950, // 1930-1939
|
||||
0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557, // 1940-1949
|
||||
0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,0x052b0,0x0a9a8,0x0e950,0x06aa0, // 1950-1959
|
||||
0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0, // 1960-1969
|
||||
0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b6a0,0x195a6, // 1970-1979
|
||||
0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570, // 1980-1989
|
||||
0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06aa0,0x0a6b6,0x056a0,0x02b60,0x09570, // 1990-1999
|
||||
0x049b0,0x0a4b0,0x0aa50,0x1b255,0x06d40,0x0ad50,0x14b55,0x056a0,0x0a6d0,0x055d4, // 2000-2009
|
||||
0x052d0,0x0a9b8,0x0a950,0x0b4a0,0x0b6a6,0x0ad50,0x055a0,0x0aba4,0x0a5b0,0x052b0, // 2010-2019
|
||||
0x0b273,0x06930,0x07337,0x06aa0,0x0ad50,0x14b55,0x04b60,0x0a570,0x054e4,0x0d160, // 2020-2029
|
||||
0x0e968,0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252, // 2030-2039
|
||||
0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252,0x0d520, // 2040-2049
|
||||
0x0b54f,0x0d6a0,0x0ada0,0x14955,0x056a0,0x0a6d0,0x0155b,0x025d0,0x092d0,0x0d954, // 2050-2059
|
||||
0x0d4a0,0x0b550,0x0b4a9,0x04da0,0x0a5b0,0x15176,0x052b0,0x0a930,0x07954,0x06aa0, // 2060-2069
|
||||
0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,0x05aa0,0x076a3, // 2070-2079
|
||||
0x096d0,0x04afb,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,0x0b5a0,0x056d0, // 2080-2089
|
||||
0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0,0x14b63,0x09370 // 2090-2099
|
||||
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909
|
||||
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919
|
||||
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929
|
||||
0x06566, 0x0d4a0, 0x0ea50, 0x16a95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939
|
||||
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949
|
||||
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959
|
||||
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969
|
||||
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979
|
||||
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989
|
||||
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999
|
||||
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009
|
||||
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019
|
||||
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029
|
||||
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039
|
||||
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049
|
||||
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, // 2050-2059
|
||||
0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, // 2060-2069
|
||||
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, // 2070-2079
|
||||
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, // 2080-2089
|
||||
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, // 2090-2099
|
||||
0x0d520 // 2100
|
||||
]
|
||||
|
||||
// 1900年1月31日是农历正月初一(庚子年)
|
||||
|
||||
+68
-9
@@ -1,7 +1,17 @@
|
||||
/**
|
||||
* 本地存储管理工具(含缓存层)
|
||||
* 本地存储管理工具(含缓存层 + 云端 fire-and-forget 同步)
|
||||
*/
|
||||
|
||||
const api = require('./api')
|
||||
const sync = require('./sync')
|
||||
|
||||
// 异步同步人员到后端;失败自动入队,启动时 flush
|
||||
function _syncPerson(action, data) {
|
||||
const openid = wx.getStorageSync('openid')
|
||||
if (!openid) return
|
||||
sync.syncOrEnqueue({ kind: 'person', action, data })
|
||||
}
|
||||
|
||||
// 内存缓存
|
||||
const _cache = {
|
||||
persons: null,
|
||||
@@ -61,7 +71,9 @@ function addPerson(person) {
|
||||
}
|
||||
persons.push(newPerson)
|
||||
const result = savePersons(persons)
|
||||
return result.success ? newPerson : null
|
||||
if (!result.success) return null
|
||||
_syncPerson('add', newPerson)
|
||||
return newPerson
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,11 +82,23 @@ function addPerson(person) {
|
||||
function updatePerson(id, updates) {
|
||||
const persons = getPersons()
|
||||
const index = persons.findIndex(p => p.id === id)
|
||||
if (index !== -1) {
|
||||
persons[index] = { ...persons[index], ...updates, updateTime: new Date().getTime() }
|
||||
return savePersons(persons).success
|
||||
}
|
||||
return false
|
||||
if (index === -1) return false
|
||||
persons[index] = { ...persons[index], ...updates, updateTime: new Date().getTime() }
|
||||
const ok = savePersons(persons).success
|
||||
if (ok) _syncPerson('update', persons[index])
|
||||
return ok
|
||||
}
|
||||
|
||||
/**
|
||||
* 按姓名查找已有人员;找不到则创建一个新的 person(仅 name),返回 person 对象
|
||||
* Why:方案 B 流程合并——用户在「添加纪念日」页直接输入姓名,不再强制先建人
|
||||
*/
|
||||
function ensurePerson(name) {
|
||||
const trimmed = (name || '').trim()
|
||||
if (!trimmed) return null
|
||||
const existing = getPersons().find(p => p.name === trimmed)
|
||||
if (existing) return existing
|
||||
return addPerson({ name: trimmed })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +106,9 @@ function updatePerson(id, updates) {
|
||||
*/
|
||||
function deletePerson(id) {
|
||||
const persons = getPersons()
|
||||
return savePersons(persons.filter(p => p.id !== id)).success
|
||||
const ok = savePersons(persons.filter(p => p.id !== id)).success
|
||||
if (ok) _syncPerson('delete', { id })
|
||||
return ok
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +194,8 @@ function deletePersonWithAnniversaries(personId) {
|
||||
wx.setStorageSync('anniversaries', anniversaries)
|
||||
_cache.persons = persons
|
||||
_cache.anniversaries = anniversaries
|
||||
// 后端 delete person 会级联删除该 personId 的所有 anniversaries(在事务里),无需单独再调
|
||||
_syncPerson('delete', { id: personId })
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error('删除人员及纪念日失败', e)
|
||||
@@ -238,6 +266,35 @@ function importData(data) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅当本地为空时,从后端拉取所有人员和纪念日落地到本地
|
||||
* Why:换设备/重装时,云端是唯一真相源,需要把数据拉回来恢复
|
||||
*/
|
||||
async function pullFromCloudIfEmpty() {
|
||||
const hasLocal = getPersons().length > 0 || getAnniversaries().length > 0
|
||||
if (hasLocal) return false
|
||||
const openid = wx.getStorageSync('openid')
|
||||
if (!openid) return false
|
||||
try {
|
||||
const [personsRes, annivRes] = await Promise.all([
|
||||
api.person('get'),
|
||||
api.anniversary('get')
|
||||
])
|
||||
if (personsRes && personsRes.success && Array.isArray(personsRes.data) && personsRes.data.length > 0) {
|
||||
wx.setStorageSync('persons', personsRes.data)
|
||||
_cache.persons = personsRes.data
|
||||
}
|
||||
if (annivRes && annivRes.success && Array.isArray(annivRes.data) && annivRes.data.length > 0) {
|
||||
wx.setStorageSync('anniversaries', annivRes.data)
|
||||
_cache.anniversaries = annivRes.data
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error('[pullFromCloud] 失败', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据
|
||||
*/
|
||||
@@ -258,6 +315,7 @@ module.exports = {
|
||||
getPersons,
|
||||
getPersonById,
|
||||
addPerson,
|
||||
ensurePerson,
|
||||
updatePerson,
|
||||
deletePerson,
|
||||
deletePersonWithAnniversaries,
|
||||
@@ -273,5 +331,6 @@ module.exports = {
|
||||
// 工具函数
|
||||
exportData,
|
||||
importData,
|
||||
clearAllData
|
||||
clearAllData,
|
||||
pullFromCloudIfEmpty
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 同步管理:失败入队,启动时重试
|
||||
* Why:自建后端、HTTPS 链路、定时任务任一环节抖动都会导致单次写入丢失。本地队列兜底,保证最终一致。
|
||||
*/
|
||||
|
||||
const api = require('./api')
|
||||
|
||||
const QUEUE_KEY = 'pending_sync_queue'
|
||||
|
||||
function loadQueue() {
|
||||
try { return wx.getStorageSync(QUEUE_KEY) || [] } catch (e) { return [] }
|
||||
}
|
||||
|
||||
function saveQueue(q) {
|
||||
try { wx.setStorageSync(QUEUE_KEY, q) } catch (e) {}
|
||||
}
|
||||
|
||||
function enqueue(item) {
|
||||
const q = loadQueue()
|
||||
q.push({ ...item, ts: Date.now() })
|
||||
saveQueue(q)
|
||||
}
|
||||
|
||||
async function dispatch(item) {
|
||||
if (item.kind === 'person') return api.person(item.action, item.data)
|
||||
if (item.kind === 'anniversary') return api.anniversary(item.action, item.data)
|
||||
throw new Error('unknown sync kind: ' + item.kind)
|
||||
}
|
||||
|
||||
// 立即尝试同步;失败入队
|
||||
async function syncOrEnqueue(item) {
|
||||
try {
|
||||
const res = await dispatch(item)
|
||||
if (res && res.success === false) throw new Error(res.error || 'server returned failure')
|
||||
return true
|
||||
} catch (err) {
|
||||
console.warn('[sync] 失败已入队:', item.kind, item.action, err.message || err)
|
||||
enqueue(item)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试 flush 整个队列,失败的留下下次再试
|
||||
async function flush() {
|
||||
const q = loadQueue()
|
||||
if (q.length === 0) return { flushed: 0, remaining: 0 }
|
||||
const remaining = []
|
||||
let flushed = 0
|
||||
for (const item of q) {
|
||||
try {
|
||||
const res = await dispatch(item)
|
||||
if (res && res.success === false) throw new Error(res.error || 'server returned failure')
|
||||
flushed++
|
||||
} catch (err) {
|
||||
remaining.push(item)
|
||||
}
|
||||
}
|
||||
saveQueue(remaining)
|
||||
if (flushed > 0) console.log(`[sync] 队列 flush 完成:成功 ${flushed},剩余 ${remaining.length}`)
|
||||
return { flushed, remaining: remaining.length }
|
||||
}
|
||||
|
||||
function getPendingCount() {
|
||||
return loadQueue().length
|
||||
}
|
||||
|
||||
module.exports = { syncOrEnqueue, flush, getPendingCount }
|
||||
Reference in New Issue
Block a user