已继续成组推进“统一鉴权 + 数据范围控制”主线:新增通用 token 鉴权依赖,订单/客户/司机任务/系统管理关键接口已补齐角色限制;业务员现仅可访问本人订单与客户,司机现仅可访问本人任务与轨迹,订单详情已按接口文档对业务员隐藏成本/利润等敏感字段。
This commit is contained in:
parent
2a36276793
commit
ca04891991
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Header
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_auth_service
|
||||
from backend.app.api.deps import get_auth_service, get_current_user
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.schemas.auth import LoginRequest
|
||||
from backend.app.schemas.common import success_payload
|
||||
@ -21,20 +21,15 @@ def login(
|
||||
|
||||
@router.get("/me")
|
||||
def me(
|
||||
authorization: str | None = Header(default=None),
|
||||
auth_service: AuthService = Depends(get_auth_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
token = (authorization or "").removeprefix("Bearer").strip()
|
||||
user = auth_service.get_me(token, session)
|
||||
if not user:
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
|
||||
raise AppException(code=ErrorCode.UNAUTHORIZED, message="未登录或登录失效", status_code=401)
|
||||
return success_payload(user)
|
||||
return success_payload(current_user)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout() -> dict:
|
||||
return success_payload({"success": True})
|
||||
def logout(
|
||||
authorization: str | None = Header(default=None),
|
||||
auth_service: AuthService = Depends(get_auth_service),
|
||||
) -> dict:
|
||||
token = (authorization or "").removeprefix("Bearer").strip()
|
||||
return success_payload(auth_service.logout(token))
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import require_roles
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
from backend.app.db import get_db_session
|
||||
@ -21,16 +22,20 @@ def list_customers(
|
||||
page_no: int = Query(default=1),
|
||||
page_size: int = Query(default=20),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
result = customer_service.list_customers(
|
||||
session,
|
||||
{
|
||||
customer_service.normalize_list_filters(
|
||||
{
|
||||
"customer_name": customer_name,
|
||||
"mobile": mobile,
|
||||
"customer_type": customer_type,
|
||||
"settlement_type": settlement_type,
|
||||
"salesman_id": salesman_id,
|
||||
},
|
||||
},
|
||||
current_user,
|
||||
),
|
||||
)
|
||||
start = max(page_no - 1, 0) * page_size
|
||||
result["list"] = result["list"][start : start + page_size]
|
||||
@ -43,18 +48,24 @@ def list_customers(
|
||||
def create_customer(
|
||||
payload: CreateCustomerRequest,
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(
|
||||
customer_service.create_customer(
|
||||
session=session,
|
||||
payload=payload.model_dump(),
|
||||
current_user=current_user,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{customer_id}")
|
||||
def get_customer(customer_id: int, session: Session = Depends(get_db_session)) -> dict:
|
||||
customer = customer_service.get_customer(customer_id, session)
|
||||
def get_customer(
|
||||
customer_id: int,
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
customer = customer_service.get_customer(customer_id, session, current_user)
|
||||
if not customer:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="客户不存在", status_code=404)
|
||||
return success_payload(customer)
|
||||
@ -64,5 +75,6 @@ def get_customer(customer_id: int, session: Session = Depends(get_db_session)) -
|
||||
def import_customers(
|
||||
payload: ImportCustomerRequest,
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(customer_service.import_customers_from_file(session, payload.model_dump()))
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
from fastapi import Depends, Header
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.services.audit_service import audit_service
|
||||
from backend.app.services.auth_service import auth_service
|
||||
from backend.app.services.config_service import config_service
|
||||
@ -16,6 +22,27 @@ def get_auth_service():
|
||||
return auth_service
|
||||
|
||||
|
||||
def get_current_user(
|
||||
authorization: str | None = Header(default=None),
|
||||
auth_service=Depends(get_auth_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
token = (authorization or "").removeprefix("Bearer").strip()
|
||||
user = auth_service.get_me(token, session)
|
||||
if not user:
|
||||
raise AppException(code=ErrorCode.UNAUTHORIZED, message="未登录或登录失效", status_code=401)
|
||||
return user
|
||||
|
||||
|
||||
def require_roles(*role_codes: str):
|
||||
def _require_roles(current_user: dict = Depends(get_current_user)) -> dict:
|
||||
if role_codes and current_user.get("role_code") not in role_codes:
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
|
||||
return current_user
|
||||
|
||||
return _require_roles
|
||||
|
||||
|
||||
def get_order_service():
|
||||
return order_service
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_logistics_service
|
||||
from backend.app.api.deps import get_logistics_service, require_roles
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.schemas.common import success_payload
|
||||
from backend.app.schemas.logistics import CreateLogisticsTaskRequest, CreateLogisticsTraceRequest, DriverTaskOperateRequest
|
||||
@ -19,6 +19,7 @@ def list_logistics_tasks(
|
||||
factory_id: int | None = Query(default=None),
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(
|
||||
logistics_service.list_tasks(
|
||||
@ -30,6 +31,7 @@ def list_logistics_tasks(
|
||||
"driver_id": driver_id,
|
||||
"factory_id": factory_id,
|
||||
},
|
||||
current_user=current_user,
|
||||
)
|
||||
)
|
||||
|
||||
@ -39,8 +41,9 @@ def create_logistics_task(
|
||||
payload: CreateLogisticsTaskRequest,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.create_task(payload.model_dump(), session))
|
||||
return success_payload(logistics_service.create_task(payload.model_dump(), session, current_user))
|
||||
|
||||
|
||||
@router.get("/api/logistics/tasks/{task_id}")
|
||||
@ -48,16 +51,18 @@ def get_logistics_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.get_task(task_id, session))
|
||||
return success_payload(logistics_service.get_task(task_id, session, current_user))
|
||||
|
||||
|
||||
@router.get("/api/driver/tasks")
|
||||
def list_driver_tasks(
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.list_driver_tasks(session))
|
||||
return success_payload(logistics_service.list_driver_tasks(session, current_user))
|
||||
|
||||
|
||||
@router.get("/api/driver/tasks/{task_id}")
|
||||
@ -65,8 +70,9 @@ def get_driver_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.get_driver_task(task_id, session))
|
||||
return success_payload(logistics_service.get_driver_task(task_id, session, current_user))
|
||||
|
||||
|
||||
@router.post("/api/driver/tasks/{task_id}/accept")
|
||||
@ -75,8 +81,9 @@ def accept_driver_task(
|
||||
payload: DriverTaskOperateRequest,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.accept_task(task_id, payload.model_dump(), session))
|
||||
return success_payload(logistics_service.accept_task(task_id, payload.model_dump(), session, current_user))
|
||||
|
||||
|
||||
@router.post("/api/driver/tasks/{task_id}/pickup")
|
||||
@ -85,8 +92,9 @@ def pickup_driver_task(
|
||||
payload: DriverTaskOperateRequest,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.pickup_task(task_id, payload.model_dump(), session))
|
||||
return success_payload(logistics_service.pickup_task(task_id, payload.model_dump(), session, current_user))
|
||||
|
||||
|
||||
@router.post("/api/driver/tasks/{task_id}/deliver")
|
||||
@ -95,8 +103,9 @@ def deliver_driver_task(
|
||||
payload: DriverTaskOperateRequest,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.deliver_task(task_id, payload.model_dump(), session))
|
||||
return success_payload(logistics_service.deliver_task(task_id, payload.model_dump(), session, current_user))
|
||||
|
||||
|
||||
@router.get("/api/logistics/{order_id}/trace")
|
||||
@ -104,8 +113,9 @@ def get_logistics_trace(
|
||||
order_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin", "driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.get_trace(order_id, session))
|
||||
return success_payload(logistics_service.get_trace(order_id, session, current_user))
|
||||
|
||||
|
||||
@router.post("/api/logistics/{order_id}/trace")
|
||||
@ -114,5 +124,6 @@ def create_logistics_trace(
|
||||
payload: CreateLogisticsTraceRequest,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin", "driver")),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.create_trace(order_id, payload.model_dump(), session))
|
||||
return success_payload(logistics_service.create_trace(order_id, payload.model_dump(), session, current_user))
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_order_service
|
||||
from backend.app.api.deps import get_order_service, require_roles
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
from backend.app.db import get_db_session
|
||||
@ -34,10 +34,12 @@ def list_orders(
|
||||
page_size: int = Query(default=20),
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
orders = order_service.list_orders(
|
||||
session,
|
||||
{
|
||||
order_service.normalize_list_filters(
|
||||
{
|
||||
"order_no": order_no,
|
||||
"order_status": order_status,
|
||||
"customer_name": customer_name,
|
||||
@ -47,7 +49,9 @@ def list_orders(
|
||||
"order_source": order_source,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
},
|
||||
},
|
||||
current_user,
|
||||
),
|
||||
)
|
||||
start = max(page_no - 1, 0) * page_size
|
||||
page_list = orders[start : start + page_size]
|
||||
@ -66,10 +70,11 @@ def create_order(
|
||||
payload: CreateOrderRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "admin")),
|
||||
) -> dict:
|
||||
if not payload.items:
|
||||
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
|
||||
order = order_service.create_order(payload.model_dump(), session)
|
||||
order = order_service.create_order(payload.model_dump(), session, current_user)
|
||||
return success_payload(order)
|
||||
|
||||
|
||||
@ -78,8 +83,9 @@ def get_order(
|
||||
order_id: int,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.get_order(order_id, session)
|
||||
order = order_service.get_order(order_id, session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -90,8 +96,9 @@ def submit_order(
|
||||
order_id: int,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.submit_order(order_id, session)
|
||||
order = order_service.submit_order(order_id, session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -103,8 +110,9 @@ def cancel_order(
|
||||
payload: CancelOrderRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.cancel_order(order_id, payload.model_dump(), session)
|
||||
order = order_service.cancel_order(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -116,8 +124,9 @@ def change_order_status(
|
||||
payload: ChangeOrderStatusRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.change_order_status(order_id, payload.model_dump(), session)
|
||||
order = order_service.change_order_status(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -129,8 +138,9 @@ def approve_order(
|
||||
payload: ApproveOrderRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.approve_order(order_id, payload.model_dump(), session)
|
||||
order = order_service.approve_order(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -142,8 +152,9 @@ def cancel_approve_order(
|
||||
payload: ApproveOrderRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.cancel_approve_order(order_id, payload.model_dump(), session)
|
||||
order = order_service.cancel_approve_order(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -155,8 +166,9 @@ def supplier_text(
|
||||
payload: SupplierTextRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.get_supplier_text(order_id, payload.model_dump(), session)
|
||||
order = order_service.get_supplier_text(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
@ -168,8 +180,9 @@ def confirm_supplier_text(
|
||||
payload: ConfirmSupplierTextRequest,
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
order = order_service.confirm_supplier_text(order_id, payload.model_dump(), session)
|
||||
order = order_service.confirm_supplier_text(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
return success_payload(order)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_system_service
|
||||
from backend.app.api.deps import get_system_service, require_roles
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.schemas.common import success_payload
|
||||
from backend.app.schemas.system import (
|
||||
@ -30,6 +30,7 @@ def list_users(
|
||||
page_size: int = Query(default=20),
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
result = system_service.list_users(
|
||||
session,
|
||||
@ -53,6 +54,7 @@ def create_user(
|
||||
payload: CreateUserRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.create_user(payload.model_dump(), session))
|
||||
|
||||
@ -63,6 +65,7 @@ def update_user(
|
||||
payload: UpdateUserRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.update_user(user_id, payload.model_dump(), session))
|
||||
|
||||
@ -73,6 +76,7 @@ def reset_password(
|
||||
payload: ResetPasswordRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.reset_password(user_id, payload.new_password, session))
|
||||
|
||||
@ -86,6 +90,7 @@ def list_roles(
|
||||
page_size: int = Query(default=20),
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
result = system_service.list_roles(
|
||||
session,
|
||||
@ -103,6 +108,7 @@ def create_role(
|
||||
payload: CreateRoleRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.create_role(payload.model_dump(), session))
|
||||
|
||||
@ -113,6 +119,7 @@ def update_role(
|
||||
payload: UpdateRoleRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.update_role(role_id, payload.model_dump(), session))
|
||||
|
||||
@ -122,6 +129,7 @@ def get_role_menus(
|
||||
role_id: int,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.get_role_menu_assignment(role_id, session))
|
||||
|
||||
@ -132,6 +140,7 @@ def assign_role_menus(
|
||||
payload: AssignRoleMenusRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.assign_role_menus(role_id, payload.model_dump(), session))
|
||||
|
||||
@ -142,6 +151,7 @@ def list_menus(
|
||||
menu_type: str | None = Query(default=None),
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(
|
||||
system_service.list_menus(
|
||||
@ -156,6 +166,7 @@ def create_menu(
|
||||
payload: CreateMenuRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.create_menu(payload.model_dump(), session))
|
||||
|
||||
@ -166,5 +177,6 @@ def update_menu(
|
||||
payload: UpdateMenuRequest,
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.update_menu(menu_id, payload.model_dump(), session))
|
||||
|
||||
45
backend/app/core/security.py
Normal file
45
backend/app/core/security.py
Normal file
@ -0,0 +1,45 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from backend.app.core.config import get_settings
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
secret = settings.secret_key.encode("utf-8")
|
||||
return hashlib.sha256(secret + password.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
return hmac.compare_digest(hash_password(password), password_hash or "")
|
||||
|
||||
|
||||
def create_access_token(payload: dict) -> str:
|
||||
data = dict(payload)
|
||||
expire_at = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
|
||||
data["exp"] = int(expire_at.timestamp())
|
||||
body = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
body_b64 = base64.urlsafe_b64encode(body).decode("utf-8").rstrip("=")
|
||||
signature = hmac.new(settings.secret_key.encode("utf-8"), body_b64.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
return f"{body_b64}.{signature}"
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> dict | None:
|
||||
try:
|
||||
body_b64, signature = token.split(".", 1)
|
||||
expected = hmac.new(settings.secret_key.encode("utf-8"), body_b64.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(signature, expected):
|
||||
return None
|
||||
padded = body_b64 + "=" * (-len(body_b64) % 4)
|
||||
payload = json.loads(base64.urlsafe_b64decode(padded.encode("utf-8")).decode("utf-8"))
|
||||
exp = int(payload.get("exp", 0))
|
||||
if exp <= int(datetime.now(timezone.utc).timestamp()):
|
||||
return None
|
||||
return payload
|
||||
except Exception:
|
||||
return None
|
||||
@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
from backend.app.core.security import create_access_token, decode_access_token, verify_password
|
||||
from backend.app.repositories.system_repository import SystemRepository
|
||||
from backend.app.services.demo_store import demo_store
|
||||
|
||||
@ -24,12 +25,13 @@ class AuthService:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.repository = SystemRepository()
|
||||
self.revoked_tokens: set[str] = set()
|
||||
|
||||
def login(self, username: str, password: str, role_type: str, session: Session | None = None) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
user = self.repository.get_user_by_username(session, username)
|
||||
if user is None or user.password_hash != password:
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
raise AppException(
|
||||
code=ErrorCode.UNAUTHORIZED,
|
||||
message="账号不存在或密码错误",
|
||||
@ -53,21 +55,32 @@ class AuthService:
|
||||
return demo_store.login(username, role_type)
|
||||
|
||||
def get_me(self, token: str, session: Session | None = None) -> dict | None:
|
||||
if session is not None and token.startswith("db-user-"):
|
||||
if session is not None:
|
||||
try:
|
||||
user_id = int(token.split("-")[2])
|
||||
if token in self.revoked_tokens:
|
||||
return None
|
||||
payload = decode_access_token(token)
|
||||
if payload is None:
|
||||
return None
|
||||
user_id = int(payload["user_id"])
|
||||
role_code = str(payload["role_code"])
|
||||
user = self.repository.get_user(session, user_id)
|
||||
if user is None or user.status != 1:
|
||||
return None
|
||||
role = self.repository.get_role(session, user.role_id)
|
||||
if role is None or role.status != 1:
|
||||
if role is None or role.status != 1 or role.role_code != role_code:
|
||||
return None
|
||||
return self._build_profile(session, user, role, include_token=False)
|
||||
except (SQLAlchemyError, ValueError, IndexError):
|
||||
except (SQLAlchemyError, ValueError, KeyError, TypeError):
|
||||
pass
|
||||
|
||||
return demo_store.get_me(token)
|
||||
|
||||
def logout(self, token: str) -> dict:
|
||||
if token:
|
||||
self.revoked_tokens.add(token)
|
||||
return {"success": True}
|
||||
|
||||
def _build_profile(self, session: Session, user, role, include_token: bool) -> dict:
|
||||
menus = self._load_role_menus(session, role.role_code)
|
||||
permissions = self._load_role_permissions(session, role.role_code)
|
||||
@ -79,7 +92,7 @@ class AuthService:
|
||||
"role_id": role.id,
|
||||
"role_name": role.role_name,
|
||||
"role_code": role.role_code,
|
||||
"token": f"db-user-{user.id}-{role.role_code}" if include_token else None,
|
||||
"token": create_access_token({"user_id": user.id, "role_code": role.role_code}) if include_token else None,
|
||||
"menus": menus,
|
||||
"permissions": permissions,
|
||||
}
|
||||
|
||||
@ -59,7 +59,12 @@ class CustomerService:
|
||||
],
|
||||
}
|
||||
|
||||
def create_customer(self, session: Session | None = None, payload: dict | None = None) -> dict:
|
||||
def create_customer(
|
||||
self,
|
||||
session: Session | None = None,
|
||||
payload: dict | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None and payload is not None:
|
||||
try:
|
||||
if not payload["customer_name"].strip():
|
||||
@ -84,7 +89,7 @@ class CustomerService:
|
||||
"settlement_type": payload.get("settlement_type"),
|
||||
"settlement_days": payload.get("settlement_days", 0),
|
||||
"customer_type": payload.get("customer_type"),
|
||||
"salesman_id": payload.get("salesman_id"),
|
||||
"salesman_id": self._resolve_salesman_id(payload, current_user),
|
||||
"credit_limit": payload.get("credit_limit", 0),
|
||||
"remark": payload.get("remark"),
|
||||
},
|
||||
@ -107,11 +112,17 @@ class CustomerService:
|
||||
"mobile": "13800000009",
|
||||
}
|
||||
|
||||
def get_customer(self, customer_id: int, session: Session | None = None) -> dict:
|
||||
def get_customer(
|
||||
self,
|
||||
customer_id: int,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
customer = self.repository.get_customer(session, customer_id)
|
||||
if customer is not None:
|
||||
self._ensure_customer_access(customer, current_user)
|
||||
return {
|
||||
"customer_id": customer.id,
|
||||
"customer_name": customer.customer_name,
|
||||
@ -214,5 +225,28 @@ class CustomerService:
|
||||
"fail_list": [{"row_no": 12, "reason": "手机号为空"}],
|
||||
}
|
||||
|
||||
def normalize_list_filters(self, filters: dict | None, current_user: dict | None = None) -> dict:
|
||||
normalized = dict(filters or {})
|
||||
if current_user and current_user.get("role_code") == "salesman":
|
||||
normalized["salesman_id"] = current_user.get("user_id")
|
||||
return normalized
|
||||
|
||||
def _resolve_salesman_id(self, payload: dict, current_user: dict | None) -> int | None:
|
||||
if current_user and current_user.get("role_code") == "salesman":
|
||||
return current_user.get("user_id")
|
||||
return payload.get("salesman_id")
|
||||
|
||||
def _ensure_customer_access(self, customer: object, current_user: dict | None) -> None:
|
||||
if not current_user:
|
||||
return
|
||||
role_code = current_user.get("role_code")
|
||||
if role_code in {"admin", "manager"}:
|
||||
return
|
||||
if role_code == "salesman":
|
||||
if customer.salesman_id != current_user.get("user_id"):
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问他人客户", status_code=403)
|
||||
return
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
|
||||
|
||||
|
||||
customer_service = CustomerService()
|
||||
|
||||
@ -17,10 +17,18 @@ class LogisticsService:
|
||||
self.order_repository = OrderRepository()
|
||||
self.trace_records: dict[int, list[dict]] = {}
|
||||
|
||||
def list_tasks(self, session: Session | None = None, filters: dict | None = None) -> dict:
|
||||
def list_tasks(
|
||||
self,
|
||||
session: Session | None = None,
|
||||
filters: dict | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
tasks = self.logistics_repository.list_tasks_by_filters(session, filters or {})
|
||||
tasks = self.logistics_repository.list_tasks_by_filters(
|
||||
session,
|
||||
self.normalize_task_filters(filters, current_user),
|
||||
)
|
||||
return {
|
||||
"total": len(tasks),
|
||||
"page_no": 1,
|
||||
@ -31,7 +39,12 @@ class LogisticsService:
|
||||
pass
|
||||
return demo_store.list_logistics_tasks()
|
||||
|
||||
def create_task(self, payload: dict, session: Session | None = None) -> dict:
|
||||
def create_task(
|
||||
self,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, payload["order_id"])
|
||||
@ -56,7 +69,7 @@ class LogisticsService:
|
||||
"pickup_content": payload["pickup_content"],
|
||||
"quantity": payload["quantity"],
|
||||
"status": "pending",
|
||||
"created_by": None,
|
||||
"created_by": current_user.get("user_id") if current_user else None,
|
||||
"remark": payload.get("remark"),
|
||||
},
|
||||
)
|
||||
@ -70,12 +83,18 @@ class LogisticsService:
|
||||
|
||||
return demo_store.create_logistics_task(payload)
|
||||
|
||||
def get_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
def get_task(
|
||||
self,
|
||||
task_id: int,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
task = self.logistics_repository.get_task(session, task_id)
|
||||
if task is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404)
|
||||
self._ensure_task_access(task, current_user)
|
||||
return self._map_task_detail(task, session)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
@ -84,14 +103,20 @@ class LogisticsService:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404)
|
||||
return task
|
||||
|
||||
def list_driver_tasks(self, session: Session | None = None) -> dict:
|
||||
return self.list_tasks(session=session, filters={})
|
||||
def list_driver_tasks(self, session: Session | None = None, current_user: dict | None = None) -> dict:
|
||||
return self.list_tasks(session=session, filters={}, current_user=current_user)
|
||||
|
||||
def get_driver_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
return self.get_task(task_id, session=session)
|
||||
def get_driver_task(self, task_id: int, session: Session | None = None, current_user: dict | None = None) -> dict:
|
||||
return self.get_task(task_id, session=session, current_user=current_user)
|
||||
|
||||
def accept_task(self, task_id: int, payload: dict | None = None, session: Session | None = None) -> dict:
|
||||
result = self._change_task_status(task_id, "pending", "accepted", "accepted", session)
|
||||
def accept_task(
|
||||
self,
|
||||
task_id: int,
|
||||
payload: dict | None = None,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
result = self._change_task_status(task_id, "pending", "accepted", "accepted", session, current_user)
|
||||
self._append_trace(
|
||||
result["order_id"],
|
||||
{
|
||||
@ -105,8 +130,14 @@ class LogisticsService:
|
||||
)
|
||||
return result
|
||||
|
||||
def pickup_task(self, task_id: int, payload: dict | None = None, session: Session | None = None) -> dict:
|
||||
result = self._change_task_status(task_id, "accepted", "picked_up", "picked_up", session)
|
||||
def pickup_task(
|
||||
self,
|
||||
task_id: int,
|
||||
payload: dict | None = None,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
result = self._change_task_status(task_id, "accepted", "picked_up", "picked_up", session, current_user)
|
||||
self._append_trace(
|
||||
result["order_id"],
|
||||
{
|
||||
@ -122,14 +153,26 @@ class LogisticsService:
|
||||
)
|
||||
return result
|
||||
|
||||
def deliver_task(self, task_id: int, payload: dict | None = None, session: Session | None = None) -> dict:
|
||||
result = self._change_task_status(task_id, "picked_up", "delivered", "delivered", session)
|
||||
def deliver_task(
|
||||
self,
|
||||
task_id: int,
|
||||
payload: dict | None = None,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
result = self._change_task_status(task_id, "picked_up", "delivered", "delivered", session, current_user)
|
||||
self._append_trace(result["order_id"], self._build_trace_payload(task_id, "司机已送达", "delivered", payload))
|
||||
return result
|
||||
|
||||
def get_trace(self, order_id: int, session: Session | None = None) -> dict:
|
||||
def get_trace(
|
||||
self,
|
||||
order_id: int,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
self._ensure_trace_access(order_id, session, current_user)
|
||||
traces = self.logistics_repository.list_traces_by_order_id(session, order_id)
|
||||
if traces:
|
||||
return {
|
||||
@ -164,9 +207,16 @@ class LogisticsService:
|
||||
),
|
||||
}
|
||||
|
||||
def create_trace(self, order_id: int, payload: dict, session: Session | None = None) -> dict:
|
||||
def create_trace(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
self._ensure_trace_access(order_id, session, current_user)
|
||||
node_time = datetime.strptime(payload["node_time"], "%Y-%m-%d %H:%M:%S")
|
||||
self.logistics_repository.create_trace(
|
||||
session,
|
||||
@ -198,12 +248,14 @@ class LogisticsService:
|
||||
target_status: str,
|
||||
order_status: str,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
task = self.logistics_repository.get_task(session, task_id)
|
||||
if task is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404)
|
||||
self._ensure_task_access(task, current_user)
|
||||
if task.status != current_status:
|
||||
raise AppException(code=ErrorCode.INVALID_STATUS, message="当前任务状态不允许执行该操作", status_code=400)
|
||||
|
||||
@ -238,6 +290,37 @@ class LogisticsService:
|
||||
"order_id": task["order_id"] if task else 0,
|
||||
}
|
||||
|
||||
def normalize_task_filters(self, filters: dict | None, current_user: dict | None = None) -> dict:
|
||||
normalized = dict(filters or {})
|
||||
if current_user and current_user.get("role_code") == "driver":
|
||||
normalized["driver_id"] = current_user.get("user_id")
|
||||
return normalized
|
||||
|
||||
def _ensure_task_access(self, task: object, current_user: dict | None) -> None:
|
||||
if not current_user:
|
||||
return
|
||||
role_code = current_user.get("role_code")
|
||||
if role_code in {"admin", "manager"}:
|
||||
return
|
||||
if role_code == "driver":
|
||||
if task.driver_id != current_user.get("user_id"):
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人司机任务", status_code=403)
|
||||
return
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
|
||||
|
||||
def _ensure_trace_access(self, order_id: int, session: Session, current_user: dict | None) -> None:
|
||||
if not current_user:
|
||||
return
|
||||
role_code = current_user.get("role_code")
|
||||
if role_code in {"admin", "manager"}:
|
||||
return
|
||||
if role_code == "driver":
|
||||
task = self.logistics_repository.get_task_by_order_id(session, order_id)
|
||||
if task is None or task.driver_id != current_user.get("user_id"):
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人订单轨迹", status_code=403)
|
||||
return
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
|
||||
|
||||
def _map_task_summary(self, task, session: Session) -> dict:
|
||||
order = self.order_repository.get_order(session, task.order_id)
|
||||
supplier = self.order_repository.get_supplier(session, task.factory_id)
|
||||
|
||||
@ -43,17 +43,23 @@ class OrderService:
|
||||
pass
|
||||
return demo_store.list_orders()
|
||||
|
||||
def get_order(self, order_id: int, session: Session | None = None) -> dict | None:
|
||||
def get_order(
|
||||
self,
|
||||
order_id: int,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user)
|
||||
|
||||
supplier = self.order_repository.get_supplier(session, order.factory_id)
|
||||
items = self.order_repository.list_order_items(session, order.id)
|
||||
approve_logs = self.order_approve_log_repository.list_by_order_id(session, order.id)
|
||||
return {
|
||||
result = {
|
||||
"order_id": order.id,
|
||||
"order_no": order.order_no,
|
||||
"customer_id": order.customer_id,
|
||||
@ -111,16 +117,23 @@ class OrderService:
|
||||
"attachments": [],
|
||||
"remark": order.remark,
|
||||
}
|
||||
return self._filter_order_detail_by_role(result, current_user)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
return demo_store.get_order(order_id)
|
||||
|
||||
def submit_order(self, order_id: int, session: Session | None = None) -> dict | None:
|
||||
def submit_order(
|
||||
self,
|
||||
order_id: int,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user, owner_only=True)
|
||||
self._ensure_status(order.order_status, {"draft", "rejected"}, "当前状态不允许提交审核")
|
||||
self.order_repository.update_order_status(session, order, "pending_approve")
|
||||
session.commit()
|
||||
@ -140,12 +153,19 @@ class OrderService:
|
||||
order["submitted_at"] = "2026-05-14 12:30:00"
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: int, payload: dict, session: Session | None = None) -> dict | None:
|
||||
def cancel_order(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user, owner_only=True)
|
||||
|
||||
if order.order_status in {"canceled", "settled"}:
|
||||
raise AppException(
|
||||
@ -188,12 +208,19 @@ class OrderService:
|
||||
"canceled_at": None,
|
||||
}
|
||||
|
||||
def approve_order(self, order_id: int, payload: dict, session: Session | None = None) -> dict | None:
|
||||
def approve_order(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user)
|
||||
|
||||
self._ensure_status(order.order_status, {"pending_approve"}, "当前状态不允许审批")
|
||||
previous_status = order.order_status
|
||||
@ -238,12 +265,19 @@ class OrderService:
|
||||
"approve_opinion": payload.get("approve_opinion"),
|
||||
}
|
||||
|
||||
def cancel_approve_order(self, order_id: int, payload: dict, session: Session | None = None) -> dict | None:
|
||||
def cancel_approve_order(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user)
|
||||
|
||||
self._ensure_status(order.order_status, {"cancel_pending"}, "当前状态不允许取消审批")
|
||||
previous_status = order.order_status
|
||||
@ -282,12 +316,19 @@ class OrderService:
|
||||
"approve_opinion": payload.get("approve_opinion"),
|
||||
}
|
||||
|
||||
def get_supplier_text(self, order_id: int, payload: dict, session: Session | None = None) -> dict | None:
|
||||
def get_supplier_text(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user)
|
||||
self._ensure_status(order.order_status, {"approved"}, "当前状态不允许生成发厂文案")
|
||||
supplier = self.order_repository.get_supplier(session, payload["supplier_id"])
|
||||
if supplier is None:
|
||||
@ -312,12 +353,19 @@ class OrderService:
|
||||
"text_content": f"订单 {order['order_no']} 请安排生产与发货",
|
||||
}
|
||||
|
||||
def confirm_supplier_text(self, order_id: int, payload: dict, session: Session | None = None) -> dict | None:
|
||||
def confirm_supplier_text(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user)
|
||||
self._ensure_status(order.order_status, {"approved"}, "当前状态不允许确认发厂")
|
||||
supplier = self.order_repository.get_supplier(session, payload["supplier_id"])
|
||||
if supplier is None:
|
||||
@ -346,12 +394,19 @@ class OrderService:
|
||||
"confirmed_at": "2026-05-14 13:00:00",
|
||||
}
|
||||
|
||||
def change_order_status(self, order_id: int, payload: dict, session: Session | None = None) -> dict | None:
|
||||
def change_order_status(
|
||||
self,
|
||||
order_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, order_id)
|
||||
if order is None:
|
||||
return None
|
||||
self._ensure_order_access(order, current_user)
|
||||
|
||||
target_status = (payload.get("target_status") or "").strip()
|
||||
if not target_status:
|
||||
@ -406,7 +461,12 @@ class OrderService:
|
||||
"order_status": payload.get("target_status", order.get("order_status")),
|
||||
}
|
||||
|
||||
def create_order(self, payload: dict, session: Session | None = None) -> dict:
|
||||
def create_order(
|
||||
self,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
if not payload["customer_name"].strip():
|
||||
@ -459,7 +519,7 @@ class OrderService:
|
||||
"customer_name": payload["customer_name"],
|
||||
"customer_mobile": payload["customer_mobile"],
|
||||
"customer_address": payload.get("customer_address"),
|
||||
"salesman_id": None,
|
||||
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
|
||||
"order_status": "draft",
|
||||
"order_source": payload.get("order_source"),
|
||||
"delivery_type": payload.get("delivery_type"),
|
||||
@ -493,6 +553,44 @@ class OrderService:
|
||||
|
||||
return demo_store.create_order(payload)
|
||||
|
||||
def normalize_list_filters(self, filters: dict | None, current_user: dict | None = None) -> dict:
|
||||
normalized = dict(filters or {})
|
||||
if not current_user:
|
||||
return normalized
|
||||
if current_user.get("role_code") == "salesman":
|
||||
normalized["salesman_id"] = current_user.get("user_id")
|
||||
return normalized
|
||||
|
||||
def _ensure_order_access(self, order: object, current_user: dict | None, owner_only: bool = False) -> None:
|
||||
if not current_user:
|
||||
return
|
||||
role_code = current_user.get("role_code")
|
||||
if role_code in {"admin", "manager"}:
|
||||
return
|
||||
if role_code == "salesman":
|
||||
if order.salesman_id != current_user.get("user_id"):
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问他人订单", status_code=403)
|
||||
return
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
|
||||
|
||||
def _filter_order_detail_by_role(self, order: dict, current_user: dict | None) -> dict:
|
||||
if not current_user or current_user.get("role_code") != "salesman":
|
||||
return order
|
||||
filtered = dict(order)
|
||||
filtered["cost_price_total"] = None
|
||||
filtered["rebate_total"] = None
|
||||
filtered["profit_total"] = None
|
||||
filtered["profit_rate"] = None
|
||||
filtered["items"] = [
|
||||
{
|
||||
**item,
|
||||
"cost_price": None,
|
||||
"rebate_amount": None,
|
||||
}
|
||||
for item in order.get("items", [])
|
||||
]
|
||||
return filtered
|
||||
|
||||
def _ensure_status(self, current_status: str, allowed_statuses: set[str], message: str) -> None:
|
||||
if current_status not in allowed_statuses:
|
||||
raise AppException(
|
||||
|
||||
@ -3,7 +3,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
from backend.app.core.security import hash_password
|
||||
from backend.app.repositories.system_repository import SystemRepository
|
||||
from backend.app.services.audit_service import audit_service
|
||||
|
||||
|
||||
class SystemService:
|
||||
@ -65,7 +67,18 @@ class SystemService:
|
||||
session,
|
||||
{
|
||||
**payload,
|
||||
"password_hash": payload["password"],
|
||||
"password_hash": hash_password(payload["password"]),
|
||||
},
|
||||
)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_user_create",
|
||||
"biz_type": "sys_user",
|
||||
"biz_id": user.id,
|
||||
"before_value": None,
|
||||
"after_value": self._build_user_row(user, role),
|
||||
"remark": f"新增用户 {user.username}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
@ -105,7 +118,19 @@ class SystemService:
|
||||
if role is None or role.status != 1:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在或已停用", status_code=404)
|
||||
|
||||
before_snapshot = self._build_user_row(user, self.repository.get_role(session, user.role_id))
|
||||
self.repository.update_user(user, payload)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_user_update",
|
||||
"biz_type": "sys_user",
|
||||
"biz_id": user.id,
|
||||
"before_value": before_snapshot,
|
||||
"after_value": self._build_user_row(user, role),
|
||||
"remark": f"更新用户 {user.username}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {"user_id": user_id, "updated": True}
|
||||
except AppException:
|
||||
@ -125,7 +150,19 @@ class SystemService:
|
||||
if user is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="用户不存在", status_code=404)
|
||||
|
||||
self.repository.reset_password(user, new_password)
|
||||
before_hash = user.password_hash
|
||||
self.repository.reset_password(user, hash_password(new_password))
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_user_reset_password",
|
||||
"biz_type": "sys_user",
|
||||
"biz_id": user.id,
|
||||
"before_value": {"password_hash": before_hash},
|
||||
"after_value": {"password_hash": user.password_hash},
|
||||
"remark": f"重置用户密码 {user.username}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {"user_id": user_id, "reset": True}
|
||||
except AppException:
|
||||
@ -179,6 +216,17 @@ class SystemService:
|
||||
raise AppException(code=ErrorCode.DUPLICATE, message="角色编码已存在", status_code=400)
|
||||
|
||||
role = self.repository.create_role(session, payload)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_role_create",
|
||||
"biz_type": "sys_role",
|
||||
"biz_id": role.id,
|
||||
"before_value": None,
|
||||
"after_value": self._build_role_row(role),
|
||||
"remark": f"新增角色 {role.role_code}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {
|
||||
"role_id": role.id,
|
||||
@ -212,7 +260,19 @@ class SystemService:
|
||||
if payload.get("status", 1) != 1 and self.repository.count_active_users_by_role(session, role_id) > 0:
|
||||
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="当前角色下仍有启用用户", status_code=400)
|
||||
|
||||
before_snapshot = self._build_role_row(role)
|
||||
self.repository.update_role(role, payload)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_role_update",
|
||||
"biz_type": "sys_role",
|
||||
"biz_id": role.id,
|
||||
"before_value": before_snapshot,
|
||||
"after_value": self._build_role_row(role),
|
||||
"remark": f"更新角色 {role.role_code}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {"role_id": role_id, "updated": True}
|
||||
except AppException:
|
||||
@ -250,9 +310,25 @@ class SystemService:
|
||||
if role is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404)
|
||||
|
||||
before_assignment = self.get_role_menu_assignment(role_id, session)
|
||||
menu_ids = self._resolve_assign_menu_ids(session, payload)
|
||||
self._validate_assign_role_menus(session, role.role_code, menu_ids)
|
||||
self.repository.replace_role_menu_relations(session, role_id, menu_ids)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_role_assign_menus",
|
||||
"biz_type": "sys_role",
|
||||
"biz_id": role.id,
|
||||
"before_value": before_assignment,
|
||||
"after_value": {
|
||||
"role_id": role_id,
|
||||
"menu_ids": menu_ids,
|
||||
"permission_codes": payload.get("permission_codes", []),
|
||||
},
|
||||
"remark": f"更新角色授权 {role.role_code}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {
|
||||
"role_id": role_id,
|
||||
@ -324,6 +400,17 @@ class SystemService:
|
||||
raise AppException(code=ErrorCode.DUPLICATE, message="权限编码已存在", status_code=400)
|
||||
|
||||
menu = self.repository.create_menu(session, payload)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_menu_create",
|
||||
"biz_type": "sys_menu",
|
||||
"biz_id": menu.id,
|
||||
"before_value": None,
|
||||
"after_value": self._build_menu_row(menu),
|
||||
"remark": f"新增菜单 {menu.permission_code}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {
|
||||
"menu_id": menu.id,
|
||||
@ -358,7 +445,19 @@ class SystemService:
|
||||
if existed is not None and existed.id != menu_id:
|
||||
raise AppException(code=ErrorCode.DUPLICATE, message="权限编码已存在", status_code=400)
|
||||
|
||||
before_snapshot = self._build_menu_row(menu)
|
||||
self.repository.update_menu(menu, payload)
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "system_menu_update",
|
||||
"biz_type": "sys_menu",
|
||||
"biz_id": menu.id,
|
||||
"before_value": before_snapshot,
|
||||
"after_value": self._build_menu_row(menu),
|
||||
"remark": f"更新菜单 {menu.permission_code}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {"menu_id": menu_id, "updated": True}
|
||||
except AppException:
|
||||
@ -396,6 +495,29 @@ class SystemService:
|
||||
|
||||
return roots
|
||||
|
||||
def _build_role_row(self, role: object) -> dict:
|
||||
return {
|
||||
"role_id": role.id,
|
||||
"role_name": role.role_name,
|
||||
"role_code": role.role_code,
|
||||
"status": role.status,
|
||||
"remark": role.remark,
|
||||
"created_at": role.created_at.strftime("%Y-%m-%d %H:%M:%S") if getattr(role, "created_at", None) else "",
|
||||
}
|
||||
|
||||
def _build_menu_row(self, menu: object) -> dict:
|
||||
return {
|
||||
"menu_id": menu.id,
|
||||
"parent_id": menu.parent_id,
|
||||
"menu_name": menu.menu_name,
|
||||
"menu_path": menu.menu_path,
|
||||
"menu_type": menu.menu_type,
|
||||
"permission_code": menu.permission_code,
|
||||
"icon": menu.icon,
|
||||
"sort_no": menu.sort_no,
|
||||
"status": menu.status,
|
||||
}
|
||||
|
||||
def _validate_create_user(self, payload: dict) -> None:
|
||||
if not payload["username"].strip():
|
||||
raise AppException(code=ErrorCode.PARAM_ERROR, message="用户名不能为空", status_code=400)
|
||||
|
||||
@ -28,19 +28,19 @@
|
||||
|
||||
| 任务 | 来源 | 验证方式 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 认证接口 | 02-API细化设计 | 已有路由、service、统一异常;未接真实鉴权 | 待完成 |
|
||||
| 订单接口 | 02-API细化设计 | 已支持真实建单、列表筛选、详情、提交、取消、审批、发厂确认与状态变更,并在 `shipped/delivered` 等节点接入欠款生成;更细粒度权限与异常场景待补 | 待完成 |
|
||||
| 审批接口 | 02-API细化设计 | 已支持真实订单审批、取消审批和审批日志写入;权限与更多角色场景待补 | 待完成 |
|
||||
| 客户接口 | 02-API细化设计 | 已有接口与 service;客户列表、创建、详情已接入真实数据库优先链路;导入仅完成最小落库样例,尚未按文件内容做真实解析导入 | 待完成 |
|
||||
| 认证接口 | 02-API细化设计 | 已接入数据库优先登录校验、密码哈希校验、签名 token 生成、`/me` 过期校验、`/logout` 失效处理,并补齐关键业务接口统一 token 鉴权入口与基础角色拦截;仍待继续补 token 持久化黑名单与更细粒度权限点 | 待完成 |
|
||||
| 订单接口 | 02-API细化设计 | 已支持真实建单、列表筛选、详情、提交、取消、审批、发厂确认与状态变更,并在 `shipped/delivered` 等节点接入欠款生成;已补业务员本人订单数据范围控制与详情敏感字段基础收口,更多异常场景仍待补 | 待完成 |
|
||||
| 审批接口 | 02-API细化设计 | 已支持真实订单审批、取消审批和审批日志写入,并补齐管理层/管理员角色限制;更多审批流分支与权限颗粒度仍待补 | 待完成 |
|
||||
| 客户接口 | 02-API细化设计 | 已有接口与 service;客户列表、创建、详情已接入真实数据库优先链路,并补齐业务员本人客户数据范围控制;导入仅完成最小落库样例,尚未按文件内容做真实解析导入 | 待完成 |
|
||||
| 产品与分类接口 | 02-API细化设计 | 已有接口与 service;产品分类列表/新增/更新、产品列表/详情已接入真实数据库优先链路,基础资料页已形成真实联调闭环 | 待完成 |
|
||||
| 供应商接口 | 02-API细化设计 | 已有接口与 service;供应商列表、创建已接入真实数据库优先链路;前端详情展示仍为演示态补充,不计入真实接口完成度 | 待完成 |
|
||||
| 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动;司机端页面尚待补齐 | 待完成 |
|
||||
| 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动,并补齐司机本人任务/轨迹访问限制与管理端创建权限限制;附件留痕细节与更多异常场景仍待补 | 待完成 |
|
||||
| 文件接口 | 02-API细化设计 | 上传凭证生成和附件记录保存已接入数据库优先链路,并补齐 OSS 元数据口径;仍未接真实 OSS 签名与上传校验 | 待完成 |
|
||||
| 物流与 AI 接口 | 02-API细化设计 | 物流轨迹查询与新增已接入数据库优先链路,司机接单/揽货/送达会写入真实轨迹;AI 识别记录保存、置信度回包、人工修正落库已接入数据库优先链路,真实阿里云 OCR 与第三方轨迹仍未接入 | 待完成 |
|
||||
| 提醒接口 | 02-API细化设计 | 提醒列表、标记已读、欠款提醒检查、沉默客户提醒检查已接入数据库优先链路,并按配置做基础去重与阈值判断;物流超时提醒规则已补到 service,提醒触发入口与更多业务类型仍待收口 | 待完成 |
|
||||
| 报表接口 | 02-API细化设计 | 业绩统计已接入真实数据库优先聚合,支持按月/季度/年度、日期区间、产品分类统计订单金额与固定提成;导出已生成真实 CSV 文件并写入附件记录,后续可再补 Excel/OSS 发布链路 | 待完成 |
|
||||
| 配置与审计接口 | 02-API细化设计 | 配置查询与更新已接入 `system_config` 数据库优先链路,并补齐欠款模式/提醒阈值等基础校验;`audit_log` 已接入真实查询,并覆盖订单状态变更、配置更新、附件保存、提醒已读等关键写操作 | 待完成 |
|
||||
| 系统管理接口 | 02-API细化设计 | 已接入真实用户/角色/菜单查询、新增、编辑、重置密码与角色授权链路;仍待进一步补齐更细粒度审计与迁移落地 | 待完成 |
|
||||
| 系统管理接口 | 02-API细化设计 | 已接入真实用户/角色/菜单查询、新增、编辑、重置密码与角色授权链路,并补齐对应审计写入;现已补齐 admin 角色访问限制,仍待进一步补齐迁移落地与更细粒度权限控制 | 待完成 |
|
||||
|
||||
## 四、数据库
|
||||
|
||||
@ -74,12 +74,10 @@
|
||||
|
||||
### 仍未真正完成
|
||||
|
||||
- 真实登录鉴权与权限体系
|
||||
- 客户、产品、供应商、系统管理等完整后台能力
|
||||
- 真实 OSS / AI / 报表导出 OSS 发布链路
|
||||
- 更完整的附件上传与异常处理
|
||||
- 更完整的审计写入覆盖面与提醒任务入口收口
|
||||
- 测试、联调、部署
|
||||
- 更完整的权限拦截与数据范围控制(当前已完成统一鉴权入口、关键角色拦截和订单/客户/司机任务的基础数据范围控制)
|
||||
- 客户导入真实文件解析、供应商详情前后端完全真实化
|
||||
- 真实 OSS / 阿里云 OCR / 报表导出 OSS 发布链路
|
||||
- Alembic 迁移、初始化脚本、系统测试与部署收口
|
||||
|
||||
## 修正记录
|
||||
|
||||
@ -127,3 +125,6 @@
|
||||
| 2026-05-15 | 已继续成组推进审计主线:补齐 `audit_log` 模型、仓储和真实查询入口,并让订单状态变更、配置新增/更新、附件保存、提醒已读等关键写操作开始写入真实审计日志;审批、系统管理更多操作仍可继续补齐覆盖面。 |
|
||||
| 2026-05-15 | 已继续按 `02-API细化设计.md` 推进 AI 接口主线:`/api/ai/recognize` 与 `/api/ai/recognize/{log_id}/correct` 已补齐请求模型、数据库优先 service/repository 链路、识别记录落库、人工修正落库与审计留痕;当前识别结果仍为后端 mock provider,未接真实阿里云 OCR。 |
|
||||
| 2026-05-15 | 已继续按 `02-API细化设计.md` 推进报表导出主线:`/api/reports/performance/export` 已开始基于真实统计结果生成 CSV 导出文件,并通过附件保存链路写入导出记录与审计日志;当前返回的是本地文件路径,后续可再切到 OSS 公网发布。 |
|
||||
| 2026-05-15 | 已继续成组推进系统管理审计主线:用户新增/编辑/重置密码、角色新增/编辑、角色授权、菜单新增/编辑等系统管理关键写操作已开始写入 `audit_log`,系统管理模块已从“有写接口”推进到“有写接口且可追溯”。 |
|
||||
| 2026-05-15 | 已继续成组推进认证主线:数据库优先登录已切到密码哈希校验与签名 token,`/api/auth/me` 已支持 token 解析和过期判断,`/api/auth/logout` 已接入基础失效处理;当前仍未统一补到所有业务接口的角色/数据范围拦截。 |
|
||||
| 2026-05-15 | 已继续成组推进“统一鉴权 + 数据范围控制”主线:新增通用 token 鉴权依赖,订单/客户/司机任务/系统管理关键接口已补齐角色限制;业务员现仅可访问本人订单与客户,司机现仅可访问本人任务与轨迹,订单详情已按接口文档对业务员隐藏成本/利润等敏感字段。 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user