Skip to content

Commit 6d5e741

Browse files
authored
Optimize api with semantic HTTP status codes (#681)
1 parent f9bfe8f commit 6d5e741

File tree

20 files changed

+91
-62
lines changed

20 files changed

+91
-62
lines changed

backend/app/admin/service/auth_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def login(
9393
user = await self.user_verify(db, obj.username, obj.password)
9494
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
9595
if not captcha_code:
96-
raise errors.ForbiddenError(msg='验证码失效,请重新获取')
96+
raise errors.RequestError(msg='验证码失效,请重新获取')
9797
if captcha_code.lower() != obj.captcha.lower():
9898
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
9999
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
@@ -122,7 +122,7 @@ async def login(
122122
except errors.NotFoundError as e:
123123
log.error('登陆错误: 用户名不存在')
124124
raise errors.NotFoundError(msg=e.msg)
125-
except (errors.ForbiddenError, errors.CustomError) as e:
125+
except (errors.RequestError, errors.CustomError) as e:
126126
if not user:
127127
log.error('登陆错误: 用户密码有误')
128128
task = BackgroundTask(

backend/app/admin/service/data_rule_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async def create(*, obj: CreateDataRuleParam) -> None:
8787
async with async_db_session.begin() as db:
8888
data_rule = await data_rule_dao.get_by_name(db, obj.name)
8989
if data_rule:
90-
raise errors.ForbiddenError(msg='数据规则已存在')
90+
raise errors.ConflictError(msg='数据规则已存在')
9191
await data_rule_dao.create(db, obj)
9292

9393
@staticmethod
@@ -105,7 +105,7 @@ async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
105105
raise errors.NotFoundError(msg='数据规则不存在')
106106
if data_rule.name != obj.name:
107107
if await data_rule_dao.get_by_name(db, obj.name):
108-
raise errors.ForbiddenError(msg='数据规则已存在')
108+
raise errors.ConflictError(msg='数据规则已存在')
109109
count = await data_rule_dao.update(db, pk, obj)
110110
return count
111111

backend/app/admin/service/data_scope_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def create(*, obj: CreateDataScopeParam) -> None:
7878
async with async_db_session.begin() as db:
7979
data_scope = await data_scope_dao.get_by_name(db, obj.name)
8080
if data_scope:
81-
raise errors.ForbiddenError(msg='数据范围已存在')
81+
raise errors.ConflictError(msg='数据范围已存在')
8282
await data_scope_dao.create(db, obj)
8383

8484
@staticmethod
@@ -96,7 +96,7 @@ async def update(*, pk: int, obj: UpdateDataScopeParam) -> int:
9696
raise errors.NotFoundError(msg='数据范围不存在')
9797
if data_scope.name != obj.name:
9898
if await data_scope_dao.get_by_name(db, obj.name):
99-
raise errors.ForbiddenError(msg='数据范围已存在')
99+
raise errors.ConflictError(msg='数据范围已存在')
100100
count = await data_scope_dao.update(db, pk, obj)
101101
for role in await data_scope.awaitable_attrs.roles:
102102
for user in await role.awaitable_attrs.users:

backend/app/admin/service/dept_service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def create(*, obj: CreateDeptParam) -> None:
6161
async with async_db_session.begin() as db:
6262
dept = await dept_dao.get_by_name(db, obj.name)
6363
if dept:
64-
raise errors.ForbiddenError(msg='部门名称已存在')
64+
raise errors.ConflictError(msg='部门名称已存在')
6565
if obj.parent_id:
6666
parent_dept = await dept_dao.get(db, obj.parent_id)
6767
if not parent_dept:
@@ -83,7 +83,7 @@ async def update(*, pk: int, obj: UpdateDeptParam) -> int:
8383
raise errors.NotFoundError(msg='部门不存在')
8484
if dept.name != obj.name:
8585
if await dept_dao.get_by_name(db, obj.name):
86-
raise errors.ForbiddenError(msg='部门名称已存在')
86+
raise errors.ConflictError(msg='部门名称已存在')
8787
if obj.parent_id:
8888
parent_dept = await dept_dao.get(db, obj.parent_id)
8989
if not parent_dept:
@@ -104,10 +104,10 @@ async def delete(*, pk: int) -> int:
104104
async with async_db_session.begin() as db:
105105
dept = await dept_dao.get_with_relation(db, pk)
106106
if dept.users:
107-
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
107+
raise errors.ConflictError(msg='部门下存在用户,无法删除')
108108
children = await dept_dao.get_children(db, pk)
109109
if children:
110-
raise errors.ForbiddenError(msg='部门下存在子部门,无法删除')
110+
raise errors.ConflictError(msg='部门下存在子部门,无法删除')
111111
count = await dept_dao.delete(db, pk)
112112
for user in dept.users:
113113
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')

backend/app/admin/service/menu_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def create(*, obj: CreateMenuParam) -> None:
7878
async with async_db_session.begin() as db:
7979
title = await menu_dao.get_by_title(db, obj.title)
8080
if title:
81-
raise errors.ForbiddenError(msg='菜单标题已存在')
81+
raise errors.ConflictError(msg='菜单标题已存在')
8282
if obj.parent_id:
8383
parent_menu = await menu_dao.get(db, obj.parent_id)
8484
if not parent_menu:
@@ -100,7 +100,7 @@ async def update(*, pk: int, obj: UpdateMenuParam) -> int:
100100
raise errors.NotFoundError(msg='菜单不存在')
101101
if menu.title != obj.title:
102102
if await menu_dao.get_by_title(db, obj.title):
103-
raise errors.ForbiddenError(msg='菜单标题已存在')
103+
raise errors.ConflictError(msg='菜单标题已存在')
104104
if obj.parent_id:
105105
parent_menu = await menu_dao.get(db, obj.parent_id)
106106
if not parent_menu:
@@ -124,7 +124,7 @@ async def delete(*, pk: int) -> int:
124124
async with async_db_session.begin() as db:
125125
children = await menu_dao.get_children(db, pk)
126126
if children:
127-
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
127+
raise errors.ConflictError(msg='菜单下存在子菜单,无法删除')
128128
menu = await menu_dao.get(db, pk)
129129
count = await menu_dao.delete(db, pk)
130130
if menu:

backend/app/admin/service/plugin_service.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,24 @@ async def install_zip(*, file: UploadFile) -> None:
5555
contents = await file.read()
5656
file_bytes = io.BytesIO(contents)
5757
if not zipfile.is_zipfile(file_bytes):
58-
raise errors.ForbiddenError(msg='插件压缩包格式非法')
58+
raise errors.RequestError(msg='插件压缩包格式非法')
5959
with zipfile.ZipFile(file_bytes) as zf:
6060
# 校验压缩包
6161
plugin_namelist = zf.namelist()
6262
plugin_name = plugin_namelist[0].split('/')[0]
6363
if not plugin_namelist or plugin_name not in file.filename:
64-
raise errors.ForbiddenError(msg='插件压缩包内容非法')
64+
raise errors.RequestError(msg='插件压缩包内容非法')
6565
if (
6666
len(plugin_namelist) <= 3
6767
or f'{plugin_name}/plugin.toml' not in plugin_namelist
6868
or f'{plugin_name}/README.md' not in plugin_namelist
6969
):
70-
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
70+
raise errors.RequestError(msg='插件压缩包内缺少必要文件')
7171

7272
# 插件是否可安装
7373
full_plugin_path = os.path.join(PLUGIN_DIR, plugin_name)
7474
if os.path.exists(full_plugin_path):
75-
raise errors.ForbiddenError(msg='此插件已安装')
75+
raise errors.ConflictError(msg='此插件已安装')
7676
else:
7777
os.makedirs(full_plugin_path, exist_ok=True)
7878

@@ -99,11 +99,11 @@ async def install_git(*, repo_url: str):
9999
"""
100100
match = is_git_url(repo_url)
101101
if not match:
102-
raise errors.ForbiddenError(msg='Git 仓库地址格式非法')
102+
raise errors.RequestError(msg='Git 仓库地址格式非法')
103103
repo_name = match.group('repo')
104104
plugins = await redis_client.lrange(settings.PLUGIN_REDIS_PREFIX, 0, -1)
105105
if repo_name in plugins:
106-
raise errors.ForbiddenError(msg=f'{repo_name} 插件已安装')
106+
raise errors.ConflictError(msg=f'{repo_name} 插件已安装')
107107
try:
108108
porcelain.clone(repo_url, os.path.join(PLUGIN_DIR, repo_name), checkout=True)
109109
except Exception as e:
@@ -124,11 +124,11 @@ async def install(self, *, type: PluginType, file: UploadFile | None = None, rep
124124
"""
125125
if type == PluginType.zip:
126126
if not file:
127-
raise errors.ForbiddenError(msg='ZIP 压缩包不能为空')
127+
raise errors.RequestError(msg='ZIP 压缩包不能为空')
128128
await self.install_zip(file=file)
129129
elif type == PluginType.git:
130130
if not repo_url:
131-
raise errors.ForbiddenError(msg='Git 仓库地址不能为空')
131+
raise errors.RequestError(msg='Git 仓库地址不能为空')
132132
await self.install_git(repo_url=repo_url)
133133

134134
@staticmethod
@@ -141,7 +141,7 @@ async def uninstall(*, plugin: str):
141141
"""
142142
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
143143
if not os.path.exists(plugin_dir):
144-
raise errors.ForbiddenError(msg='插件不存在')
144+
raise errors.NotFoundError(msg='插件不存在')
145145
await uninstall_requirements_async(plugin)
146146
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
147147
shutil.move(plugin_dir, bacup_dir)
@@ -159,7 +159,7 @@ async def update_status(*, plugin: str):
159159
"""
160160
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
161161
if not plugin_info:
162-
raise errors.ForbiddenError(msg='插件不存在')
162+
raise errors.NotFoundError(msg='插件不存在')
163163
plugin_info = json.loads(plugin_info)
164164

165165
# 更新持久缓存状态
@@ -184,7 +184,7 @@ async def build(*, plugin: str) -> io.BytesIO:
184184
"""
185185
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
186186
if not os.path.exists(plugin_dir):
187-
raise errors.ForbiddenError(msg='插件不存在')
187+
raise errors.NotFoundError(msg='插件不存在')
188188

189189
bio = io.BytesIO()
190190
with zipfile.ZipFile(bio, 'w') as zf:

backend/app/admin/service/role_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async def create(*, obj: CreateRoleParam) -> None:
9898
async with async_db_session.begin() as db:
9999
role = await role_dao.get_by_name(db, obj.name)
100100
if role:
101-
raise errors.ForbiddenError(msg='角色已存在')
101+
raise errors.ConflictError(msg='角色已存在')
102102
await role_dao.create(db, obj)
103103

104104
@staticmethod
@@ -116,7 +116,7 @@ async def update(*, pk: int, obj: UpdateRoleParam) -> int:
116116
raise errors.NotFoundError(msg='角色不存在')
117117
if role.name != obj.name:
118118
if await role_dao.get_by_name(db, obj.name):
119-
raise errors.ForbiddenError(msg='角色已存在')
119+
raise errors.ConflictError(msg='角色已存在')
120120
count = await role_dao.update(db, pk, obj)
121121
for user in await role.awaitable_attrs.users:
122122
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')

backend/app/admin/service/user_service.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ async def create(*, request: Request, obj: AddUserParam) -> None:
8181
async with async_db_session.begin() as db:
8282
superuser_verify(request)
8383
if await user_dao.get_by_username(db, obj.username):
84-
raise errors.ForbiddenError(msg='用户名已注册')
84+
raise errors.ConflictError(msg='用户名已注册')
8585
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(88888, 99999)}'
8686
if not obj.password:
87-
raise errors.ForbiddenError(msg='密码不允许为空')
87+
raise errors.RequestError(msg='密码不允许为空')
8888
if not await dept_dao.get(db, obj.dept_id):
8989
raise errors.NotFoundError(msg='部门不存在')
9090
for role_id in obj.roles:
@@ -110,7 +110,7 @@ async def update(*, request: Request, pk: int, obj: UpdateUserParam) -> int:
110110
raise errors.ForbiddenError(msg='只能修改自己的信息')
111111
if obj.username != user.username:
112112
if await user_dao.get_by_username(db, obj.username):
113-
raise errors.ForbiddenError(msg='用户名已注册')
113+
raise errors.ConflictError(msg='用户名已注册')
114114
for role_id in obj.roles:
115115
if not await role_dao.get(db, role_id):
116116
raise errors.NotFoundError(msg='角色不存在')
@@ -222,16 +222,17 @@ async def update_permission(self, *, request: Request, pk: int, type: UserPermis
222222
:param type: 权限类型
223223
:return:
224224
"""
225-
if type == UserPermissionType.superuser:
226-
count = await self.update_superuser(request=request, pk=pk)
227-
elif type == UserPermissionType.staff:
228-
count = await self.update_staff(request=request, pk=pk)
229-
elif type == UserPermissionType.status:
230-
count = await self.update_status(request=request, pk=pk)
231-
elif type == UserPermissionType.multi_login:
232-
count = await self.update_multi_login(request=request, pk=pk)
233-
else:
234-
raise errors.ForbiddenError(msg='权限类型不存在')
225+
match type:
226+
case UserPermissionType.superuser:
227+
count = await self.update_superuser(request=request, pk=pk)
228+
case UserPermissionType.staff:
229+
count = await self.update_staff(request=request, pk=pk)
230+
case UserPermissionType.status:
231+
count = await self.update_status(request=request, pk=pk)
232+
case UserPermissionType.multi_login:
233+
count = await self.update_multi_login(request=request, pk=pk)
234+
case _:
235+
raise errors.RequestError(msg='权限类型不存在')
235236
return count
236237

237238
@staticmethod
@@ -248,9 +249,9 @@ async def reset_pwd(*, pk: int, obj: ResetPasswordParam) -> int:
248249
if not user:
249250
raise errors.NotFoundError(msg='用户不存在')
250251
if not password_verify(obj.old_password, user.password):
251-
raise errors.ForbiddenError(msg='原密码错误')
252+
raise errors.RequestError(msg='原密码错误')
252253
if obj.new_password != obj.confirm_password:
253-
raise errors.ForbiddenError(msg='密码输入不一致')
254+
raise errors.RequestError(msg='密码输入不一致')
254255
new_pwd = get_hash_password(obj.new_password, user.salt)
255256
count = await user_dao.reset_password(db, user.id, new_pwd)
256257
key_prefix = [

backend/app/task/service/task_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async def get_all() -> list[str]:
3939
"""获取所有已注册的 Celery 任务列表"""
4040
registered_tasks = await run_in_threadpool(celery_app.control.inspect().registered)
4141
if not registered_tasks:
42-
raise errors.ForbiddenError(msg='Celery 服务未启动')
42+
raise errors.ServerError(msg='Celery 服务未启动')
4343
tasks = list(registered_tasks.values())[0]
4444
return tasks
4545

backend/common/exception/errors.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,12 @@ class TokenError(HTTPError):
9898

9999
def __init__(self, *, msg: str = 'Not Authenticated', headers: dict[str, Any] | None = None):
100100
super().__init__(code=self.code, msg=msg, headers=headers or {'WWW-Authenticate': 'Bearer'})
101+
102+
103+
class ConflictError(BaseExceptionMixin):
104+
"""资源冲突异常"""
105+
106+
code = StandardResponseCode.HTTP_409
107+
108+
def __init__(self, *, msg: str = 'Conflict', data: Any = None, background: BackgroundTask | None = None):
109+
super().__init__(msg=msg, data=data, background=background)

0 commit comments

Comments
 (0)