系统总体设计
本系统采用微信小程序作为前端载体,后端完全基于微信云开发(WeChat Cloud Base)架构,实现了“Serverless”无服务器模式。
- 前端:微信小程序原生框架(WXML, WXSS, JavaScript/TypeScript)。
- 后端逻辑:云函数(Node.js环境),处理核心业务逻辑、权限校验及数据聚合。
- 数据库:云数据库(JSON文档型,类似MongoDB),存储用户、活动、打卡等数据。
- 存储:云存储,用于存放活动图片、打卡照片及用户头像。
- 优势:无需域名备案、无需运维服务器、自动弹性扩容、内网链路安全防DDoS攻击。
功能模块划分
用户端:
活动模块:活动列表展示、详情查看、在线报名、现场签到、活动评价。 打卡模块:打卡项目浏览、每日打卡(图文)、打卡排行、我的打卡记录。 个人中心:我的报名、消息通知、后台管理入口(管理员专用)。
管理端(内置于小程序):
活动管理:发布活动、审核报名、导出数据、活动核销。
打卡管理:配置打卡规则、审核打卡记录、导出报表。
系统管理:管理员权限设置、基础数据配置。
数据库设计
系统主要包含五个核心实体:用户 (User)、活动 (Activity)、报名记录 (Signup)、打卡项目 (CheckinProject)、打卡记录 (CheckinRecord)。
实体关系描述:
用户与活动是多对多关系,通过报名记录关联。 用户与打卡项目是多对多关系,通过打卡记录关联。 活动由用户(组织者)发布。
| 字段名 | 类型 | 说明 |
|---|---|---|
_id | String | 自动生成,主键 |
title | String | 活动标题 |
description | String | 活动详细介绍 |
location | String | 活动地点 |
start_time | Date | 活动开始时间 |
end_time | Date | 活动结束时间 |
max_count | Number | 人数上限 |
current_count | Number | 当前报名人数 |
organizer_id | String | 组织者OpenID |
status | String | 状态:'ongoing', 'ended', 'cancelled' |
form_config | Array | 自定义报名表单配置(如是否需要身份证) |
create_time | Date | 创建时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
_id | String | 主键 |
activity_id | String | 关联活动ID |
user_id | String | 用户OpenID |
user_info | Object | 快照:昵称、头像 |
form_data | Object | 用户填写的自定义表单数据 |
status | String | 状态:'pending'(待审核), 'pass'(通过), 'signed'(已签到), 'absent'(缺席) |
signup_time | Date | 报名时间 |
checkin_time | Date | 签到时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
_id | String | 主键 |
project_id | String | 关联打卡项目ID |
user_id | String | 用户OpenID |
content | String | 打卡文字心得 |
images | Array | 图片URL列表 |
record_date | Date | 打卡日期(精确到天) |
points | Number | 获得积分 |
is_verified | Boolean | 是否已通过审核 |
关键业务流程设计
flowchart TD A[用户浏览活动列表] --> B{活动是否已满/结束?} B -- 是 --> C[显示不可报名] B -- 否 --> D[进入详情页并填写表单] D --> E[提交报名请求] E --> F{云函数: 检查库存与资格} F -- 失败 --> G[返回失败提示] F -- 成功 --> H[写入signups集合, 更新活动人数] H --> I[报名成功通知]
subgraph 线下活动现场
J[组织者打开管理端] --> K[扫描用户二维码/输入手机号]
K --> L[云函数: 验证报名状态]
L -- 无效 --> M[提示未报名]
L -- 有效 --> N[更新signups状态为'signed']
N --> O[签到成功]
end
系统实现与核心代码
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
const db = cloud.database();
const _ = db.command;
// 报名云函数入口
exports.main = async (event, context) => {
const { activityId, formData, userId } = event;
try {
// 1. 开启事务 (确保数据一致性)
const transaction = await db.startTransaction();
// 2. 查询活动剩余名额
const activityRes = await transaction.collection('activities').doc(activityId).get();
const activity = activityRes.data;
if (activity.currentCount >= activity.maxCount) {
throw new Error('活动人数已满');
}
// 3. 检查是否重复报名
const existRes = await transaction.collection('signups').where({
activity_id: activityId,
user_id: userId
}).get();
if (existRes.data.length > 0) {
throw new Error('您已报名该活动');
}
// 4. 插入报名记录 & 更新活动人数 (原子操作)
await transaction.collection('signups').add({
data: {
activity_id: activityId,
user_id: userId,
form_data: formData,
status: 'pass', // 默认自动通过,也可设为pending待审核
signup_time: db.serverDate()
}
});
await transaction.collection('activities').doc(activityId).update({
data: {
current_count: _.inc(1)
}
});
// 5. 提交事务
await transaction.commit();
return { success: true, msg: '报名成功' };
} catch (err) {
// 异常回滚
console.error(err);
return { success: false, msg: err.message };
}
};
const app = getApp();
Page({
data: {
content: '',
images: [],
projectId: ''
},
// 选择图片
onChooseImage() {
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
this.setData({ images: res.tempFiles.map(item => item.tempFilePath) });
}
});
},
// 提交打卡
async onSubmit() {
if (!this.data.content) return wx.showToast({ title: '请填写心得', icon: 'none' });
wx.showLoading({ title: '提交中...' });
try {
// 调用云函数处理图片和入库
const result = await wx.cloud.callFunction({
name: 'mcloud',
data: {
route: 'checkin/submit', // 路由分发
projectId: this.data.projectId,
content: this.data.content,
images: this.data.images // 云函数内会处理上传至云存储
}
});
if (result.result.success) {
wx.showToast({ title: '打卡成功', icon: 'success' });
setTimeout(() => wx.navigateBack(), 1500);
} else {
wx.showToast({ title: result.result.msg, icon: 'none' });
}
} catch (err) {
wx.showToast({ title: '网络错误', icon: 'none' });
} finally {
wx.hideLoading();
}
}
});
// 伪代码逻辑
async function exportSignupData(activityId) {
// 1. 获取所有报名记录
const list = await db.collection('signups')
.where({ activity_id: activityId })
.orderBy('signup_time', 'asc')
.get();
// 2. 数据格式化 (扁平化处理,方便Excel生成)
const rows = list.data.map(item => ({
姓名: item.form_data.name,
手机: item.form_data.phone,
报名时间: formatTime(item.signup_time),
状态: transformStatus(item.status)
}));
// 3. 生成CSV或Excel文件流 (需引入第三方库如node-xlsx或在云函数生成临时文件)
// 4. 返回文件临时下载链接
return { fileUrl: tempFileUrl };
}
部署与测试说明
环境ID配置:必须分别在 cloudfunctions/mcloud/config/config.js 和 miniprogram/setting/setting.js 中填入相同的云环境ID,否则会导致“找不到CLOUD_ID”错误。 云函数依赖:上传云函数时必须选择“上传并部署:云端安装依赖”,以确保 wx-server-sdk 等库正确安装。 权限设置:在云开发控制台数据库设置中,建议将集合权限设置为“所有用户可读写”配合云函数校验,或“仅创建者可读写”,具体业务逻辑应在云函数中通过 OPENID 进行二次鉴权。
| 测试模块 | 测试项 | 预期结果 |
|---|---|---|
| 活动报名 | 满员报名 | 提示“人数已满”,数据库人数不增加 |
| 活动报名 | 重复报名 | 提示“您已报名”,不产生重复记录 |
| 现场签到 | 非报名人员签到 | 提示“未找到报名记录” |
| 打卡功能 | 同日多次打卡 | 第二次提交被拦截,提示“今日已打卡” |
| 后台管理 | 导出数据 | 成功下载包含所有报名者信息的Excel/CSV文件 |
UI设计
后台管理系统设计