diff --git a/services/backend/db.sqlite3 b/services/backend/db.sqlite3 new file mode 100644 index 0000000..3d70898 Binary files /dev/null and b/services/backend/db.sqlite3 differ diff --git a/services/backend/db.sqlite3-shm b/services/backend/db.sqlite3-shm new file mode 100644 index 0000000..e0963b0 Binary files /dev/null and b/services/backend/db.sqlite3-shm differ diff --git a/services/backend/db.sqlite3-wal b/services/backend/db.sqlite3-wal new file mode 100644 index 0000000..c6fa58c Binary files /dev/null and b/services/backend/db.sqlite3-wal differ diff --git a/services/backend/src/auth/jwthandler.py b/services/backend/src/auth/jwthandler.py index c35d9fc..f477ee9 100644 --- a/services/backend/src/auth/jwthandler.py +++ b/services/backend/src/auth/jwthandler.py @@ -14,7 +14,7 @@ from src.database.models import Users -SECRET_KEY = os.environ.get("SECRET_KEY") +SECRET_KEY = os.environ.get("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 diff --git a/services/backend/src/crud/notes.py b/services/backend/src/crud/notes.py index 52e450d..31be191 100644 --- a/services/backend/src/crud/notes.py +++ b/services/backend/src/crud/notes.py @@ -1,12 +1,14 @@ from fastapi import HTTPException from tortoise.exceptions import DoesNotExist -from src.database.models import Notes +from src.database.models import Notes, Tags from src.schemas.notes import NoteOutSchema from src.schemas.token import Status -async def get_notes(): +async def get_notes(tag_id: int = None): + if tag_id: + return await NoteOutSchema.from_queryset(Notes.filter(tags__id=tag_id)) return await NoteOutSchema.from_queryset(Notes.all()) @@ -17,7 +19,11 @@ async def get_note(note_id) -> NoteOutSchema: async def create_note(note, current_user) -> NoteOutSchema: note_dict = note.dict(exclude_unset=True) note_dict["author_id"] = current_user.id + tag_ids = note_dict.pop("tags", []) note_obj = await Notes.create(**note_dict) + if tag_ids: + tags = await Tags.filter(id__in=tag_ids) + await note_obj.tags.add(*tags) return await NoteOutSchema.from_tortoise_orm(note_obj) @@ -28,7 +34,18 @@ async def update_note(note_id, note, current_user) -> NoteOutSchema: raise HTTPException(status_code=404, detail=f"Note {note_id} not found") if db_note.author.id == current_user.id: - await Notes.filter(id=note_id).update(**note.dict(exclude_unset=True)) + note_dict = note.dict(exclude_unset=True) + tag_ids = note_dict.pop("tags", None) + if note_dict: + await Notes.filter(id=note_id).update(**note_dict) + + if tag_ids is not None: + note_obj = await Notes.get(id=note_id) + await note_obj.tags.clear() + if tag_ids: + tags = await Tags.filter(id__in=tag_ids) + await note_obj.tags.add(*tags) + return await NoteOutSchema.from_queryset_single(Notes.get(id=note_id)) raise HTTPException(status_code=403, detail=f"Not authorized to update") diff --git a/services/backend/src/crud/tags.py b/services/backend/src/crud/tags.py new file mode 100644 index 0000000..155dd6e --- /dev/null +++ b/services/backend/src/crud/tags.py @@ -0,0 +1,42 @@ +from fastapi import HTTPException +from tortoise.exceptions import DoesNotExist + +from src.database.models import Tags +from src.schemas.tags import TagOutSchema +from src.schemas.token import Status + + +async def get_tags(): + return await TagOutSchema.from_queryset(Tags.all()) + + +async def get_tag(tag_id) -> TagOutSchema: + return await TagOutSchema.from_queryset_single(Tags.get(id=tag_id)) + + +async def create_tag(tag) -> TagOutSchema: + tag_dict = tag.dict(exclude_unset=True) + tag_obj = await Tags.create(**tag_dict) + return await TagOutSchema.from_tortoise_orm(tag_obj) + + +async def update_tag(tag_id, tag) -> TagOutSchema: + try: + await TagOutSchema.from_queryset_single(Tags.get(id=tag_id)) + except DoesNotExist: + raise HTTPException(status_code=404, detail=f"Tag {tag_id} not found") + + await Tags.filter(id=tag_id).update(**tag.dict(exclude_unset=True)) + return await TagOutSchema.from_queryset_single(Tags.get(id=tag_id)) + + +async def delete_tag(tag_id) -> Status: + try: + await TagOutSchema.from_queryset_single(Tags.get(id=tag_id)) + except DoesNotExist: + raise HTTPException(status_code=404, detail=f"Tag {tag_id} not found") + + deleted_count = await Tags.filter(id=tag_id).delete() + if not deleted_count: + raise HTTPException(status_code=404, detail=f"Tag {tag_id} not found") + return Status(message=f"Deleted tag {tag_id}") diff --git a/services/backend/src/crud/users.py b/services/backend/src/crud/users.py index 79e1705..054652b 100644 --- a/services/backend/src/crud/users.py +++ b/services/backend/src/crud/users.py @@ -1,5 +1,5 @@ from fastapi import HTTPException -from passlib.context import CryptContext +from src.auth.users import get_password_hash from tortoise.exceptions import DoesNotExist, IntegrityError from src.database.models import Users @@ -7,11 +7,11 @@ from src.schemas.users import UserOutSchema -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + async def create_user(user) -> UserOutSchema: - user.password = pwd_context.encrypt(user.password) + user.password = get_password_hash(user.password) try: user_obj = await Users.create(**user.dict(exclude_unset=True)) diff --git a/services/backend/src/database/config.py b/services/backend/src/database/config.py index 0e2329d..3b5bb61 100644 --- a/services/backend/src/database/config.py +++ b/services/backend/src/database/config.py @@ -2,7 +2,7 @@ TORTOISE_ORM = { - "connections": {"default": os.environ.get("DATABASE_URL")}, + "connections": {"default": os.environ.get("DATABASE_URL", "sqlite://./db.sqlite3")}, "apps": { "models": { "models": [ diff --git a/services/backend/src/database/models.py b/services/backend/src/database/models.py index 68be6a5..f3b7d00 100644 --- a/services/backend/src/database/models.py +++ b/services/backend/src/database/models.py @@ -10,11 +10,22 @@ class Users(models.Model): modified_at = fields.DatetimeField(auto_now=True) +class Tags(models.Model): + id = fields.IntField(pk=True) + name = fields.CharField(max_length=50, unique=True) + created_at = fields.DatetimeField(auto_now_add=True) + modified_at = fields.DatetimeField(auto_now=True) + + def __str__(self): + return self.name + + class Notes(models.Model): id = fields.IntField(pk=True) title = fields.CharField(max_length=225) content = fields.TextField() author = fields.ForeignKeyField("models.Users", related_name="note") + tags = fields.ManyToManyField("models.Tags", related_name="notes") created_at = fields.DatetimeField(auto_now_add=True) modified_at = fields.DatetimeField(auto_now=True) diff --git a/services/backend/src/main.py b/services/backend/src/main.py index 5c44f12..701b3d2 100644 --- a/services/backend/src/main.py +++ b/services/backend/src/main.py @@ -14,7 +14,7 @@ why? https://stackoverflow.com/questions/65531387/tortoise-orm-for-python-no-returns-relations-of-entities-pyndantic-fastapi """ -from src.routes import users, notes +from src.routes import users, notes, tags app = FastAPI() @@ -27,8 +27,9 @@ ) app.include_router(users.router) app.include_router(notes.router) +app.include_router(tags.router) -register_tortoise(app, config=TORTOISE_ORM, generate_schemas=False) +register_tortoise(app, config=TORTOISE_ORM, generate_schemas=True) @app.get("/") diff --git a/services/backend/src/routes/notes.py b/services/backend/src/routes/notes.py index 9ba4a61..07b7468 100644 --- a/services/backend/src/routes/notes.py +++ b/services/backend/src/routes/notes.py @@ -1,12 +1,12 @@ -from typing import List +from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from tortoise.contrib.fastapi import HTTPNotFoundError from tortoise.exceptions import DoesNotExist import src.crud.notes as crud from src.auth.jwthandler import get_current_user -from src.schemas.notes import NoteOutSchema, NoteInSchema, UpdateNote +from src.schemas.notes import NoteOutSchema, NoteInSchema, NoteCreate, UpdateNote from src.schemas.token import Status from src.schemas.users import UserOutSchema @@ -19,8 +19,8 @@ response_model=List[NoteOutSchema], dependencies=[Depends(get_current_user)], ) -async def get_notes(): - return await crud.get_notes() +async def get_notes(tag_id: Optional[int] = Query(None, description="Filter notes by tag ID")): + return await crud.get_notes(tag_id) @router.get( @@ -42,7 +42,7 @@ async def get_note(note_id: int) -> NoteOutSchema: "/notes", response_model=NoteOutSchema, dependencies=[Depends(get_current_user)] ) async def create_note( - note: NoteInSchema, current_user: UserOutSchema = Depends(get_current_user) + note: NoteCreate, current_user: UserOutSchema = Depends(get_current_user) ) -> NoteOutSchema: return await crud.create_note(note, current_user) diff --git a/services/backend/src/routes/tags.py b/services/backend/src/routes/tags.py new file mode 100644 index 0000000..a7190d3 --- /dev/null +++ b/services/backend/src/routes/tags.py @@ -0,0 +1,73 @@ +from typing import List + +from fastapi import APIRouter, Depends, HTTPException +from tortoise.contrib.fastapi import HTTPNotFoundError +from tortoise.exceptions import DoesNotExist + +import src.crud.tags as crud +from src.auth.jwthandler import get_current_user +from src.schemas.tags import TagOutSchema, TagInSchema, UpdateTag +from src.schemas.token import Status +from src.schemas.users import UserOutSchema + + +router = APIRouter() + + +@router.get( + "/tags", + response_model=List[TagOutSchema], + dependencies=[Depends(get_current_user)], +) +async def get_tags(): + return await crud.get_tags() + + +@router.get( + "/tag/{tag_id}", + response_model=TagOutSchema, + dependencies=[Depends(get_current_user)], +) +async def get_tag(tag_id: int) -> TagOutSchema: + try: + return await crud.get_tag(tag_id) + except DoesNotExist: + raise HTTPException( + status_code=404, + detail="Tag does not exist", + ) + + +@router.post( + "/tags", response_model=TagOutSchema, dependencies=[Depends(get_current_user)] +) +async def create_tag( + tag: TagInSchema, current_user: UserOutSchema = Depends(get_current_user) +) -> TagOutSchema: + return await crud.create_tag(tag) + + +@router.patch( + "/tag/{tag_id}", + dependencies=[Depends(get_current_user)], + response_model=TagOutSchema, + responses={404: {"model": HTTPNotFoundError}}, +) +async def update_tag( + tag_id: int, + tag: UpdateTag, + current_user: UserOutSchema = Depends(get_current_user), +) -> TagOutSchema: + return await crud.update_tag(tag_id, tag) + + +@router.delete( + "/tag/{tag_id}", + response_model=Status, + responses={404: {"model": HTTPNotFoundError}}, + dependencies=[Depends(get_current_user)], +) +async def delete_tag( + tag_id: int, current_user: UserOutSchema = Depends(get_current_user) +): + return await crud.delete_tag(tag_id) diff --git a/services/backend/src/schemas/notes.py b/services/backend/src/schemas/notes.py index 4ea3422..f946e0f 100644 --- a/services/backend/src/schemas/notes.py +++ b/services/backend/src/schemas/notes.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List from pydantic import BaseModel from tortoise.contrib.pydantic import pydantic_model_creator @@ -15,6 +15,13 @@ ) +class NoteCreate(BaseModel): + title: str + content: str + tags: Optional[List[int]] = [] + + class UpdateNote(BaseModel): title: Optional[str] content: Optional[str] + tags: Optional[List[int]] diff --git a/services/backend/src/schemas/tags.py b/services/backend/src/schemas/tags.py new file mode 100644 index 0000000..c27b239 --- /dev/null +++ b/services/backend/src/schemas/tags.py @@ -0,0 +1,19 @@ +from typing import Optional, List + +from pydantic import BaseModel +from tortoise.contrib.pydantic import pydantic_model_creator + +from src.database.models import Tags + + +TagInSchema = pydantic_model_creator( + Tags, name="TagIn", exclude_readonly=True) +TagOutSchema = pydantic_model_creator( + Tags, name="Tag", exclude =[ + "modified_at", "created_at" + ] +) + + +class UpdateTag(BaseModel): + name: Optional[str] diff --git a/services/frontend/package.json b/services/frontend/package.json index 8c486fa..85dc97e 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -37,7 +37,9 @@ "parserOptions": { "parser": "@babel/eslint-parser" }, - "rules": {} + "rules": { + "vue/multi-word-component-names": "off" + } }, "browserslist": [ "> 1%", diff --git a/services/frontend/src/components/TagManager.vue b/services/frontend/src/components/TagManager.vue new file mode 100644 index 0000000..542bdad --- /dev/null +++ b/services/frontend/src/components/TagManager.vue @@ -0,0 +1,81 @@ + + + diff --git a/services/frontend/src/store/index.js b/services/frontend/src/store/index.js index 2102841..c06050f 100644 --- a/services/frontend/src/store/index.js +++ b/services/frontend/src/store/index.js @@ -2,10 +2,12 @@ import { createStore } from "vuex"; import notes from './modules/notes'; import users from './modules/users'; +import tags from './modules/tags'; export default createStore({ modules: { notes, users, + tags, } }); diff --git a/services/frontend/src/store/modules/notes.js b/services/frontend/src/store/modules/notes.js index c8ce2a5..a14b0bd 100644 --- a/services/frontend/src/store/modules/notes.js +++ b/services/frontend/src/store/modules/notes.js @@ -15,8 +15,13 @@ const actions = { await axios.post('notes', note); await dispatch('getNotes'); }, - async getNotes({commit}) { - let {data} = await axios.get('notes'); + async getNotes({commit, rootState}) { + const selectedTagId = rootState.tags.selectedTagId; + let url = 'notes'; + if (selectedTagId) { + url += `?tag_id=${selectedTagId}`; + } + let {data} = await axios.get(url); commit('setNotes', data); }, async viewNote({commit}, id) { diff --git a/services/frontend/src/store/modules/tags.js b/services/frontend/src/store/modules/tags.js new file mode 100644 index 0000000..b520f39 --- /dev/null +++ b/services/frontend/src/store/modules/tags.js @@ -0,0 +1,58 @@ +import axios from 'axios'; + +const state = { + tags: null, + tag: null, + selectedTagId: null +}; + +const getters = { + stateTags: state => state.tags, + stateTag: state => state.tag, + stateSelectedTagId: state => state.selectedTagId +}; + +const actions = { + async createTag({dispatch}, tag) { + await axios.post('tags', tag); + await dispatch('getTags'); + }, + async getTags({commit}) { + let {data} = await axios.get('tags'); + commit('setTags', data); + }, + async viewTag({commit}, id) { + let {data} = await axios.get(`tag/${id}`); + commit('setTag', data); + }, + // eslint-disable-next-line no-empty-pattern + async updateTag({}, tag) { + await axios.patch(`tag/${tag.id}`, tag.form); + }, + // eslint-disable-next-line no-empty-pattern + async deleteTag({}, id) { + await axios.delete(`tag/${id}`); + }, + selectTag({commit}, tagId) { + commit('setSelectedTagId', tagId); + } +}; + +const mutations = { + setTags(state, tags){ + state.tags = tags; + }, + setTag(state, tag){ + state.tag = tag; + }, + setSelectedTagId(state, tagId){ + state.selectedTagId = tagId; + } +}; + +export default { + state, + getters, + actions, + mutations +}; diff --git a/services/frontend/src/views/DashboardView.vue b/services/frontend/src/views/DashboardView.vue index 66b517a..15a4910 100644 --- a/services/frontend/src/views/DashboardView.vue +++ b/services/frontend/src/views/DashboardView.vue @@ -1,79 +1,135 @@ diff --git a/services/frontend/src/views/EditNoteView.vue b/services/frontend/src/views/EditNoteView.vue index 87a5dde..6ce6c9c 100644 --- a/services/frontend/src/views/EditNoteView.vue +++ b/services/frontend/src/views/EditNoteView.vue @@ -1,22 +1,42 @@ @@ -33,17 +53,19 @@ export default defineComponent({ form: { title: '', content: '', + tags: [] }, }; }, created: function() { this.GetNote(); + this.$store.dispatch('getTags'); }, computed: { - ...mapGetters({ note: 'stateNote' }), + ...mapGetters({ note: 'stateNote', tags: 'stateTags' }), }, methods: { - ...mapActions(['updateNote', 'viewNote']), + ...mapActions(['updateNote', 'viewNote', 'getTags']), async submit() { try { let note = { @@ -61,6 +83,9 @@ export default defineComponent({ await this.viewNote(this.id); this.form.title = this.note.title; this.form.content = this.note.content; + if (this.note.tags && this.note.tags.length) { + this.form.tags = this.note.tags.map(tag => tag.id); + } } catch (error) { console.error(error); this.$router.push('/dashboard'); diff --git a/services/frontend/src/views/NoteView.vue b/services/frontend/src/views/NoteView.vue index 4f10c88..df076ed 100644 --- a/services/frontend/src/views/NoteView.vue +++ b/services/frontend/src/views/NoteView.vue @@ -1,12 +1,19 @@ diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..6801290 --- /dev/null +++ b/test_api.py @@ -0,0 +1,138 @@ +import httpx +import asyncio + +BASE_URL = "http://localhost:5000" + +async def test_api(): + async with httpx.AsyncClient(base_url=BASE_URL) as client: + print("=" * 50) + print("测试用户注册和登录") + print("=" * 50) + + user_data = { + "username": "testuser", + "password": "testpass123", + "full_name": "Test User" + } + + print("\n1. 注册用户...") + try: + response = await client.post("/register", json=user_data) + print(f"注册状态码: {response.status_code}") + print(f"注册响应: {response.json()}") + except Exception as e: + print(f"注册失败: {e}") + + print("\n2. 登录用户...") + try: + login_data = { + "username": "testuser", + "password": "testpass123" + } + response = await client.post("/login", data=login_data) + print(f"登录状态码: {response.status_code}") + print(f"登录响应: {response.json()}") + + if response.status_code == 200: + cookies = response.cookies + print(f"获取到Cookie: {dict(cookies)}") + + print("\n3. 验证登录状态 (获取当前用户)...") + response = await client.get("/users/whoami", cookies=cookies) + print(f"获取用户状态码: {response.status_code}") + print(f"当前用户: {response.json()}") + + print("\n" + "=" * 50) + print("创建标签") + print("=" * 50) + + tags = ["工作", "学习", "生活", "重要"] + tag_ids = [] + + for tag_name in tags: + print(f"\n创建标签: {tag_name}") + response = await client.post("/tags", json={"name": tag_name}, cookies=cookies) + print(f"状态码: {response.status_code}") + if response.status_code == 200: + tag_data = response.json() + print(f"创建成功: {tag_data}") + tag_ids.append(tag_data["id"]) + else: + print(f"创建失败: {response.text}") + + print("\n" + "=" * 50) + print("创建笔记") + print("=" * 50) + + notes = [ + { + "title": "Python学习笔记", + "content": "今天学习了Python的基础语法,包括变量、数据类型、条件语句和循环。Python是一门非常优雅的语言,缩进让代码更加整洁。", + "tags": [tag_ids[1], tag_ids[3]] if len(tag_ids) > 3 else [] + }, + { + "title": "工作计划", + "content": "下周需要完成的任务:\n1. 完成项目文档\n2. 代码审查\n3. 团队会议\n4. 客户演示", + "tags": [tag_ids[0], tag_ids[3]] if len(tag_ids) > 3 else [] + }, + { + "title": "周末计划", + "content": "这个周末打算:\n- 去公园散步\n- 看一部电影\n- 整理房间\n- 学习新技能", + "tags": [tag_ids[2]] if len(tag_ids) > 2 else [] + }, + { + "title": "FastAPI学习", + "content": "FastAPI是一个现代、快速的Web框架,基于Python类型提示。它自动生成API文档,性能非常好。", + "tags": [tag_ids[1]] if len(tag_ids) > 1 else [] + } + ] + + for note in notes: + print(f"\n创建笔记: {note['title']}") + response = await client.post("/notes", json=note, cookies=cookies) + print(f"状态码: {response.status_code}") + if response.status_code == 200: + print(f"创建成功: {response.json()}") + else: + print(f"创建失败: {response.text}") + + print("\n" + "=" * 50) + print("获取所有笔记") + print("=" * 50) + + response = await client.get("/notes", cookies=cookies) + print(f"状态码: {response.status_code}") + notes_data = response.json() + print(f"共 {len(notes_data)} 条笔记") + for note in notes_data: + print(f"\n- 标题: {note['title']}") + print(f" 作者: {note['author']['username']}") + if note.get('tags'): + print(f" 标签: {[t['name'] for t in note['tags']]}") + + if tag_ids: + print("\n" + "=" * 50) + print(f"按标签筛选笔记 (标签ID: {tag_ids[1]})") + print("=" * 50) + + response = await client.get(f"/notes?tag_id={tag_ids[1]}", cookies=cookies) + print(f"状态码: {response.status_code}") + filtered_notes = response.json() + print(f"筛选后共 {len(filtered_notes)} 条笔记") + for note in filtered_notes: + print(f"\n- 标题: {note['title']}") + + print("\n" + "=" * 50) + print("测试完成!") + print("=" * 50) + print("\n您可以访问 http://localhost:8080 来查看应用") + print(f"登录账号: testuser") + print(f"登录密码: testpass123") + + except Exception as e: + print(f"登录失败: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + asyncio.run(test_api())