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