From 2261626bf30b58f47712a3c85baf6f259de50bf5 Mon Sep 17 00:00:00 2001 From: Ching L Date: Wed, 16 Apr 2025 15:02:37 +0800 Subject: [PATCH] Add initial implementation of Habit Tracker API with database setup and endpoints --- README.md | 76 ++++++++++++++++++++++++- app.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++ init_db.py | 4 ++ models.py | 41 ++++++++++++++ requirements.txt | 3 + 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 app.py create mode 100644 init_db.py create mode 100644 models.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 99c9e28..60b70fa 100644 --- a/README.md +++ b/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` diff --git a/app.py b/app.py new file mode 100644 index 0000000..091ac2a --- /dev/null +++ b/app.py @@ -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//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//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//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/', 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/', 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) diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..63cccc3 --- /dev/null +++ b/init_db.py @@ -0,0 +1,4 @@ +from models import create_tables + +if __name__ == '__main__': + create_tables() diff --git a/models.py b/models.py new file mode 100644 index 0000000..2bcbef4 --- /dev/null +++ b/models.py @@ -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]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b48a534 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask==3.1.0 +peewee==3.17.9 +python-dotenv==1.1.0