Add initial implementation of Habit Tracker API with database setup and endpoints
This commit is contained in:
parent
29d1e3f575
commit
2261626bf3
76
README.md
76
README.md
@ -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
145
app.py
Normal 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
4
init_db.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from models import create_tables
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
create_tables()
|
||||||
41
models.py
Normal file
41
models.py
Normal 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
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
flask==3.1.0
|
||||||
|
peewee==3.17.9
|
||||||
|
python-dotenv==1.1.0
|
||||||
Loading…
x
Reference in New Issue
Block a user