diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index c84a5f4..f168e44 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -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)) diff --git a/backend/app/api/customers.py b/backend/app/api/customers.py index 9f8568c..85318af 100644 --- a/backend/app/api/customers.py +++ b/backend/app/api/customers.py @@ -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())) diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 5a72a43..9948e87 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -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 diff --git a/backend/app/api/logistics.py b/backend/app/api/logistics.py index 7f2fad6..a21b242 100644 --- a/backend/app/api/logistics.py +++ b/backend/app/api/logistics.py @@ -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)) diff --git a/backend/app/api/orders.py b/backend/app/api/orders.py index bc5ca13..9743a9f 100644 --- a/backend/app/api/orders.py +++ b/backend/app/api/orders.py @@ -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) diff --git a/backend/app/api/system.py b/backend/app/api/system.py index 413354c..c3ed892 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -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)) diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..9674bb1 --- /dev/null +++ b/backend/app/core/security.py @@ -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 diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 3a43894..8fd1a09 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -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, } diff --git a/backend/app/services/customer_service.py b/backend/app/services/customer_service.py index a25143e..3dcc552 100644 --- a/backend/app/services/customer_service.py +++ b/backend/app/services/customer_service.py @@ -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() diff --git a/backend/app/services/logistics_service.py b/backend/app/services/logistics_service.py index d49d461..21ec366 100644 --- a/backend/app/services/logistics_service.py +++ b/backend/app/services/logistics_service.py @@ -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) diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index ff7b9f6..2962961 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -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( diff --git a/backend/app/services/system_service.py b/backend/app/services/system_service.py index a0b8e7d..ba54408 100644 --- a/backend/app/services/system_service.py +++ b/backend/app/services/system_service.py @@ -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) diff --git a/开发细节/任务状态跟踪.md b/开发细节/任务状态跟踪.md index de5ea17..3161a17 100644 --- a/开发细节/任务状态跟踪.md +++ b/开发细节/任务状态跟踪.md @@ -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 鉴权依赖,订单/客户/司机任务/系统管理关键接口已补齐角色限制;业务员现仅可访问本人订单与客户,司机现仅可访问本人任务与轨迹,订单详情已按接口文档对业务员隐藏成本/利润等敏感字段。 |