Add initial implementation of Habit Tracker API with database setup and endpoints

This commit is contained in:
Ching L 2025-04-16 15:02:37 +08:00
parent 29d1e3f575
commit 2261626bf3
5 changed files with 268 additions and 1 deletions

View File

@ -1,2 +1,76 @@
# habit-tracker # 习惯打卡记录器 (Habit Tracker)
一个基于 Flask 和 SQLite 的习惯打卡记录器后端 API。
## 技术栈
- Python
- Flask (Web 框架)
- SQLite (数据库)
- Peewee (ORM)
## 项目设置
1. 安装依赖:
```bash
python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate
pip install -r requirements.txt
```
2. 初始化数据库:
```bash
python init_db.py
```
3. 运行服务器:
```bash
python app.py
```
## API 文档
### Base URL: `/api/v1`
### 1. 事项管理
#### 创建事项
- **POST** `/habits`
```json
{
"name": "晨跑",
"description": "每天早上7点跑步",
"periodicity": "daily",
"reminder_time": "07:00",
"icon": "🏃‍♂️",
"color": "#FFAA00"
}
```
#### 获取所有事项
- **GET** `/habits`
#### 修改事项
- **PUT** `/habits/{habit_id}`
#### 删除事项
- **DELETE** `/habits/{habit_id}`
### 2. 打卡管理
#### 打卡
- **POST** `/habits/{habit_id}/checkins`
```json
{
"date": "2025-04-15",
"note": "状态不错!"
}
```
#### 获取打卡记录
- **GET** `/habits/{habit_id}/checkins?from=2025-04-01&to=2025-04-30`
### 3. 数据统计
#### 获取统计信息
- **GET** `/habits/{habit_id}/stats`

145
app.py Normal file
View File

@ -0,0 +1,145 @@
from flask import Flask, request, jsonify
from models import database, Habit, CheckIn
from datetime import datetime
import os
from dotenv import load_dotenv
load_dotenv() # 加载 .env 文件中的环境变量
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'default-secret-key')
@app.before_request
def before_request():
database.connect()
@app.after_request
def after_request(response):
database.close()
return response
# 1. 创建事项
@app.route('/api/v1/habits', methods=['POST'])
def create_habit():
data = request.json
try:
habit = Habit.create(
name=data['name'],
description=data.get('description', ''),
periodicity=data.get('periodicity', 'daily'),
reminder_time=data.get('reminder_time'),
icon=data.get('icon'),
color=data.get('color', '#000000')
)
return jsonify({
'id': habit.id,
'message': '创建成功'
}), 201
except Exception as e:
return jsonify({'error': str(e)}), 400
# 2. 获取所有事项
@app.route('/api/v1/habits', methods=['GET'])
def get_habits():
habits = [
{
'id': habit.id,
'name': habit.name,
'icon': habit.icon,
'color': habit.color,
'created_at': habit.created_at.isoformat()
}
for habit in Habit.select()
]
return jsonify(habits)
# 3. 打卡某事项
@app.route('/api/v1/habits/<int:habit_id>/checkins', methods=['POST'])
def check_in_habit(habit_id):
data = request.json
try:
habit = Habit.get_by_id(habit_id)
checkin_date = datetime.strptime(data['date'], '%Y-%m-%d').date()
CheckIn.create(
habit=habit,
date=checkin_date,
note=data.get('note', '')
)
return jsonify({'message': '打卡成功'})
except Habit.DoesNotExist:
return jsonify({'error': '事项不存在'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 400
# 4. 获取某事项的打卡记录
@app.route('/api/v1/habits/<int:habit_id>/checkins', methods=['GET'])
def get_habit_checkins(habit_id):
try:
habit = Habit.get_by_id(habit_id)
from_date = request.args.get('from')
to_date = request.args.get('to')
query = CheckIn.select().where(CheckIn.habit == habit)
if from_date:
query = query.where(CheckIn.date >= datetime.strptime(from_date, '%Y-%m-%d').date())
if to_date:
query = query.where(CheckIn.date <= datetime.strptime(to_date, '%Y-%m-%d').date())
checkins = [
{
'date': checkin.date.isoformat(),
'note': checkin.note
}
for checkin in query
]
return jsonify(checkins)
except Habit.DoesNotExist:
return jsonify({'error': '事项不存在'}), 404
# 5. 获取统计信息
@app.route('/api/v1/habits/<int:habit_id>/stats', methods=['GET'])
def get_habit_stats(habit_id):
try:
habit = Habit.get_by_id(habit_id)
total_checkins = CheckIn.select().where(CheckIn.habit == habit).count()
# TODO: Implement streak calculations
return jsonify({
'total_checkins': total_checkins,
'current_streak': 0, # To be implemented
'longest_streak': 0 # To be implemented
})
except Habit.DoesNotExist:
return jsonify({'error': '事项不存在'}), 404
# 6. 删除事项
@app.route('/api/v1/habits/<int:habit_id>', methods=['DELETE'])
def delete_habit(habit_id):
try:
habit = Habit.get_by_id(habit_id)
habit.delete_instance(recursive=True) # This will also delete associated check-ins
return jsonify({'message': '删除成功'})
except Habit.DoesNotExist:
return jsonify({'error': '事项不存在'}), 404
# 7. 修改事项
@app.route('/api/v1/habits/<int:habit_id>', methods=['PUT'])
def update_habit(habit_id):
try:
habit = Habit.get_by_id(habit_id)
data = request.json
for field in ['name', 'description', 'periodicity', 'reminder_time', 'icon', 'color']:
if field in data:
setattr(habit, field, data[field])
habit.save()
return jsonify({'message': '更新成功'})
except Habit.DoesNotExist:
return jsonify({'error': '事项不存在'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 400
if __name__ == '__main__':
app.run(debug=True)

4
init_db.py Normal file
View File

@ -0,0 +1,4 @@
from models import create_tables
if __name__ == '__main__':
create_tables()

41
models.py Normal file
View File

@ -0,0 +1,41 @@
from peewee import *
from datetime import datetime
import os
from dotenv import load_dotenv
load_dotenv() # 加载 .env 文件中的环境变量
database = SqliteDatabase(os.getenv('DATABASE_PATH', 'habits.db'))
class BaseModel(Model):
class Meta:
database = database
class Habit(BaseModel):
name = CharField()
description = TextField(null=True)
periodicity = CharField(default='daily') # daily, weekly, monthly
reminder_time = TimeField(null=True)
icon = CharField(null=True)
color = CharField(default='#000000')
created_at = DateTimeField(default=datetime.now)
class Meta:
table_name = 'habits'
class CheckIn(BaseModel):
habit = ForeignKeyField(Habit, backref='checkins', on_delete='CASCADE')
date = DateField()
note = TextField(null=True)
created_at = DateTimeField(default=datetime.now)
class Meta:
table_name = 'checkins'
indexes = (
# Ensure no duplicate check-ins for the same habit on the same day
(('habit', 'date'), True),
)
def create_tables():
with database:
database.create_tables([Habit, CheckIn])

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
flask==3.1.0
peewee==3.17.9
python-dotenv==1.1.0