feat: complete auth and reminder workflow hardening
This commit is contained in:
parent
bf4cb6b9a0
commit
3f8e16e6d9
4
.gitignore
vendored
4
.gitignore
vendored
@ -55,10 +55,12 @@ coverage/
|
||||
|
||||
# python环境
|
||||
/backend/venv
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# 其他文件
|
||||
*.exe
|
||||
*.wav
|
||||
*.mp3
|
||||
*.mp4
|
||||
*.zip
|
||||
*.zip
|
||||
|
||||
@ -3,6 +3,7 @@ APP_NAME=order-flow
|
||||
API_PREFIX=/api
|
||||
SECRET_KEY=change-me
|
||||
JWT_EXPIRE_MINUTES=1440
|
||||
AUTH_TOKEN_STORE_PATH=D:/tmp/order-flow-auth/revoked_tokens.json
|
||||
MYSQL_HOST=127.0.0.1
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=order_flow
|
||||
|
||||
@ -50,3 +50,9 @@ python scripts/bootstrap_data.py
|
||||
- `/api/ai/recognize` 当前支持通过 `AI_PROVIDER` 切换 OCR 提供方。
|
||||
- `AI_PROVIDER=mock` 时,返回可联调用的 mock 识别结果,并保留识别记录、人工修正和审计日志链路。
|
||||
- `AI_PROVIDER=aliyun` 时,会按 `ALIYUN_OCR_ENDPOINT`、`ALIYUN_OCR_PATH`、`ALIYUN_OCR_APPCODE` 发起真实 HTTP OCR 请求;如果配置不完整或第三方调用失败,会明确返回第三方调用失败,而不是继续伪装成功。
|
||||
|
||||
## 认证与提醒补充说明
|
||||
|
||||
- `/api/auth/logout` 当前会把 token 失效信息持久化到 `AUTH_TOKEN_STORE_PATH`,避免服务重启后已退出 token 重新生效。
|
||||
- 订单与系统管理关键接口已开始同时走角色校验和权限码校验,和真实菜单授权结果对齐。
|
||||
- `/api/reminders/check-all` 已补齐统一提醒触发入口,会串行执行欠款提醒、沉默客户提醒和物流超时提醒检查。
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Header
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_auth_service, get_current_user
|
||||
|
||||
@ -43,6 +43,19 @@ def require_roles(*role_codes: str):
|
||||
return _require_roles
|
||||
|
||||
|
||||
def require_permissions(*permission_codes: str):
|
||||
def _require_permissions(current_user: dict = Depends(get_current_user)) -> dict:
|
||||
if not permission_codes:
|
||||
return current_user
|
||||
granted = set(current_user.get("permissions") or [])
|
||||
missing = [code for code in permission_codes if code not in granted]
|
||||
if missing:
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="缺少必要权限", status_code=403)
|
||||
return current_user
|
||||
|
||||
return _require_permissions
|
||||
|
||||
|
||||
def get_order_service():
|
||||
return order_service
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_order_service, require_roles
|
||||
from backend.app.api.deps import get_order_service, require_permissions, 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
|
||||
@ -35,6 +35,7 @@ def list_orders(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:list")),
|
||||
) -> dict:
|
||||
orders = order_service.list_orders(
|
||||
session,
|
||||
@ -71,6 +72,7 @@ def create_order(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:create")),
|
||||
) -> dict:
|
||||
if not payload.items:
|
||||
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
|
||||
@ -84,6 +86,7 @@ def get_order(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:list")),
|
||||
) -> dict:
|
||||
order = order_service.get_order(order_id, session, current_user)
|
||||
if not order:
|
||||
@ -97,6 +100,7 @@ def submit_order(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:submit")),
|
||||
) -> dict:
|
||||
order = order_service.submit_order(order_id, session, current_user)
|
||||
if not order:
|
||||
@ -111,6 +115,7 @@ def cancel_order(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:list")),
|
||||
) -> dict:
|
||||
order = order_service.cancel_order(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
@ -125,6 +130,7 @@ def change_order_status(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("logistics:task:create")),
|
||||
) -> dict:
|
||||
order = order_service.change_order_status(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
@ -139,6 +145,7 @@ def approve_order(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:approve")),
|
||||
) -> dict:
|
||||
order = order_service.approve_order(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
@ -153,6 +160,7 @@ def cancel_approve_order(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:cancel-approve")),
|
||||
) -> dict:
|
||||
order = order_service.cancel_approve_order(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
@ -167,6 +175,7 @@ def supplier_text(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:supplier-text")),
|
||||
) -> dict:
|
||||
order = order_service.get_supplier_text(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
@ -181,6 +190,7 @@ def confirm_supplier_text(
|
||||
order_service: OrderService = Depends(get_order_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
_permission_user: dict = Depends(require_permissions("order:supplier-text")),
|
||||
) -> dict:
|
||||
order = order_service.confirm_supplier_text(order_id, payload.model_dump(), session, current_user)
|
||||
if not order:
|
||||
|
||||
@ -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.db import get_db_session
|
||||
from backend.app.schemas.common import success_payload
|
||||
from backend.app.services.reminder_service import reminder_service
|
||||
@ -16,6 +17,7 @@ def list_reminders(
|
||||
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:
|
||||
return success_payload(
|
||||
reminder_service.list_reminders(
|
||||
@ -35,6 +37,7 @@ def list_reminders(
|
||||
def read_reminder(
|
||||
reminder_id: int,
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(reminder_service.read_reminder(reminder_id, session))
|
||||
|
||||
@ -42,6 +45,7 @@ def read_reminder(
|
||||
@router.post("/arrears/check")
|
||||
def check_arrears_reminders(
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(reminder_service.check_arrears(session))
|
||||
|
||||
@ -49,5 +53,22 @@ def check_arrears_reminders(
|
||||
@router.post("/inactive-customers/check")
|
||||
def check_inactive_customer_reminders(
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(reminder_service.check_inactive_customers(session))
|
||||
|
||||
|
||||
@router.post("/logistics-timeout/check")
|
||||
def check_logistics_timeout_reminders(
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(reminder_service.check_logistics_timeout(session))
|
||||
|
||||
|
||||
@router.post("/check-all")
|
||||
def check_all_reminders(
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("manager", "admin")),
|
||||
) -> dict:
|
||||
return success_payload(reminder_service.check_all(session))
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_system_service, require_roles
|
||||
from backend.app.api.deps import get_system_service, require_permissions, require_roles
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.schemas.common import success_payload
|
||||
from backend.app.schemas.system import (
|
||||
@ -31,6 +31,7 @@ def list_users(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:user:list")),
|
||||
) -> dict:
|
||||
result = system_service.list_users(
|
||||
session,
|
||||
@ -55,6 +56,7 @@ def create_user(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:user:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.create_user(payload.model_dump(), session))
|
||||
|
||||
@ -66,6 +68,7 @@ def update_user(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:user:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.update_user(user_id, payload.model_dump(), session))
|
||||
|
||||
@ -77,6 +80,7 @@ def reset_password(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:user:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.reset_password(user_id, payload.new_password, session))
|
||||
|
||||
@ -91,6 +95,7 @@ def list_roles(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:role:list")),
|
||||
) -> dict:
|
||||
result = system_service.list_roles(
|
||||
session,
|
||||
@ -109,6 +114,7 @@ def create_role(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:role:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.create_role(payload.model_dump(), session))
|
||||
|
||||
@ -120,6 +126,7 @@ def update_role(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:role:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.update_role(role_id, payload.model_dump(), session))
|
||||
|
||||
@ -130,6 +137,7 @@ def get_role_menus(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:role:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.get_role_menu_assignment(role_id, session))
|
||||
|
||||
@ -141,6 +149,7 @@ def assign_role_menus(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:role:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.assign_role_menus(role_id, payload.model_dump(), session))
|
||||
|
||||
@ -152,6 +161,7 @@ def list_menus(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:menu:list")),
|
||||
) -> dict:
|
||||
return success_payload(
|
||||
system_service.list_menus(
|
||||
@ -167,6 +177,7 @@ def create_menu(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:menu:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.create_menu(payload.model_dump(), session))
|
||||
|
||||
@ -178,5 +189,6 @@ def update_menu(
|
||||
system_service: SystemService = Depends(get_system_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("admin")),
|
||||
_permission_user: dict = Depends(require_permissions("system:menu:list")),
|
||||
) -> dict:
|
||||
return success_payload(system_service.update_menu(menu_id, payload.model_dump(), session))
|
||||
|
||||
@ -16,6 +16,7 @@ class Settings(BaseSettings):
|
||||
api_prefix: str = Field(default="/api", alias="API_PREFIX")
|
||||
secret_key: str = Field(default="change-me", alias="SECRET_KEY")
|
||||
jwt_expire_minutes: int = Field(default=1440, alias="JWT_EXPIRE_MINUTES")
|
||||
auth_token_store_path: str = Field(default="D:/tmp/order-flow-auth/revoked_tokens.json", alias="AUTH_TOKEN_STORE_PATH")
|
||||
cors_allow_origins: str = Field(default="http://localhost:5173", alias="CORS_ALLOW_ORIGINS")
|
||||
mysql_host: str = Field(default="127.0.0.1", alias="MYSQL_HOST")
|
||||
mysql_port: int = Field(default=3306, alias="MYSQL_PORT")
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.core.config import get_settings
|
||||
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
|
||||
@ -25,7 +30,10 @@ class AuthService:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.repository = SystemRepository()
|
||||
self.revoked_tokens: set[str] = set()
|
||||
self.settings = get_settings()
|
||||
self.token_store_path = Path(self.settings.auth_token_store_path)
|
||||
self.token_store_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.revoked_tokens = self._load_revoked_tokens()
|
||||
|
||||
def login(self, username: str, password: str, role_type: str, session: Session | None = None) -> dict | None:
|
||||
if session is not None:
|
||||
@ -57,7 +65,7 @@ class AuthService:
|
||||
def get_me(self, token: str, session: Session | None = None) -> dict | None:
|
||||
if session is not None:
|
||||
try:
|
||||
if token in self.revoked_tokens:
|
||||
if self._is_token_revoked(token):
|
||||
return None
|
||||
payload = decode_access_token(token)
|
||||
if payload is None:
|
||||
@ -78,7 +86,8 @@ class AuthService:
|
||||
|
||||
def logout(self, token: str) -> dict:
|
||||
if token:
|
||||
self.revoked_tokens.add(token)
|
||||
# token 失效需要跨进程持久化,避免服务重启后已退出的 token 又重新可用。
|
||||
self._revoke_token(token)
|
||||
return {"success": True}
|
||||
|
||||
def _build_profile(self, session: Session, user, role, include_token: bool) -> dict:
|
||||
@ -152,5 +161,42 @@ class AuthService:
|
||||
permissions = [menu.permission_code for menu in menus if menu.permission_code in allowed]
|
||||
return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
|
||||
|
||||
def _load_revoked_tokens(self) -> dict[str, int]:
|
||||
if not self.token_store_path.exists():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(self.token_store_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
now_ts = int(datetime.now(timezone.utc).timestamp())
|
||||
tokens = {
|
||||
token: int(exp)
|
||||
for token, exp in payload.items()
|
||||
if isinstance(token, str) and isinstance(exp, int) and exp > now_ts
|
||||
}
|
||||
if len(tokens) != len(payload):
|
||||
self._save_revoked_tokens(tokens)
|
||||
return tokens
|
||||
|
||||
def _save_revoked_tokens(self, tokens: dict[str, int]) -> None:
|
||||
self.token_store_path.write_text(json.dumps(tokens, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
def _is_token_revoked(self, token: str) -> bool:
|
||||
if not token:
|
||||
return False
|
||||
now_ts = int(datetime.now(timezone.utc).timestamp())
|
||||
expired_tokens = [item for item, exp in self.revoked_tokens.items() if exp <= now_ts]
|
||||
if expired_tokens:
|
||||
for item in expired_tokens:
|
||||
self.revoked_tokens.pop(item, None)
|
||||
self._save_revoked_tokens(self.revoked_tokens)
|
||||
return token in self.revoked_tokens
|
||||
|
||||
def _revoke_token(self, token: str) -> None:
|
||||
payload = decode_access_token(token)
|
||||
expire_at = int(payload.get("exp", 0)) if payload else int(datetime.now(timezone.utc).timestamp()) + 60
|
||||
self.revoked_tokens[token] = expire_at
|
||||
self._save_revoked_tokens(self.revoked_tokens)
|
||||
|
||||
|
||||
auth_service = AuthService()
|
||||
|
||||
@ -241,6 +241,33 @@ class ReminderService:
|
||||
|
||||
return {"checked": True, "created_count": 0}
|
||||
|
||||
def check_all(self, session: Session | None = None) -> dict:
|
||||
if session is None:
|
||||
return {
|
||||
"checked": True,
|
||||
"arrears_created_count": 0,
|
||||
"inactive_customer_created_count": 0,
|
||||
"logistics_timeout_created_count": 0,
|
||||
"total_created_count": 0,
|
||||
}
|
||||
|
||||
# 统一触发入口直接串起三类提醒,便于管理端定时任务或手动补跑。
|
||||
arrears_result = self.check_arrears(session)
|
||||
inactive_result = self.check_inactive_customers(session)
|
||||
logistics_result = self.check_logistics_timeout(session)
|
||||
total_created_count = (
|
||||
int(arrears_result.get("created_count", 0))
|
||||
+ int(inactive_result.get("created_count", 0))
|
||||
+ int(logistics_result.get("created_count", 0))
|
||||
)
|
||||
return {
|
||||
"checked": True,
|
||||
"arrears_created_count": arrears_result.get("created_count", 0),
|
||||
"inactive_customer_created_count": inactive_result.get("created_count", 0),
|
||||
"logistics_timeout_created_count": logistics_result.get("created_count", 0),
|
||||
"total_created_count": total_created_count,
|
||||
}
|
||||
|
||||
def _get_config_value(self, session: Session, config_key: str, default: str) -> str:
|
||||
config = self.config_repository.get_by_key(session, config_key)
|
||||
return config.config_value if config is not None and config.config_value else default
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
|
||||
| 任务 | 来源 | 验证方式 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 认证接口 | 02-API细化设计 | 已接入数据库优先登录校验、密码哈希校验、签名 token 生成、`/me` 过期校验、`/logout` 失效处理,并补齐关键业务接口统一 token 鉴权入口与基础角色拦截;仍待继续补 token 持久化黑名单与更细粒度权限点 | 待完成 |
|
||||
| 认证接口 | 02-API细化设计 | 已接入数据库优先登录校验、密码哈希校验、签名 token 生成、`/me` 过期校验、`/logout` 持久化失效处理,并补齐关键业务接口统一 token 鉴权入口、基础角色拦截以及订单/系统管理关键接口的权限码校验 | 已完成 |
|
||||
| 订单接口 | 02-API细化设计 | 已支持真实建单、列表筛选、详情、提交、取消、审批、发厂确认与状态变更,并在 `shipped/delivered` 等节点接入欠款生成;已补业务员本人订单数据范围控制与详情敏感字段基础收口,更多异常场景仍待补 | 待完成 |
|
||||
| 审批接口 | 02-API细化设计 | 已支持真实订单审批、取消审批和审批日志写入,并补齐管理层/管理员角色限制;更多审批流分支与权限颗粒度仍待补 | 待完成 |
|
||||
| 客户接口 | 02-API细化设计 | 已有接口与 service;客户列表、创建、详情已接入真实数据库优先链路,并补齐业务员本人客户数据范围控制;客户导入已支持本地 `csv/xlsx/xls` 真实解析、逐行校验、跳过重复/覆盖重复导入,并兼容系统统一对象存储 OSS URL 口径文件来源 | 已完成 |
|
||||
@ -38,7 +38,7 @@
|
||||
| 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动,并补齐司机本人任务/轨迹访问限制与管理端创建权限限制;附件留痕细节与更多异常场景仍待补 | 待完成 |
|
||||
| 文件接口 | 02-API细化设计 | 上传凭证生成和附件记录保存已接入数据库优先链路,并统一切到 OSS URL / object_key / bucket 元数据口径;当前已形成“上传凭证 + 附件落库 + 本地对象存储发布目录模拟 OSS 公网地址”的完整链路,云上 SDK 直传签名与上传校验仍待补 | 待完成 |
|
||||
| 物流与 AI 接口 | 02-API细化设计 | 物流轨迹查询与新增已接入数据库优先链路,司机接单/揽货/送达会写入真实轨迹;AI 识别记录保存、置信度回包、人工修正落库已接入数据库优先链路,并新增可切换的阿里云 OCR provider 接入结构,配置齐全可走真实 HTTP OCR,请求失败会明确按第三方异常返回;第三方物流轨迹仍待接入 | 待完成 |
|
||||
| 提醒接口 | 02-API细化设计 | 提醒列表、标记已读、欠款提醒检查、沉默客户提醒检查已接入数据库优先链路,并按配置做基础去重与阈值判断;物流超时提醒规则已补到 service,提醒触发入口与更多业务类型仍待收口 | 待完成 |
|
||||
| 提醒接口 | 02-API细化设计 | 提醒列表、标记已读、欠款提醒检查、沉默客户提醒检查、物流超时提醒检查均已接入数据库优先链路,并按配置做基础去重与阈值判断;已补统一 `/api/reminders/check-all` 触发入口,当前主链路已闭环 | 已完成 |
|
||||
| 报表接口 | 02-API细化设计 | 业绩统计已接入真实数据库优先聚合,支持按月/季度/年度、日期区间、产品分类统计订单金额与固定提成;导出已生成真实 CSV、发布到本地对象存储模拟目录并返回 OSS 公网 URL 口径,同时写入附件记录与审计日志;Excel 格式与真实云上 OSS SDK 上传仍待补 | 待完成 |
|
||||
| 配置与审计接口 | 02-API细化设计 | 配置查询与更新已接入 `system_config` 数据库优先链路,并补齐欠款模式/提醒阈值等基础校验;`audit_log` 已接入真实查询,并覆盖订单状态变更、配置更新、附件保存、提醒已读等关键写操作 | 已完成 |
|
||||
| 系统管理接口 | 02-API细化设计 | 已接入真实用户/角色/菜单查询、新增、编辑、重置密码与角色授权链路,并补齐对应审计写入;现已补齐 admin 角色访问限制,仍待进一步补齐迁移落地与更细粒度权限控制 | 待完成 |
|
||||
@ -74,14 +74,16 @@
|
||||
- 订单、审批、发厂确认、司机任务创建等主链路的数据库优先实现
|
||||
- `web-admin` 管理后台真实登录入口与 token 鉴权联调
|
||||
- `web-sales` 业务员端真实登录入口与 token 鉴权联调
|
||||
- 认证接口持久化失效与关键权限码校验闭环
|
||||
- 客户接口真实数据库联调闭环
|
||||
- 产品与分类接口真实数据库联调闭环
|
||||
- 供应商接口真实数据库联调闭环
|
||||
- 提醒接口真实数据库联调闭环
|
||||
- 配置与审计接口真实数据库联调闭环
|
||||
|
||||
### 仍未真正完成
|
||||
|
||||
- 更完整的权限拦截与数据范围控制(当前已完成统一鉴权入口、关键角色拦截和订单/客户/司机任务的基础数据范围控制)
|
||||
- 更完整的权限拦截与数据范围控制(当前已完成统一鉴权入口、关键角色拦截、token 持久化失效以及订单/系统管理关键权限码校验,并已覆盖订单/客户/司机任务的基础数据范围控制)
|
||||
- 更多客户导入模板兼容
|
||||
- 真实阿里云 OSS SDK 直传签名 / 上传校验、第三方物流轨迹接入与更完整导出格式
|
||||
- 系统测试与部署收口
|
||||
@ -141,3 +143,4 @@
|
||||
| 2026-05-15 | 已修正第三项“后端接口”状态口径:产品与分类接口、供应商接口、配置与审计接口已按真实联调闭环更新为“已完成”;同时确认认证、订单、客户、文件、物流与 AI 等条目仍存在待收口事项,继续保留为“待完成”。 |
|
||||
| 2026-05-15 | 已继续推进 AI/OCR 主线:`backend/app/services/ai_service.py` 已从固定 mock 适配器升级为可切换 provider 结构,支持 `AI_PROVIDER=aliyun` 时按配置发起真实 OCR HTTP 请求,配置缺失或第三方失败会明确返回错误,并保留识别落库、人工修正和审计链路。 |
|
||||
| 2026-05-15 | 已继续收口客户接口:客户导入除本地 `csv/xlsx/xls` 外,已兼容系统统一对象存储 OSS URL 口径文件来源;`frontend/web-admin` 客户导入入口文案已同步改为支持本地路径或 OSS 公网 URL。 |
|
||||
| 2026-05-15 | 已继续收口认证与提醒主线:`/api/auth/logout` 已补 token 持久化失效存储,避免服务重启后 token 恢复可用;订单与系统管理关键接口已补权限码校验;提醒模块已新增 `/api/reminders/logistics-timeout/check` 与 `/api/reminders/check-all` 统一触发入口。 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user