1. 项目背景与需求分析
日常活动记录系统是现代生活中非常实用的工具,它能帮助我们追踪和统计各类日常活动的发生频率、持续时间和相关数据。基于Python和微信小程序的组合开发这样一套系统,可以充分利用两者的优势:Python强大的后端处理能力,以及微信小程序便捷的前端体验。
这类系统通常需要解决以下几个核心需求:
- 活动数据的采集与录入
- 数据的存储与管理
- 数据的可视化展示
- 多终端同步与分享
- 数据统计与分析
2. 技术选型与架构设计
2.1 前端技术选型
微信小程序作为前端是理想的选择,原因在于:
- 无需安装,即用即走
- 跨平台兼容性(iOS/Android)
- 丰富的API支持(位置、相机、存储等)
- 成熟的开发工具链
小程序端主要需要实现:
- 活动录入界面
- 活动列表展示
- 数据可视化图表
- 用户设置页面
2.2 后端技术选型
Python作为后端语言的优势:
- 丰富的数据处理库(Pandas, NumPy)
- 强大的Web框架选择(Flask, Django, FastAPI)
- 成熟的数据库连接方案
- 简洁高效的语法
推荐技术栈组合:
- Web框架:FastAPI(高性能,异步支持)
- 数据库:MySQL(关系型)或MongoDB(非关系型)
- ORM:SQLAlchemy或Tortoise-ORM
- 部署:Docker + Nginx
3. 数据库设计
3.1 核心数据表结构
用户表(users)
sql复制CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
openid VARCHAR(64) UNIQUE NOT NULL,
nickname VARCHAR(64),
avatar_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
活动类型表(activity_types)
sql复制CREATE TABLE activity_types (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
name VARCHAR(64) NOT NULL,
icon VARCHAR(64),
color VARCHAR(16),
FOREIGN KEY (user_id) REFERENCES users(id)
);
活动记录表(activity_records)
sql复制CREATE TABLE activity_records (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
type_id INT NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME,
duration INT COMMENT '单位:分钟',
notes TEXT,
location VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (type_id) REFERENCES activity_types(id)
);
3.2 数据关系设计
系统采用关系型数据库设计,主要关系包括:
- 用户与活动类型:一对多
- 用户与活动记录:一对多
- 活动类型与活动记录:一对多
4. 后端API设计与实现
4.1 用户认证模块
微信小程序使用wx.login获取code,后端通过code向微信服务器换取openid:
python复制from fastapi import APIRouter, Depends, HTTPException
import httpx
from pydantic import BaseModel
router = APIRouter()
class LoginRequest(BaseModel):
code: str
@router.post("/login")
async def wechat_login(request: LoginRequest):
appid = "你的小程序appid"
secret = "你的小程序secret"
url = f"https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={request.code}&grant_type=authorization_code"
async with httpx.AsyncClient() as client:
response = await client.get(url)
data = response.json()
if "errcode" in data:
raise HTTPException(status_code=400, detail=data["errmsg"])
openid = data["openid"]
session_key = data["session_key"]
# 处理用户注册或登录逻辑
user = await get_or_create_user(openid)
return {
"user_id": user.id,
"token": create_jwt_token(user.id)
}
4.2 活动记录API
创建活动记录
python复制@router.post("/activities")
async def create_activity(
activity: ActivityCreate,
user: User = Depends(get_current_user)
):
# 计算持续时间(分钟)
duration = None
if activity.end_time:
duration = int((activity.end_time - activity.start_time).total_seconds() / 60)
db_activity = ActivityRecord(
user_id=user.id,
type_id=activity.type_id,
start_time=activity.start_time,
end_time=activity.end_time,
duration=duration,
notes=activity.notes,
location=activity.location
)
db.add(db_activity)
await db.commit()
await db.refresh(db_activity)
return db_activity
获取活动统计
python复制@router.get("/activities/stats")
async def get_activity_stats(
start_date: date,
end_date: date,
type_id: Optional[int] = None,
user: User = Depends(get_current_user)
):
query = select(ActivityRecord).where(
ActivityRecord.user_id == user.id,
ActivityRecord.start_time >= start_date,
ActivityRecord.start_time <= end_date
)
if type_id:
query = query.where(ActivityRecord.type_id == type_id)
activities = await db.execute(query)
activities = activities.scalars().all()
# 使用Pandas进行数据分析
df = pd.DataFrame([{
'date': a.start_time.date(),
'type_id': a.type_id,
'duration': a.duration or 0
} for a in activities])
if df.empty:
return {}
# 按日期和类型分组统计
stats = df.groupby(['date', 'type_id'])['duration'].sum().unstack(fill_value=0)
return {
'daily_stats': stats.to_dict(orient='index'),
'total_by_type': df.groupby('type_id')['duration'].sum().to_dict()
}
5. 微信小程序前端实现
5.1 活动录入界面
javascript复制Page({
data: {
activityTypes: [],
form: {
typeId: '',
startTime: util.formatTime(new Date()),
endTime: '',
notes: '',
location: null
}
},
onLoad() {
this.loadActivityTypes()
},
loadActivityTypes() {
wx.request({
url: 'https://your-api.com/api/activity-types',
header: { 'Authorization': `Bearer ${getApp().globalData.token}` },
success: res => {
this.setData({ activityTypes: res.data })
}
})
},
handleStartTimeChange(e) {
this.setData({ 'form.startTime': e.detail.value })
},
handleEndTimeChange(e) {
this.setData({ 'form.endTime': e.detail.value })
},
submitForm() {
wx.request({
url: 'https://your-api.com/api/activities',
method: 'POST',
header: { 'Authorization': `Bearer ${getApp().globalData.token}` },
data: this.data.form,
success: () => {
wx.showToast({ title: '记录成功' })
this.resetForm()
}
})
},
resetForm() {
this.setData({
form: {
typeId: '',
startTime: util.formatTime(new Date()),
endTime: '',
notes: '',
location: null
}
})
}
})
5.2 数据可视化实现
使用微信小程序的图表库(如echarts-for-weixin)展示活动数据:
javascript复制import * as echarts from '../../ec-canvas/echarts';
function initChart(canvas, width, height) {
const chart = echarts.init(canvas, null, {
width: width,
height: height
});
canvas.setChart(chart);
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['阅读', '运动', '学习']
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: '阅读',
type: 'bar',
data: [20, 32, 18, 34, 50, 30, 20]
},
{
name: '运动',
type: 'bar',
data: [30, 25, 40, 35, 30, 50, 40]
},
{
name: '学习',
type: 'bar',
data: [120, 150, 110, 140, 160, 90, 100]
}
]
};
chart.setOption(option);
return chart;
}
Page({
data: {
ec: {
onInit: initChart
}
}
});
6. 部署与运维
6.1 服务器部署
推荐使用Docker容器化部署,示例Dockerfile:
dockerfile复制FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
6.2 性能优化建议
-
数据库优化:
- 为常用查询字段添加索引
- 对大表进行分表处理
- 使用Redis缓存热点数据
-
API优化:
- 实现分页查询
- 使用ETag缓存
- 启用Gzip压缩
-
小程序优化:
- 使用分包加载
- 图片资源使用CDN
- 减少setData调用频率
7. 扩展功能与未来方向
7.1 数据导出与分享
实现将活动数据导出为Excel或PDF的功能,并支持分享到微信好友或朋友圈:
python复制@router.get("/activities/export")
async def export_activities(
start_date: date,
end_date: date,
user: User = Depends(get_current_user)
):
activities = await get_activities(user.id, start_date, end_date)
# 使用pandas创建Excel
df = pd.DataFrame([{
'日期': a.start_time.date(),
'类型': a.type.name,
'开始时间': a.start_time.time(),
'结束时间': a.end_time.time() if a.end_time else None,
'持续时间(分钟)': a.duration,
'备注': a.notes
} for a in activities])
output = BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name='活动记录', index=False)
return StreamingResponse(
BytesIO(output.getvalue()),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment;filename=activities.xlsx"}
)
7.2 智能分析与建议
基于历史数据,使用机器学习算法提供活动建议:
python复制from sklearn.cluster import KMeans
import numpy as np
def analyze_activity_patterns(user_id):
# 获取用户历史活动数据
activities = await get_activities(user_id)
# 准备特征数据:时间、持续时间、活动类型
X = np.array([
[a.start_time.hour, a.duration or 0, a.type_id]
for a in activities
])
# 使用K-Means聚类分析活动模式
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
# 分析每个簇的特征
clusters = {}
for i, label in enumerate(kmeans.labels_):
if label not in clusters:
clusters[label] = []
clusters[label].append(activities[i])
# 生成分析报告
report = []
for label, acts in clusters.items():
avg_hour = np.mean([a.start_time.hour for a in acts])
avg_duration = np.mean([a.duration or 0 for a in acts])
common_type = max(set([a.type_id for a in acts]), key=[a.type_id for a in acts].count)
report.append({
'time_slot': f"{int(avg_hour)}:00-{int(avg_hour)+2}:00",
'average_duration': round(avg_duration),
'recommended_activity': common_type
})
return sorted(report, key=lambda x: x['time_slot'])
8. 常见问题与解决方案
8.1 微信登录失败处理
常见错误及解决方案:
- code无效:确保code在5分钟内使用,且只使用一次
- appid/secret错误:检查后台配置
- 网络问题:添加重试机制
改进后的登录代码:
python复制async def wechat_login(request: LoginRequest):
appid = settings.WECHAT_APPID
secret = settings.WECHAT_SECRET
url = f"https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={request.code}&grant_type=authorization_code"
retry_count = 0
while retry_count < 3:
try:
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(url)
data = response.json()
if "errcode" in data:
if data["errcode"] == 40029: # 无效code
raise HTTPException(status_code=400, detail="登录code无效,请重新获取")
elif data["errcode"] == 40163: # code已被使用
raise HTTPException(status_code=400, detail="登录code已过期")
else:
raise HTTPException(status_code=400, detail=data["errmsg"])
openid = data["openid"]
user = await get_or_create_user(openid)
return {
"user_id": user.id,
"token": create_jwt_token(user.id)
}
except (httpx.ConnectError, httpx.TimeoutException):
retry_count += 1
if retry_count == 3:
raise HTTPException(status_code=503, detail="微信登录服务暂时不可用")
await asyncio.sleep(1 * retry_count)
8.2 数据同步冲突
处理多设备同时编辑的冲突问题:
- 乐观锁机制:
python复制@router.put("/activities/{activity_id}")
async def update_activity(
activity_id: int,
activity: ActivityUpdate,
user: User = Depends(get_current_user),
db_version: int = Query(..., alias="version")
):
db_activity = await db.get(ActivityRecord, activity_id)
if not db_activity or db_activity.user_id != user.id:
raise HTTPException(status_code=404)
if db_activity.version != db_version:
raise HTTPException(status_code=409, detail="数据版本冲突")
# 更新字段
for field, value in activity.dict(exclude_unset=True).items():
setattr(db_activity, field, value)
db_activity.version += 1
await db.commit()
return db_activity
- 客户端处理策略:
- 获取数据时记录版本号
- 提交修改时带上版本号
- 如果服务端返回409冲突,提示用户解决冲突
9. 安全注意事项
9.1 接口安全
- JWT验证:所有API接口需要验证JWT token
- 权限控制:确保用户只能访问自己的数据
- 速率限制:防止暴力破解
FastAPI依赖项示例:
python复制async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await db.get(User, int(user_id))
if user is None:
raise credentials_exception
return user
9.2 数据安全
- 敏感数据加密:用户位置等敏感信息加密存储
- HTTPS传输:确保所有API请求通过HTTPS
- 输入验证:防止SQL注入和XSS攻击
python复制from pydantic import BaseModel, constr, validator
class ActivityCreate(BaseModel):
type_id: int
start_time: datetime
end_time: Optional[datetime]
notes: Optional[constr(max_length=500)]
location: Optional[str]
@validator('notes')
def prevent_xss(cls, v):
if v:
# 简单的XSS过滤
v = v.replace('<', '<').replace('>', '>')
return v
10. 测试策略
10.1 单元测试
使用pytest编写核心逻辑的单元测试:
python复制@pytest.mark.asyncio
async def test_create_activity():
user = await create_test_user()
activity_type = await create_test_activity_type(user.id)
activity_data = {
"type_id": activity_type.id,
"start_time": "2023-01-01T10:00:00",
"end_time": "2023-01-01T11:30:00",
"notes": "测试活动"
}
activity = await create_activity(user.id, activity_data)
assert activity.id is not None
assert activity.duration == 90
10.2 集成测试
测试API端点与数据库的集成:
python复制@pytest.mark.asyncio
async def test_activity_api(client, test_user, test_token):
# 创建活动类型
response = await client.post(
"/api/activity-types",
json={"name": "阅读", "icon": "book", "color": "#3498db"},
headers={"Authorization": f"Bearer {test_token}"}
)
assert response.status_code == 200
type_id = response.json()["id"]
# 创建活动记录
activity_data = {
"type_id": type_id,
"start_time": "2023-01-01T10:00:00",
"end_time": "2023-01-01T11:00:00"
}
response = await client.post(
"/api/activities",
json=activity_data,
headers={"Authorization": f"Bearer {test_token}"}
)
assert response.status_code == 200
assert response.json()["duration"] == 60
10.3 小程序端测试
- 单元测试:使用Jest测试工具函数
- E2E测试:使用小程序自动化测试工具
- UI测试:截图对比关键页面
11. 性能监控与优化
11.1 监控指标
- API响应时间:使用Prometheus监控
- 数据库查询性能:记录慢查询
- 小程序加载速度:使用微信性能面板
11.2 优化案例
问题:活动统计接口在数据量大时响应慢
解决方案:
- 添加数据库索引
- 实现分页查询
- 使用Redis缓存统计结果
优化后的统计查询:
python复制async def get_activity_stats(user_id, start_date, end_date, type_id=None):
cache_key = f"stats:{user_id}:{start_date}:{end_date}:{type_id or 'all'}"
cached = await redis.get(cache_key)
if cached:
return json.loads(cached)
# 优化后的查询
query = select(
ActivityRecord.type_id,
func.date(ActivityRecord.start_time).label("date"),
func.sum(ActivityRecord.duration).label("total_duration")
).where(
ActivityRecord.user_id == user_id,
ActivityRecord.start_time >= start_date,
ActivityRecord.start_time <= end_date
)
if type_id:
query = query.where(ActivityRecord.type_id == type_id)
query = query.group_by(
func.date(ActivityRecord.start_time),
ActivityRecord.type_id
)
result = await db.execute(query)
stats = result.all()
# 处理为前端需要的格式
processed = {}
for row in stats:
if row.date not in processed:
processed[row.date] = {}
processed[row.date][row.type_id] = row.total_duration
await redis.setex(cache_key, 3600, json.dumps(processed)) # 缓存1小时
return processed
12. 项目总结与经验分享
在实际开发这个系统的过程中,有几个关键点值得特别注意:
-
微信登录流程:确保正确处理各种边界情况,特别是网络不稳定的场景。建议实现自动重试机制,并给用户清晰的错误提示。
-
时间处理:跨时区的时间处理是个常见痛点。我们的解决方案是:
- 数据库统一存储UTC时间
- 前端根据用户时区进行显示转换
- 所有API参数和响应都使用ISO 8601格式
-
数据同步:小程序端实现本地缓存可以大幅提升用户体验。我们采用的策略是:
- 优先显示本地缓存数据
- 后台静默同步最新数据
- 使用版本号解决冲突
-
性能优化:对于统计类接口,三个有效的优化手段:
- 添加适当的数据库索引
- 实现分页加载
- 使用Redis缓存计算结果
-
错误监控:建议集成Sentry等错误监控工具,特别是要捕获:
- API异常
- 数据库查询超时
- 微信接口调用失败
这个系统虽然功能看似简单,但要做好用户体验需要处理好很多细节。特别是在数据同步和性能优化方面,需要根据实际使用情况不断调整。
