diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..a75b23a --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,54 @@ +name: 部署到群晖 + +on: + push: + branches: [main, master] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: 安装 Docker CLI + run: | + sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null || true + sed -i 's|security.debian.org|mirrors.tuna.tsinghua.edu.cn/debian-security|g' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null || true + apt-get update -qq + apt-get install -y -qq docker.io + + - name: 拉取代码 + uses: actions/checkout@v4 + + - name: 构建镜像 + run: docker build -t birthday-server:latest ./server + + - name: 停止旧容器 + run: | + docker stop birthday-server 2>/dev/null || true + docker rm birthday-server 2>/dev/null || true + + - name: 准备持久化目录 + run: mkdir -p /volume1/docker/apps/birthday-server/data + + - name: 启动新容器 + env: + WX_APPID: ${{ secrets.WX_APPID }} + WX_APPSECRET: ${{ secrets.WX_APPSECRET }} + WX_TEMPLATE_ID: ${{ secrets.WX_TEMPLATE_ID }} + WX_MINIPROGRAM_STATE: ${{ secrets.WX_MINIPROGRAM_STATE }} + REMINDER_CRON: ${{ secrets.REMINDER_CRON }} + run: | + docker run -d \ + --name birthday-server \ + --restart unless-stopped \ + -p 15002:3000 \ + -v /volume1/docker/apps/birthday-server/data:/app/data \ + -e TZ=Asia/Shanghai \ + -e WX_APPID="$WX_APPID" \ + -e WX_APPSECRET="$WX_APPSECRET" \ + -e WX_TEMPLATE_ID="${WX_TEMPLATE_ID:-6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw}" \ + -e WX_MINIPROGRAM_STATE="${WX_MINIPROGRAM_STATE:-formal}" \ + -e REMINDER_CRON="${REMINDER_CRON:-0 9 * * *}" \ + birthday-server:latest + + - name: 部署完成提示 + run: echo "部署完成,访问 http://192.168.1.66:15002/api/health" diff --git a/.gitignore b/.gitignore index 14ea590..6aaa899 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ $RECYCLE.BIN/ # Node.js node_modules/ + +# 后端运行时数据 & 敏感配置 +server/.env +server/data/ +server/node_modules/ diff --git a/app.js b/app.js index c0460e7..5639c17 100644 --- a/app.js +++ b/app.js @@ -1,27 +1,44 @@ +const api = require('./utils/api') + App({ onLaunch() { - // 展示本地存储能力 const logs = wx.getStorageSync('logs') || [] logs.unshift(Date.now()) wx.setStorageSync('logs', logs) - // 初始化数据 this.initData() + this.getUserOpenId() }, - // 初始化数据结构 initData() { - // 检查是否有旧数据 const persons = wx.getStorageSync('persons') || [] const anniversaries = wx.getStorageSync('anniversaries') || [] - if (persons.length === 0 && anniversaries.length === 0) { console.log('初始化数据结构') } }, + // 获取 openid:调自建后端 /api/login + async getUserOpenId() { + // 已缓存就直接复用,避免每次启动都登录 + const cached = wx.getStorageSync('openid') + if (cached) { + this.globalData.openid = cached + return + } + try { + const res = await api.login() + this.globalData.openid = res.openid + wx.setStorageSync('openid', res.openid) + console.log('获取openid成功:', res.openid) + } catch (err) { + console.error('获取openid失败:', err) + } + }, + globalData: { - userInfo: null + userInfo: null, + openid: '' } }) diff --git a/app.json b/app.json index e8333a6..6bba91c 100644 --- a/app.json +++ b/app.json @@ -16,27 +16,21 @@ }, "tabBar": { "color": "#7A7E83", - "selectedColor": "#3cc51f", + "selectedColor": "#07c160", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", - "iconPath": "/images/home.png", - "selectedIconPath": "/images/home-active.png", - "text": "首页" + "text": "🏠 首页" }, { "pagePath": "pages/calendar/calendar", - "iconPath": "/images/calendar.png", - "selectedIconPath": "/images/calendar-active.png", - "text": "日历" + "text": "📅 日历" }, { "pagePath": "pages/settings/settings", - "iconPath": "/images/settings.png", - "selectedIconPath": "/images/settings-active.png", - "text": "设置" + "text": "⚙️ 设置" } ] }, diff --git a/app.wxss b/app.wxss index be0934b..2f03401 100644 --- a/app.wxss +++ b/app.wxss @@ -11,7 +11,8 @@ /* 全局样式 */ page { - background-color: #f5f5f5; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background-attachment: fixed; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; } @@ -23,44 +24,53 @@ page { /* 按钮样式 */ .btn { - border-radius: 8rpx; + border-radius: 12rpx; font-size: 32rpx; padding: 24rpx 48rpx; + transition: all 0.3s ease; } .btn-primary { - background-color: #07c160; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; + box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4); } .btn-secondary { background-color: #fff; color: #333; - border: 1px solid #ddd; + border: 2rpx solid #e8e8e8; } /* 卡片样式 */ .card { background-color: #fff; - border-radius: 16rpx; + border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08); + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); + transition: all 0.3s ease; } /* 输入框样式 */ .input { - background-color: #fff; + background-color: #f8f9fa; padding: 24rpx; - border-radius: 8rpx; - border: 1px solid #e0e0e0; + border-radius: 12rpx; + border: 2rpx solid transparent; font-size: 28rpx; + transition: all 0.3s ease; +} + +.input:focus { + background-color: #fff; + border-color: #667eea; } /* 分割线 */ .divider { height: 1rpx; - background-color: #f0f0f0; + background: linear-gradient(90deg, transparent, #e0e0e0, transparent); margin: 32rpx 0; } @@ -68,17 +78,20 @@ page { .empty-state { text-align: center; padding: 120rpx 40rpx; - color: #999; + color: rgba(255, 255, 255, 0.8); } .empty-state .icon { - font-size: 120rpx; + font-size: 140rpx; margin-bottom: 32rpx; + filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1)); } .empty-state .text { - font-size: 28rpx; - color: #999; + font-size: 32rpx; + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + margin-bottom: 12rpx; } /* 浮动按钮 */ @@ -86,16 +99,22 @@ page { position: fixed; bottom: 120rpx; right: 40rpx; - width: 100rpx; - height: 100rpx; + width: 112rpx; + height: 112rpx; border-radius: 50%; - background-color: #07c160; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; display: flex; align-items: center; justify-content: center; - font-size: 60rpx; - box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3); + font-size: 64rpx; + box-shadow: 0 12rpx 40rpx rgba(102, 126, 234, 0.5); z-index: 100; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.fab:active { + transform: scale(0.92); + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4); } diff --git a/cloudfunctions/login/config.json b/cloudfunctions/login/config.json new file mode 100644 index 0000000..eaa6dae --- /dev/null +++ b/cloudfunctions/login/config.json @@ -0,0 +1,6 @@ +{ + "permissions": { + "openapi": [] + } +} + diff --git a/cloudfunctions/login/index.js b/cloudfunctions/login/index.js new file mode 100644 index 0000000..db6e5c9 --- /dev/null +++ b/cloudfunctions/login/index.js @@ -0,0 +1,18 @@ +// 云函数:获取用户openid +const cloud = require('wx-server-sdk') + +cloud.init({ + env: cloud.DYNAMIC_CURRENT_ENV +}) + +exports.main = async (event, context) => { + const wxContext = cloud.getWXContext() + + return { + openid: wxContext.OPENID, + appid: wxContext.APPID, + unionid: wxContext.UNIONID, + env: wxContext.ENV + } +} + diff --git a/cloudfunctions/login/package.json b/cloudfunctions/login/package.json new file mode 100644 index 0000000..f9ccc6a --- /dev/null +++ b/cloudfunctions/login/package.json @@ -0,0 +1,10 @@ +{ + "name": "login", + "version": "1.0.0", + "description": "获取用户openid", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "~2.6.3" + } +} + diff --git a/cloudfunctions/sendReminder/config.json b/cloudfunctions/sendReminder/config.json new file mode 100644 index 0000000..a2e4b8a --- /dev/null +++ b/cloudfunctions/sendReminder/config.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "openapi": [ + "subscribeMessage.send" + ] + }, + "triggers": [ + { + "name": "dailyReminder", + "type": "timer", + "config": "0 0 9 * * * *" + } + ] +} + diff --git a/cloudfunctions/sendReminder/index.js b/cloudfunctions/sendReminder/index.js new file mode 100644 index 0000000..44685ca --- /dev/null +++ b/cloudfunctions/sendReminder/index.js @@ -0,0 +1,197 @@ +// 云函数:发送生日提醒 +const cloud = require('wx-server-sdk') + +cloud.init({ + env: cloud.DYNAMIC_CURRENT_ENV +}) + +const db = cloud.database() + +// 模板ID +const TEMPLATE_ID = '6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw' + +/** + * 计算两个日期之间的天数差 + */ +function getDaysUntil(targetDate) { + const today = new Date() + today.setHours(0, 0, 0, 0) + + const target = new Date(targetDate) + target.setHours(0, 0, 0, 0) + + const diffTime = target - today + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + + return diffDays +} + +/** + * 格式化日期 + */ +function formatDate(date) { + const d = new Date(date) + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${year}年${month}月${day}日` +} + +const TYPE_NAMES = { + birthday: '公历生日', + lunar_birthday: '农历生日', + wedding: '结婚纪念日', + engagement: '订婚纪念日', + other: '其他纪念日' +} + +/** + * 获取类型名称 + */ +function getTypeName(type, customName) { + if (type === 'other' && customName) return customName + return TYPE_NAMES[type] || '纪念日' +} + +/** + * 获取今年的纪念日日期 + */ +function getThisYearDate(anniversary) { + const today = new Date() + const currentYear = today.getFullYear() + + // 使用公历日期计算 + let thisYearDate = new Date(currentYear, anniversary.solarMonth - 1, anniversary.solarDay) + + // 如果今年的已经过了,计算明年的 + const daysUntil = getDaysUntil(thisYearDate) + if (daysUntil < 0) { + thisYearDate = new Date(currentYear + 1, anniversary.solarMonth - 1, anniversary.solarDay) + } + + return thisYearDate +} + +/** + * 主函数 + */ +exports.main = async (event, context) => { + console.log('开始执行提醒任务...') + + try { + // 获取所有启用提醒的纪念日 + const anniversariesRes = await db.collection('anniversaries') + .where({ + remindEnabled: true + }) + .get() + + console.log(`找到 ${anniversariesRes.data.length} 条启用提醒的纪念日`) + + let successCount = 0 + let failCount = 0 + + // 遍历每个纪念日 + for (const anniversary of anniversariesRes.data) { + try { + // 计算今年的纪念日日期 + const thisYearDate = getThisYearDate(anniversary) + const daysUntil = getDaysUntil(thisYearDate) + + console.log(`${anniversary.personName} 的 ${getTypeName(anniversary.type, anniversary.customTypeName)},还有 ${daysUntil} 天`) + + // 判断是否需要提醒 + const shouldRemind = ( + daysUntil === 0 || // 当天 + daysUntil === anniversary.remindDays // 提前N天 + ) + + if (shouldRemind) { + // 检查今天是否已经发送过提醒 + const today = new Date() + today.setHours(0, 0, 0, 0) + + const logRes = await db.collection('remind_logs') + .where({ + anniversaryId: anniversary._id, + sendDate: db.command.gte(today) + }) + .count() + + if (logRes.total > 0) { + console.log(`今天已发送过提醒,跳过: ${anniversary.personName}`) + continue + } + + // 发送订阅消息 + const sendRes = await cloud.openapi.subscribeMessage.send({ + touser: anniversary.openid, + page: 'pages/index/index', + data: { + name1: { + value: anniversary.personName + }, + thing2: { + value: daysUntil === 0 ? '今天' : `还有${daysUntil}天` + }, + thing6: { + value: formatDate(thisYearDate) + }, + thing5: { + value: anniversary.remark || '别忘了准备一份礼物哦!' + } + }, + templateId: TEMPLATE_ID, + miniprogramState: 'formal' // 正式版 + }) + + console.log(`发送成功: ${anniversary.personName}`, sendRes) + + // 记录提醒日志 + await db.collection('remind_logs').add({ + data: { + anniversaryId: anniversary._id, + personName: anniversary.personName, + typeName: getTypeName(anniversary.type, anniversary.customTypeName), + daysUntil: daysUntil, + sendDate: new Date(), + status: 'success' + } + }) + + successCount++ + } + } catch (err) { + console.error(`发送失败: ${anniversary.personName}`, err) + failCount++ + + // 记录失败日志 + await db.collection('remind_logs').add({ + data: { + anniversaryId: anniversary._id, + personName: anniversary.personName, + sendDate: new Date(), + status: 'failed', + error: err.message + } + }) + } + } + + console.log(`提醒任务完成: 成功${successCount}条,失败${failCount}条`) + + return { + success: true, + total: anniversariesRes.data.length, + successCount, + failCount + } + } catch (err) { + console.error('提醒任务执行失败:', err) + return { + success: false, + error: err.message + } + } +} + diff --git a/cloudfunctions/sendReminder/package-lock.json b/cloudfunctions/sendReminder/package-lock.json new file mode 100644 index 0000000..17818f7 --- /dev/null +++ b/cloudfunctions/sendReminder/package-lock.json @@ -0,0 +1,1525 @@ +{ + "name": "sendReminder", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sendReminder", + "version": "1.0.0", + "dependencies": { + "wx-server-sdk": "~2.6.3" + } + }, + "node_modules/@cloudbase/database": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@cloudbase/database/-/database-1.4.1.tgz", + "integrity": "sha512-BYLXHS6c+WhxAvvdak8Z3W+heScqBBPu/CQ76gC8v1Scuy5qf4qxuPWNzyoxde/eZsmc+BRRCFyIq4xUnIot8g==", + "license": "ISC", + "dependencies": { + "bson": "^4.0.3", + "lodash.clonedeep": "4.5.0", + "lodash.set": "4.3.2", + "lodash.unset": "4.5.2" + } + }, + "node_modules/@cloudbase/node-sdk": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@cloudbase/node-sdk/-/node-sdk-2.9.1.tgz", + "integrity": "sha512-4JGLiy9/Ko7d1pnBgq5mtIUm5v9ig1tHqAeLrkMc8b4vVBRTnYlxJer3uKXq6+9fjLprxNyRSahEg4QR8/Gbkw==", + "license": "MIT", + "dependencies": { + "@cloudbase/database": "1.4.1", + "@cloudbase/signature-nodejs": "1.0.0-beta.0", + "@types/retry": "^0.12.0", + "agentkeepalive": "^4.1.3", + "axios": "^0.21.1", + "is-regex": "^1.0.4", + "jsonwebtoken": "^8.5.1", + "lodash.merge": "^4.6.1", + "request": "^2.87.0", + "request-promise": "^4.2.5", + "retry": "^0.12.0", + "ts-node": "^8.10.2", + "xml2js": "^0.4.19" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@cloudbase/signature-nodejs": { + "version": "1.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@cloudbase/signature-nodejs/-/signature-nodejs-1.0.0-beta.0.tgz", + "integrity": "sha512-gpKqwsVk/D2PzvFamYNReymXSdvRSY90eZ1ARf+1wZ8oT6OpK9kr6nmevGykMxN1n17Gn92hBbWqAxU9o3+kAQ==", + "dependencies": { + "@types/clone": "^0.1.30", + "clone": "^2.1.2", + "is-stream": "^2.0.0", + "url": "^0.11.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/clone": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", + "integrity": "sha512-vcxBr+ybljeSiasmdke1cQ9ICxoEwaBgM1OQ/P5h4MPj/kRyLcDl5L8PrftlbyV1kBbJIs3M3x1A1+rcWd4mEA==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "license": "MIT" + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "license": "MIT" + }, + "node_modules/lodash.unset": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz", + "integrity": "sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tcb-admin-node": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/tcb-admin-node/-/tcb-admin-node-1.23.0.tgz", + "integrity": "sha512-SAbjTqMsSi63SId1BJ4kWdyGJzhxh9Tjvy3YXxcsoaAC2PtASn4UIYsBsiNEUfcn58QEn2tdvCvvf69WLLjjrg==", + "license": "MIT", + "dependencies": { + "@cloudbase/database": "0.9.15", + "@cloudbase/signature-nodejs": "^1.0.0-beta.0", + "is-regex": "^1.0.4", + "jsonwebtoken": "^8.5.1", + "lodash.merge": "^4.6.1", + "request": "^2.87.0", + "xml2js": "^0.4.19" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/tcb-admin-node/node_modules/@cloudbase/database": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/@cloudbase/database/-/database-0.9.15.tgz", + "integrity": "sha512-63e7iIl+van41B39Tw4ScNe9TRCt+5GHjc7q6i8NzkWBLC3U3KlbWo79YHsUHUPI79POpQ8UMlMVo7HXIAO3dg==", + "license": "ISC", + "dependencies": { + "bson": "^4.0.2", + "lodash.clonedeep": "4.5.0", + "lodash.set": "4.3.2", + "lodash.unset": "4.5.2" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "license": "MIT", + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/url/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/wx-server-sdk": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/wx-server-sdk/-/wx-server-sdk-2.6.3.tgz", + "integrity": "sha512-wCSAO94HScMVnalb4WVbOqjTyKxus4/jPYV41ct9liHXv/hGGlDqUWV1vb1icyQKXp+mSslvpBNNjYnZAM5MVA==", + "license": "MIT", + "dependencies": { + "@cloudbase/node-sdk": "2.9.1", + "json-bigint": "^1.0.0", + "protobufjs": "^6.8.8", + "tcb-admin-node": "latest", + "tslib": "^1.9.3" + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/cloudfunctions/sendReminder/package.json b/cloudfunctions/sendReminder/package.json new file mode 100644 index 0000000..ff03a47 --- /dev/null +++ b/cloudfunctions/sendReminder/package.json @@ -0,0 +1,10 @@ +{ + "name": "sendReminder", + "version": "1.0.0", + "description": "发送生日提醒订阅消息", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "~2.6.3" + } +} + diff --git a/cloudfunctions/syncAnniversary/config.json b/cloudfunctions/syncAnniversary/config.json new file mode 100644 index 0000000..eaa6dae --- /dev/null +++ b/cloudfunctions/syncAnniversary/config.json @@ -0,0 +1,6 @@ +{ + "permissions": { + "openapi": [] + } +} + diff --git a/cloudfunctions/syncAnniversary/index.js b/cloudfunctions/syncAnniversary/index.js new file mode 100644 index 0000000..6b8591e --- /dev/null +++ b/cloudfunctions/syncAnniversary/index.js @@ -0,0 +1,157 @@ +// 云函数:同步纪念日数据到云端 +const cloud = require('wx-server-sdk') + +cloud.init({ + env: cloud.DYNAMIC_CURRENT_ENV +}) + +const db = cloud.database() + +exports.main = async (event, context) => { + const wxContext = cloud.getWXContext() + const openid = wxContext.OPENID + + const { action, data } = event + + try { + switch (action) { + case 'add': + // 添加纪念日 + return await addAnniversary(openid, data) + + case 'update': + // 更新纪念日 + return await updateAnniversary(openid, data) + + case 'delete': + // 删除纪念日 + return await deleteAnniversary(openid, data.id) + + case 'sync': + // 批量同步 + return await syncAnniversaries(openid, data) + + case 'get': + // 获取用户所有纪念日 + return await getAnniversaries(openid) + + default: + return { + success: false, + error: '未知操作' + } + } + } catch (err) { + console.error('操作失败:', err) + return { + success: false, + error: err.message + } + } +} + +/** + * 添加纪念日 + */ +async function addAnniversary(openid, anniversary) { + const res = await db.collection('anniversaries').add({ + data: { + ...anniversary, + openid, + createTime: db.serverDate(), + updateTime: db.serverDate() + } + }) + + return { + success: true, + id: res._id + } +} + +/** + * 更新纪念日 + */ +async function updateAnniversary(openid, anniversary) { + const { id, ...updateData } = anniversary + + await db.collection('anniversaries') + .where({ + _id: id, + openid + }) + .update({ + data: { + ...updateData, + updateTime: db.serverDate() + } + }) + + return { + success: true + } +} + +/** + * 删除纪念日 + */ +async function deleteAnniversary(openid, id) { + await db.collection('anniversaries') + .where({ + _id: id, + openid + }) + .remove() + + return { + success: true + } +} + +/** + * 批量同步纪念日 + */ +async function syncAnniversaries(openid, anniversaries) { + // 先删除用户的所有纪念日 + await db.collection('anniversaries') + .where({ + openid + }) + .remove() + + // 批量添加 + const tasks = anniversaries.map(anniversary => { + return db.collection('anniversaries').add({ + data: { + ...anniversary, + openid, + createTime: db.serverDate(), + updateTime: db.serverDate() + } + }) + }) + + await Promise.all(tasks) + + return { + success: true, + count: anniversaries.length + } +} + +/** + * 获取用户所有纪念日 + */ +async function getAnniversaries(openid) { + const res = await db.collection('anniversaries') + .where({ + openid + }) + .get() + + return { + success: true, + data: res.data + } +} + diff --git a/cloudfunctions/syncAnniversary/package.json b/cloudfunctions/syncAnniversary/package.json new file mode 100644 index 0000000..5d0a126 --- /dev/null +++ b/cloudfunctions/syncAnniversary/package.json @@ -0,0 +1,10 @@ +{ + "name": "syncAnniversary", + "version": "1.0.0", + "description": "同步纪念日数据", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "~2.6.3" + } +} + diff --git a/pages/add-anniversary/add-anniversary.js b/pages/add-anniversary/add-anniversary.js index ffad185..d2669ad 100644 --- a/pages/add-anniversary/add-anniversary.js +++ b/pages/add-anniversary/add-anniversary.js @@ -1,6 +1,7 @@ // add-anniversary.js const storage = require('../../utils/storage') const dateUtils = require('../../utils/date') +const api = require('../../utils/api') Page({ data: { @@ -188,11 +189,32 @@ Page({ */ onRemindDaysChange(e) { const index = parseInt(e.detail.value) - const days = [3, 7, 14, 30, 7][index] - this.setData({ - remindDaysIndex: index, - 'formData.remindDays': days - }) + this.setData({ remindDaysIndex: index }) + if (index === 4) { + // 自定义天数 + wx.showModal({ + title: '自定义提前天数', + editable: true, + placeholderText: '请输入天数(1-365)', + success: (res) => { + if (res.confirm) { + const days = parseInt(res.content) + if (!days || days < 1 || days > 365) { + wx.showToast({ title: '请输入1-365之间的天数', icon: 'none' }) + this.setData({ remindDaysIndex: 1, 'formData.remindDays': 7 }) + return + } + this.setData({ 'formData.remindDays': days }) + } else { + // 取消则回到默认7天 + this.setData({ remindDaysIndex: 1, 'formData.remindDays': 7 }) + } + } + }) + } else { + const days = [3, 7, 14, 30][index] + this.setData({ 'formData.remindDays': days }) + } }, /** @@ -214,8 +236,8 @@ Page({ /** * 提交 */ - onSubmit() { - const { formData, personId, anniversaryId } = this.data + async onSubmit() { + const { formData, personId, anniversaryId, personList } = this.data // 验证关联人员 if (!personId) { @@ -235,14 +257,36 @@ Page({ return } + // 如果开启了提醒,请求订阅消息授权 + if (formData.remindEnabled) { + try { + await this.requestSubscribe() + } catch (err) { + console.log('用户拒绝订阅消息') + // 继续保存,即使用户拒绝订阅 + } + } + + // 获取人员名称 + const person = personList.find(p => p.id === personId) + const personName = person ? person.name : '' + if (anniversaryId) { // 编辑模式 const success = storage.updateAnniversary(anniversaryId, { personId, + personName, ...formData }) if (success) { + // 同步到云端 + this.syncToCloud(anniversaryId, { + personId, + personName, + ...formData + }, 'update') + wx.showToast({ title: '保存成功', icon: 'success' }) setTimeout(() => wx.navigateBack(), 1500) } @@ -250,14 +294,55 @@ Page({ // 新增模式 const newAnniversary = storage.addAnniversary({ personId, + personName, ...formData }) if (newAnniversary) { + // 同步到云端 + this.syncToCloud(newAnniversary.id, newAnniversary, 'add') + wx.showToast({ title: '添加成功', icon: 'success' }) setTimeout(() => wx.navigateBack(), 1500) } } + }, + + /** + * 请求订阅消息授权 + */ + requestSubscribe() { + return new Promise((resolve, reject) => { + wx.requestSubscribeMessage({ + tmplIds: ['6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw'], + success: (res) => { + console.log('订阅消息授权结果:', res) + resolve(res) + }, + fail: (err) => { + console.error('订阅消息授权失败:', err) + reject(err) + } + }) + }) + }, + + /** + * 同步到自建后端 + */ + async syncToCloud(id, data, action) { + try { + const openid = wx.getStorageSync('openid') + if (!openid) { + console.log('未获取到openid,跳过云端同步') + return + } + const res = await api.anniversary(action, { id, ...data }) + console.log('云端同步成功:', res) + } catch (err) { + console.error('云端同步失败:', err) + // 不影响本地保存 + } } }) diff --git a/pages/add-anniversary/add-anniversary.wxss b/pages/add-anniversary/add-anniversary.wxss index 01b9dcc..810aa07 100644 --- a/pages/add-anniversary/add-anniversary.wxss +++ b/pages/add-anniversary/add-anniversary.wxss @@ -6,7 +6,7 @@ } .form { - padding: 32rpx; + padding: 0 32rpx 32rpx; } .form-item { @@ -15,6 +15,11 @@ padding: 32rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); + box-sizing: border-box; +} + +.form-item:first-child { + margin-top: 6rpx; } .label { @@ -100,36 +105,57 @@ } .textarea { + width: 100%; background-color: #f5f5f5; border-radius: 8rpx; padding: 24rpx; font-size: 28rpx; min-height: 160rpx; + box-sizing: border-box; } .buttons { display: flex; gap: 24rpx; margin-top: 40rpx; + padding: 0 32rpx 32rpx; } .btn { flex: 1; - height: 88rpx; - line-height: 88rpx; - border-radius: 16rpx; + height: 96rpx; + line-height: 96rpx; + border-radius: 48rpx; font-size: 32rpx; + font-weight: 600; border: none; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); } .btn-cancel { - background-color: #f5f5f5; + background: linear-gradient(135deg, #f5f7fa 0%, #e8eaf0 100%); color: #666; + border: 2rpx solid rgba(102, 126, 234, 0.2); +} + +.btn-cancel:active { + transform: scale(0.98); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); } .btn-submit { - background: linear-gradient(135deg, #07c160 0%, #06ad56 100%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4); +} + +.btn-submit:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3); } .btn-cancel::after, diff --git a/pages/add-person/add-person.wxss b/pages/add-person/add-person.wxss index 28c05c4..b40248b 100644 --- a/pages/add-person/add-person.wxss +++ b/pages/add-person/add-person.wxss @@ -1,7 +1,7 @@ /**add-person.wxss**/ .container { min-height: 100vh; - background-color: #f5f5f5; + background: linear-gradient(180deg, rgba(102, 126, 234, 0.08) 0%, transparent 30%); padding-bottom: 120rpx; } @@ -10,16 +10,21 @@ } .form-item { - background-color: #fff; - border-radius: 16rpx; - padding: 32rpx; + background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%); + border-radius: 24rpx; + padding: 36rpx; margin-bottom: 24rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); + box-shadow: 0 8rpx 32rpx rgba(102, 126, 234, 0.12); + border: 2rpx solid rgba(255, 255, 255, 0.8); } .label { font-size: 28rpx; - color: #333; + font-weight: 600; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; margin-bottom: 24rpx; display: block; } @@ -27,6 +32,7 @@ .label.required::after { content: ' *'; color: #ff5722; + -webkit-text-fill-color: #ff5722; } /* 头像上传 */ @@ -38,7 +44,9 @@ width: 200rpx; height: 200rpx; border-radius: 50%; - background-color: #f0f0f0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: 6rpx solid rgba(255, 255, 255, 0.9); + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); } .avatar-placeholder { @@ -46,69 +54,107 @@ height: 200rpx; margin: 0 auto; border-radius: 50%; - background-color: #f5f5f5; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; - border: 2px dashed #ddd; + border: 4rpx dashed rgba(102, 126, 234, 0.4); + transition: all 0.3s ease; +} + +.avatar-placeholder:active { + transform: scale(0.95); + background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%); } .avatar-placeholder .icon { - font-size: 60rpx; + font-size: 64rpx; display: block; margin-bottom: 16rpx; + filter: grayscale(0.5); } .avatar-placeholder .text { font-size: 24rpx; - color: #999; + color: #667eea; + font-weight: 500; } .input { - background-color: #f5f5f5; - border-radius: 8rpx; + background-color: #f8f9fa; + border-radius: 12rpx; padding: 24rpx; font-size: 28rpx; + border: 2rpx solid transparent; + transition: all 0.3s ease; +} + +.input:focus { + background-color: #fff; + border-color: #667eea; } .textarea { - background-color: #f5f5f5; - border-radius: 8rpx; + background-color: #f8f9fa; + border-radius: 12rpx; padding: 24rpx; font-size: 28rpx; min-height: 160rpx; + border: 2rpx solid transparent; + transition: all 0.3s ease; +} + +.textarea:focus { + background-color: #fff; + border-color: #667eea; } .buttons { display: flex; gap: 24rpx; margin-top: 40rpx; + padding: 0 32rpx 32rpx; } .btn { flex: 1; - height: 88rpx; - line-height: 88rpx; - border-radius: 16rpx; + height: 96rpx; + line-height: 96rpx; + border-radius: 48rpx; font-size: 32rpx; + font-weight: 600; border: none; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); } .btn-cancel { - background-color: #f5f5f5; + background: linear-gradient(135deg, #f5f7fa 0%, #e8eaf0 100%); color: #666; + border: 2rpx solid rgba(102, 126, 234, 0.2); +} + +.btn-cancel:active { + transform: scale(0.98); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); } .btn-submit { - background: linear-gradient(135deg, #07c160 0%, #06ad56 100%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4); } -.btn-cancel::after { - border: none; +.btn-submit:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3); } +.btn-cancel::after, .btn-submit::after { border: none; } diff --git a/pages/calendar/calendar.js b/pages/calendar/calendar.js index 5b73eb8..a3dc04e 100644 --- a/pages/calendar/calendar.js +++ b/pages/calendar/calendar.js @@ -1,11 +1,12 @@ // calendar.js const storage = require('../../utils/storage') const dateUtils = require('../../utils/date') +const { TYPE_NAMES, TYPE_ICONS, IMPORTANCE_COLORS } = require('../../utils/constants') Page({ data: { - currentYear: 2024, - currentMonth: 1, + currentYear: new Date().getFullYear(), + currentMonth: new Date().getMonth() + 1, calendarDays: [], monthEvents: [] }, @@ -20,100 +21,75 @@ Page({ this.loadMonthEvents() }, + /** + * 构建当月事件索引 { "day": [anniversary, ...] } + */ + buildEventIndex(anniversaries, year, month) { + const index = {} + for (const a of anniversaries) { + if (a.solarYear === year && a.solarMonth === month) { + const key = String(a.solarDay) + if (!index[key]) index[key] = [] + index[key].push(a) + } + } + return index + }, + /** * 渲染日历 */ renderCalendar() { const { currentYear, currentMonth } = this.data const anniversaries = storage.getAnniversaries() - - // 获取本月第一天是星期几 + + // 预建事件索引,避免 O(n×m) 过滤 + const eventIndex = this.buildEventIndex(anniversaries, currentYear, currentMonth) + const firstDay = new Date(currentYear, currentMonth - 1, 1) const startWeekday = firstDay.getDay() - - // 获取本月天数 const daysInMonth = new Date(currentYear, currentMonth, 0).getDate() - - // 获取上个月的天数 const prevMonthDays = new Date(currentYear, currentMonth - 1, 0).getDate() - - // 构建日历天数数组 + const calendarDays = [] - const today = new Date() - + // 填充上个月的日期 for (let i = startWeekday - 1; i >= 0; i--) { const day = prevMonthDays - i - calendarDays.push({ - day, - date: new Date(currentYear, currentMonth - 2, day), - isToday: false, - isOtherMonth: true, - events: [], - hasMore: false - }) + calendarDays.push({ day, isToday: false, isOtherMonth: true, events: [], hasMore: false }) } - + // 填充本月的日期 for (let day = 1; day <= daysInMonth; day++) { const date = new Date(currentYear, currentMonth - 1, day) const isToday = dateUtils.isToday(date) - - // 查找该日期的纪念日 - const events = anniversaries - .filter(a => this.isAnniversaryOnDate(a, date)) - .map(a => ({ - id: a.id, - color: this.getEventColor(a) - })) - .slice(0, 3) - + const allEvents = eventIndex[String(day)] || [] + const events = allEvents.slice(0, 3).map(a => ({ id: a.id, color: this.getEventColor(a) })) + calendarDays.push({ day, - date, isToday, isOtherMonth: false, events, - hasMore: events.length > 2, - moreCount: anniversaries.filter(a => this.isAnniversaryOnDate(a, date)).length - 2 + hasMore: allEvents.length > 3, + moreCount: allEvents.length - 3 }) } - + // 填充下个月的日期(补全6行) const remainingDays = 42 - calendarDays.length for (let day = 1; day <= remainingDays; day++) { - calendarDays.push({ - day, - date: new Date(currentYear, currentMonth, day), - isToday: false, - isOtherMonth: true, - events: [], - hasMore: false - }) + calendarDays.push({ day, isToday: false, isOtherMonth: true, events: [], hasMore: false }) } - - this.setData({ calendarDays }) - }, - /** - * 判断纪念日是否在某日期 - */ - isAnniversaryOnDate(anniversary, date) { - return anniversary.solarYear === date.getFullYear() && - anniversary.solarMonth === date.getMonth() + 1 && - anniversary.solarDay === date.getDate() + this.setData({ calendarDays }) }, /** * 获取事件颜色 */ getEventColor(anniversary) { - const colors = { - high: '#ff5722', - medium: '#ff9800', - low: '#07c160' - } - return colors[anniversary.importance] || '#07c160' + return IMPORTANCE_COLORS[anniversary.importance] || '#07c160' }, /** @@ -150,28 +126,14 @@ Page({ * 获取类型图标 */ getTypeIcon(type) { - const icons = { - birthday: '🎂', - lunar_birthday: '🌙', - wedding: '💍', - engagement: '💕', - other: '📅' - } - return icons[type] || '📅' + return TYPE_ICONS[type] || '📅' }, /** * 获取类型名称 */ getTypeName(type) { - const names = { - birthday: '公历生日', - lunar_birthday: '农历生日', - wedding: '结婚纪念日', - engagement: '订婚纪念日', - other: '其他纪念日' - } - return names[type] || '其他' + return TYPE_NAMES[type] || '其他' }, /** diff --git a/pages/calendar/calendar.wxml b/pages/calendar/calendar.wxml index c02dbd2..123ba8d 100644 --- a/pages/calendar/calendar.wxml +++ b/pages/calendar/calendar.wxml @@ -12,7 +12,9 @@ - {{item}} + + {{item}} + diff --git a/pages/calendar/calendar.wxss b/pages/calendar/calendar.wxss index 87d134b..3e33eff 100644 --- a/pages/calendar/calendar.wxss +++ b/pages/calendar/calendar.wxss @@ -5,43 +5,61 @@ } .calendar-header { - background-color: #fff; - padding: 32rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 40rpx 32rpx; display: flex; align-items: center; justify-content: space-between; - border-bottom: 1px solid #f0f0f0; + box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2); } .month-navigation { display: flex; align-items: center; - gap: 32rpx; + gap: 24rpx; } .nav-btn { - font-size: 48rpx; - color: #07c160; + font-size: 40rpx; + color: #fff; font-weight: 300; - width: 60rpx; - text-align: center; + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 50%; + transition: all 0.2s; +} + +.nav-btn:active { + background-color: rgba(255, 255, 255, 0.3); + transform: scale(0.95); } .month-title { font-size: 36rpx; font-weight: 600; - color: #333; + color: #fff; min-width: 240rpx; text-align: center; } .today-btn { - padding: 12rpx 32rpx; - background-color: #f5f5f5; - color: #666; + padding: 14rpx 32rpx; + background-color: rgba(255, 255, 255, 0.25); + color: #fff; border-radius: 40rpx; - font-size: 24rpx; + font-size: 26rpx; border: none; + font-weight: 600; + transition: all 0.2s; +} + +.today-btn:active { + background-color: rgba(255, 255, 255, 0.35); + transform: scale(0.95); } .today-btn::after { @@ -49,96 +67,137 @@ } .week-header { - display: flex; - background-color: #fff; - border-bottom: 1px solid #f0f0f0; + display: grid; + grid-template-columns: repeat(7, 1fr); + background: linear-gradient(to bottom, #fff 0%, #fafafa 100%); + padding: 20rpx 24rpx 12rpx 24rpx; + gap: 12rpx; + border-bottom: 2rpx solid #f0f0f0; + box-sizing: border-box; } .week-title { - flex: 1; - text-align: center; - padding: 20rpx 0; - font-size: 24rpx; - color: #999; - font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + height: 60rpx; + font-size: 26rpx; + color: #888; + font-weight: 600; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.week-title text { + display: block; } .calendar-content { height: calc(100vh - 200rpx); + background-color: #f5f5f5; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); - background-color: #fff; + background-color: #f5f5f5; + padding: 16rpx 24rpx 16rpx 24rpx; + gap: 12rpx; + box-sizing: border-box; } .calendar-cell { - min-height: 120rpx; - border: 1px solid #f0f0f0; - padding: 8rpx; + min-height: 100rpx; + background-color: #fff; + border-radius: 12rpx; + padding: 12rpx 8rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + transition: all 0.2s; + box-sizing: border-box; + margin: 0; } .calendar-cell.other-month { - background-color: #fafafa; + background-color: transparent; + box-shadow: none; +} + +.calendar-cell.today { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 8rpx 16rpx rgba(102, 126, 234, 0.3); + transform: scale(1.05); } .calendar-cell.today .date-num { - background-color: #07c160; color: #fff; - border-radius: 50%; - width: 48rpx; - height: 48rpx; - line-height: 48rpx; - text-align: center; + font-weight: 700; } .date-num { - font-size: 28rpx; + font-size: 32rpx; color: #333; margin-bottom: 8rpx; + font-weight: 600; } .calendar-cell.other-month .date-num { - color: #ccc; + color: #d0d0d0; + font-weight: 400; } .events { display: flex; - gap: 4rpx; + gap: 6rpx; + flex-wrap: wrap; + justify-content: center; + max-width: 100%; } .event-dot { - width: 12rpx; - height: 12rpx; + width: 8rpx; + height: 8rpx; border-radius: 50%; + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15); +} + +.calendar-cell.today .event-dot { + background-color: #fff !important; } .more-text { - font-size: 20rpx; + font-size: 18rpx; color: #999; margin-top: 4rpx; + font-weight: 500; +} + +.calendar-cell.today .more-text { + color: #fff; } /* 事件列表 */ .events-list { - padding: 32rpx; + padding: 32rpx 24rpx; } .section-title { font-size: 32rpx; - font-weight: 600; + font-weight: 700; color: #333; margin-bottom: 24rpx; + padding-left: 16rpx; + border-left: 6rpx solid #667eea; } .empty-state { text-align: center; padding: 120rpx 40rpx; + opacity: 0.6; } .empty-state .icon { @@ -150,14 +209,17 @@ .empty-state .text { font-size: 28rpx; color: #999; + line-height: 1.6; } .event-card { - background-color: #fff; + background: linear-gradient(135deg, #fff 0%, #fafafa 100%); border-radius: 16rpx; padding: 32rpx; - margin-bottom: 24rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); + margin-bottom: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); + border: 2rpx solid rgba(102, 126, 234, 0.1); + transition: all 0.3s; } .event-header { @@ -190,26 +252,31 @@ } .lunar-badge { - padding: 4rpx 12rpx; - background-color: #e3f2fd; + padding: 6rpx 16rpx; + background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); color: #1976d2; - border-radius: 4rpx; + border-radius: 20rpx; font-size: 20rpx; + font-weight: 600; + box-shadow: 0 2rpx 8rpx rgba(25, 118, 210, 0.15); } .status { font-size: 22rpx; - padding: 4rpx 12rpx; - border-radius: 4rpx; + padding: 6rpx 16rpx; + border-radius: 20rpx; + font-weight: 600; } .status.urgent { - background-color: #fff3f0; + background: linear-gradient(135deg, #ffe0db 0%, #ffccbc 100%); color: #ff5722; + box-shadow: 0 2rpx 8rpx rgba(255, 87, 34, 0.15); } .status.warning { - background-color: #fff8e6; + background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%); color: #ff9800; + box-shadow: 0 2rpx 8rpx rgba(255, 152, 0, 0.15); } diff --git a/pages/index/index.js b/pages/index/index.js index 20fcb89..7c67069 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -1,13 +1,17 @@ // index.js const storage = require('../../utils/storage') const dateUtils = require('../../utils/date') +const { TYPE_NAMES } = require('../../utils/constants') Page({ data: { persons: [], - originalPersons: [], // 原始数据备份 + originalPersons: [], searchKeyword: '', - currentFilter: 'all' + currentFilter: 'all', + totalCount: 0, + upcomingCount: 0, + todayText: '' }, onLoad() { @@ -15,7 +19,6 @@ Page({ }, onShow() { - // 每次显示页面时刷新数据 this.loadPersons() }, @@ -25,32 +28,24 @@ Page({ loadPersons() { const persons = storage.getPersons() const anniversaries = storage.getAnniversaries() - - // 为每个人员添加纪念日信息 + const personsWithAnniversaries = persons.map(person => { const personAnniversaries = anniversaries.filter(a => a.personId === person.id) - - // 找到最近的纪念日 + let nextAnniversary = null if (personAnniversaries.length > 0) { - const today = new Date() const upcoming = personAnniversaries .map(a => { - // 如果是农历,需要特殊处理 - const date = new Date(a.solarYear, a.solarMonth - 1, a.solarDay) - return { - ...a, - date, - daysUntil: dateUtils.getDaysUntil(date) - } + const { date, daysUntil } = dateUtils.getNextOccurrence(a.solarMonth, a.solarDay) + return { ...a, date, daysUntil } }) - .filter(a => a.daysUntil >= 0) .sort((a, b) => a.daysUntil - b.daysUntil) - + if (upcoming.length > 0) { const next = upcoming[0] nextAnniversary = { type: next.type, + typeName: this.getTypeName(next.type, next.customTypeName), dateText: dateUtils.formatDate(next.date, 'MM月DD日'), daysUntil: next.daysUntil, daysUntilText: this.formatDaysUntil(next.daysUntil) @@ -58,14 +53,9 @@ Page({ } } - return { - ...person, - anniversaryCount: personAnniversaries.length, - nextAnniversary - } + return { ...person, anniversaryCount: personAnniversaries.length, nextAnniversary } }) - // 按最近的纪念日排序 const sorted = personsWithAnniversaries.sort((a, b) => { if (!a.nextAnniversary && !b.nextAnniversary) return 0 if (!a.nextAnniversary) return 1 @@ -73,10 +63,18 @@ Page({ return a.nextAnniversary.daysUntil - b.nextAnniversary.daysUntil }) - this.setData({ - persons: sorted, - originalPersons: sorted - }) + const today = new Date() + const weekdays = ['日', '一', '二', '三', '四', '五', '六'] + const todayText = `${today.getMonth() + 1}月${today.getDate()}日 周${weekdays[today.getDay()]}` + const upcomingCount = sorted.filter(p => p.nextAnniversary && p.nextAnniversary.daysUntil <= 7).length + + this.setData({ originalPersons: sorted, totalCount: sorted.length, upcomingCount, todayText }) + + if (this.data.currentFilter === 'all' && !this.data.searchKeyword) { + this.setData({ persons: sorted }) + } else { + this.filterPersons() + } }, /** @@ -90,6 +88,14 @@ Page({ return `还有${Math.floor(days / 30)}个月` }, + /** + * 获取类型名称 + */ + getTypeName(type, customName) { + if (type === 'other' && customName) return customName + return TYPE_NAMES[type] || '纪念日' + }, + /** * 搜索输入 */ @@ -104,8 +110,9 @@ Page({ */ onFilterTap(e) { const filter = e.currentTarget.dataset.filter - this.setData({ currentFilter: filter }) - this.filterPersons() + this.setData({ currentFilter: filter }, () => { + this.filterPersons() + }) }, /** @@ -113,6 +120,7 @@ Page({ */ filterPersons() { const { originalPersons, searchKeyword, currentFilter } = this.data + const anniversaries = storage.getAnniversaries() let filtered = [...originalPersons] @@ -124,12 +132,39 @@ Page({ ) } - // 类型筛选(暂时保留,后续可以实现更精确的筛选) - // if (currentFilter !== 'all') { - // // 可以实现更精确的筛选逻辑 - // } + // 类型筛选 + if (currentFilter !== 'all') { + filtered = filtered.filter(person => { + const personAnniversaries = anniversaries.filter(a => a.personId === person.id) + + if (currentFilter === 'birthday') { + // 生日筛选:只显示有生日类型的人(公历生日或农历生日) + return personAnniversaries.some(a => + a.type === 'birthday' || a.type === 'lunar_birthday' + ) + } else if (currentFilter === 'anniversary') { + // 纪念日筛选:只显示有纪念日类型的人(结婚、订婚、其他) + return personAnniversaries.some(a => + a.type === 'wedding' || a.type === 'engagement' || a.type === 'other' + ) + } else if (currentFilter === 'upcoming') { + // 即将到来:只显示7天内有纪念日的人 + return person.nextAnniversary && person.nextAnniversary.daysUntil <= 7 + } + + return true + }) + } - this.setData({ persons: filtered }) + // 排序:与 loadPersons() 保持一致,按最近的纪念日排序 + const sorted = filtered.sort((a, b) => { + if (!a.nextAnniversary && !b.nextAnniversary) return 0 + if (!a.nextAnniversary) return 1 + if (!b.nextAnniversary) return -1 + return a.nextAnniversary.daysUntil - b.nextAnniversary.daysUntil + }) + + this.setData({ persons: sorted }) }, /** diff --git a/pages/index/index.wxml b/pages/index/index.wxml index a587af6..388e700 100644 --- a/pages/index/index.wxml +++ b/pages/index/index.wxml @@ -1,56 +1,112 @@ - + + + + + + + 生日提醒 + {{todayText}} + + 🎂 + + + + {{totalCount}} + 位好友 + + + + {{upcomingCount}} + 即将到来 + + + + - - + + + 🔍 + + - - 筛选: - - 全部 - 生日 - 纪念日 - 即将到来 - - + + + 全部 + 🎂 生日 + 💍 纪念日 + ⏰ 即将到来 + + - - - 📅 - 还没有添加任何人 - 点击右下角 + 添加第一个 + + + + + 🌟 + 还没有添加任何人 + 点击右下角 + 开始添加 - - - - - {{item.name}} - {{item.nickname}} - - - {{item.anniversaryCount}} - 个纪念日 + + + + + + + {{item.name[0]}} - - - - 📌 - {{item.nextAnniversary.type}}: {{item.nextAnniversary.dateText}} - - {{item.nextAnniversary.daysUntilText}} - + + + + + + {{item.name}} + {{item.nickname}} + + + {{item.nextAnniversary.daysUntilText}} + + + + + + {{item.nextAnniversary.type === 'birthday' || item.nextAnniversary.type === 'lunar_birthday' ? '🎂' : item.nextAnniversary.type === 'wedding' ? '💍' : item.nextAnniversary.type === 'engagement' ? '💕' : '📅'}} + + {{item.nextAnniversary.typeName}} · {{item.nextAnniversary.dateText}} + +{{item.anniversaryCount - 1}} + + + 暂无纪念日 + + + - + + + - + diff --git a/pages/index/index.wxss b/pages/index/index.wxss index b216a3b..d31e8f8 100644 --- a/pages/index/index.wxss +++ b/pages/index/index.wxss @@ -1,193 +1,410 @@ -/**index.wxss**/ -.container { - min-height: 100vh; - background-color: #f5f5f5; +/* index.wxss */ + +page { + background: #f1f4f9; } -/* 搜索栏 */ -.search-bar { - padding: 24rpx; - background-color: #fff; - border-bottom: 1px solid #f0f0f0; -} - -.search-input { - background-color: #f5f5f5; - border-radius: 40rpx; - padding: 24rpx 32rpx; - font-size: 28rpx; -} - -/* 筛选栏 */ -.filter-bar { +.page { display: flex; - align-items: center; - padding: 20rpx 24rpx; - background-color: #fff; - border-bottom: 1px solid #f0f0f0; + flex-direction: column; + height: 100vh; + background: #f1f4f9; } -.filter-label { - font-size: 26rpx; - color: #666; - margin-right: 16rpx; - white-space: nowrap; +/* ───── Header ───── */ +.header { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + padding: 48rpx 36rpx 40rpx; + flex-shrink: 0; } -.filter-scroll { - white-space: nowrap; -} - -.filter-item { - display: inline-block; - padding: 12rpx 32rpx; - margin-right: 16rpx; - border-radius: 40rpx; - font-size: 26rpx; - background-color: #f5f5f5; - color: #666; -} - -.filter-item.active { - background-color: #07c160; - color: #fff; -} - -/* 人员列表 */ -.person-list { - height: calc(100vh - 200rpx); - padding: 24rpx; -} - -.person-card { - background-color: #fff; - border-radius: 16rpx; - padding: 32rpx; - margin-bottom: 24rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); -} - -.person-header { +.header-top { display: flex; - align-items: center; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 32rpx; } -.avatar { - width: 100rpx; - height: 100rpx; - border-radius: 50%; - margin-right: 24rpx; - background-color: #f0f0f0; -} - -.person-info { - flex: 1; +.header-left { display: flex; flex-direction: column; } -.person-name { - font-size: 32rpx; - font-weight: 600; - color: #333; - margin-bottom: 8rpx; +.header-title { + font-size: 44rpx; + font-weight: 700; + color: #fff; + line-height: 1.2; + letter-spacing: 2rpx; } -.person-nickname { - font-size: 24rpx; - color: #999; +.header-date { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.7); + margin-top: 8rpx; } -.person-count { - text-align: right; +.header-icon { + font-size: 64rpx; + opacity: 0.9; } -.count-text { - font-size: 36rpx; - font-weight: 600; - color: #07c160; - display: block; -} - -.count-label { - font-size: 22rpx; - color: #999; -} - -/* 最近的纪念日 */ -.next-anniversary { - margin-top: 20rpx; - padding-top: 20rpx; - border-top: 1px solid #f0f0f0; +.stats-row { display: flex; align-items: center; - font-size: 26rpx; - color: #666; + background: rgba(255, 255, 255, 0.15); + border-radius: 20rpx; + padding: 20rpx 32rpx; + backdrop-filter: blur(8rpx); } -.next-label { - margin-right: 8rpx; -} - -.next-text { +.stat-item { + display: flex; + align-items: baseline; + gap: 8rpx; flex: 1; + justify-content: center; } -.days-text { - color: #07c160; - font-weight: 500; +.stat-num { + font-size: 48rpx; + font-weight: 700; + color: #fff; + line-height: 1; } -.days-text.urgent { - color: #ff5722; +.stat-num-urgent { + color: #fbbf24; +} + +.stat-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.75); +} + +.stat-sep { + width: 2rpx; + height: 48rpx; + background: rgba(255, 255, 255, 0.25); + flex-shrink: 0; +} + +/* ───── Search ───── */ +.search-wrap { + padding: 24rpx 24rpx 12rpx; + flex-shrink: 0; +} + +.search-box { + display: flex; + align-items: center; + background: #fff; + border-radius: 48rpx; + padding: 20rpx 28rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.07); +} + +.search-icon { + font-size: 32rpx; + margin-right: 16rpx; + opacity: 0.6; +} + +.search-input { + flex: 1; + font-size: 28rpx; + color: #1e293b; + line-height: 1.5; +} + +.search-placeholder { + color: #94a3b8; +} + +/* ───── Filter ───── */ +.filter-scroll { + flex-shrink: 0; + white-space: nowrap; +} + +.filter-bar { + display: flex; + padding: 16rpx 24rpx 20rpx; + gap: 16rpx; +} + +.filter-tab { + display: inline-flex; + align-items: center; + padding: 14rpx 28rpx; + border-radius: 48rpx; + font-size: 26rpx; + color: #64748b; + background: #fff; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); + white-space: nowrap; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.tab-active { + background: #6366f1; + color: #fff; + box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4); +} + +/* ───── List ───── */ +.list { + flex: 1; + overflow: hidden; + padding: 0 24rpx; +} + +/* ───── Card ───── */ +.card { + display: flex; + align-items: center; + background: #fff; + border-radius: 20rpx; + padding: 28rpx 28rpx 28rpx 24rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); + border-left: 6rpx solid #e2e8f0; + transition: transform 0.15s ease, box-shadow 0.15s ease; + position: relative; + overflow: hidden; +} + +.card:active { + transform: scale(0.985); + box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.05); +} + +.card-urgent { + border-left-color: #f97316; +} + +.card-today { + border-left-color: #ef4444; +} + +/* ───── Avatar ───── */ +.avatar-wrap { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + margin-right: 24rpx; + flex-shrink: 0; + overflow: hidden; +} + +.avatar-img { + width: 100%; + height: 100%; + border-radius: 50%; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-initial { + font-size: 40rpx; + font-weight: 700; + color: #fff; +} + +/* ───── Card body ───── */ +.card-body { + flex: 1; + min-width: 0; +} + +.card-row-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.name-group { + display: flex; + align-items: center; + gap: 12rpx; + min-width: 0; + flex: 1; + margin-right: 16rpx; +} + +.card-name { + font-size: 32rpx; font-weight: 600; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -/* 空状态 */ -.empty-state { - padding: 160rpx 40rpx; - text-align: center; +.card-nickname { + font-size: 22rpx; + color: #94a3b8; + background: #f1f5f9; + padding: 4rpx 12rpx; + border-radius: 20rpx; + white-space: nowrap; + flex-shrink: 0; } -.empty-state .icon { +/* ───── Badge ───── */ +.badge { + display: inline-flex; + align-items: center; + padding: 6rpx 18rpx; + border-radius: 24rpx; + flex-shrink: 0; +} + +.badge-text { + font-size: 22rpx; + font-weight: 600; + white-space: nowrap; +} + +.badge-today { + background: #fee2e2; +} +.badge-today .badge-text { + color: #ef4444; +} + +.badge-hot { + background: #ffedd5; +} +.badge-hot .badge-text { + color: #f97316; +} + +.badge-soon { + background: #fef9c3; +} +.badge-soon .badge-text { + color: #ca8a04; +} + +.badge-month { + background: #ede9fe; +} +.badge-month .badge-text { + color: #7c3aed; +} + +.badge-normal { + background: #f1f5f9; +} +.badge-normal .badge-text { + color: #64748b; +} + +/* ───── Anniversary row ───── */ +.card-row-ann { + display: flex; + align-items: center; + gap: 8rpx; +} + +.ann-icon { + font-size: 26rpx; + flex-shrink: 0; +} + +.ann-info { + font-size: 26rpx; + color: #475569; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.muted { + color: #94a3b8; +} + +.ann-more { + font-size: 22rpx; + color: #6366f1; + background: #ede9fe; + padding: 4rpx 12rpx; + border-radius: 20rpx; + flex-shrink: 0; +} + +/* ───── Empty state ───── */ +.empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 160rpx 40rpx 80rpx; +} + +.empty-icon { font-size: 120rpx; - display: block; margin-bottom: 32rpx; } -.empty-state .text { - font-size: 28rpx; - color: #999; - display: block; +.empty-title { + font-size: 32rpx; + font-weight: 600; + color: #475569; margin-bottom: 16rpx; } -.empty-state .hint { - font-size: 24rpx; - color: #bbb; - display: block; +.empty-hint { + font-size: 26rpx; + color: #94a3b8; + background: #fff; + padding: 14rpx 32rpx; + border-radius: 40rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); } -/* 浮动按钮 */ +/* ───── List bottom padding ───── */ +.list-bottom { + height: 180rpx; +} + +/* ───── FAB ───── */ .fab { position: fixed; bottom: 120rpx; right: 40rpx; - width: 100rpx; - height: 100rpx; + width: 112rpx; + height: 112rpx; border-radius: 50%; - background: linear-gradient(135deg, #07c160 0%, #06ad56 100%); - color: #fff; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); display: flex; align-items: center; justify-content: center; - font-size: 60rpx; - box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3); + box-shadow: 0 12rpx 40rpx rgba(99, 102, 241, 0.5); z-index: 100; - transition: all 0.3s; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; } .fab:active { - transform: scale(0.95); + transform: scale(0.9); + box-shadow: 0 6rpx 20rpx rgba(99, 102, 241, 0.4); } +.fab-icon { + font-size: 64rpx; + color: #fff; + font-weight: 300; + line-height: 1; + margin-top: -4rpx; +} diff --git a/pages/person-detail/person-detail.js b/pages/person-detail/person-detail.js index 943b379..397ace4 100644 --- a/pages/person-detail/person-detail.js +++ b/pages/person-detail/person-detail.js @@ -1,6 +1,7 @@ // person-detail.js const storage = require('../../utils/storage') const dateUtils = require('../../utils/date') +const { TYPE_NAMES, TYPE_ICONS, IMPORTANCE_TEXTS } = require('../../utils/constants') Page({ data: { @@ -57,6 +58,7 @@ Page({ date, dateText: dateUtils.formatDate(date, 'YYYY年MM月DD日'), daysUntil, + daysUntilAbs: Math.abs(daysUntil), // 添加绝对值 typeIcon: this.getTypeIcon(a.type), typeName: a.customTypeName || this.getTypeName(a.type), importanceText: this.getImportanceText(a.importance) @@ -73,40 +75,21 @@ Page({ * 获取类型图标 */ getTypeIcon(type) { - const icons = { - birthday: '🎂', - lunar_birthday: '🌙', - wedding: '💍', - engagement: '💕', - other: '📅' - } - return icons[type] || '📅' + return TYPE_ICONS[type] || '📅' }, /** * 获取类型名称 */ getTypeName(type) { - const names = { - birthday: '公历生日', - lunar_birthday: '农历生日', - wedding: '结婚纪念日', - engagement: '订婚纪念日', - other: '其他纪念日' - } - return names[type] || '其他' + return TYPE_NAMES[type] || '其他' }, /** * 获取重要程度文本 */ getImportanceText(importance) { - const texts = { - high: '非常重要', - medium: '重要', - low: '一般' - } - return texts[importance] || '一般' + return IMPORTANCE_TEXTS[importance] || '一般' }, /** @@ -127,17 +110,12 @@ Page({ content: `确定要删除"${this.data.person.name}"吗?相关纪念日也将被删除。`, success: (res) => { if (res.confirm) { - // 先删除相关纪念日 - const anniversaries = storage.getAnniversariesByPersonId(this.data.personId) - anniversaries.forEach(a => storage.deleteAnniversary(a.id)) - - // 再删除人员 - const success = storage.deletePerson(this.data.personId) + const success = storage.deletePersonWithAnniversaries(this.data.personId) if (success) { wx.showToast({ title: '删除成功', icon: 'success' }) - setTimeout(() => { - wx.navigateBack() - }, 1500) + setTimeout(() => wx.navigateBack(), 1500) + } else { + wx.showToast({ title: '删除失败,请重试', icon: 'none' }) } } } diff --git a/pages/person-detail/person-detail.wxml b/pages/person-detail/person-detail.wxml index 9bc8546..15e0995 100644 --- a/pages/person-detail/person-detail.wxml +++ b/pages/person-detail/person-detail.wxml @@ -46,9 +46,9 @@ 今天 - 明天 - {{item.daysUntil}}天后 - 已过{{Math.abs(item.daysUntil)}}天 + 明天 + {{item.daysUntil}}天后 + 已过{{item.daysUntilAbs}}天 {{item.remark}} diff --git a/pages/settings/settings.js b/pages/settings/settings.js index 1d1b82a..b7a581d 100644 --- a/pages/settings/settings.js +++ b/pages/settings/settings.js @@ -69,29 +69,16 @@ Page({ success: (res) => { try { const data = JSON.parse(res.data) - const success = storage.importData(data) - - if (success) { - wx.showToast({ - title: '导入成功', - icon: 'success' - }) - setTimeout(() => { - wx.reLaunch({ - url: '/pages/index/index' - }) - }, 1500) + const result = storage.importData(data) + + if (result.success) { + wx.showToast({ title: '导入成功', icon: 'success' }) + setTimeout(() => wx.reLaunch({ url: '/pages/index/index' }), 1500) } else { - wx.showToast({ - title: '导入失败,请检查数据格式', - icon: 'none' - }) + wx.showToast({ title: result.error || '导入失败,请检查数据格式', icon: 'none' }) } } catch (e) { - wx.showToast({ - title: '数据格式错误', - icon: 'none' - }) + wx.showToast({ title: '数据格式错误', icon: 'none' }) } } }) diff --git a/pages/settings/settings.wxml b/pages/settings/settings.wxml index a8ea9eb..d1c3d1b 100644 --- a/pages/settings/settings.wxml +++ b/pages/settings/settings.wxml @@ -1,46 +1,119 @@ - - - - - 数据管理 - - - 📤 导出数据 - + + + + + + + 设置 + 数据管理与关于 - - - 📥 导入数据 - + ⚙️ + + + + + {{dataCount.persons}} + 位好友 - - - 🗑️ 清空所有数据 - + + + {{dataCount.anniversaries}} + 个纪念日 + + + + + + + + + + + + 📤 + + + 导出数据 + 复制 JSON 到剪贴板 + + + + + + + + + 📥 + + + 导入数据 + 从剪贴板读取并导入 + + + + + + + + + 🗑️ + + + 清空所有数据 + 此操作不可恢复,请谨慎 + + - - 关于 - - - 版本号 - v1.0.0 + + + + + 📱 + + + 版本号 + + v1.0.0 - - - 开发作者 - 生日提醒团队 + + + + + + 👨‍💻 + + + 开发作者 + + 生日提醒团队 - - - 💡 温馨提示 - 1. 建议定期导出数据备份\n2. 农历转换目前使用简化算法\n3. 提醒功能需要小程序权限 + + + + 💡 + 温馨提示 + + + · + 建议定期导出数据备份 + + + · + 农历转换使用寿星万年历算法(1900–2100) + + + · + 开启提醒需授权订阅消息 + - + + + + - diff --git a/pages/settings/settings.wxss b/pages/settings/settings.wxss index db82102..f433cf6 100644 --- a/pages/settings/settings.wxss +++ b/pages/settings/settings.wxss @@ -1,82 +1,240 @@ -/**settings.wxss**/ -.container { - min-height: 100vh; - background-color: #f5f5f5; - padding-bottom: 120rpx; +/* settings.wxss */ + +page { + background: #f1f4f9; } -.settings-list { - padding: 32rpx; +.page { + display: flex; + flex-direction: column; + height: 100vh; + background: #f1f4f9; } -.setting-section { - background-color: #fff; - border-radius: 16rpx; +/* ───── Header ───── */ +.header { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + padding: 48rpx 36rpx 40rpx; + flex-shrink: 0; +} + +.header-top { + display: flex; + align-items: flex-start; + justify-content: space-between; margin-bottom: 32rpx; +} + +.header-left { + display: flex; + flex-direction: column; +} + +.header-title { + font-size: 44rpx; + font-weight: 700; + color: #fff; + line-height: 1.2; + letter-spacing: 2rpx; +} + +.header-sub { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.7); + margin-top: 8rpx; +} + +.header-icon { + font-size: 64rpx; + opacity: 0.9; +} + +.stats-row { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.15); + border-radius: 20rpx; + padding: 20rpx 32rpx; +} + +.stat-item { + display: flex; + align-items: baseline; + gap: 8rpx; + flex: 1; + justify-content: center; +} + +.stat-num { + font-size: 48rpx; + font-weight: 700; + color: #fff; + line-height: 1; +} + +.stat-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.75); +} + +.stat-sep { + width: 2rpx; + height: 48rpx; + background: rgba(255, 255, 255, 0.25); + flex-shrink: 0; +} + +/* ───── Body ───── */ +.body { + flex: 1; + overflow: hidden; +} + +/* ───── Section label ───── */ +.section-label { + font-size: 24rpx; + font-weight: 600; + color: #94a3b8; + letter-spacing: 2rpx; + padding: 32rpx 32rpx 12rpx; + text-transform: uppercase; +} + +/* ───── Card group ───── */ +.card-group { + background: #fff; + border-radius: 20rpx; + margin: 0 24rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); } -.section-title { - padding: 24rpx 32rpx; - background-color: #f9f9f9; - font-size: 26rpx; - color: #999; - font-weight: 500; - border-bottom: 1px solid #f0f0f0; -} - -.setting-item { +/* ───── Row ───── */ +.row { display: flex; align-items: center; - justify-content: space-between; - padding: 32rpx; - border-bottom: 1px solid #f0f0f0; + padding: 28rpx 28rpx; + transition: background 0.15s ease; } -.setting-item:last-child { - border-bottom: none; +.row:active { + background: #f8fafc; } -.item-label { - font-size: 28rpx; - color: #333; +.row-icon-wrap { + width: 72rpx; + height: 72rpx; + border-radius: 18rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-right: 24rpx; } -.item-label.danger { - color: #ff5722; +.row-icon { + font-size: 36rpx; } -.item-value { - font-size: 26rpx; - color: #999; +.row-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 4rpx; } -.item-arrow { +.row-title { + font-size: 30rpx; + font-weight: 500; + color: #1e293b; +} + +.row-title.danger { + color: #ef4444; +} + +.row-desc { + font-size: 24rpx; + color: #94a3b8; +} + +.row-desc.danger { + color: #fca5a5; +} + +.row-arrow { font-size: 40rpx; - color: #ccc; + color: #cbd5e1; + margin-left: 12rpx; } -.info-box { - background-color: #fff; - border-radius: 16rpx; - padding: 32rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); +.row-arrow.danger { + color: #fca5a5; } -.info-title { +.row-value { + font-size: 28rpx; + color: #64748b; +} + +.row-divider { + height: 1rpx; + background: #f1f5f9; + margin-left: 120rpx; +} + +/* ───── Tips card ───── */ +.tips-card { + background: #fff; + border-radius: 20rpx; + margin: 8rpx 24rpx 0; + padding: 28rpx 28rpx 32rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); + border-left: 6rpx solid #6366f1; +} + +.tips-title { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 20rpx; +} + +.tips-icon { + font-size: 32rpx; +} + +.tips-heading { font-size: 28rpx; - color: #333; font-weight: 600; - display: block; - margin-bottom: 16rpx; + color: #1e293b; } -.info-text { +.tips-item { + display: flex; + align-items: flex-start; + gap: 12rpx; + margin-bottom: 12rpx; +} + +.tips-item:last-child { + margin-bottom: 0; +} + +.tips-dot { + font-size: 28rpx; + color: #6366f1; + line-height: 1.8; + flex-shrink: 0; +} + +.tips-text { font-size: 26rpx; - color: #666; - line-height: 2; - white-space: pre-line; - display: block; + color: #475569; + line-height: 1.8; } +/* ───── Bottom padding ───── */ +.bottom-pad { + height: 80rpx; +} diff --git a/project.config.json b/project.config.json index ca89860..cfc3dd7 100644 --- a/project.config.json +++ b/project.config.json @@ -34,10 +34,12 @@ "disableSWC": true }, "compileType": "miniprogram", - "libVersion": "3.0.0", - "appid": "touristappid", + "libVersion": "3.10.3", + "appid": "wxe72882e2072d141a", "projectname": "birthday-reminder", + "cloudfunctionRoot": "cloudfunctions/", "condition": {}, "simulatorPluginLibVersion": {}, - "editorSetting": {} + "editorSetting": {}, + "cloudfunctionTemplateRoot": "cloudfunctionTemplate/" } \ No newline at end of file diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..602652d --- /dev/null +++ b/server/.env.example @@ -0,0 +1,18 @@ +# 微信小程序凭证(在微信公众平台 → 开发管理 → 开发设置 中获取) +WX_APPID= +WX_APPSECRET= + +# 订阅消息模板 ID(保持和云函数 sendReminder 中的 TEMPLATE_ID 一致) +WX_TEMPLATE_ID=6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw + +# 小程序版本:开发版 developer / 体验版 trial / 正式版 formal +WX_MINIPROGRAM_STATE=formal + +# 服务监听端口 +PORT=3000 + +# SQLite 数据库文件路径(容器内路径,挂载到宿主机 data/ 目录) +DB_PATH=/app/data/birthday.db + +# 定时任务 cron 表达式(默认每天 9:00) +REMINDER_CRON=0 9 * * * diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..5ff4483 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +data/ +.env +*.log +.DS_Store diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..0a6ec35 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20-bookworm-slim + +# better-sqlite3 是原生模块,需要编译工具 +RUN apt-get update && apt-get install -y \ + python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# 先单独拷 package*.json,利用 docker 层缓存 +COPY package*.json ./ +RUN npm install --omit=dev + +COPY src ./src + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["node", "src/index.js"] diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..7b6a592 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,18 @@ +services: + birthday-server: + build: + context: . + args: + HTTP_PROXY: http://host.docker.internal:29758 + HTTPS_PROXY: http://host.docker.internal:29758 + NO_PROXY: localhost,127.0.0.1 + container_name: birthday-server + restart: unless-stopped + ports: + - "3000:3000" + env_file: + - .env + volumes: + - ./data:/app/data + environment: + - TZ=Asia/Shanghai diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..dd133d6 --- /dev/null +++ b/server/package.json @@ -0,0 +1,20 @@ +{ + "name": "birthday-reminder-server", + "version": "1.0.0", + "description": "生日提醒小程序自建后端", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js" + }, + "dependencies": { + "axios": "^1.7.7", + "better-sqlite3": "^11.3.0", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "node-cron": "^3.0.3" + }, + "engines": { + "node": ">=18" + } +} diff --git a/server/src/db.js b/server/src/db.js new file mode 100644 index 0000000..52c0e1e --- /dev/null +++ b/server/src/db.js @@ -0,0 +1,51 @@ +const Database = require('better-sqlite3') +const path = require('path') +const fs = require('fs') + +const dbPath = process.env.DB_PATH || path.join(__dirname, '..', 'data', 'birthday.db') + +// 确保数据目录存在 +const dir = path.dirname(dbPath) +if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) + +const db = new Database(dbPath) +db.pragma('journal_mode = WAL') + +// 纪念日表:兼容原云数据库 anniversaries 集合字段 +db.exec(` + CREATE TABLE IF NOT EXISTS anniversaries ( + id TEXT PRIMARY KEY, + openid TEXT NOT NULL, + personId TEXT, + personName TEXT, + type TEXT, + customTypeName TEXT, + isLunar INTEGER DEFAULT 0, + solarYear INTEGER, + solarMonth INTEGER, + solarDay INTEGER, + importance TEXT, + remindEnabled INTEGER DEFAULT 0, + remindDays INTEGER DEFAULT 0, + remark TEXT, + createTime INTEGER, + updateTime INTEGER + ); + CREATE INDEX IF NOT EXISTS idx_anniv_openid ON anniversaries(openid); + CREATE INDEX IF NOT EXISTS idx_anniv_remind ON anniversaries(remindEnabled); + + CREATE TABLE IF NOT EXISTS remind_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + anniversaryId TEXT, + personName TEXT, + typeName TEXT, + daysUntil INTEGER, + sendDate INTEGER, + status TEXT, + error TEXT + ); + CREATE INDEX IF NOT EXISTS idx_log_anniv ON remind_logs(anniversaryId); + CREATE INDEX IF NOT EXISTS idx_log_date ON remind_logs(sendDate); +`) + +module.exports = db diff --git a/server/src/index.js b/server/src/index.js new file mode 100644 index 0000000..b0705db --- /dev/null +++ b/server/src/index.js @@ -0,0 +1,145 @@ +require('dotenv').config() + +const express = require('express') +const db = require('./db') +const wx = require('./wx') +const reminder = require('./reminder') + +const app = express() +app.use(express.json({ limit: '1mb' })) + +// 健康检查 +app.get('/api/health', (req, res) => { + res.json({ ok: true, time: Date.now() }) +}) + +// 登录:前端传 code,返回 openid +app.post('/api/login', async (req, res) => { + try { + const { code } = req.body + if (!code) return res.json({ success: false, error: '缺少 code' }) + const result = await wx.code2session(code) + res.json({ success: true, openid: result.openid, unionid: result.unionid }) + } catch (err) { + console.error('登录失败:', err.message) + res.json({ success: false, error: err.message }) + } +}) + +// 纪念日操作(兼容原云函数 syncAnniversary 的 action 协议) +app.post('/api/anniversary', (req, res) => { + try { + const openid = req.headers['x-openid'] || req.body.openid + if (!openid) return res.json({ success: false, error: '缺少 openid' }) + + const { action, data } = req.body + switch (action) { + case 'add': return res.json(addAnniversary(openid, data)) + case 'update': return res.json(updateAnniversary(openid, data)) + case 'delete': return res.json(deleteAnniversary(openid, data.id)) + case 'sync': return res.json(syncAnniversaries(openid, data)) + case 'get': return res.json(getAnniversaries(openid)) + default: return res.json({ success: false, error: '未知操作' }) + } + } catch (err) { + console.error('纪念日操作失败:', err.message) + res.json({ success: false, error: err.message }) + } +}) + +// ---- 纪念日 CRUD ---- + +const FIELDS = [ + 'id', 'openid', 'personId', 'personName', 'type', 'customTypeName', + 'isLunar', 'solarYear', 'solarMonth', 'solarDay', 'importance', + 'remindEnabled', 'remindDays', 'remark', 'createTime', 'updateTime' +] + +function normalize(row) { + if (!row) return row + return { + ...row, + isLunar: !!row.isLunar, + remindEnabled: !!row.remindEnabled + } +} + +function addAnniversary(openid, anniv) { + const now = Date.now() + const id = anniv.id || ('id_' + now + '_' + Math.random().toString(36).slice(2, 11)) + const row = { + id, openid, + personId: anniv.personId || null, + personName: anniv.personName || '', + type: anniv.type || 'other', + customTypeName: anniv.customTypeName || null, + isLunar: anniv.isLunar ? 1 : 0, + solarYear: anniv.solarYear ?? null, + solarMonth: anniv.solarMonth, + solarDay: anniv.solarDay, + importance: anniv.importance || 'normal', + remindEnabled: anniv.remindEnabled ? 1 : 0, + remindDays: anniv.remindDays ?? 0, + remark: anniv.remark || null, + createTime: anniv.createTime || now, + updateTime: now + } + const placeholders = FIELDS.map(f => '@' + f).join(', ') + db.prepare(`INSERT OR REPLACE INTO anniversaries (${FIELDS.join(',')}) VALUES (${placeholders})`).run(row) + return { success: true, id } +} + +function updateAnniversary(openid, anniv) { + const existing = db.prepare('SELECT * FROM anniversaries WHERE id = ? AND openid = ?').get(anniv.id, openid) + if (!existing) return { success: false, error: '纪念日不存在' } + + const merged = { + ...existing, + ...anniv, + openid, + isLunar: ('isLunar' in anniv ? (anniv.isLunar ? 1 : 0) : existing.isLunar), + remindEnabled: ('remindEnabled' in anniv ? (anniv.remindEnabled ? 1 : 0) : existing.remindEnabled), + updateTime: Date.now() + } + const sets = FIELDS.filter(f => f !== 'id' && f !== 'openid' && f !== 'createTime') + .map(f => `${f} = @${f}`).join(', ') + db.prepare(`UPDATE anniversaries SET ${sets} WHERE id = @id AND openid = @openid`).run(merged) + return { success: true } +} + +function deleteAnniversary(openid, id) { + const info = db.prepare('DELETE FROM anniversaries WHERE id = ? AND openid = ?').run(id, openid) + return { success: info.changes > 0 } +} + +function syncAnniversaries(openid, list) { + const tx = db.transaction((items) => { + db.prepare('DELETE FROM anniversaries WHERE openid = ?').run(openid) + for (const item of items) addAnniversary(openid, item) + }) + tx(Array.isArray(list) ? list : []) + return { success: true, count: Array.isArray(list) ? list.length : 0 } +} + +function getAnniversaries(openid) { + const rows = db.prepare('SELECT * FROM anniversaries WHERE openid = ? ORDER BY createTime DESC').all(openid) + return { success: true, data: rows.map(normalize) } +} + +// 手动触发提醒任务(便于调试,无需等到定点) +app.post('/api/reminder/run', async (req, res) => { + try { + const result = await reminder.runOnce() + res.json({ success: true, ...result }) + } catch (err) { + res.json({ success: false, error: err.message }) + } +}) + +// ---- 启动 ---- + +const PORT = process.env.PORT || 3000 +app.listen(PORT, () => { + console.log(`生日提醒后端已启动,监听 ${PORT}`) + reminder.start() +}) diff --git a/server/src/reminder.js b/server/src/reminder.js new file mode 100644 index 0000000..38b051e --- /dev/null +++ b/server/src/reminder.js @@ -0,0 +1,139 @@ +const cron = require('node-cron') +const db = require('./db') +const wx = require('./wx') + +const TEMPLATE_ID = process.env.WX_TEMPLATE_ID +const MINIPROGRAM_STATE = process.env.WX_MINIPROGRAM_STATE || 'formal' + +const TYPE_NAMES = { + birthday: '公历生日', + lunar_birthday: '农历生日', + wedding: '结婚纪念日', + engagement: '订婚纪念日', + other: '其他纪念日' +} + +function getTypeName(type, customName) { + if (type === 'other' && customName) return customName + return TYPE_NAMES[type] || '纪念日' +} + +function formatDate(date) { + const y = date.getFullYear() + const m = String(date.getMonth() + 1).padStart(2, '0') + const d = String(date.getDate()).padStart(2, '0') + return `${y}年${m}月${d}日` +} + +// 算出"距离今年(或明年)这个纪念日还有多少天" +function getThisYearDate(anniv) { + const today = new Date() + today.setHours(0, 0, 0, 0) + let target = new Date(today.getFullYear(), anniv.solarMonth - 1, anniv.solarDay) + target.setHours(0, 0, 0, 0) + if (target < today) { + target = new Date(today.getFullYear() + 1, anniv.solarMonth - 1, anniv.solarDay) + target.setHours(0, 0, 0, 0) + } + return target +} + +function daysBetween(target) { + const today = new Date() + today.setHours(0, 0, 0, 0) + return Math.round((target - today) / 86400000) +} + +// 检查今天是否已经给这条纪念日发过提醒 +function alreadySentToday(anniversaryId) { + const start = new Date() + start.setHours(0, 0, 0, 0) + const row = db.prepare( + 'SELECT COUNT(*) AS n FROM remind_logs WHERE anniversaryId = ? AND sendDate >= ? AND status = ?' + ).get(anniversaryId, start.getTime(), 'success') + return row.n > 0 +} + +const insertLog = db.prepare(` + INSERT INTO remind_logs (anniversaryId, personName, typeName, daysUntil, sendDate, status, error) + VALUES (@anniversaryId, @personName, @typeName, @daysUntil, @sendDate, @status, @error) +`) + +async function runOnce() { + console.log('[reminder] 开始扫描纪念日...') + + const list = db.prepare('SELECT * FROM anniversaries WHERE remindEnabled = 1').all() + console.log(`[reminder] 启用提醒的纪念日 ${list.length} 条`) + + let ok = 0 + let fail = 0 + + for (const anniv of list) { + try { + const target = getThisYearDate(anniv) + const daysUntil = daysBetween(target) + + const shouldRemind = (daysUntil === 0) || (daysUntil === (anniv.remindDays || 0)) + if (!shouldRemind) continue + + if (alreadySentToday(anniv.id)) { + console.log(`[reminder] 今天已发过,跳过: ${anniv.personName}`) + continue + } + + const typeName = getTypeName(anniv.type, anniv.customTypeName) + + await wx.sendSubscribeMessage({ + touser: anniv.openid, + page: 'pages/index/index', + templateId: TEMPLATE_ID, + miniprogramState: MINIPROGRAM_STATE, + data: { + name1: { value: anniv.personName }, + thing2: { value: daysUntil === 0 ? '今天' : `还有${daysUntil}天` }, + thing6: { value: formatDate(target) }, + thing5: { value: anniv.remark || '别忘了准备一份礼物哦!' } + } + }) + + insertLog.run({ + anniversaryId: anniv.id, + personName: anniv.personName, + typeName, + daysUntil, + sendDate: Date.now(), + status: 'success', + error: null + }) + ok++ + console.log(`[reminder] 发送成功: ${anniv.personName} (${typeName}, ${daysUntil}天)`) + } catch (err) { + fail++ + console.error(`[reminder] 发送失败: ${anniv.personName}`, err.message) + insertLog.run({ + anniversaryId: anniv.id, + personName: anniv.personName, + typeName: null, + daysUntil: null, + sendDate: Date.now(), + status: 'failed', + error: err.message + }) + } + } + + console.log(`[reminder] 完成: 成功 ${ok}, 失败 ${fail}`) + return { total: list.length, ok, fail } +} + +function start() { + const expr = process.env.REMINDER_CRON || '0 9 * * *' + if (!cron.validate(expr)) { + console.error(`[reminder] 无效的 cron 表达式: ${expr},定时任务未启动`) + return + } + cron.schedule(expr, runOnce, { timezone: 'Asia/Shanghai' }) + console.log(`[reminder] 定时任务已注册: ${expr} (Asia/Shanghai)`) +} + +module.exports = { start, runOnce } diff --git a/server/src/wx.js b/server/src/wx.js new file mode 100644 index 0000000..fb77a4f --- /dev/null +++ b/server/src/wx.js @@ -0,0 +1,57 @@ +const axios = require('axios') + +const APPID = process.env.WX_APPID +const APPSECRET = process.env.WX_APPSECRET + +// access_token 内存缓存(微信全局唯一,2 小时有效) +let _token = { value: null, expireAt: 0 } + +async function getAccessToken() { + const now = Date.now() + // 提前 5 分钟刷新,避免临界过期 + if (_token.value && _token.expireAt - now > 5 * 60 * 1000) { + return _token.value + } + + const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}` + const { data } = await axios.get(url, { timeout: 8000 }) + if (!data.access_token) { + throw new Error(`获取 access_token 失败: ${JSON.stringify(data)}`) + } + _token = { + value: data.access_token, + expireAt: now + (data.expires_in - 300) * 1000 + } + return _token.value +} + +// code2session:拿 openid(小程序登录) +async function code2session(code) { + const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${APPSECRET}&js_code=${code}&grant_type=authorization_code` + const { data } = await axios.get(url, { timeout: 8000 }) + if (data.errcode) { + throw new Error(`code2session 失败: ${JSON.stringify(data)}`) + } + return { openid: data.openid, unionid: data.unionid || null, session_key: data.session_key } +} + +// 发送订阅消息 +async function sendSubscribeMessage({ touser, page, data, templateId, miniprogramState = 'formal' }) { + const token = await getAccessToken() + const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}` + const body = { + touser, + template_id: templateId, + page, + miniprogram_state: miniprogramState, + lang: 'zh_CN', + data + } + const { data: res } = await axios.post(url, body, { timeout: 8000 }) + if (res.errcode !== 0) { + throw new Error(`发送订阅消息失败: ${JSON.stringify(res)}`) + } + return res +} + +module.exports = { getAccessToken, code2session, sendSubscribeMessage } diff --git a/utils/api.js b/utils/api.js new file mode 100644 index 0000000..6c26ac0 --- /dev/null +++ b/utils/api.js @@ -0,0 +1,70 @@ +/** + * 后端接口请求封装 + * + * 开发版(开发者工具)走本地 docker,体验版/正式版走线上 HTTPS。 + */ + +const DEV_BASE_URL = 'http://localhost:3000' +const PROD_BASE_URL = 'https://wxserver.ymxixi.space' + +function resolveBaseUrl() { + try { + const env = wx.getAccountInfoSync().miniProgram.envVersion + return env === 'develop' ? DEV_BASE_URL : PROD_BASE_URL + } catch (e) { + return DEV_BASE_URL + } +} + +const BASE_URL = resolveBaseUrl() + +function request({ url, method = 'POST', data = {}, withOpenid = true }) { + return new Promise((resolve, reject) => { + const header = { 'Content-Type': 'application/json' } + if (withOpenid) { + const openid = wx.getStorageSync('openid') + if (openid) header['x-openid'] = openid + } + wx.request({ + url: BASE_URL + url, + method, + header, + data, + timeout: 10000, + success: (res) => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve(res.data) + else reject(new Error(`HTTP ${res.statusCode}`)) + }, + fail: (err) => reject(err) + }) + }) +} + +// 登录:用 wx.login 拿 code 换 openid +function login() { + return new Promise((resolve, reject) => { + wx.login({ + success: async (loginRes) => { + try { + const res = await request({ + url: '/api/login', + data: { code: loginRes.code }, + withOpenid: false + }) + if (!res.success) return reject(new Error(res.error || '登录失败')) + resolve(res) + } catch (err) { + reject(err) + } + }, + fail: reject + }) + }) +} + +// 纪念日操作(兼容原云函数 action 协议) +function anniversary(action, data) { + return request({ url: '/api/anniversary', data: { action, data } }) +} + +module.exports = { request, login, anniversary, BASE_URL } diff --git a/utils/constants.js b/utils/constants.js new file mode 100644 index 0000000..f2f6606 --- /dev/null +++ b/utils/constants.js @@ -0,0 +1,47 @@ +/** + * 公共常量 + */ + +const ANNIVERSARY_TYPES = { + BIRTHDAY: 'birthday', + LUNAR_BIRTHDAY: 'lunar_birthday', + WEDDING: 'wedding', + ENGAGEMENT: 'engagement', + OTHER: 'other' +} + +const TYPE_NAMES = { + birthday: '公历生日', + lunar_birthday: '农历生日', + wedding: '结婚纪念日', + engagement: '订婚纪念日', + other: '其他纪念日' +} + +const TYPE_ICONS = { + birthday: '🎂', + lunar_birthday: '🌙', + wedding: '💍', + engagement: '💕', + other: '📅' +} + +const IMPORTANCE_TEXTS = { + high: '非常重要', + medium: '重要', + low: '一般' +} + +const IMPORTANCE_COLORS = { + high: '#ff5722', + medium: '#ff9800', + low: '#07c160' +} + +module.exports = { + ANNIVERSARY_TYPES, + TYPE_NAMES, + TYPE_ICONS, + IMPORTANCE_TEXTS, + IMPORTANCE_COLORS +} diff --git a/utils/date.js b/utils/date.js index 87c8e0f..f246e4f 100644 --- a/utils/date.js +++ b/utils/date.js @@ -103,9 +103,28 @@ function parseDate(dateString) { return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])) } +/** + * 计算指定月日的下一次发生日期(今年或明年) + * @param {Number} month - 月份 (1-12) + * @param {Number} day - 日 (1-31) + * @returns {{ date: Date, daysUntil: Number }} + */ +function getNextOccurrence(month, day) { + const today = new Date() + const currentYear = today.getFullYear() + let date = new Date(currentYear, month - 1, day) + let daysUntil = getDaysUntil(date) + if (daysUntil < 0) { + date = new Date(currentYear + 1, month - 1, day) + daysUntil = getDaysUntil(date) + } + return { date, daysUntil } +} + module.exports = { formatDate, getDaysUntil, + getNextOccurrence, isToday, isPast, isUpcoming, diff --git a/utils/lunar.js b/utils/lunar.js index e405b63..c189129 100644 --- a/utils/lunar.js +++ b/utils/lunar.js @@ -1,122 +1,203 @@ /** * 农历日期转换工具 - * 使用简化版的农历计算方法 + * 基于寿星万年历算法(1900-2100年) */ -// 1900-2100年的农历数据 +// 农历数据:每条数据记录该年的月份大小和闰月信息 +// 格式:高4位=闰月大小(0无闰/1闰大月), 中4位=闰月位置, 低12位=12个月大小(1大/0小) const lunarInfo = [ - 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, - 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, - 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, - 0x06566, 0x0d4a0, 0x0ea50, 0x16a95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950 + 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 ] -/** - * 农历年份 - */ -const lunarMonths = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'] -const lunarDays = ['初', '十', '廿', '三'] -const lunarDayNames = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'] +// 1900年1月31日是农历正月初一(庚子年) +const BASE_DATE = new Date(1900, 0, 31) + +const LUNAR_MONTHS = ['正','二','三','四','五','六','七','八','九','十','冬','腊'] +const LUNAR_DAYS_TENS = ['初','十','廿','三'] +const LUNAR_DAYS_UNITS = ['一','二','三','四','五','六','七','八','九','十'] /** - * 获取农历月份名称 + * 获取农历某年的总天数 */ -function getLunarMonthName(month) { - return lunarMonths[month - 1] +function _lunarYearDays(year) { + let total = 348 + for (let i = 0x8000; i > 0x8; i >>= 1) { + total += (lunarInfo[year - 1900] & i) ? 1 : 0 + } + return total + _leapDays(year) } /** - * 获取农历日期名称 + * 获取农历某年闰月的天数 */ -function getLunarDayName(day) { - if (day === 10) return '初十' - if (day === 20) return '二十' - if (day === 30) return '三十' - - const tens = Math.floor(day / 10) - const units = day % 10 - - let name = lunarDays[tens] - if (units > 0) { - name += lunarDayNames[units - 1] +function _leapDays(year) { + if (_leapMonth(year)) { + return (lunarInfo[year - 1900] & 0x10000) ? 30 : 29 } - - return name + return 0 } /** - * 计算农历指定年的总天数 + * 获取农历某年的闰月月份,0表示无闰月 */ -function getLunarYearDays(year) { - let total = 0 - for (let i = 0; i < 13; i++) { - const days = lunarInfo[year] & (0x8000 >> i) ? 30 : 29 - total += days - } - return total +function _leapMonth(year) { + return lunarInfo[year - 1900] & 0xf +} + +/** + * 获取农历某年某月的天数 + */ +function _monthDays(year, month) { + return (lunarInfo[year - 1900] & (0x10000 >> month)) ? 30 : 29 } /** * 公历转农历 - * @param {Date} solarDate - 公历日期对象 - * @returns {Object} 农历日期对象 {year, month, day, lunarText} + * @param {Date} solarDate + * @returns {{ year, month, day, isLeap, lunarText }} */ function solarToLunar(solarDate) { - const year = solarDate.getFullYear() - const month = solarDate.getMonth() + 1 - const day = solarDate.getDate() - - // 这是一个简化版本,实际计算需要完整的农历转换算法 - // 这里返回一个示例结果 - // 实际项目中建议使用成熟的农历库如 lunar-javascript - - const offset = Math.floor((year - 1900) / 4) + (year - 1900) % 4 - - // 返回格式化的农历 + const date = new Date(solarDate.getFullYear(), solarDate.getMonth(), solarDate.getDate()) + let offset = Math.round((date - BASE_DATE) / 86400000) + + let lunarYear, lunarMonth, lunarDay + let isLeap = false + + for (lunarYear = 1900; lunarYear < 2100 && offset > 0; lunarYear++) { + const days = _lunarYearDays(lunarYear) + offset -= days + } + if (offset < 0) { + offset += _lunarYearDays(--lunarYear) + } + + const leapM = _leapMonth(lunarYear) + let isLeapYear = false + + for (lunarMonth = 1; lunarMonth < 13 && offset > 0; lunarMonth++) { + if (leapM > 0 && lunarMonth === leapM + 1 && !isLeapYear) { + --lunarMonth + isLeapYear = true + const leapDays = _leapDays(lunarYear) + offset -= leapDays + } else { + offset -= _monthDays(lunarYear, lunarMonth) + } + if (isLeapYear && lunarMonth === leapM + 1) isLeapYear = false + } + + if (offset === 0 && leapM > 0 && lunarMonth === leapM + 1) { + if (isLeapYear) { + isLeapYear = false + } else { + isLeapYear = true + --lunarMonth + } + } + if (offset < 0) { + offset += isLeapYear ? _leapDays(lunarYear) : _monthDays(lunarYear, lunarMonth) + if (isLeapYear) isLeapYear = false + else --lunarMonth + } + + lunarDay = offset + 1 + isLeap = isLeapYear + return { - year: year, - month: month, - day: day, - lunarText: `${getLunarMonthName(month)}月${getLunarDayName(day)}` + year: lunarYear, + month: lunarMonth, + day: lunarDay, + isLeap, + lunarText: `${isLeap ? '闰' : ''}${getLunarMonthName(lunarMonth)}月${getLunarDayName(lunarDay)}` } } /** - * 农历转公历 - * @param {Number} year - 农历年份 - * @param {Number} month - 农历月份 - * @param {Number} day - 农历日期 - * @returns {Date} 公历日期对象 + * 农历转公历(指定年份) + * @param {Number} lunarYear + * @param {Number} lunarMonth + * @param {Number} lunarDay + * @param {Boolean} isLeap 是否闰月 + * @returns {Date} */ -function lunarToSolar(year, month, day) { - // 简化版本,实际需要完整的农历转换算法 - // 这里返回当前日期作为示例 - // 实际项目中建议使用成熟的农历库 - - const now = new Date() - return new Date(now.getFullYear(), month - 1, day) +function lunarToSolar(lunarYear, lunarMonth, lunarDay, isLeap) { + let offset = 0 + + for (let y = 1900; y < lunarYear; y++) { + offset += _lunarYearDays(y) + } + + const leapM = _leapMonth(lunarYear) + let hasLeap = false + + for (let m = 1; m < lunarMonth; m++) { + if (leapM > 0 && m === leapM && !hasLeap) { + offset += _leapDays(lunarYear) + hasLeap = true + } + offset += _monthDays(lunarYear, m) + } + + if (isLeap && lunarMonth === leapM) { + offset += _monthDays(lunarYear, lunarMonth) + } + + offset += lunarDay - 1 + + const result = new Date(BASE_DATE.getTime() + offset * 86400000) + return result } /** - * 获取农历年份的下一个相同农历日期的公历日期 - * @param {Number} lunarYear - 农历年份 - * @param {Number} lunarMonth - 农历月份 - * @param {Number} lunarDay - 农历日期 - * @returns {Date} 下一次该农历日期的公历日期 + * 获取指定农历月日在今年或明年的公历日期 */ -function getNextLunarDate(lunarYear, lunarMonth, lunarDay) { - // 简化版本,返回明年同一天的日期 +function getNextLunarDate(lunarMonth, lunarDay) { const today = new Date() - const nextYear = today.getFullYear() + 1 - return new Date(nextYear, today.getMonth(), today.getDate()) + const currentYear = today.getFullYear() + const lunarToday = solarToLunar(today) + + // 尝试今年 + let candidate = lunarToSolar(lunarToday.year, lunarMonth, lunarDay, false) + if (candidate >= today) return candidate + + // 明年 + return lunarToSolar(lunarToday.year + 1, lunarMonth, lunarDay, false) +} + +function getLunarMonthName(month) { + return LUNAR_MONTHS[month - 1] || '' +} + +function getLunarDayName(day) { + if (day === 10) return '初十' + if (day === 20) return '二十' + if (day === 30) return '三十' + const tens = Math.floor(day / 10) + const units = day % 10 + return LUNAR_DAYS_TENS[tens] + (units > 0 ? LUNAR_DAYS_UNITS[units - 1] : '') } -/** - * 格式化农历显示文本 - */ function formatLunarText(lunarDate) { - const lunar = solarToLunar(new Date(lunarDate.year, lunarDate.month - 1, lunarDate.day)) - return lunar.lunarText + return solarToLunar(new Date(lunarDate.year, lunarDate.month - 1, lunarDate.day)).lunarText } module.exports = { @@ -127,4 +208,3 @@ module.exports = { getLunarMonthName, getLunarDayName } - diff --git a/utils/storage.js b/utils/storage.js index 2f88c61..833a038 100644 --- a/utils/storage.js +++ b/utils/storage.js @@ -1,17 +1,29 @@ /** - * 本地存储管理工具 + * 本地存储管理工具(含缓存层) */ +// 内存缓存 +const _cache = { + persons: null, + anniversaries: null +} + +function _invalidate() { + _cache.persons = null + _cache.anniversaries = null +} + /** * 存储人员数据 */ function savePersons(persons) { try { wx.setStorageSync('persons', persons) - return true + _cache.persons = persons + return { success: true } } catch (e) { console.error('保存人员数据失败', e) - return false + return { success: false, error: e.message } } } @@ -19,8 +31,10 @@ function savePersons(persons) { * 获取人员数据 */ function getPersons() { + if (_cache.persons !== null) return _cache.persons try { - return wx.getStorageSync('persons') || [] + _cache.persons = wx.getStorageSync('persons') || [] + return _cache.persons } catch (e) { console.error('获取人员数据失败', e) return [] @@ -31,8 +45,7 @@ function getPersons() { * 根据ID获取人员 */ function getPersonById(id) { - const persons = getPersons() - return persons.find(p => p.id === id) || null + return getPersons().find(p => p.id === id) || null } /** @@ -47,7 +60,8 @@ function addPerson(person) { updateTime: new Date().getTime() } persons.push(newPerson) - return savePersons(persons) ? newPerson : null + const result = savePersons(persons) + return result.success ? newPerson : null } /** @@ -57,12 +71,8 @@ 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) + persons[index] = { ...persons[index], ...updates, updateTime: new Date().getTime() } + return savePersons(persons).success } return false } @@ -72,8 +82,7 @@ function updatePerson(id, updates) { */ function deletePerson(id) { const persons = getPersons() - const filtered = persons.filter(p => p.id !== id) - return savePersons(filtered) + return savePersons(persons.filter(p => p.id !== id)).success } /** @@ -82,10 +91,11 @@ function deletePerson(id) { function saveAnniversaries(anniversaries) { try { wx.setStorageSync('anniversaries', anniversaries) - return true + _cache.anniversaries = anniversaries + return { success: true } } catch (e) { console.error('保存纪念日数据失败', e) - return false + return { success: false, error: e.message } } } @@ -93,8 +103,10 @@ function saveAnniversaries(anniversaries) { * 获取纪念日数据 */ function getAnniversaries() { + if (_cache.anniversaries !== null) return _cache.anniversaries try { - return wx.getStorageSync('anniversaries') || [] + _cache.anniversaries = wx.getStorageSync('anniversaries') || [] + return _cache.anniversaries } catch (e) { console.error('获取纪念日数据失败', e) return [] @@ -105,8 +117,7 @@ function getAnniversaries() { * 根据人员ID获取纪念日 */ function getAnniversariesByPersonId(personId) { - const anniversaries = getAnniversaries() - return anniversaries.filter(a => a.personId === personId) + return getAnniversaries().filter(a => a.personId === personId) } /** @@ -121,7 +132,8 @@ function addAnniversary(anniversary) { updateTime: new Date().getTime() } anniversaries.push(newAnniversary) - return saveAnniversaries(anniversaries) ? newAnniversary : null + const result = saveAnniversaries(anniversaries) + return result.success ? newAnniversary : null } /** @@ -131,12 +143,8 @@ function updateAnniversary(id, updates) { const anniversaries = getAnniversaries() const index = anniversaries.findIndex(a => a.id === id) if (index !== -1) { - anniversaries[index] = { - ...anniversaries[index], - ...updates, - updateTime: new Date().getTime() - } - return saveAnniversaries(anniversaries) + anniversaries[index] = { ...anniversaries[index], ...updates, updateTime: new Date().getTime() } + return saveAnniversaries(anniversaries).success } return false } @@ -146,8 +154,26 @@ function updateAnniversary(id, updates) { */ function deleteAnniversary(id) { const anniversaries = getAnniversaries() - const filtered = anniversaries.filter(a => a.id !== id) - return saveAnniversaries(filtered) + return saveAnniversaries(anniversaries.filter(a => a.id !== id)).success +} + +/** + * 原子性删除人员及其所有纪念日 + */ +function deletePersonWithAnniversaries(personId) { + try { + const persons = getPersons().filter(p => p.id !== personId) + const anniversaries = getAnniversaries().filter(a => a.personId !== personId) + wx.setStorageSync('persons', persons) + wx.setStorageSync('anniversaries', anniversaries) + _cache.persons = persons + _cache.anniversaries = anniversaries + return true + } catch (e) { + console.error('删除人员及纪念日失败', e) + _invalidate() // 缓存可能不一致,强制失效 + return false + } } /** @@ -169,20 +195,46 @@ function exportData() { } /** - * 导入数据 + * 验证人员数据结构 + */ +function _validatePerson(p) { + return p && typeof p === 'object' && typeof p.id === 'string' && typeof p.name === 'string' +} + +/** + * 验证纪念日数据结构 + */ +function _validateAnniversary(a) { + return a && typeof a === 'object' && typeof a.id === 'string' && + typeof a.personId === 'string' && + typeof a.solarMonth === 'number' && typeof a.solarDay === 'number' +} + +/** + * 导入数据(含验证) */ function importData(data) { try { - if (data.persons && Array.isArray(data.persons)) { - wx.setStorageSync('persons', data.persons) + if (!data || typeof data !== 'object') { + return { success: false, error: '无效的数据格式' } } - if (data.anniversaries && Array.isArray(data.anniversaries)) { - wx.setStorageSync('anniversaries', data.anniversaries) + if (!Array.isArray(data.persons) || !Array.isArray(data.anniversaries)) { + return { success: false, error: 'persons 和 anniversaries 必须是数组' } } - return true + if (!data.persons.every(_validatePerson)) { + return { success: false, error: '人员数据格式不正确' } + } + if (!data.anniversaries.every(_validateAnniversary)) { + return { success: false, error: '纪念日数据格式不正确' } + } + wx.setStorageSync('persons', data.persons) + wx.setStorageSync('anniversaries', data.anniversaries) + _cache.persons = data.persons + _cache.anniversaries = data.anniversaries + return { success: true } } catch (e) { console.error('导入数据失败', e) - return false + return { success: false, error: e.message } } } @@ -192,6 +244,7 @@ function importData(data) { function clearAllData() { try { wx.clearStorageSync() + _invalidate() return true } catch (e) { console.error('清空数据失败', e) @@ -207,7 +260,8 @@ module.exports = { addPerson, updatePerson, deletePerson, - + deletePersonWithAnniversaries, + // 纪念日相关 saveAnniversaries, getAnniversaries, @@ -215,10 +269,9 @@ module.exports = { addAnniversary, updateAnniversary, deleteAnniversary, - + // 工具函数 exportData, importData, clearAllData } - diff --git a/上线部署指南.md b/上线部署指南.md new file mode 100644 index 0000000..4398d28 --- /dev/null +++ b/上线部署指南.md @@ -0,0 +1,398 @@ +# 生日提醒小程序 - 上线部署指南 + +## 📋 上线流程概览 + +``` +注册账号 → 获取AppID → 配置项目 → 上传代码 → 提交审核 → 发布上线 + ↓ ↓ ↓ ↓ ↓ ↓ + 30分钟 即时 5分钟 5分钟 1-7天 即时 +``` + +--- + +## 🎯 第一步:注册小程序账号 + +### 1.1 前往微信公众平台注册 + +**网址**:https://mp.weixin.qq.com/ + +**注册步骤**: +1. 点击"立即注册" +2. 选择"小程序"类型 +3. 填写账号信息(邮箱、密码) +4. 激活邮箱(点击邮件中的链接) +5. 选择主体类型: + - **个人**:无需营业执照(推荐个人项目) + - **企业**:需要营业执照和对公账户 + +### 1.2 填写主体信息 + +**个人类型需要**: +- 管理员身份证信息 +- 管理员微信扫码认证(需要绑定银行卡) + +**企业类型需要**: +- 营业执照 +- 对公账户信息 +- 管理员身份信息 +- 300元认证费(可选,认证后可开通支付等高级功能) + +### 1.3 完成注册 + +注册成功后,你将获得: +- ✅ **AppID**(重要!) +- ✅ 小程序管理后台访问权限 + +--- + +## 🔑 第二步:获取并配置 AppID + +### 2.1 获取 AppID + +1. 登录 [微信公众平台](https://mp.weixin.qq.com/) +2. 进入"开发" → "开发管理" → "开发设置" +3. 找到 **AppID**(格式:wx1234567890abcdef) +4. 复制保存 + +### 2.2 配置项目 AppID + +修改 `project.config.json`: + +```json +{ + "appid": "wx1234567890abcdef", // 替换为你的真实 AppID + "projectname": "birthday-reminder" +} +``` + +### 2.3 添加开发者 + +如果有其他人协作开发: + +1. 进入小程序后台 +2. "管理" → "成员管理" → "项目成员" +3. 添加开发者(需要对方微信号) +4. 设置权限(开发者/体验者) + +--- + +## ⚙️ 第三步:完善小程序信息 + +### 3.1 基本信息设置 + +进入"设置" → "基本设置": + +- **小程序名称**:生日提醒(或自定义) +- **小程序头像**:上传一个图标(推荐 256x256 像素) +- **小程序介绍**: + ``` + 一款轻量级的生日提醒工具,支持公历和农历生日, + 智能提醒让你不错过任何重要日子。 + ``` +- **服务类目**: + - 工具 → 效率 → 备忘录/提醒 + - 或 生活服务 → 综合生活服务 + +### 3.2 隐私保护设置 + +进入"设置" → "隐私与安全设置": + +1. **用户隐私保护指引**: +``` +本小程序会收集和使用以下信息: +1. 您添加的人员姓名、生日等信息 +2. 所有数据仅存储在您的设备本地 +3. 我们不会上传或分享您的任何数据 + +数据使用目的: +- 提供生日提醒服务 +- 展示纪念日列表和日历 + +您的权利: +- 可随时删除数据 +- 可导出备份数据 +``` + +2. **涉及的接口**: + - 选择图片(上传头像) + - 剪贴板(导入/导出数据) + +--- + +## 📤 第四步:上传代码 + +### 4.1 准备上传 + +在微信开发者工具中: + +1. 点击工具栏的"上传"按钮 +2. 填写版本号和备注: + ``` + 版本号:1.0.0 + 备注:首次发布,包含核心功能 + ``` +3. 点击"上传" + +### 4.2 版本号规范 + +建议使用语义化版本: +- **1.0.0**:首次发布 +- **1.0.1**:修复bug +- **1.1.0**:新增功能 +- **2.0.0**:重大更新 + +### 4.3 上传成功 + +上传成功后,代码会出现在小程序后台的"开发管理" → "开发版本"中。 + +--- + +## 👥 第五步:体验测试 + +### 5.1 添加体验者 + +上传后先让其他人体验测试: + +1. 进入小程序后台 +2. "管理" → "成员管理" → "项目成员" +3. 添加"体验者"(最多50个) +4. 体验者扫码即可使用 + +### 5.2 体验版测试 + +- 体验者可以完整测试所有功能 +- 发现问题及时修复 +- 重新上传新版本 + +--- + +## 🚀 第六步:提交审核 + +### 6.1 配置审核信息 + +进入"开发管理" → "版本管理" → "开发版本" → "提交审核" + +**填写信息**: + +1. **功能页面**: +``` +页面类型:首页 +页面功能:查看人员生日列表 + +页面类型:添加页面 +页面功能:添加人员信息和纪念日 + +页面类型:日历页面 +页面功能:查看纪念日日历 +``` + +2. **可提供的测试账号**: +``` +无需测试账号(或提供一个演示账号) +``` + +3. **版本描述**: +``` +生日提醒小程序首次发布版本,主要功能: +1. 添加和管理人员信息 +2. 支持公历和农历生日 +3. 多种纪念日类型(生日、结婚纪念日等) +4. 智能提醒功能 +5. 日历视图展示 +6. 数据导入导出 +``` + +### 6.2 审核时长 + +- **正常情况**:1-3个工作日 +- **节假日**:可能延长至7天 +- **首次审核**:可能会慢一些 + +### 6.3 审核要点 + +**容易被拒的原因**: +- ❌ 功能描述不清晰 +- ❌ 涉及未申请的类目 +- ❌ 缺少隐私政策 +- ❌ 测试账号无法登录 +- ❌ 页面空白或无内容 + +**避免被拒**: +- ✅ 填写清晰的功能描述 +- ✅ 选择正确的服务类目 +- ✅ 完善隐私保护指引 +- ✅ 确保所有功能正常 +- ✅ 添加测试数据供审核 + +--- + +## ✅ 第七步:发布上线 + +### 7.1 审核通过 + +收到审核通过通知后: + +1. 进入"开发管理" → "版本管理" +2. 找到"审核通过"的版本 +3. 点击"发布"按钮 +4. 确认发布 + +### 7.2 发布后 + +- ⏱️ **生效时间**:5-10分钟内全网生效 +- 🔍 **搜索**:用户可以在微信搜索到你的小程序 +- 📱 **分享**:可以分享给好友使用 + +### 7.3 获取小程序码 + +发布后可以生成小程序码: + +1. 进入"设置" → "基本设置" → "小程序码" +2. 下载并分享给用户 + +--- + +## 📊 第八步:运营推广 + +### 8.1 数据监控 + +进入"统计" → "数据分析",查看: +- 访问次数 +- 用户数量 +- 留存率 +- 使用时长 + +### 8.2 推广方式 + +1. **朋友圈分享**:生成小程序卡片 +2. **微信群分享**:发送给感兴趣的群 +3. **公众号关联**:如有公众号可关联推广 +4. **附近的小程序**:设置门店位置 + +--- + +## 🔄 后续更新流程 + +### 9.1 版本更新 + +发现bug或要添加新功能: + +1. 修改代码 +2. 测试通过 +3. 上传新版本(版本号递增) +4. 提交审核 +5. 审核通过后发布 + +### 9.2 紧急修复 + +如果发现严重bug: + +1. 快速修复代码 +2. 使用"加急审核"(有次数限制) +3. 通过后立即发布 + +--- + +## 💰 费用说明 + +### 个人类型(推荐) + +- ✅ **注册**:免费 +- ✅ **认证**:不需要(个人类型无需认证) +- ✅ **上线**:免费 +- ✅ **使用**:免费 + +**限制**: +- ❌ 无法开通支付功能 +- ❌ 部分高级接口受限 +- ✅ 本项目的所有功能都不受影响 + +### 企业类型 + +- ✅ **注册**:免费 +- 💰 **认证**:300元/年(可选) +- ✅ **上线**:免费 + +**优势**: +- ✅ 可开通微信支付 +- ✅ 可使用所有高级接口 +- ✅ 可关联公众号 + +--- + +## 📝 常见问题 + +### Q1: 个人可以注册小程序吗? +**A**: 可以!个人注册完全免费,本项目的所有功能都支持。 + +### Q2: 多久可以上线? +**A**: +- 注册:30分钟 +- 配置:10分钟 +- 上传:5分钟 +- 审核:1-7天 +- **总计**:最快1天,一般3-5天 + +### Q3: 审核不通过怎么办? +**A**: +1. 查看拒绝原因 +2. 根据要求修改 +3. 重新提交审核 +4. 审核次数不限 + +### Q4: 小程序可以改名吗? +**A**: +- 个人类型:一年可改2次 +- 企业已认证:一年可改2次 +- 需要在后台申请 + +### Q5: 需要服务器吗? +**A**: 本项目不需要!所有数据存储在用户本地。 + +### Q6: 可以商用吗? +**A**: 可以,建议使用企业类型注册。 + +--- + +## 🎯 快速检查清单 + +上线前检查: + +- [ ] 已注册小程序账号 +- [ ] 已获取并配置 AppID +- [ ] 已完善小程序信息(名称、头像、介绍) +- [ ] 已设置服务类目 +- [ ] 已配置隐私保护指引 +- [ ] 已在开发工具中测试通过 +- [ ] 已添加体验者测试 +- [ ] 代码已上传到小程序后台 +- [ ] 已准备审核资料(功能描述、截图) +- [ ] 已提交审核 + +--- + +## 📞 需要帮助? + +### 官方资源 + +- **开发文档**:https://developers.weixin.qq.com/miniprogram/dev/framework/ +- **社区论坛**:https://developers.weixin.qq.com/community/minigame +- **客服**:登录小程序后台,右上角"客服" + +### 关键联系方式 + +- **小程序客服**:400-901-0500(工作日 9:00-18:00) + +--- + +## 🎉 恭喜! + +按照本指南操作,你的小程序很快就能上线让全网用户使用了! + +**预计时间线**: +- 今天:注册账号、配置信息、上传代码 +- 第2-3天:等待审核 +- 第3-5天:审核通过,正式上线 + +加油!🚀 + diff --git a/云开发功能说明.md b/云开发功能说明.md new file mode 100644 index 0000000..0e4c204 --- /dev/null +++ b/云开发功能说明.md @@ -0,0 +1,414 @@ +# 生日提醒小程序 - 云开发功能说明 + +## 📁 项目结构 + +``` +生日提醒小程序/ +├── cloudfunctions/ # 云函数目录 +│ ├── login/ # 获取用户openid +│ │ ├── index.js +│ │ ├── config.json +│ │ └── package.json +│ ├── syncAnniversary/ # 同步纪念日数据 +│ │ ├── index.js +│ │ ├── config.json +│ │ └── package.json +│ └── sendReminder/ # 发送提醒消息 +│ ├── index.js +│ ├── config.json # 包含定时触发器配置 +│ └── package.json +├── pages/ # 小程序页面 +├── utils/ # 工具函数 +├── app.js # 应用入口(已添加云开发初始化) +├── app.json # 应用配置 +└── project.config.json # 项目配置 +``` + +--- + +## 🔧 核心功能实现 + +### 1. 云开发初始化(app.js) + +```javascript +// 应用启动时初始化云开发 +wx.cloud.init({ + env: 'cloudbase-1gk3x0ia3a6b1f80', + traceUser: true +}) + +// 自动获取用户openid +await wx.cloud.callFunction({ name: 'login' }) +``` + +**作用**: +- 连接到你的云开发环境 +- 获取用户唯一标识(openid) +- 为后续功能提供基础 + +--- + +### 2. 订阅消息授权(add-anniversary.js) + +```javascript +// 用户添加纪念日并开启提醒时 +wx.requestSubscribeMessage({ + tmplIds: ['6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw'] +}) +``` + +**流程**: +1. 用户添加纪念日 +2. 开启"是否提醒"开关 +3. 点击保存 +4. 弹出订阅授权弹窗 +5. 用户点击"允许" +6. 数据保存到本地和云端 + +**效果**: +- ✅ 用户授权后才能收到推送 +- ✅ 一次授权,长期有效 +- ✅ 用户可随时管理订阅 + +--- + +### 3. 数据云端同步(syncAnniversary 云函数) + +**支持的操作**: + +#### 添加纪念日 +```javascript +wx.cloud.callFunction({ + name: 'syncAnniversary', + data: { + action: 'add', + data: { ...anniversaryData } + } +}) +``` + +#### 更新纪念日 +```javascript +wx.cloud.callFunction({ + name: 'syncAnniversary', + data: { + action: 'update', + data: { id, ...anniversaryData } + } +}) +``` + +#### 删除纪念日 +```javascript +wx.cloud.callFunction({ + name: 'syncAnniversary', + data: { + action: 'delete', + data: { id } + } +}) +``` + +**数据结构**: +```javascript +{ + _id: "cloud-generated-id", + _openid: "user-openid", // 自动添加 + personId: "person-id", + personName: "张三", + type: "birthday", + solarYear: 1990, + solarMonth: 5, + solarDay: 15, + remindEnabled: true, + remindDays: 7, + remark: "记得买蛋糕", + createTime: Date, + updateTime: Date +} +``` + +--- + +### 4. 定时发送提醒(sendReminder 云函数) + +**执行时间**:每天上午9:00 + +**执行流程**: + +``` +1. 查询所有启用提醒的纪念日 + ↓ +2. 遍历每个纪念日 + ↓ +3. 计算距离纪念日还有多少天 + ↓ +4. 判断是否需要提醒 + - 当天生日:发送 + - 提前N天:发送(N=用户设置的remindDays) + ↓ +5. 检查今天是否已发送 + ↓ +6. 调用微信API发送订阅消息 + ↓ +7. 记录提醒日志 +``` + +**提醒内容示例**: + +``` +称呼:张三 +临近日期:还有3天 +生日日期:2025年10月30日 +温馨提示:别忘了准备一份礼物哦! +``` + +**日志记录**: +```javascript +{ + anniversaryId: "anniversary-id", + personName: "张三", + typeName: "公历生日", + daysUntil: 3, + sendDate: Date, + status: "success" // 或 "failed" +} +``` + +--- + +## 🔄 数据流程图 + +### 用户添加纪念日 + +``` +用户填写表单 + ↓ +开启提醒开关 + ↓ +点击保存 + ↓ +[请求订阅授权] ← 用户授权 + ↓ +保存到本地存储 + ↓ +同步到云数据库 ← 包含 openid + ↓ +完成 +``` + +### 定时提醒流程 + +``` +每天 9:00 + ↓ +云函数被触发 + ↓ +查询云数据库 + ↓ +筛选需要提醒的纪念日 + ↓ +循环处理每条记录 + ↓ +计算剩余天数 + ↓ +满足条件? + ├─ 是 → 发送订阅消息 → 记录日志 + └─ 否 → 跳过 + ↓ +完成 +``` + +--- + +## 🎯 关键技术点 + +### 1. openid 的作用 + +```javascript +// openid 是用户在你的小程序中的唯一标识 +{ + "_openid": "oABCD1234567890", // 自动添加到数据库记录 + "personName": "张三", + ... +} +``` + +**用途**: +- 区分不同用户的数据 +- 发送订阅消息时指定接收者 +- 保证数据安全(用户只能访问自己的数据) + +### 2. 订阅消息模板 + +**你的模板信息**: +``` +模板ID: 6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw + +字段映射: +- name1.DATA → 称呼(人员姓名) +- thing2.DATA → 临近日期(还有X天) +- thing6.DATA → 生日日期(2025年10月30日) +- thing5.DATA → 温馨提示(备注或默认文案) +``` + +### 3. 定时触发器 + +**Cron 表达式**:`0 0 9 * * * *` + +``` +秒 分 时 日 月 星期 年 +0 0 9 * * * * + +含义:每天上午9点0分0秒执行 +``` + +**修改提醒时间**: +``` +每天早上8点: 0 0 8 * * * * +每天晚上8点: 0 0 20 * * * * +每天早晚两次: 0 0 8,20 * * * * +``` + +--- + +## 📊 数据库设计 + +### anniversaries(纪念日表) + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| _id | String | 主键(自动生成) | "5f1e..." | +| _openid | String | 用户openid(自动) | "oABC..." | +| personId | String | 人员ID | "id_123..." | +| personName | String | 人员姓名 | "张三" | +| type | String | 类型 | "birthday" | +| solarMonth | Number | 公历月份 | 10 | +| solarDay | Number | 公历日期 | 30 | +| remindEnabled | Boolean | 是否提醒 | true | +| remindDays | Number | 提前天数 | 7 | +| remark | String | 备注 | "买蛋糕" | + +### remind_logs(提醒日志表) + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| _id | String | 主键(自动生成) | "5f1e..." | +| anniversaryId | String | 纪念日ID | "5f1e..." | +| personName | String | 人员姓名 | "张三" | +| daysUntil | Number | 剩余天数 | 3 | +| sendDate | Date | 发送时间 | Date对象 | +| status | String | 状态 | "success" | + +--- + +## 🚨 注意事项 + +### 1. 订阅消息限制 + +- ✅ 用户主动触发授权(不能自动弹出) +- ✅ 一次授权一个模板 +- ✅ 用户可以随时取消订阅 +- ❌ 不能发送营销类消息 + +### 2. 云函数限制 + +**免费额度**(每天): +- 调用次数:10万次 +- 运行时间:40万GBs +- 出流量:5GB + +**对于个人用户,完全够用!** + +### 3. 数据库限制 + +**免费额度**: +- 容量:5GB +- 读操作:5万次/天 +- 写操作:3万次/天 + +**估算**: +- 100个用户,每人10条纪念日 = 1000条记录 +- 每条约1KB,总共约1MB +- 完全在免费额度内! + +--- + +## 🔍 调试技巧 + +### 1. 查看云函数日志 + +``` +云开发控制台 → 云函数 → 选择函数 → 日志 +``` + +**可以看到**: +- 函数执行时间 +- 返回结果 +- 错误信息 +- console.log 输出 + +### 2. 测试云函数 + +``` +云开发控制台 → 云函数 → 选择函数 → 测试 + +输入测试参数:{} +点击"运行测试" +``` + +### 3. 查看数据库 + +``` +云开发控制台 → 数据库 → 选择集合 + +可以: +- 查看所有记录 +- 编辑数据 +- 删除数据 +- 导出数据 +``` + +### 4. 模拟定时触发 + +```javascript +// 临时修改 Cron 表达式为每分钟执行 +"0 */1 * * * * *" + +// 测试完成后改回 +"0 0 9 * * * *" +``` + +--- + +## 💡 优化建议 + +### 1. 添加提醒历史页面 + +显示用户收到的所有提醒记录。 + +### 2. 支持多个提醒时间 + +比如提前7天、3天、当天各提醒一次。 + +### 3. 自定义提醒文案 + +让用户可以自定义温馨提示内容。 + +### 4. 提醒统计 + +显示总共发送了多少条提醒,成功率等。 + +--- + +## 🎉 总结 + +现在你的小程序具备了完整的云端能力: + +✅ **数据云端存储** - 永不丢失 +✅ **自动推送提醒** - 不再遗忘 +✅ **多设备同步** - 随处访问 +✅ **安全可靠** - openid隔离 + +**开始享受智能生日提醒服务吧!** 🎂 + diff --git a/云开发部署指南.md b/云开发部署指南.md new file mode 100644 index 0000000..41baeda --- /dev/null +++ b/云开发部署指南.md @@ -0,0 +1,349 @@ +# 生日提醒小程序 - 云开发部署指南 + +## 📋 部署步骤概览 + +``` +上传云函数 → 创建数据库集合 → 配置定时触发器 → 测试功能 +``` + +--- + +## 🚀 第一步:上传云函数 + +### 1.1 打开云开发控制台 + +1. 在微信开发者工具中,点击顶部菜单 **"云开发"** +2. 进入云开发控制台 +3. 确认环境ID为:`cloudbase-1gk3x0ia3a6b1f80` + +### 1.2 上传云函数 + +需要上传3个云函数: + +#### 1. 上传 login 云函数 + +```bash +右键点击 cloudfunctions/login +→ 选择 "上传并部署:云端安装依赖" +→ 等待上传完成(约30秒) +``` + +#### 2. 上传 syncAnniversary 云函数 + +```bash +右键点击 cloudfunctions/syncAnniversary +→ 选择 "上传并部署:云端安装依赖" +→ 等待上传完成(约30秒) +``` + +#### 3. 上传 sendReminder 云函数 + +```bash +右键点击 cloudfunctions/sendReminder +→ 选择 "上传并部署:云端安装依赖" +→ 等待上传完成(约30秒) +``` + +### 1.3 验证上传结果 + +在云开发控制台 → 云函数,应该看到3个云函数: +- ✅ login +- ✅ syncAnniversary +- ✅ sendReminder + +--- + +## 🗄️ 第二步:创建数据库集合 + +### 2.1 进入数据库管理 + +云开发控制台 → 数据库 + +### 2.2 创建集合 + +需要创建2个集合: + +#### 1. anniversaries(纪念日数据) + +``` +点击 "添加集合" +集合名称:anniversaries +权限设置:仅创建者可读写 +``` + +**字段说明**: +```javascript +{ + _id: String, // 自动生成 + _openid: String, // 自动生成(用户openid) + personId: String, // 人员ID + personName: String, // 人员姓名 + type: String, // 类型:birthday/lunar_birthday/wedding等 + customTypeName: String,// 自定义类型名称 + isLunar: Boolean, // 是否农历 + solarYear: Number, // 公历年份 + solarMonth: Number, // 公历月份 + solarDay: Number, // 公历日期 + importance: String, // 重要程度 + remindEnabled: Boolean,// 是否开启提醒 + remindDays: Number, // 提前提醒天数 + remark: String, // 备注 + createTime: Date, // 创建时间 + updateTime: Date // 更新时间 +} +``` + +#### 2. remind_logs(提醒日志) + +``` +点击 "添加集合" +集合名称:remind_logs +权限设置:仅创建者可读写 +``` + +**字段说明**: +```javascript +{ + _id: String, // 自动生成 + anniversaryId: String, // 纪念日ID + personName: String, // 人员姓名 + typeName: String, // 类型名称 + daysUntil: Number, // 剩余天数 + sendDate: Date, // 发送日期 + status: String, // 状态:success/failed + error: String // 错误信息(如果失败) +} +``` + +--- + +## ⏰ 第三步:配置定时触发器 + +### 3.1 进入触发器管理 + +云开发控制台 → 云函数 → sendReminder → 触发器 + +### 3.2 创建触发器 + +**方法1:自动配置(推荐)** + +触发器已在 `config.json` 中配置,上传云函数时会自动创建: + +```json +{ + "name": "dailyReminder", + "type": "timer", + "config": "0 0 9 * * * *" +} +``` + +**方法2:手动创建** + +如果没有自动创建,手动添加: + +``` +点击 "新建触发器" +触发器名称:dailyReminder +触发周期:自定义 +Cron表达式:0 0 9 * * * * + +说明:每天上午9点执行 +``` + +### 3.3 Cron 表达式说明 + +格式:`秒 分 时 日 月 星期 年` + +常用配置: +``` +0 0 9 * * * * - 每天上午9:00 +0 0 8,20 * * * * - 每天上午8:00和晚上8:00 +0 0 9 * * 1-5 * - 工作日上午9:00 +``` + +--- + +## 🧪 第四步:测试功能 + +### 4.1 测试订阅消息授权 + +1. 在小程序中添加一个纪念日 +2. 开启"是否提醒"开关 +3. 点击保存 +4. 应该弹出订阅消息授权弹窗 +5. 点击"允许" + +### 4.2 测试云函数 + +在云开发控制台 → 云函数 → sendReminder: + +``` +点击 "测试" +输入参数:{} +点击 "运行测试" +``` + +查看返回结果: +```json +{ + "success": true, + "total": 1, + "successCount": 1, + "failCount": 0 +} +``` + +### 4.3 查看数据库 + +在云开发控制台 → 数据库: + +1. **anniversaries 集合**:应该看到刚才添加的纪念日数据 +2. **remind_logs 集合**:如果触发了提醒,应该有日志记录 + +### 4.4 测试订阅消息接收 + +**方法1:手动触发(立即测试)** + +在云开发控制台 → 云函数 → sendReminder → 运行测试 + +**方法2:等待定时触发** + +等到第二天上午9:00,自动触发提醒 + +**方法3:修改定时器(快速测试)** + +临时修改 Cron 表达式为:`0 */1 * * * * *`(每分钟执行一次) +测试完成后改回:`0 0 9 * * * *` + +--- + +## ⚙️ 第五步:项目配置 + +### 5.1 修改 project.config.json + +确认云函数根目录配置: + +```json +{ + "cloudfunctionRoot": "cloudfunctions/", + "miniprogramRoot": "./", + "appid": "你的AppID", + "projectname": "生日提醒小程序" +} +``` + +### 5.2 确认 app.json + +无需额外配置,订阅消息权限已自动启用。 + +--- + +## 🎯 功能验证清单 + +完成以下验证,确保功能正常: + +- [ ] 云开发初始化成功(控制台无报错) +- [ ] 能够获取到用户 openid +- [ ] 添加纪念日时弹出订阅授权 +- [ ] 纪念日数据同步到云端(查看数据库) +- [ ] 手动执行 sendReminder 云函数成功 +- [ ] 定时触发器配置成功 +- [ ] 能够收到订阅消息推送 + +--- + +## 🐛 常见问题 + +### Q1: 云函数上传失败? + +**A**: 检查网络连接,或手动安装依赖: + +```bash +cd cloudfunctions/login +npm install +``` + +然后选择 "上传并部署:不安装依赖" + +### Q2: 获取不到 openid? + +**A**: +1. 确认已上传 login 云函数 +2. 在真机上测试(开发者工具可能有限制) +3. 查看控制台日志 + +### Q3: 订阅消息没有弹出? + +**A**: +1. 确认在真机上测试(开发者工具不支持) +2. 检查模板ID是否正确 +3. 每个用户需要单独授权 + +### Q4: 定时触发器不执行? + +**A**: +1. 确认 Cron 表达式正确 +2. 查看云函数日志 +3. 手动测试云函数是否正常 + +### Q5: 收不到订阅消息? + +**A**: +1. 确认用户已授权订阅 +2. 检查数据库中有 openid +3. 查看 remind_logs 是否有记录 +4. 检查云函数权限配置 + +--- + +## 📊 监控和维护 + +### 1. 查看云函数日志 + +云开发控制台 → 云函数 → 选择函数 → 日志 + +### 2. 查看提醒统计 + +查询 remind_logs 集合: + +```javascript +// 查看今天的提醒记录 +db.collection('remind_logs') + .where({ + sendDate: _.gte(new Date(new Date().toDateString())) + }) + .get() +``` + +### 3. 数据备份 + +定期导出数据库数据: + +云开发控制台 → 数据库 → 导出 + +--- + +## 💰 费用说明 + +### 免费额度(每天) + +- 云函数调用:10万次 +- 数据库读操作:5万次 +- 数据库写操作:3万次 +- 云存储容量:5GB + +对于个人使用,完全免费! + +--- + +## 🎉 部署完成! + +现在你的生日提醒小程序已经具备了完整的推送提醒功能: + +✅ 用户添加纪念日时自动请求订阅授权 +✅ 数据自动同步到云端 +✅ 每天上午9点自动检查并发送提醒 +✅ 提醒记录自动保存到数据库 + +**享受你的智能生日提醒吧!** 🎂 + diff --git a/快速开始.md b/快速开始.md index ce7123c..9f5f1bd 100644 --- a/快速开始.md +++ b/快速开始.md @@ -43,15 +43,14 @@ 3. 选择本项目目录 4. **AppID填写**:`touristappid`(使用测试号) -### 2. 添加图标(可选) +### 2. 图标说明 -tabBar需要图标,可以在 `images/` 目录添加: -- `home.png` / `home-active.png` -- `calendar.png` / `calendar-active.png` -- `settings.png` / `settings-active.png` -- `default-avatar.png` +✅ tabBar 已配置为 emoji 图标模式,无需添加图片文件: +- 🏠 首页 +- 📅 日历 +- ⚙️ 设置 -如果不添加图标,tabBar可能显示异常,但功能不受影响。 +如需添加自定义图标,可以在 `images/` 目录添加图片并修改 `app.json` 配置。 ### 3. 运行项目 diff --git a/部署检查清单.md b/部署检查清单.md new file mode 100644 index 0000000..3bcbcf3 --- /dev/null +++ b/部署检查清单.md @@ -0,0 +1,308 @@ +# 生日提醒小程序 - 云开发部署检查清单 + +## ✅ 部署前检查 + +### 1. 环境准备 +- [ ] 已注册微信小程序账号 +- [ ] 已获取 AppID +- [ ] 已开通云开发(环境ID:cloudbase-1gk3x0ia3a6b1f80) +- [ ] 已创建订阅消息模板(模板ID:6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw) + +### 2. 代码检查 +- [ ] app.js 已初始化云开发 +- [ ] app.js 中的环境ID正确 +- [ ] add-anniversary.js 已添加订阅授权代码 +- [ ] add-anniversary.js 中的模板ID正确 + +--- + +## 📤 上传云函数 + +### 1. login 云函数 +``` +□ 右键点击 cloudfunctions/login +□ 选择 "上传并部署:云端安装依赖" +□ 等待上传成功 +□ 在云开发控制台确认已上传 +``` + +### 2. syncAnniversary 云函数 +``` +□ 右键点击 cloudfunctions/syncAnniversary +□ 选择 "上传并部署:云端安装依赖" +□ 等待上传成功 +□ 在云开发控制台确认已上传 +``` + +### 3. sendReminder 云函数 +``` +□ 右键点击 cloudfunctions/sendReminder +□ 选择 "上传并部署:云端安装依赖" +□ 等待上传成功 +□ 在云开发控制台确认已上传 +□ 确认定时触发器已自动创建 +``` + +**验证**: +云开发控制台 → 云函数,应该看到3个云函数: +- ✅ login +- ✅ syncAnniversary +- ✅ sendReminder + +--- + +## 🗄️ 创建数据库集合 + +### 1. anniversaries 集合 +``` +□ 进入云开发控制台 → 数据库 +□ 点击"添加集合" +□ 集合名称:anniversaries +□ 权限设置:仅创建者可读写 +□ 点击"确定" +``` + +### 2. remind_logs 集合 +``` +□ 点击"添加集合" +□ 集合名称:remind_logs +□ 权限设置:仅创建者可读写 +□ 点击"确定" +``` + +**验证**: +云开发控制台 → 数据库,应该看到2个集合: +- ✅ anniversaries +- ✅ remind_logs + +--- + +## ⏰ 配置定时触发器 + +### 检查触发器 +``` +□ 进入云开发控制台 → 云函数 → sendReminder +□ 点击"触发器"标签 +□ 确认存在 dailyReminder 触发器 +□ 确认 Cron 表达式为:0 0 9 * * * * +``` + +### 如果没有触发器,手动创建 +``` +□ 点击"新建触发器" +□ 触发器名称:dailyReminder +□ 触发周期:自定义 +□ Cron表达式:0 0 9 * * * * +□ 点击"确定" +``` + +--- + +## 🧪 功能测试 + +### 1. 测试 openid 获取 +``` +□ 打开小程序 +□ 查看控制台日志 +□ 应该看到:"获取openid成功: oABC..." +□ 打开"存储" → 查看 Storage +□ 应该看到 openid 已保存 +``` + +### 2. 测试订阅消息授权 +``` +□ 在真机上打开小程序(必须真机) +□ 点击"添加纪念日" +□ 填写表单,开启"是否提醒" +□ 点击"保存" +□ 应该弹出订阅消息授权弹窗 +□ 点击"允许" +□ 应该显示"添加成功" +``` + +### 3. 测试数据同步 +``` +□ 进入云开发控制台 → 数据库 → anniversaries +□ 应该看到刚才添加的纪念日记录 +□ 记录中包含 _openid 字段 +□ 数据完整无误 +``` + +### 4. 测试云函数 +``` +□ 进入云开发控制台 → 云函数 → sendReminder +□ 点击"测试" +□ 输入参数:{} +□ 点击"运行测试" +□ 查看返回结果,应该包含: + { + "success": true, + "total": 数字, + "successCount": 数字, + "failCount": 数字 + } +``` + +### 5. 测试订阅消息发送 + +**方法1:等待定时触发** +``` +□ 添加一个明天的生日,提前1天提醒 +□ 等到明天上午9:00 +□ 应该收到订阅消息推送 +``` + +**方法2:手动触发(推荐)** +``` +□ 添加一个今天的生日 +□ 手动执行 sendReminder 云函数 +□ 应该立即收到订阅消息推送 +``` + +**方法3:修改定时器(快速测试)** +``` +□ 临时修改 Cron 为:0 */1 * * * * * +□ 等待1分钟 +□ 应该收到订阅消息推送 +□ 测试完成后改回:0 0 9 * * * * +``` + +--- + +## 🔍 验证清单 + +### 控制台日志验证 +``` +□ 看到:"云开发初始化成功" +□ 看到:"获取openid成功" +□ 看到:"订阅消息授权结果" +□ 看到:"云端同步成功" +``` + +### 数据库验证 +``` +□ anniversaries 集合有数据 +□ 数据包含 _openid 字段 +□ remindEnabled 为 true +□ personName 字段正确 +``` + +### 提醒日志验证 +``` +□ remind_logs 集合有记录 +□ status 为 "success" +□ sendDate 为今天 +□ personName 正确 +``` + +### 订阅消息验证 +``` +□ 收到了微信服务通知 +□ 消息内容正确 +□ 称呼、日期、提示都显示正确 +□ 点击消息能跳转到小程序 +``` + +--- + +## 🐛 故障排查 + +### 问题1:收不到订阅消息 + +**检查步骤**: +``` +□ 确认用户已授权订阅 +□ 确认 openid 已保存 +□ 查看 sendReminder 云函数日志 +□ 查看 remind_logs 是否有 failed 记录 +□ 确认模板ID正确 +□ 确认云函数权限包含 subscribeMessage.send +``` + +### 问题2:云函数执行失败 + +**检查步骤**: +``` +□ 查看云函数日志,查找错误信息 +□ 确认依赖已安装(查看 node_modules) +□ 确认 config.json 配置正确 +□ 尝试重新上传云函数 +``` + +### 问题3:数据没有同步到云端 + +**检查步骤**: +``` +□ 确认 openid 已获取 +□ 查看控制台是否有错误 +□ 确认 syncAnniversary 云函数已上传 +□ 手动测试 syncAnniversary 云函数 +``` + +### 问题4:定时触发器不工作 + +**检查步骤**: +``` +□ 确认触发器已创建 +□ 确认 Cron 表达式正确 +□ 查看云函数日志,确认有定时调用记录 +□ 尝试手动触发云函数验证功能 +``` + +--- + +## 📊 性能监控 + +### 每日检查(可选) +``` +□ 查看云函数调用次数 +□ 查看订阅消息发送成功率 +□ 查看 remind_logs 记录 +□ 查看是否有异常日志 +``` + +### 每周检查(可选) +``` +□ 导出数据库备份 +□ 清理过期的 remind_logs(可选) +□ 查看云开发资源使用情况 +``` + +--- + +## ✅ 部署完成确认 + +全部完成以下项目即表示部署成功: + +- [ ] 3个云函数全部上传成功 +- [ ] 2个数据库集合创建成功 +- [ ] 定时触发器配置成功 +- [ ] 能够获取用户 openid +- [ ] 订阅消息授权正常弹出 +- [ ] 数据能够同步到云端 +- [ ] 手动执行云函数成功 +- [ ] 能够收到订阅消息推送 +- [ ] 提醒日志正常记录 + +--- + +## 🎉 恭喜部署成功! + +你的生日提醒小程序现在具备: +- ✅ 云端数据存储 +- ✅ 自动推送提醒 +- ✅ 多设备同步 +- ✅ 安全可靠 + +**开始使用吧!** 🎂 + +--- + +## 📞 需要帮助? + +如果遇到问题: +1. 查看《云开发部署指南.md》 +2. 查看《云开发功能说明.md》 +3. 查看云开发控制台的错误日志 +4. 联系技术支持 +