diff --git a/.gitignore b/.gitignore index 74963ed..9ddffcc 100644 --- a/.gitignore +++ b/.gitignore @@ -55,10 +55,12 @@ coverage/ # python环境 /backend/venv +__pycache__/ +*.py[cod] # 其他文件 *.exe *.wav *.mp3 *.mp4 -*.zip \ No newline at end of file +*.zip diff --git a/backend/.env.example b/backend/.env.example index 4702859..beb72ff 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 diff --git a/backend/README.md b/backend/README.md index b79761d..9a40299 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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` 已补齐统一提醒触发入口,会串行执行欠款提醒、沉默客户提醒和物流超时提醒检查。 diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index f168e44..d158c0a 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -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 diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 9948e87..dc8c17d 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -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 diff --git a/backend/app/api/orders.py b/backend/app/api/orders.py index 9743a9f..364eeec 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, 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: diff --git a/backend/app/api/reminders.py b/backend/app/api/reminders.py index 7df9f6e..e487351 100644 --- a/backend/app/api/reminders.py +++ b/backend/app/api/reminders.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.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)) diff --git a/backend/app/api/system.py b/backend/app/api/system.py index c3ed892..4c2423b 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, 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)) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 17963b8..4fdb043 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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") diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 8fd1a09..dc9b9a8 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -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() diff --git a/backend/app/services/reminder_service.py b/backend/app/services/reminder_service.py index 06e7696..fae7470 100644 --- a/backend/app/services/reminder_service.py +++ b/backend/app/services/reminder_service.py @@ -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 diff --git a/开发细节/任务状态跟踪.md b/开发细节/任务状态跟踪.md index dd12ae1..d44ee10 100644 --- a/开发细节/任务状态跟踪.md +++ b/开发细节/任务状态跟踪.md @@ -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` 统一触发入口。 |