From aa7c62299944672f7971d3fbb0c6bc4b5d414790 Mon Sep 17 00:00:00 2001 From: Acheng <3496079213@qq.com> Date: Wed, 15 Apr 2026 14:25:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前后端添加标签管理功能 - 笔记支持关联标签 - 实现按标签筛选笔记 - 更新UI显示标签信息 --- services/backend/db.sqlite3 | Bin 0 -> 4096 bytes services/backend/db.sqlite3-shm | Bin 0 -> 32768 bytes services/backend/db.sqlite3-wal | Bin 0 -> 65952 bytes services/backend/src/crud/notes.py | 21 ++- services/backend/src/crud/tags.py | 42 ++++++ services/backend/src/database/config.py | 2 +- services/backend/src/database/models.py | 11 ++ services/backend/src/main.py | 5 +- services/backend/src/routes/notes.py | 8 +- services/backend/src/routes/tags.py | 73 +++++++++ services/backend/src/schemas/notes.py | 3 +- services/backend/src/schemas/tags.py | 19 +++ services/frontend/package.json | 4 +- .../frontend/src/components/TagManager.vue | 81 ++++++++++ services/frontend/src/store/index.js | 2 + services/frontend/src/store/modules/notes.js | 9 +- services/frontend/src/store/modules/tags.js | 58 ++++++++ services/frontend/src/views/DashboardView.vue | 140 ++++++++++++------ services/frontend/src/views/EditNoteView.vue | 37 ++++- services/frontend/src/views/NoteView.vue | 17 ++- 20 files changed, 465 insertions(+), 67 deletions(-) create mode 100644 services/backend/db.sqlite3 create mode 100644 services/backend/db.sqlite3-shm create mode 100644 services/backend/db.sqlite3-wal create mode 100644 services/backend/src/crud/tags.py create mode 100644 services/backend/src/routes/tags.py create mode 100644 services/backend/src/schemas/tags.py create mode 100644 services/frontend/src/components/TagManager.vue create mode 100644 services/frontend/src/store/modules/tags.js diff --git a/services/backend/db.sqlite3 b/services/backend/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..3d708988723be9a081e454b8800b513ce9ceea67 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYCM3<^^dNAlr;ljiVtj n8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O6ovo*Iy?r8 literal 0 HcmV?d00001 diff --git a/services/backend/db.sqlite3-shm b/services/backend/db.sqlite3-shm new file mode 100644 index 0000000000000000000000000000000000000000..b8e435bd0ad18215e720db2353b392a8e9638f69 GIT binary patch literal 32768 zcmeI)F-}535C-5uQ9uwWNlZ*cDeXyUC@tve?Yw}MXS47Gp22tzoiR^iYpig;Kbh=% zFPnWk-vMU+dJ~o7tX{-ymg^{My||Cn+vMSDdwuz|df7cco?qPEE;sXE?$4t>Q|G?_ zNcZ`B$$!T|)?rpZtKN^Yx_iZ-`!?*>x?}D8+LMn$fB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNB! zcLKefA2E!RnC#unU6{aM1!~@k5NK1NCJhOJHU;`IZL@J*6UeD@XA}rDDlmwe#wY|@ z6sXCeLZD57ahq-Hnm`JiQXoKp009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF M5FkK+z`qgr0G$jT@c;k- literal 0 HcmV?d00001 diff --git a/services/backend/db.sqlite3-wal b/services/backend/db.sqlite3-wal new file mode 100644 index 0000000000000000000000000000000000000000..76d86f916550cb657fb01f8b267dd568f5650542 GIT binary patch literal 65952 zcmeI*duUr#90%}w(>AZIt5Qm~iFg(ZHg2s+x^`Xb*zU61m@Ti}Ce{42%k6U8UTBiF zN$MQPXf{P~{^REBFWGR2=s?gv%TUHdL{U-EzlP`(1O@-v>^f;JaqYit=D3GARvr(xZoAy`-Lp3~#JU@<$nVf9zP4VwbK#@j zZ|qwK4u`dZN``Y28Kpov>DCqtw~r9Iu7#fK**4|Ewx7Lh+_%uRh3k+XX>7X1)omK* zHeG4@mR_Mi00Izz00bZa0SG_<0uX?}dJu@$SzX(=^QnR|G^Xaq$LPOLIj@cv)$GyP zi}e9X6cQpy2>Zh#nYC_PR>`P4=$4+6gSWX_TKH2%eV+woB+t$k)7EZQ3PCpb@jFIoR&?i$ID_$v5?c3Wo97z*kv7ktJ%wMH}*fj(q3@=*T4S!{A8SO zWR){b3G5aM1Rwwb2tWV=5P$##AOHafKmY=3Q-HM*RN4jJv%S%~_xHXZoU9V<0!#q~ z0uX=z1Rwwb2tWV=5P$##Ah4DNSPnqFkzX*q;pua)fBMxC#0#wDmmJ3i0SG_<0uX=z z1Rwwb2tWV=5U2>4#uvDgxv+Q7%fV}lj4xoVY(4*Y12uQgd$TgWK*Pq{9!acoRBuS_%9%bDlLlzjgA+yqUIH?rJ){vPAb2btFHX|THXxE4xl*9m2x zer3h@0xNBNfd=*gGvWpQbp`)vzxmmth!I|_m%a?W zxK&#ukHA**RM@zM_dQRcb;Bz)c?5I;1p*L&00bZa0SG_<0_#|yl(p(%?JxD|VeQ#m zLCxzIHgj0JUbnQc_JURztC?`k@9(P4xMobXmdh5@ESqOdd~VKsZ4WMBz5MLx#A~Cu z3AsGkTy+LGN$eNt%x?j4fVhqDe>NqZhs2_EcDS%e=e!FD0|DWH==30uz?eesf0!N* zc?7F?oC;~yg&Is_m90SG_<0uX=z1Rwwb2&^XoQ@p^9 zC*OYS5AK6s7KsR==?`CD(L7Q{h6fZy*Q6K;T2tWV=5P$##AaG9t zQz4tpwPg!m%IJOUiaMbk9i6$fn|s-3Dwo#HK1m0r({kDBm~Z~BxgG7wv18R0eBPSj z~!*7#0%WhPZ3sz00bZa0SG_<0uX=z1Rwwb2&`5CQ@lX$7vKMQ`NN0Pi^K~! zY7UmoXK>%+3)mfh&x#kI6(|sZ00bZa0SG_<0?R5e6|vh~+qUr~wVJ~&vwk!^XzaVZoLq!c+M zrihSC#6nT}T1IGpd!<{`fXDa(7++v+6hCn?@dZ|K zya4-;?=a#8ni@y1oj-r>EaC;0^`{s+hX4d1009U<00Izz00bZa0SK%V0llx^XcsuG z^}Kp!*T;>lU4U=8!O;^61Rwwb2tWV=5P$##AOHafKmY=3O2E$7xg7eG9rUnR9sp;w z3tS3h@^4(!r)L=Jw6`m z92}QMjt>oH{9b=|yLV^1uZwuQd;OigUT;U|&Yj)6{M)@=y0F;&6vIl-l{^AYbN5Yd syx!xam3-4}j-F5;009U<00Izz00bZa0SG_<0uWeh0@k`_2W^x14^X0<;s5{u literal 0 HcmV?d00001 diff --git a/services/backend/src/crud/notes.py b/services/backend/src/crud/notes.py index 52e450d..16754dc 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,10 @@ 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 + tags = note_dict.pop("tags", []) note_obj = await Notes.create(**note_dict) + if tags: + await note_obj.tags.add(*tags) return await NoteOutSchema.from_tortoise_orm(note_obj) @@ -28,7 +33,17 @@ 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) + tags = note_dict.pop("tags", None) + if note_dict: + await Notes.filter(id=note_id).update(**note_dict) + + if tags is not None: + note_obj = await Notes.get(id=note_id) + await note_obj.tags.clear() + if tags: + 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/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..21880a5 100644 --- a/services/backend/src/routes/notes.py +++ b/services/backend/src/routes/notes.py @@ -1,6 +1,6 @@ -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 @@ -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( 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..2a06dd5 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 @@ -18,3 +18,4 @@ 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 @@ From d7766edbc6749db9eea926fcd3b26ac1f291cdf5 Mon Sep 17 00:00:00 2001 From: Acheng <3496079213@qq.com> Date: Wed, 15 Apr 2026 15:01:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(notes):=20=E6=B7=BB=E5=8A=A0NoteCreate?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B9=B6=E4=BC=98=E5=8C=96=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加NoteCreate模型用于创建笔记时的输入验证 重构标签处理逻辑,使用tag_ids查询标签对象后再关联 更新相关路由和CRUD操作以使用新的模型和逻辑 --- services/backend/db.sqlite3-shm | Bin 32768 -> 32768 bytes services/backend/db.sqlite3-wal | Bin 65952 -> 177192 bytes services/backend/src/auth/jwthandler.py | 2 +- services/backend/src/crud/notes.py | 12 ++- services/backend/src/crud/users.py | 6 +- services/backend/src/routes/notes.py | 4 +- services/backend/src/schemas/notes.py | 6 ++ test_api.py | 138 ++++++++++++++++++++++++ 8 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 test_api.py diff --git a/services/backend/db.sqlite3-shm b/services/backend/db.sqlite3-shm index b8e435bd0ad18215e720db2353b392a8e9638f69..e0963b0894f0ce75249dd1668a35d48f1a69d618 100644 GIT binary patch delta 341 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 MB0vzO>0^%Ie_$K7>;M1& delta 578 zcmZ4Sf@?uDi-mbTTN8usBMAlu0R|9I_EAyXXK^9?8@In1>*4sBKp{qC@y!2=)>xh8 zked9UKz?KROMU^Eh9?hRK08<+z_>X;fI~nKCc1=us+Ij?>&sl50|l4_L|K4FvB2c^ zaNA6owB=OX<^}@}0k}E;a~Ij$wz=N}S+IG#1Ck9N#NFTOem@|)c|!n_4c`M8mi{;@ z!Ope0(SRLcOkB^}6P~qFnVTOJAQ^rpF~jQU=IeV zzpr)9EuOYC*MS!lzWl`u{KcCS1v2?l8@U)c7=(ozS$%yCjf{av#VAR|&`2fGEHcB% zx3ntKQ_sK5**((IHQiFb)IT88z0AF=D$y}DGBd0)EGaLwqR`(stS~6OA}K7_$iT=< z*T6*A&{V;|%*x2v%D_O+*u=za`niRSVkQW_xskSk0Z NoteOutSchema: async def create_note(note, current_user) -> NoteOutSchema: note_dict = note.dict(exclude_unset=True) note_dict["author_id"] = current_user.id - tags = note_dict.pop("tags", []) + tag_ids = note_dict.pop("tags", []) note_obj = await Notes.create(**note_dict) - if tags: + 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) @@ -34,14 +35,15 @@ async def update_note(note_id, note, current_user) -> NoteOutSchema: if db_note.author.id == current_user.id: note_dict = note.dict(exclude_unset=True) - tags = note_dict.pop("tags", None) + tag_ids = note_dict.pop("tags", None) if note_dict: await Notes.filter(id=note_id).update(**note_dict) - if tags is not None: + if tag_ids is not None: note_obj = await Notes.get(id=note_id) await note_obj.tags.clear() - if tags: + 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)) 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/routes/notes.py b/services/backend/src/routes/notes.py index 21880a5..07b7468 100644 --- a/services/backend/src/routes/notes.py +++ b/services/backend/src/routes/notes.py @@ -6,7 +6,7 @@ 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 @@ -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/schemas/notes.py b/services/backend/src/schemas/notes.py index 2a06dd5..f946e0f 100644 --- a/services/backend/src/schemas/notes.py +++ b/services/backend/src/schemas/notes.py @@ -15,6 +15,12 @@ ) +class NoteCreate(BaseModel): + title: str + content: str + tags: Optional[List[int]] = [] + + class UpdateNote(BaseModel): title: Optional[str] content: Optional[str] 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())