以下是 **「毛球手账」** 的完整技术实现方案,包含数据库 Schema、页面规划和云函数框架。
---
# 一、数据库 Schema(uniCloud DB Schema)
在 `uniCloud/database/` 目录下创建以下 `.schema.json` 文件。
## 1. users.schema.json
```json
{
"bsonType": "object",
"required": ["wx_openid", "createdAt"],
"permission": {
"read": "doc._id == auth.uid",
"create": "auth.uid != null",
"update": "doc._id == auth.uid",
"delete": false
},
"properties": {
"_id": {
"description": "用户ID,系统自动生成",
"bsonType": "string"
},
"wx_openid": {
"bsonType": "string",
"description": "微信openid",
"trim": "both"
},
"wx_unionid": {
"bsonType": "string",
"description": "微信unionid"
},
"nickName": {
"bsonType": "string",
"description": "微信昵称",
"trim": "both"
},
"avatarUrl": {
"bsonType": "string",
"description": "微信头像URL"
},
"phone": {
"bsonType": "string",
"description": "绑定手机号"
},
"membership": {
"bsonType": "object",
"properties": {
"type": {
"bsonType": "string",
"enum": ["free", "premium"],
"default": "free"
},
"expireAt": {
"bsonType": "timestamp",
"description": "会员过期时间"
}
}
},
"settings": {
"bsonType": "object",
"properties": {
"defaultPetId": {
"bsonType": "string",
"description": "默认选中宠物ID"
},
"reminderEnabled": {
"bsonType": "bool",
"default": true
}
}
},
"createdAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
},
"updatedAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
}
}
}
```
## 2. pets.schema.json
```json
{
"bsonType": "object",
"required": ["user_id", "name", "type", "createdAt"],
"permission": {
"read": "doc.user_id == auth.uid",
"create": "auth.uid != null",
"update": "doc.user_id == auth.uid",
"delete": "doc.user_id == auth.uid"
},
"properties": {
"_id": {
"bsonType": "string"
},
"user_id": {
"bsonType": "string",
"description": "关联用户ID",
"foreignKey": "users._id"
},
"name": {
"bsonType": "string",
"description": "宠物昵称",
"minLength": 1,
"maxLength": 10,
"trim": "both"
},
"type": {
"bsonType": "string",
"enum": ["cat", "dog", "other"],
"description": "宠物类型"
},
"breed": {
"bsonType": "string",
"description": "品种"
},
"gender": {
"bsonType": "string",
"enum": ["male", "female", "unknown"],
"default": "unknown"
},
"birthday": {
"bsonType": "timestamp",
"description": "出生日期"
},
"isBirthdayEstimated": {
"bsonType": "bool",
"default": false,
"description": "生日是否为估算"
},
"color": {
"bsonType": "string",
"description": "毛色/特征"
},
"avatar": {
"bsonType": "string",
"description": "宠物头像云存储URL"
},
"weight": {
"bsonType": "array",
"description": "体重历史记录",
"items": {
"bsonType": "object",
"properties": {
"value": {
"bsonType": "double",
"description": "体重kg"
},
"date": {
"bsonType": "timestamp"
},
"note": {
"bsonType": "string"
}
}
}
},
"neutered": {
"bsonType": "bool",
"default": false
},
"allergies": {
"bsonType": "array",
"items": {
"bsonType": "string"
}
},
"chronicDiseases": {
"bsonType": "array",
"items": {
"bsonType": "string"
}
},
"emergencyContact": {
"bsonType": "object",
"properties": {
"name": {
"bsonType": "string"
},
"phone": {
"bsonType": "string"
}
}
},
"status": {
"bsonType": "string",
"enum": ["active", "archived"],
"default": "active"
},
"createdAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
},
"updatedAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
}
}
}
```
## 3. health_records.schema.json
```json
{
"bsonType": "object",
"required": ["pet_id", "user_id", "type", "date", "createdAt"],
"permission": {
"read": "doc.user_id == auth.uid",
"create": "auth.uid != null",
"update": "doc.user_id == auth.uid",
"delete": "doc.user_id == auth.uid"
},
"properties": {
"_id": {
"bsonType": "string"
},
"pet_id": {
"bsonType": "string",
"foreignKey": "pets._id"
},
"user_id": {
"bsonType": "string",
"foreignKey": "users._id"
},
"type": {
"bsonType": "string",
"enum": ["vaccine", "deworm", "medical", "checkup", "other"],
"description": "记录类型:疫苗/驱虫/病历/体检/其他"
},
"title": {
"bsonType": "string",
"description": "记录标题"
},
"vaccineName": {
"bsonType": "string",
"description": "疫苗名称"
},
"batchNumber": {
"bsonType": "string",
"description": "疫苗批次号"
},
"dewormType": {
"bsonType": "string",
"enum": ["internal", "external", "both"],
"description": "驱虫类型"
},
"brand": {
"bsonType": "string",
"description": "品牌(驱虫药/疫苗品牌)"
},
"hospital": {
"bsonType": "string",
"description": "医院名称"
},
"doctor": {
"bsonType": "string"
},
"date": {
"bsonType": "timestamp",
"description": "发生日期"
},
"nextDate": {
"bsonType": "timestamp",
"description": "下次提醒日期"
},
"description": {
"bsonType": "string",
"description": "描述/主诉"
},
"diagnosis": {
"bsonType": "string",
"description": "诊断结果"
},
"medications": {
"bsonType": "array",
"items": {
"bsonType": "object",
"properties": {
"name": {
"bsonType": "string"
},
"dosage": {
"bsonType": "string"
},
"frequency": {
"bsonType": "string"
}
}
}
},
"images": {
"bsonType": "array",
"description": "云存储图片URL数组",
"items": {
"bsonType": "string"
}
},
"cost": {
"bsonType": "double",
"description": "费用"
},
"tags": {
"bsonType": "array",
"items": {
"bsonType": "string"
}
},
"createdAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
},
"updatedAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
}
}
}
```
## 4. schedules.schema.json
```json
{
"bsonType": "object",
"required": ["pet_id", "user_id", "title", "scheduledDate", "createdAt"],
"permission": {
"read": "doc.user_id == auth.uid",
"create": "auth.uid != null",
"update": "doc.user_id == auth.uid",
"delete": "doc.user_id == auth.uid"
},
"properties": {
"_id": {
"bsonType": "string"
},
"pet_id": {
"bsonType": "string",
"foreignKey": "pets._id"
},
"user_id": {
"bsonType": "string",
"foreignKey": "users._id"
},
"type": {
"bsonType": "string",
"enum": ["vaccine", "deworm", "checkup", "custom"],
"description": "日程类型"
},
"title": {
"bsonType": "string",
"description": "提醒标题"
},
"scheduledDate": {
"bsonType": "timestamp",
"description": "计划日期"
},
"frequency": {
"bsonType": "string",
"enum": ["once", "daily", "weekly", "monthly", "yearly"],
"default": "once"
},
"interval": {
"bsonType": "int",
"default": 1,
"description": "每N个frequency"
},
"status": {
"bsonType": "string",
"enum": ["pending", "completed", "overdue", "skipped"],
"default": "pending"
},
"reminderDays": {
"bsonType": "array",
"items": {
"bsonType": "int"
},
"default": [7, 3, 1],
"description": "提前提醒天数"
},
"relatedRecordId": {
"bsonType": "string",
"foreignKey": "health_records._id",
"description": "关联的健康记录"
},
"note": {
"bsonType": "string"
},
"lastRemindedAt": {
"bsonType": "timestamp",
"description": "上次提醒时间"
},
"createdAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
},
"updatedAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
}
}
}
```
## 5. hospitals.schema.json
```json
{
"bsonType": "object",
"required": ["name", "location", "createdAt"],
"permission": {
"read": true,
"create": "auth.uid != null",
"update": false,
"delete": false
},
"properties": {
"_id": {
"bsonType": "string"
},
"name": {
"bsonType": "string",
"description": "医院名称"
},
"address": {
"bsonType": "string"
},
"location": {
"bsonType": "object",
"description": "GeoJSON Point",
"properties": {
"type": {
"bsonType": "string",
"enum": ["Point"],
"default": "Point"
},
"coordinates": {
"bsonType": "array",
"items": {
"bsonType": "double"
},
"description": "[longitude, latitude]"
}
}
},
"phone": {
"bsonType": "string"
},
"hours": {
"bsonType": "string",
"description": "营业时间"
},
"is24Hour": {
"bsonType": "bool",
"default": false
},
"tags": {
"bsonType": "array",
"items": {
"bsonType": "string"
},
"description": "特色标签"
},
"rating": {
"bsonType": "double",
"minimum": 0,
"maximum": 5
},
"reviewCount": {
"bsonType": "int",
"default": 0
},
"verified": {
"bsonType": "bool",
"default": false,
"description": "是否官方认证"
},
"status": {
"bsonType": "string",
"enum": ["active", "pending", "rejected"],
"default": "pending"
},
"createdAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
}
}
}
```
## 6. articles.schema.json(养宠百科)
```json
{
"bsonType": "object",
"required": ["title", "category", "status", "createdAt"],
"permission": {
"read": true,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"bsonType": "string"
},
"title": {
"bsonType": "string"
},
"category": {
"bsonType": "string",
"enum": ["vaccine", "deworm", "disease", "nutrition", "behavior", "other"]
},
"petType": {
"bsonType": "string",
"enum": ["cat", "dog", "all"],
"default": "all"
},
"cover": {
"bsonType": "string"
},
"content": {
"bsonType": "string",
"description": "富文本内容"
},
"summary": {
"bsonType": "string"
},
"tags": {
"bsonType": "array",
"items": {
"bsonType": "string"
}
},
"viewCount": {
"bsonType": "int",
"default": 0
},
"status": {
"bsonType": "string",
"enum": ["published", "draft"],
"default": "draft"
},
"createdAt": {
"bsonType": "timestamp",
"defaultValue": {
"$env": "now"
}
}
}
}
```
---
# 二、核心页面前端组件结构(uni-app)
## 目录结构
```
uni-app-project/
├── pages/
│ ├── index/
│ │ └── index.vue # 首页(Dashboard)
│ ├── pet/
│ │ ├── list.vue # 宠物列表/切换
│ │ ├── detail.vue # 宠物档案详情
│ │ ├── edit.vue # 创建/编辑宠物
│ │ └── weight.vue # 体重记录
│ ├── record/
│ │ ├── list.vue # 健康记录列表(时间轴)
│ │ ├── detail.vue # 记录详情
│ │ ├── edit.vue # 创建/编辑记录
│ │ └── type-select.vue # 记录类型选择
│ ├── schedule/
│ │ ├── list.vue # 日程列表
│ │ └── edit.vue # 创建/编辑提醒
│ ├── discovery/
│ │ ├── index.vue # 发现页(医院+百科)
│ │ ├── hospital-list.vue # 附近医院列表
│ │ ├── hospital-detail.vue # 医院详情
│ │ ├── article-list.vue # 百科列表
│ │ └── article-detail.vue # 百科详情
│ └── mine/
│ ├── index.vue # 我的页面
│ ├── settings.vue # 设置
│ └── member.vue # 会员中心
├── components/
│ ├── pet-selector.vue # 顶部宠物切换器
│ ├── schedule-card.vue # 日程卡片
│ ├── record-timeline.vue # 时间轴组件
│ ├── weight-chart.vue # 体重曲线图(echarts/uni-chart)
│ ├── quick-action.vue # 底部快速操作栏
│ ├── empty-state.vue # 空状态插画
│ └── share-card.vue # 分享卡片生成
├── static/
│ └── images/ # 静态资源
├── uniCloud/
│ └── cloudfunctions/ # 云函数
└── App.vue
```
## 关键页面组件详解
### 1. 首页 `pages/index/index.vue`
```vue
📅 今日日程
查看全部
📊 {{ currentPet.name }} 健康概览
{{ latestWeight }}kg
最新体重
{{ nextVaccineDays }}天
下次疫苗
{{ nextDewormDays }}天
下次驱虫
```
### 2. 宠物切换器组件 `components/pet-selector.vue`
```vue
{{ pet.name }}
{{ calcAge(pet.birthday) }}
+
添加
```
### 3. 时间轴记录列表 `pages/record/list.vue`
```vue
{{ tab.label }}
{{ date }}
{{ record.description || record.hospital }}
```
---
# 三、云函数代码框架
在 `uniCloud/cloudfunctions/` 目录下创建以下云函数。
## 1. login(微信登录)
```javascript
// uniCloud/cloudfunctions/login/index.js
const db = uniCloud.database()
exports.main = async (event, context) => {
const { code, userInfo } = event
try {
// 1. 获取openid
const loginRes = await uniCloud.getWXContext()
const { OPENID, UNIONID } = loginRes
if (!OPENID) {
return { code: -1, message: '获取openid失败' }
}
// 2. 查询或创建用户
const userCollection = db.collection('users')
let user = await userCollection.where({ wx_openid: OPENID }).get()
let userId
if (user.data.length === 0) {
// 新用户
const createRes = await userCollection.add({
wx_openid: OPENID,
wx_unionid: UNIONID || '',
nickName: userInfo?.nickName || '',
avatarUrl: userInfo?.avatarUrl || '',
phone: '',
membership: {
type: 'free',
expireAt: null
},
settings: {
reminderEnabled: true
},
createdAt: Date.now(),
updatedAt: Date.now()
})
userId = createRes.id
} else {
// 更新用户信息
userId = user.data[0]._id
await userCollection.doc(userId).update({
nickName: userInfo?.nickName || user.data[0].nickName,
avatarUrl: userInfo?.avatarUrl || user.data[0].avatarUrl,
updatedAt: Date.now()
})
}
// 3. 生成自定义登录token(可选,用于后续请求鉴权)
const token = await uniCloud.createToken({
uid: userId
})
return {
code: 0,
message: '登录成功',
data: {
uid: userId,
token,
isNewUser: user.data.length === 0
}
}
} catch (err) {
return { code: -1, message: err.message }
}
}
```
## 2. petCrud(宠物增删改查)
```javascript
// uniCloud/cloudfunctions/petCrud/index.js
const db = uniCloud.database()
const $ = db.command.aggregate
exports.main = async (event, context) => {
const { action, data, petId } = event
const { uid } = context // 通过uni-id获取当前用户ID
const petCollection = db.collection('pets')
try {
switch (action) {
case 'create': {
const res = await petCollection.add({
...data,
user_id: uid,
createdAt: Date.now(),
updatedAt: Date.now()
})
return { code: 0, data: { id: res.id } }
}
case 'list': {
const res = await petCollection.where({
user_id: uid,
status: 'active'
}).orderBy('createdAt', 'desc').get()
return { code: 0, data: res.data }
}
case 'detail': {
const res = await petCollection.doc(petId).get()
if (res.data[0].user_id !== uid) {
return { code: 403, message: '无权限' }
}
return { code: 0, data: res.data[0] }
}
case 'update': {
const pet = await petCollection.doc(petId).get()
if (pet.data[0].user_id !== uid) {
return { code: 403, message: '无权限' }
}
await petCollection.doc(petId).update({
...data,
updatedAt: Date.now()
})
return { code: 0, message: '更新成功' }
}
case 'delete': {
// 软删除
await petCollection.doc(petId).update({
status: 'archived',
updatedAt: Date.now()
})
return { code: 0, message: '删除成功' }
}
case 'stats': {
// 获取宠物统计信息
const pet = await petCollection.doc(petId).get()
const petData = pet.data[0]
// 最新体重
const latestWeight = petData.weight?.length > 0
? petData.weight[petData.weight.length - 1].value
: 0
// 查询下次疫苗和驱虫(从schedules表)
const scheduleCollection = db.collection('schedules')
const now = Date.now()
const nextVaccine = await scheduleCollection.where({
pet_id: petId,
user_id: uid,
type: 'vaccine',
status: 'pending',
scheduledDate: db.command.gte(now)
}).orderBy('scheduledDate', 'asc').limit(1).get()
const nextDeworm = await scheduleCollection.where({
pet_id: petId,
user_id: uid,
type: 'deworm',
status: 'pending',
scheduledDate: db.command.gte(now)
}).orderBy('scheduledDate', 'asc').limit(1).get()
const calcDays = (date) => {
if (!date) return '--'
const days = Math.ceil((date - now) / (1000 * 60 * 60 * 24))
return days
}
return {
code: 0,
data: {
latestWeight,
nextVaccineDays: calcDays(nextVaccine.data[0]?.scheduledDate),
nextDewormDays: calcDays(nextDeworm.data[0]?.scheduledDate)
}
}
}
default:
return { code: -1, message: '未知操作' }
}
} catch (err) {
return { code: -1, message: err.message }
}
}
```
## 3. recordCrud(健康记录增删改查)
```javascript
// uniCloud/cloudfunctions/recordCrud/index.js
const db = uniCloud.database()
exports.main = async (event, context) => {
const { action, data, recordId, petId, type } = event
const { uid } = context
const recordCollection = db.collection('health_records')
const scheduleCollection = db.collection('schedules')
try {
switch (action) {
case 'create': {
const recordData = {
...data,
user_id: uid,
createdAt: Date.now(),
updatedAt: Date.now()
}
const res = await recordCollection.add(recordData)
// 如果有nextDate,自动创建日程提醒
if (data.nextDate) {
await scheduleCollection.add({
pet_id: data.pet_id,
user_id: uid,
type: data.type,
title: this.getScheduleTitle(data),
scheduledDate: data.nextDate,
status: 'pending',
relatedRecordId: res.id,
createdAt: Date.now()
})
}
return { code: 0, data: { id: res.id } }
}
case 'list': {
let where = { user_id: uid }
if (petId) where.pet_id = petId
if (type) where.type = type
const res = await recordCollection.where(where)
.orderBy('date', 'desc')
.get()
// 关联宠物名称
const petIds = [...new Set(res.data.map(r => r.pet_id))]
const petCollection = db.collection('pets')
const pets = await petCollection.where({
_id: db.command.in(petIds)
}).get()
const petMap = {}
pets.data.forEach(p => petMap[p._id] = p.name)
const records = res.data.map(r => ({
...r,
petName: petMap[r.pet_id] || '未知'
}))
return { code: 0, data: records }
}
case 'detail': {
const res = await recordCollection.doc(recordId).get()
if (res.data[0].user_id !== uid) {
return { code: 403, message: '无权限' }
}
return { code: 0, data: res.data[0] }
}
case 'update': {
const record = await recordCollection.doc(recordId).get()
if (record.data[0].user_id !== uid) {
return { code: 403, message: '无权限' }
}
await recordCollection.doc(recordId).update({
...data,
updatedAt: Date.now()
})
// 同步更新关联的schedule
if (data.nextDate && record.data[0].relatedRecordId) {
await scheduleCollection.where({
relatedRecordId: recordId
}).update({
scheduledDate: data.nextDate,
updatedAt: Date.now()
})
}
return { code: 0, message: '更新成功' }
}
case 'delete': {
const record = await recordCollection.doc(recordId).get()
if (record.data[0].user_id !== uid) {
return { code: 403, message: '无权限' }
}
await recordCollection.doc(recordId).remove()
// 删除关联schedule
if (record.data[0].relatedRecordId) {
await scheduleCollection.where({
relatedRecordId: recordId
}).remove()
}
return { code: 0, message: '删除成功' }
}
default:
return { code: -1, message: '未知操作' }
}
} catch (err) {
return { code: -1, message: err.message }
}
}
// 辅助方法
function getScheduleTitle(data) {
const titles = {
vaccine: `${data.vaccineName || '疫苗'}接种`,
deworm: `${data.brand || ''}驱虫`,
checkup: '体检',
medical: '复查'
}
return titles[data.type] || '健康提醒'
}
```
## 4. scheduleManager(日程管理)
```javascript
// uniCloud/cloudfunctions/scheduleManager/index.js
const db = uniCloud.database()
exports.main = async (event, context) => {
const { action, data, scheduleId, petId } = event
const { uid } = context
const scheduleCollection = db.collection('schedules')
const now = Date.now()
try {
switch (action) {
case 'create': {
const res = await scheduleCollection.add({
...data,
user_id: uid,
status: 'pending',
createdAt: now,
updatedAt: now
})
return { code: 0, data: { id: res.id } }
}
case 'list': {
let where = { user_id: uid }
if (petId) where.pet_id = petId
const res = await scheduleCollection.where(where)
.orderBy('scheduledDate', 'asc')
.get()
return { code: 0, data: res.data }
}
case 'today': {
// 获取今日及逾期的日程
const startOfDay = new Date().setHours(0, 0, 0, 0)
const endOfDay = new Date().setHours(23, 59, 59, 999)
const res = await scheduleCollection.where({
user_id: uid,
status: db.command.in(['pending', 'overdue']),
scheduledDate: db.command.lte(endOfDay)
}).orderBy('scheduledDate', 'asc').get()
return { code: 0, data: res.data }
}
case 'upcoming': {
// 获取未来7天的日程
const endOfWeek = now + 7 * 24 * 60 * 60 * 1000
const res = await scheduleCollection.where({
user_id: uid,
status: 'pending',
scheduledDate: db.command.gte(now).and(db.command.lte(endOfWeek))
}).orderBy('scheduledDate', 'asc').get()
return { code: 0, data: res.data }
}
case 'complete': {
await scheduleCollection.doc(scheduleId).update({
status: 'completed',
updatedAt: now
})
return { code: 0, message: '已完成' }
}
case 'postpone': {
const { days } = data
const schedule = await scheduleCollection.doc(scheduleId).get()
const newDate = schedule.data[0].scheduledDate + days * 24 * 60 * 60 * 1000
await scheduleCollection.doc(scheduleId).update({
scheduledDate: newDate,
status: 'pending',
updatedAt: now
})
return { code: 0, message: `已推迟${days}天` }
}
case 'skip': {
await scheduleCollection.doc(scheduleId).update({
status: 'skipped',
updatedAt: now
})
return { code: 0, message: '已忽略' }
}
case 'delete': {
await scheduleCollection.doc(scheduleId).remove()
return { code: 0, message: '删除成功' }
}
default:
return { code: -1, message: '未知操作' }
}
} catch (err) {
return { code: -1, message: err.message }
}
}
```
## 5. reminderPush(定时提醒触发器)
```javascript
// uniCloud/cloudfunctions/reminderPush/index.js
// 配置:在uniCloud后台设置定时触发器,每天上午9:00执行
const db = uniCloud.database()
exports.main = async (event, context) => {
const now = Date.now()
const today = new Date()
today.setHours(0, 0, 0, 0)
const tomorrow = today.getTime() + 24 * 60 * 60 * 1000
try {
const scheduleCollection = db.collection('schedules')
const userCollection = db.collection('users')
// 1. 标记逾期
await scheduleCollection.where({
status: 'pending',
scheduledDate: db.command.lt(today)
}).update({
status: 'overdue',
updatedAt: now
})
// 2. 查找今天需要提醒的日程
// 提醒逻辑:在 reminderDays 中的某一天,且今天就是提醒日
const schedules = await scheduleCollection.where({
status: db.command.in(['pending', 'overdue']),
scheduledDate: db.command.gte(today).and(db.command.lt(tomorrow + 7 * 24 * 60 * 60 * 1000))
}).get()
// 3. 按用户分组,批量发送订阅消息
const userSchedules = {}
schedules.data.forEach(s => {
if (!userSchedules[s.user_id]) {
userSchedules[s.user_id] = []
}
userSchedules[s.user_id].push(s)
})
for (const userId in userSchedules) {
const user = await userCollection.doc(userId).get()
const openid = user.data[0]?.wx_openid
if (!openid) continue
// 获取该用户今天需要提醒的日程
const todaySchedules = userSchedules[userId].filter(s => {
const scheduledDate = new Date(s.scheduledDate)
scheduledDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((scheduledDate - today) / (24 * 60 * 60 * 1000))
return s.reminderDays.includes(diffDays) && diffDays >= 0
})
if (todaySchedules.length === 0) continue
// 构建订阅消息(需先在微信公众平台申请模板)
const templateId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' // 你的模板ID
for (const schedule of todaySchedules) {
try {
await uniCloud.openapi.weixin.subscribeMessage.send({
touser: openid,
templateId: templateId,
page: `pages/schedule/list?petId=${schedule.pet_id}`,
data: {
thing1: { value: schedule.title }, // 事项
time2: { value: formatDate(schedule.scheduledDate) }, // 时间
thing3: { value: schedule.status === 'overdue' ? '已逾期,请尽快处理' : '即将到期' } // 备注
},
miniprogramState: 'formal'
})
// 更新最后提醒时间
await scheduleCollection.doc(schedule._id).update({
lastRemindedAt: now
})
} catch (err) {
console.error(`发送失败: ${userId}`, err)
}
}
}
return {
code: 0,
message: '提醒处理完成',
data: {
totalChecked: schedules.data.length,
notifiedUsers: Object.keys(userSchedules).length
}
}
} catch (err) {
return { code: -1, message: err.message }
}
}
function formatDate(timestamp) {
const d = new Date(timestamp)
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
}
```
**定时触发器配置**(在 `reminderPush` 云函数的 `package.json` 中):
```json
{
"name": "reminderPush",
"version": "1.0.0",
"description": "每日健康提醒推送",
"main": "index.js",
"extensions": {
"uni-cloud-jql": {}
},
"cloudfunction-config": {
"concurrency": 1,
"triggers": [
{
"name": "dailyReminder",
"type": "timer",
"config": "0 0 9 * * * *"
}
]
}
}
```
> 说明:`0 0 9 * * * *` 表示每天上午9:00执行(UTC+8 时区)。
## 6. nearbyHospitals(附近医院查询)
```javascript
// uniCloud/cloudfunctions/nearbyHospitals/index.js
const db = uniCloud.database()
exports.main = async (event, context) => {
const { longitude, latitude, maxDistance = 5000, limit = 20 } = event
// maxDistance 单位:米,默认5km
try {
const hospitalCollection = db.collection('hospitals')
const res = await hospitalCollection.where({
status: 'active',
location: db.command.geoNear({
geometry: db.Geo.Point(longitude, latitude),
maxDistance: maxDistance,
distanceField: 'distance'
})
}).limit(limit).get()
// 格式化距离
const hospitals = res.data.map(h => ({
...h,
distance: h.distance ? Math.round(h.distance) : null,
distanceText: h.distance
? (h.distance < 1000 ? `${Math.round(h.distance)}m` : `${(h.distance / 1000).toFixed(1)}km`)
: '未知'
}))
return {
code: 0,
data: hospitals
}
} catch (err) {
return { code: -1, message: err.message }
}
}
```
---
# 四、关键配置补充
## 1. 地理位置索引(必须)
在 uniCloud 控制台 → 数据库 → `hospitals` 集合 → 索引管理,创建:
```json
{
"location": "2dsphere"
}
```
## 2. manifest.json 小程序配置
```json
{
"mp-weixin": {
"appid": "你的小程序appid",
"setting": {
"urlCheck": false
},
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于查找附近宠物医院"
}
},
"requiredPrivateInfos": [
"getLocation"
]
}
}
```
## 3. 云存储目录规划
```
cloudstorage/
├── pet-avatars/ # 宠物头像
├── record-images/ # 病历/疫苗证照片
├── article-covers/ # 百科文章封面
└── share-cards/ # 生成的分享卡片
```
---
这套框架覆盖了 **MVP 阶段 90% 的核心功能**。建议你先实现 **login → petCrud → recordCrud → scheduleManager → reminderPush** 这个主线,跑通"创建宠物 → 记录疫苗 → 生成提醒 → 推送通知"的完整闭环,再逐步扩展发现页和会员功能。
需要我针对某个具体模块(如**微信支付开通会员**、**订阅消息授权引导**、**图片上传云存储**)继续展开吗?