已继续成组推进“统一鉴权 + 数据范围控制”主线:新增通用 token 鉴权依赖,订单/客户/司机任务/系统管理关键接口已补齐角色限制;业务员现仅可访问本人订单与客户,司机现仅可访问本人任务与轨迹,订单详情已按接口文档对业务员隐藏成本/利润等敏感字段。

This commit is contained in:
taiyi 2026-05-15 12:05:18 +08:00
parent 2a36276793
commit ca04891991
13 changed files with 559 additions and 93 deletions

View File

@ -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))

View File

@ -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()))

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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))

View 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

View File

@ -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,
}

View File

@ -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()

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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 鉴权依赖,订单/客户/司机任务/系统管理关键接口已补齐角色限制;业务员现仅可访问本人订单与客户,司机现仅可访问本人任务与轨迹,订单详情已按接口文档对业务员隐藏成本/利润等敏感字段。 |