Skip to content

Commit 0e43866

Browse files
committed
♻️ refactor(users): 重构用户模块使用服务层
- 将用户相关数据库操作迁移至UserService类 - 在路由层使用服务类方法替代直接CRUD操作 - 添加用户密码更新、删除等业务逻辑的集中处理 - 统一异常处理流程使用try/catch块 ♻️ refactor(items): 重构项目模块模型方法 - 在Item模型添加CRUD类方法 - 实现项目查询的分页和权限检查逻辑 - 分离数据库操作到模型层方法 ✨ feat(service): 添加用户和项目服务层 - 创建UserService处理用户相关业务逻辑 - 创建ItemService处理项目CRUD操作 - 实现完整的权限验证流程 - 添加服务层的单元测试支持 ♻️ refactor(models): 调整模型文件结构 - 拆分原models模块为独立model包 - 将User和Item模型移至对应子模块 - 更新所有相关模块的导入路径 - 添加模型间的关联关系定义 ♻️ refactor(tests): 更新测试用例依赖 - 调整测试用例使用新的模型导入路径 - 重构测试工具函数使用服务层 - 保持测试覆盖率不变的情况下优化测试结构
1 parent ad47394 commit 0e43866

File tree

7 files changed

+256
-88
lines changed

7 files changed

+256
-88
lines changed

backend/app/api/routes/users.py

Lines changed: 55 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@
44
from fastapi import APIRouter, Depends, HTTPException
55
from sqlmodel import col, delete, func, select
66

7-
from app import crud
7+
from app.service.user_service import UserService
88
from app.api.deps import (
99
CurrentUser,
1010
SessionDep,
1111
get_current_active_superuser,
1212
)
1313
from app.core.config import settings
1414
from app.core.security import get_password_hash, verify_password
15-
from app.models import (
16-
Item,
17-
Message,
15+
from app.models import Message
16+
from app.utils import generate_new_account_email, send_email
17+
from app.model.users import (
1818
UpdatePassword,
1919
User,
2020
UserCreate,
2121
UserPublic,
2222
UserRegister,
23-
UsersPublic,
2423
UserUpdate,
2524
UserUpdateMe,
25+
UsersPublic,
2626
)
27-
from app.utils import generate_new_account_email, send_email
27+
from app.model.items import Item
2828

2929
router = APIRouter(prefix="/users", tags=["users"])
3030

@@ -38,14 +38,9 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
3838
"""
3939
Retrieve users.
4040
"""
41-
42-
count_statement = select(func.count()).select_from(User)
43-
count = session.exec(count_statement).one()
44-
45-
statement = select(User).offset(skip).limit(limit)
46-
users = session.exec(statement).all()
47-
48-
return UsersPublic(data=users, count=count)
41+
user_service = UserService(session)
42+
result = user_service.get_users(skip=skip, limit=limit)
43+
return UsersPublic(data=result["data"], count=result["count"])
4944

5045

5146
@router.post(
@@ -55,14 +50,15 @@ def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
5550
"""
5651
Create new user.
5752
"""
58-
user = crud.get_user_by_email(session=session, email=user_in.email)
53+
user_service = UserService(session)
54+
user = user_service.get_user_by_email(email=user_in.email)
5955
if user:
6056
raise HTTPException(
6157
status_code=400,
6258
detail="The user with this email already exists in the system.",
6359
)
6460

65-
user = crud.create_user(session=session, user_create=user_in)
61+
user = user_service.create_user(user_in)
6662
if settings.emails_enabled and user_in.email:
6763
email_data = generate_new_account_email(
6864
email_to=user_in.email, username=user_in.email, password=user_in.password
@@ -82,19 +78,11 @@ def update_user_me(
8278
"""
8379
Update own user.
8480
"""
85-
86-
if user_in.email:
87-
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
88-
if existing_user and existing_user.id != current_user.id:
89-
raise HTTPException(
90-
status_code=409, detail="User with this email already exists"
91-
)
92-
user_data = user_in.model_dump(exclude_unset=True)
93-
current_user.sqlmodel_update(user_data)
94-
session.add(current_user)
95-
session.commit()
96-
session.refresh(current_user)
97-
return current_user
81+
user_service = UserService(session)
82+
try:
83+
return user_service.update_user_me(current_user, user_in)
84+
except ValueError as e:
85+
raise HTTPException(status_code=409, detail=str(e))
9886

9987

10088
@router.patch("/me/password", response_model=Message)
@@ -104,18 +92,16 @@ def update_password_me(
10492
"""
10593
Update own password.
10694
"""
107-
if not verify_password(body.current_password, current_user.hashed_password):
108-
raise HTTPException(status_code=400, detail="Incorrect password")
109-
if body.current_password == body.new_password:
110-
raise HTTPException(
111-
status_code=400, detail="New password cannot be the same as the current one"
95+
user_service = UserService(session)
96+
try:
97+
user_service.update_password(
98+
current_user,
99+
body.current_password,
100+
body.new_password
112101
)
113-
hashed_password = get_password_hash(body.new_password)
114-
current_user.hashed_password = hashed_password
115-
session.add(current_user)
116-
session.commit()
117-
return Message(message="Password updated successfully")
118-
102+
return Message(message="Password updated successfully")
103+
except ValueError as e:
104+
raise HTTPException(status_code=400, detail=str(e))
119105

120106
@router.get("/me", response_model=UserPublic)
121107
def read_user_me(current_user: CurrentUser) -> Any:
@@ -130,30 +116,33 @@ def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
130116
"""
131117
Delete own user.
132118
"""
133-
if current_user.is_superuser:
134-
raise HTTPException(
135-
status_code=403, detail="Super users are not allowed to delete themselves"
119+
120+
user_service = UserService(session)
121+
try:
122+
user_service.delete_user(
123+
str(current_user.id),
124+
str(current_user.id),
125+
current_user.is_superuser
136126
)
137-
statement = delete(Item).where(col(Item.owner_id) == current_user.id)
138-
session.exec(statement) # type: ignore
139-
session.delete(current_user)
140-
session.commit()
141-
return Message(message="User deleted successfully")
127+
return Message(message="User deleted successfully")
128+
except ValueError as e:
129+
raise HTTPException(status_code=403, detail=str(e))
142130

143131

144132
@router.post("/signup", response_model=UserPublic)
145133
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
146134
"""
147135
Create new user without the need to be logged in.
148136
"""
149-
user = crud.get_user_by_email(session=session, email=user_in.email)
137+
user_service = UserService(session)
138+
user = user_service.get_user_by_email(email=user_in.email)
150139
if user:
151140
raise HTTPException(
152141
status_code=400,
153142
detail="The user with this email already exists in the system",
154143
)
155144
user_create = UserCreate.model_validate(user_in)
156-
user = crud.create_user(session=session, user_create=user_create)
145+
user = user_service.create_user(user_create)
157146
return user
158147

159148

@@ -164,7 +153,10 @@ def read_user_by_id(
164153
"""
165154
Get a specific user by id.
166155
"""
167-
user = session.get(User, user_id)
156+
user_service = UserService(session)
157+
user = user_service.get_user_by_id(str(user_id))
158+
if not user:
159+
raise HTTPException(status_code=404, detail="User not found")
168160
if user == current_user:
169161
return user
170162
if not current_user.is_superuser:
@@ -189,22 +181,14 @@ def update_user(
189181
"""
190182
Update a user.
191183
"""
192-
193-
db_user = session.get(User, user_id)
184+
user_service = UserService(session)
185+
db_user = user_service.get_user_by_id(str(user_id))
194186
if not db_user:
195187
raise HTTPException(
196188
status_code=404,
197189
detail="The user with this id does not exist in the system",
198190
)
199-
if user_in.email:
200-
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
201-
if existing_user and existing_user.id != user_id:
202-
raise HTTPException(
203-
status_code=409, detail="User with this email already exists"
204-
)
205-
206-
db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in)
207-
return db_user
191+
return user_service.update_user(db_user=db_user, user_in=user_in)
208192

209193

210194
@router.delete("/{user_id}", dependencies=[Depends(get_current_active_superuser)])
@@ -214,15 +198,13 @@ def delete_user(
214198
"""
215199
Delete a user.
216200
"""
217-
user = session.get(User, user_id)
218-
if not user:
219-
raise HTTPException(status_code=404, detail="User not found")
220-
if user == current_user:
221-
raise HTTPException(
222-
status_code=403, detail="Super users are not allowed to delete themselves"
201+
user_service = UserService(session)
202+
try:
203+
user_service.delete_user(
204+
str(user_id),
205+
str(current_user.id),
206+
current_user.is_superuser
223207
)
224-
statement = delete(Item).where(col(Item.owner_id) == user_id)
225-
session.exec(statement) # type: ignore
226-
session.delete(user)
227-
session.commit()
228-
return Message(message="User deleted successfully")
208+
return Message(message="User deleted successfully")
209+
except ValueError as e:
210+
raise HTTPException(status_code=403, detail=str(e))

backend/app/model/items.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import uuid
2+
from typing import Any
3+
4+
from sqlmodel import Field, Relationship, SQLModel, Session, select, func
5+
from fastapi import HTTPException
26

37
from app.model.users import User
4-
from sqlmodel import Field, Relationship, SQLModel
8+
59

610

711
# Shared properties
@@ -19,7 +23,6 @@ class ItemCreate(ItemBase):
1923
class ItemUpdate(ItemBase):
2024
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
2125

22-
2326
# Database model, database table inferred from class name
2427
class Item(ItemBase, table=True):
2528
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
@@ -29,6 +32,59 @@ class Item(ItemBase, table=True):
2932
)
3033
owner: User | None = Relationship(back_populates="items")
3134

35+
@classmethod
36+
def create(cls, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> "Item":
37+
db_item = cls.model_validate(item_in, update={"owner_id": owner_id})
38+
session.add(db_item)
39+
session.commit()
40+
session.refresh(db_item)
41+
return db_item
42+
43+
@classmethod
44+
def get_items(
45+
cls, session: Session, owner_id: uuid.UUID, is_superuser: bool, skip: int = 0, limit: int = 100
46+
) -> Any:
47+
if is_superuser:
48+
count_statement = select(func.count()).select_from(cls)
49+
count = session.exec(count_statement).one()
50+
statement = select(cls).offset(skip).limit(limit)
51+
items = session.exec(statement).all()
52+
else:
53+
count_statement = (
54+
select(func.count())
55+
.select_from(cls)
56+
.where(cls.owner_id == owner_id)
57+
)
58+
count = session.exec(count_statement).one()
59+
statement = (
60+
select(cls)
61+
.where(cls.owner_id == owner_id)
62+
.offset(skip)
63+
.limit(limit)
64+
)
65+
items = session.exec(statement).all()
66+
return {"data": items, "count": count}
67+
68+
@classmethod
69+
def get_by_id(cls, session: Session, item_id: uuid.UUID) -> "Item | None":
70+
return session.get(cls, item_id)
71+
72+
@classmethod
73+
def update(cls, session: Session, item: "Item", item_in: ItemUpdate) -> "Item":
74+
update_dict = item_in.model_dump(exclude_unset=True)
75+
item.sqlmodel_update(update_dict)
76+
session.add(item)
77+
session.commit()
78+
session.refresh(item)
79+
return item
80+
81+
@classmethod
82+
def delete(cls, session: Session, item_id: uuid.UUID) -> None:
83+
item = session.get(cls, item_id)
84+
if item:
85+
session.delete(item)
86+
session.commit()
87+
3288

3389
# Properties to return via API, id is always required
3490
class ItemPublic(ItemBase):

backend/app/model/users.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import uuid
2+
from typing import Any, Optional
23

34
from pydantic import EmailStr
4-
from sqlmodel import Field, Relationship, SQLModel
5+
from sqlmodel import Field, Relationship, SQLModel, Session, select, func
6+
7+
from app.core.security import get_password_hash, verify_password
58

69

7-
# Shared properties
810
class UserBase(SQLModel):
911
email: EmailStr = Field(unique=True, index=True, max_length=255)
1012
is_active: bool = True
1113
is_superuser: bool = False
1214
full_name: str | None = Field(default=None, max_length=255)
1315

1416

15-
# Properties to receive via API on creation
1617
class UserCreate(UserBase):
1718
password: str = Field(min_length=8, max_length=40)
1819

@@ -38,15 +39,61 @@ class UpdatePassword(SQLModel):
3839
current_password: str = Field(min_length=8, max_length=40)
3940
new_password: str = Field(min_length=8, max_length=40)
4041

41-
42-
# Database model, database table inferred from class name
4342
class User(UserBase, table=True):
4443
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
4544
hashed_password: str
4645
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True) # type: ignore
4746

47+
@classmethod
48+
def create(cls, session: Session, user_create: UserCreate) -> "User":
49+
db_obj = cls.model_validate(
50+
user_create, update={"hashed_password": get_password_hash(user_create.password)}
51+
)
52+
session.add(db_obj)
53+
session.commit()
54+
session.refresh(db_obj)
55+
return db_obj
56+
57+
@classmethod
58+
def update(cls, session: Session, db_user: "User", user_in: UserUpdate) -> Any:
59+
user_data = user_in.model_dump(exclude_unset=True)
60+
extra_data = {}
61+
if "password" in user_data:
62+
password = user_data["password"]
63+
hashed_password = get_password_hash(password)
64+
extra_data["hashed_password"] = hashed_password
65+
db_user.sqlmodel_update(user_data, update=extra_data)
66+
session.add(db_user)
67+
session.commit()
68+
session.refresh(db_user)
69+
return db_user
70+
71+
@classmethod
72+
def get_by_email(cls, session: Session, email: str) -> "User | None":
73+
statement = select(cls).where(cls.email == email)
74+
return session.exec(statement).first()
75+
76+
@classmethod
77+
def get_by_id(cls, session: Session, user_id: str) -> "User | None":
78+
statement = select(cls).where(cls.id == uuid.UUID(user_id))
79+
return session.exec(statement).first()
80+
81+
@classmethod
82+
def get_users(cls, session: Session, skip: int = 0, limit: int = 100) -> dict:
83+
count_statement = select(func.count()).select_from(cls)
84+
count = session.exec(count_statement).one()
85+
statement = select(cls).offset(skip).limit(limit)
86+
users = session.exec(statement).all()
87+
return {"data": users, "count": count}
88+
89+
@classmethod
90+
def delete_user(cls, session: Session, user_id: str) -> None:
91+
user = cls.get_by_id(session, user_id)
92+
if user:
93+
session.delete(user)
94+
session.commit()
95+
4896

49-
# Properties to return via API, id is always required
5097
class UserPublic(UserBase):
5198
id: uuid.UUID
5299

0 commit comments

Comments
 (0)