diff --git a/backend/app/api/ai.py b/backend/app/api/ai.py index d7a7b3f..8aade99 100644 --- a/backend/app/api/ai.py +++ b/backend/app/api/ai.py @@ -1,22 +1,26 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from backend.app.db import get_db_session +from backend.app.schemas.ai import CorrectRecognizeResultRequest, RecognizeImageRequest from backend.app.schemas.common import success_payload +from backend.app.services.ai_service import ai_service router = APIRouter(prefix="/api/ai", tags=["ai"]) @router.post("/recognize") -def recognize_image() -> dict: - return success_payload( - { - "log_id": 7001, - "raw_result": {"customer_name": "李四"}, - "confidence": 0.92, - "suggested_result": {"customer_name": "李四"}, - } - ) +def recognize_image( + payload: RecognizeImageRequest, + session: Session = Depends(get_db_session), +) -> dict: + return success_payload(ai_service.recognize_image(payload.model_dump(), session)) @router.post("/recognize/{log_id}/correct") -def correct_recognize_result(log_id: int) -> dict: - return success_payload({"log_id": log_id, "corrected": True}) +def correct_recognize_result( + log_id: int, + payload: CorrectRecognizeResultRequest, + session: Session = Depends(get_db_session), +) -> dict: + return success_payload(ai_service.correct_result(log_id, payload.model_dump(), session)) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e17a444..d6b6f90 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,5 +1,6 @@ from backend.app.models.base import AuditMixin, TimestampMixin from backend.app.models.business import ( + AIRecognitionLog, Customer, CustomerArrears, FileAttachment, @@ -12,7 +13,7 @@ from backend.app.models.business import ( Supplier, SystemReminder, ) -from backend.app.models.system import Menu, Role, RoleMenu, SystemConfig, User +from backend.app.models.system import AuditLog, Menu, Role, RoleMenu, SystemConfig, User __all__ = [ "AuditMixin", @@ -22,6 +23,7 @@ __all__ = [ "FileAttachment", "LogisticsTask", "LogisticsTrace", + "AIRecognitionLog", "Product", "ProductCategory", "SalesOrder", @@ -31,6 +33,7 @@ __all__ = [ "Menu", "Role", "RoleMenu", + "AuditLog", "SystemConfig", "User", ] diff --git a/backend/app/models/business.py b/backend/app/models/business.py index 0e4d755..18fe33e 100644 --- a/backend/app/models/business.py +++ b/backend/app/models/business.py @@ -204,3 +204,17 @@ class SystemReminder(Base): status: Mapped[str] = mapped_column(String(32), default="pending") sent_at: Mapped[DateTime | None] = mapped_column(DateTime, nullable=True) created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now()) + + +class AIRecognitionLog(Base): + __tablename__ = "ai_recognition_log" + + id: Mapped[int] = mapped_column(primary_key=True) + biz_type: Mapped[str] = mapped_column(String(64), index=True) + biz_id: Mapped[int] = mapped_column(index=True) + image_url: Mapped[str] = mapped_column(String(1000)) + raw_result: Mapped[str | None] = mapped_column(Text, nullable=True) + confidence: Mapped[float | None] = mapped_column(Numeric(10, 4), nullable=True) + corrected_result: Mapped[str | None] = mapped_column(Text, nullable=True) + created_by: Mapped[int | None] = mapped_column(nullable=True) + created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now()) diff --git a/backend/app/models/system.py b/backend/app/models/system.py index e6413ec..68de87b 100644 --- a/backend/app/models/system.py +++ b/backend/app/models/system.py @@ -1,4 +1,4 @@ -from sqlalchemy import Integer, String +from sqlalchemy import DateTime, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column from backend.app.db import Base @@ -58,3 +58,19 @@ class SystemConfig(TimestampMixin, Base): config_name: Mapped[str] = mapped_column(String(128)) remark: Mapped[str | None] = mapped_column(String(255), nullable=True) status: Mapped[int] = mapped_column(Integer, default=1) + + +class AuditLog(Base): + __tablename__ = "audit_log" + + id: Mapped[int] = mapped_column(primary_key=True) + operator_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True) + operator_name: Mapped[str | None] = mapped_column(String(64), nullable=True) + operate_time: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now(), index=True) + operate_type: Mapped[str] = mapped_column(String(64)) + biz_type: Mapped[str] = mapped_column(String(64), index=True) + biz_id: Mapped[int] = mapped_column(Integer, index=True) + before_value: Mapped[str | None] = mapped_column(String(4000), nullable=True) + after_value: Mapped[str | None] = mapped_column(String(4000), nullable=True) + result: Mapped[str] = mapped_column(String(32), default="success") + remark: Mapped[str | None] = mapped_column(String(1000), nullable=True) diff --git a/backend/app/repositories/ai_repository.py b/backend/app/repositories/ai_repository.py new file mode 100644 index 0000000..42a3785 --- /dev/null +++ b/backend/app/repositories/ai_repository.py @@ -0,0 +1,16 @@ +from sqlalchemy import select +from sqlalchemy.orm import Session + +from backend.app.models.business import AIRecognitionLog + + +class AIRepository: + def create_log(self, session: Session, payload: dict) -> AIRecognitionLog: + log = AIRecognitionLog(**payload) + session.add(log) + session.flush() + return log + + def get_log(self, session: Session, log_id: int) -> AIRecognitionLog | None: + stmt = select(AIRecognitionLog).where(AIRecognitionLog.id == log_id) + return session.execute(stmt).scalar_one_or_none() diff --git a/backend/app/repositories/audit_repository.py b/backend/app/repositories/audit_repository.py new file mode 100644 index 0000000..e20e138 --- /dev/null +++ b/backend/app/repositories/audit_repository.py @@ -0,0 +1,33 @@ +from datetime import datetime + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from backend.app.models.system import AuditLog + + +class AuditRepository: + def create_log(self, session: Session, payload: dict) -> AuditLog: + log = AuditLog(**payload) + session.add(log) + session.flush() + return log + + def list_logs(self, session: Session, filters: dict) -> list[AuditLog]: + stmt = select(AuditLog) + + if filters.get("biz_type"): + stmt = stmt.where(AuditLog.biz_type == filters["biz_type"]) + if filters.get("biz_id") is not None: + stmt = stmt.where(AuditLog.biz_id == filters["biz_id"]) + if filters.get("operator_name"): + stmt = stmt.where(AuditLog.operator_name.contains(filters["operator_name"])) + if filters.get("operate_type"): + stmt = stmt.where(AuditLog.operate_type == filters["operate_type"]) + if filters.get("start_time"): + stmt = stmt.where(AuditLog.operate_time >= datetime.strptime(filters["start_time"], "%Y-%m-%d %H:%M:%S")) + if filters.get("end_time"): + stmt = stmt.where(AuditLog.operate_time <= datetime.strptime(filters["end_time"], "%Y-%m-%d %H:%M:%S")) + + stmt = stmt.order_by(AuditLog.id.desc()) + return list(session.execute(stmt).scalars()) diff --git a/backend/app/schemas/ai.py b/backend/app/schemas/ai.py new file mode 100644 index 0000000..e397caf --- /dev/null +++ b/backend/app/schemas/ai.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, Field + + +class RecognizeImageRequest(BaseModel): + image_url: str + biz_type: str + biz_id: int = Field(gt=0) + + +class CorrectRecognizeResultRequest(BaseModel): + corrected_result: dict diff --git a/backend/app/services/ai_service.py b/backend/app/services/ai_service.py new file mode 100644 index 0000000..0ef371a --- /dev/null +++ b/backend/app/services/ai_service.py @@ -0,0 +1,136 @@ +import json + +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from backend.app.core.error_codes import ErrorCode +from backend.app.core.exceptions import AppException +from backend.app.repositories.ai_repository import AIRepository +from backend.app.services.audit_service import audit_service + + +class AIService: + def __init__(self) -> None: + self.repository = AIRepository() + + def recognize_image(self, payload: dict, session: Session | None = None) -> dict: + image_url = (payload.get("image_url") or "").strip() + biz_type = (payload.get("biz_type") or "").strip() + biz_id = payload.get("biz_id") + + if not image_url: + raise AppException(code=ErrorCode.PARAM_ERROR, message="图片地址不能为空", status_code=400) + if not biz_type: + raise AppException(code=ErrorCode.PARAM_ERROR, message="业务类型不能为空", status_code=400) + if image_url.startswith("mock://fail"): + raise AppException(code=ErrorCode.THIRD_PARTY_FAILED, message="AI 识别失败", status_code=400) + + suggested_result = self._build_suggested_result(image_url, biz_type, biz_id) + raw_result = { + "provider": "mock_aliyun_adapter", + "image_url": image_url, + "fields": suggested_result, + } + confidence = 0.92 + + if session is not None: + try: + log = self.repository.create_log( + session, + { + "biz_type": biz_type, + "biz_id": biz_id, + "image_url": image_url, + "raw_result": json.dumps(raw_result, ensure_ascii=False), + "confidence": confidence, + "corrected_result": None, + "created_by": None, + }, + ) + audit_service.write_log( + session, + { + "operate_type": "ai_recognize", + "biz_type": biz_type, + "biz_id": biz_id, + "before_value": None, + "after_value": { + "log_id": log.id, + "confidence": confidence, + "suggested_result": suggested_result, + }, + "remark": f"AI识别图片 {image_url}", + }, + ) + session.commit() + return { + "log_id": log.id, + "raw_result": raw_result, + "confidence": confidence, + "suggested_result": suggested_result, + } + except AppException: + session.rollback() + raise + except SQLAlchemyError: + session.rollback() + + return { + "log_id": 7001, + "raw_result": raw_result, + "confidence": confidence, + "suggested_result": suggested_result, + } + + def correct_result(self, log_id: int, payload: dict, session: Session | None = None) -> dict: + corrected_result = payload.get("corrected_result") + if not isinstance(corrected_result, dict) or not corrected_result: + raise AppException(code=ErrorCode.PARAM_ERROR, message="修正结果不能为空", status_code=400) + + if session is not None: + try: + log = self.repository.get_log(session, log_id) + if log is None: + raise AppException(code=ErrorCode.NOT_FOUND, message="识别记录不存在", status_code=404) + before_value = self._safe_load_json(log.corrected_result) + log.corrected_result = json.dumps(corrected_result, ensure_ascii=False) + session.add(log) + audit_service.write_log( + session, + { + "operate_type": "ai_correct", + "biz_type": log.biz_type, + "biz_id": log.biz_id, + "before_value": before_value, + "after_value": corrected_result, + "remark": f"修正AI识别结果 {log_id}", + }, + ) + session.commit() + return {"log_id": log.id, "corrected": True} + except AppException: + session.rollback() + raise + except SQLAlchemyError: + session.rollback() + + return {"log_id": log_id, "corrected": True} + + def _build_suggested_result(self, image_url: str, biz_type: str, biz_id: int) -> dict: + tail = image_url.rsplit("/", 1)[-1] + return { + "image_name": tail, + "biz_type": biz_type, + "biz_id": biz_id, + } + + def _safe_load_json(self, value: str | None): + if not value: + return None + try: + return json.loads(value) + except json.JSONDecodeError: + return value + + +ai_service = AIService() diff --git a/backend/app/services/audit_service.py b/backend/app/services/audit_service.py index 189a82c..cbfc197 100644 --- a/backend/app/services/audit_service.py +++ b/backend/app/services/audit_service.py @@ -1,43 +1,38 @@ +import json + from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from backend.app.repositories.order_approve_log_repository import OrderApproveLogRepository +from backend.app.repositories.audit_repository import AuditRepository class AuditService: def __init__(self) -> None: - self.order_approve_log_repository = OrderApproveLogRepository() + self.repository = AuditRepository() def list_audit_logs(self, session: Session | None = None, filters: dict | None = None) -> dict: + filters = filters or {} if session is not None: try: - # 现阶段先把审计日志与订单审批日志打通,后续再扩展到更多业务类型。 - if filters and filters.get("biz_type") and filters["biz_type"] != "sales_order": - return { - "total": 0, - "page_no": filters.get("page_no", 1), - "page_size": filters.get("page_size", 20), - "list": [], - } - - logs = self.order_approve_log_repository.list_by_filters(session, filters or {}) - start = max((filters or {}).get("page_no", 1) - 1, 0) * (filters or {}).get("page_size", 20) - page_size = (filters or {}).get("page_size", 20) + logs = self.repository.list_logs(session, filters) + page_no = filters.get("page_no", 1) + page_size = filters.get("page_size", 20) + start = max(page_no - 1, 0) * page_size page_logs = logs[start : start + page_size] - return { "total": len(logs), - "page_no": (filters or {}).get("page_no", 1), + "page_no": page_no, "page_size": page_size, "list": [ { "log_id": item.id, - "operator_name": "系统审批", - "operate_type": item.approve_type, - "biz_type": "sales_order", - "biz_id": item.order_id, - "result": "success", - "operate_time": item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else "", + "operator_name": item.operator_name or "", + "operate_type": item.operate_type, + "biz_type": item.biz_type, + "biz_id": item.biz_id, + "result": item.result, + "operate_time": item.operate_time.strftime("%Y-%m-%d %H:%M:%S") if item.operate_time else "", + "remark": item.remark, } for item in page_logs ], @@ -52,15 +47,41 @@ class AuditService: "list": [ { "log_id": 90001, - "operator_name": "系统审批", - "operate_type": "order", + "operator_name": "系统", + "operate_type": "order_status_change", "biz_type": "sales_order", "biz_id": 9001, "result": "success", "operate_time": "2026-05-14 10:00:00", + "remark": "演示审计记录", } ], } + def write_log(self, session: Session, payload: dict) -> None: + before_value = payload.get("before_value") + after_value = payload.get("after_value") + self.repository.create_log( + session, + { + "operator_id": payload.get("operator_id"), + "operator_name": payload.get("operator_name") or "系统", + "operate_type": payload["operate_type"], + "biz_type": payload["biz_type"], + "biz_id": payload["biz_id"], + "before_value": self._serialize_value(before_value), + "after_value": self._serialize_value(after_value), + "result": payload.get("result", "success"), + "remark": payload.get("remark"), + }, + ) + + def _serialize_value(self, value) -> str | None: + if value is None: + return None + if isinstance(value, str): + return value + return json.dumps(value, ensure_ascii=False, default=str) + audit_service = AuditService() diff --git a/backend/app/services/config_service.py b/backend/app/services/config_service.py index b375d8c..ecd85be 100644 --- a/backend/app/services/config_service.py +++ b/backend/app/services/config_service.py @@ -4,6 +4,7 @@ from sqlalchemy.orm import Session from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.repositories.config_repository import ConfigRepository +from backend.app.services.audit_service import audit_service from backend.app.services.bootstrap import DEFAULT_CONFIGS @@ -53,7 +54,19 @@ class ConfigService: "status": payload.get("status", 1), }, ) + audit_service.write_log( + session, + { + "operate_type": "config_create", + "biz_type": "system_config", + "biz_id": config.id, + "before_value": None, + "after_value": self._map_config(config), + "remark": f"新增配置 {config_key}", + }, + ) else: + before_snapshot = self._map_config(config) self.repository.update_config( config, { @@ -63,6 +76,17 @@ class ConfigService: "status": payload.get("status", 1), }, ) + audit_service.write_log( + session, + { + "operate_type": "config_update", + "biz_type": "system_config", + "biz_id": config.id, + "before_value": before_snapshot, + "after_value": self._map_config(config), + "remark": f"更新配置 {config_key}", + }, + ) session.commit() return { "config_key": config.config_key, diff --git a/backend/app/services/export_service.py b/backend/app/services/export_service.py new file mode 100644 index 0000000..d865476 --- /dev/null +++ b/backend/app/services/export_service.py @@ -0,0 +1,43 @@ +import csv +from datetime import datetime +from pathlib import Path + + +class ExportService: + def __init__(self) -> None: + self.export_root = Path("D:/tmp/order-flow-exports") + self.export_root.mkdir(parents=True, exist_ok=True) + + def build_performance_export(self, report: dict) -> dict: + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + suffix = report["stat_type"] + file_name = f"performance-{suffix}-{timestamp}.csv" + file_path = self.export_root / file_name + + with file_path.open("w", encoding="utf-8-sig", newline="") as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["统计周期", "订单数", "订单金额", "固定提成", "分类明细"]) + for item in report["list"]: + category_text = ";".join( + f"{category['category_name']}:{category['amount']:.2f}" + for category in item.get("category_amounts", []) + ) + writer.writerow( + [ + item["stat_period"], + item["order_count"], + f"{item['order_amount']:.2f}", + f"{item['commission_amount']:.2f}", + category_text, + ] + ) + + return { + "file_name": file_name, + "file_path": str(file_path), + "file_size": file_path.stat().st_size, + "content_type": "text/csv", + } + + +export_service = ExportService() diff --git a/backend/app/services/file_service.py b/backend/app/services/file_service.py index 6666447..e731fac 100644 --- a/backend/app/services/file_service.py +++ b/backend/app/services/file_service.py @@ -7,6 +7,7 @@ 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.repositories.file_repository import FileRepository +from backend.app.services.audit_service import audit_service class FileService: @@ -66,6 +67,17 @@ class FileService: "file_category": file_category, }, ) + audit_service.write_log( + session, + { + "operate_type": "attachment_create", + "biz_type": payload["biz_type"], + "biz_id": payload["biz_id"], + "before_value": None, + "after_value": self._map_attachment(attachment), + "remark": f"保存附件 {payload['file_name']}", + }, + ) session.commit() return self._map_attachment(attachment) except AppException: diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index 17e7e88..ff7b9f6 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -9,6 +9,7 @@ from backend.app.repositories.customer_repository import CustomerRepository from backend.app.repositories.order_approve_log_repository import OrderApproveLogRepository from backend.app.repositories.order_repository import OrderRepository from backend.app.services.arrears_service import arrears_service +from backend.app.services.audit_service import audit_service from backend.app.services.demo_store import demo_store @@ -372,8 +373,20 @@ class OrderService: if target_status not in allowed_statuses: raise AppException(code=ErrorCode.PARAM_ERROR, message="目标状态不支持", status_code=400) + before_status = order.order_status self.order_repository.update_order_status(session, order, target_status) arrears_service.sync_order_arrears(session, order) + audit_service.write_log( + session, + { + "operate_type": "order_status_change", + "biz_type": "sales_order", + "biz_id": order.id, + "before_value": {"order_status": before_status}, + "after_value": {"order_status": order.order_status}, + "remark": payload.get("remark") or f"订单状态变更为 {target_status}", + }, + ) session.commit() return { "order_id": order.id, diff --git a/backend/app/services/reminder_service.py b/backend/app/services/reminder_service.py index 9e78767..06e7696 100644 --- a/backend/app/services/reminder_service.py +++ b/backend/app/services/reminder_service.py @@ -7,6 +7,7 @@ from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.repositories.config_repository import ConfigRepository from backend.app.repositories.reminder_repository import ReminderRepository +from backend.app.services.audit_service import audit_service class ReminderService: @@ -57,8 +58,20 @@ class ReminderService: reminder = self.repository.get_reminder(session, reminder_id) if reminder is None: raise AppException(code=ErrorCode.NOT_FOUND, message="提醒不存在", status_code=404) + before_status = reminder.status reminder.status = "read" session.add(reminder) + audit_service.write_log( + session, + { + "operate_type": "reminder_read", + "biz_type": reminder.biz_type, + "biz_id": reminder.biz_id, + "before_value": {"status": before_status}, + "after_value": {"status": reminder.status}, + "remark": f"提醒 {reminder_id} 标记已读", + }, + ) session.commit() return {"reminder_id": reminder.id, "status": reminder.status} except AppException: diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 34dea24..dde1af4 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -7,7 +7,10 @@ from sqlalchemy.orm import Session from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.repositories.report_repository import ReportRepository +from backend.app.services.audit_service import audit_service from backend.app.services.demo_store import demo_store +from backend.app.services.export_service import export_service +from backend.app.services.file_service import file_service class ReportService: @@ -32,10 +35,43 @@ class ReportService: def export_performance_report(self, filters: dict, session: Session | None = None) -> dict: report = self.performance_report(filters, session) - suffix = report["stat_type"] + export_meta = export_service.build_performance_export(report) + attachment_payload = { + "biz_type": "performance_report", + "biz_id": int(datetime.now().strftime("%Y%m%d%H%M%S")), + "file_name": export_meta["file_name"], + "file_url": export_meta["file_path"], + "file_type": export_meta["content_type"], + "file_size": export_meta["file_size"], + } + + attachment_result = file_service.save_attachment(attachment_payload, session) if session is not None else { + "file_url": export_meta["file_path"], + "file_name": export_meta["file_name"], + "attachment_id": 0, + } + + if session is not None: + audit_service.write_log( + session, + { + "operate_type": "report_export", + "biz_type": "performance_report", + "biz_id": attachment_result.get("attachment_id", 0), + "before_value": None, + "after_value": { + "file_name": export_meta["file_name"], + "stat_type": report["stat_type"], + "total_periods": len(report["list"]), + }, + "remark": "导出业绩统计报表", + }, + ) + session.commit() + return { - "file_url": f"https://oss.example.com/export/performance-{suffix}.xlsx", - "file_name": f"performance-{suffix}.xlsx", + "file_url": attachment_result["file_url"], + "file_name": export_meta["file_name"], "stat_type": report["stat_type"], "total_periods": len(report["list"]), "filters": { diff --git a/开发细节/任务状态跟踪.md b/开发细节/任务状态跟踪.md index 1865d21..de5ea17 100644 --- a/开发细节/任务状态跟踪.md +++ b/开发细节/任务状态跟踪.md @@ -36,10 +36,10 @@ | 供应商接口 | 02-API细化设计 | 已有接口与 service;供应商列表、创建已接入真实数据库优先链路;前端详情展示仍为演示态补充,不计入真实接口完成度 | 待完成 | | 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动;司机端页面尚待补齐 | 待完成 | | 文件接口 | 02-API细化设计 | 上传凭证生成和附件记录保存已接入数据库优先链路,并补齐 OSS 元数据口径;仍未接真实 OSS 签名与上传校验 | 待完成 | -| 物流与 AI 接口 | 02-API细化设计 | 物流轨迹查询与新增已接入数据库优先链路,司机接单/揽货/送达会写入真实轨迹;AI 识别与第三方轨迹仍未接入 | 待完成 | +| 物流与 AI 接口 | 02-API细化设计 | 物流轨迹查询与新增已接入数据库优先链路,司机接单/揽货/送达会写入真实轨迹;AI 识别记录保存、置信度回包、人工修正落库已接入数据库优先链路,真实阿里云 OCR 与第三方轨迹仍未接入 | 待完成 | | 提醒接口 | 02-API细化设计 | 提醒列表、标记已读、欠款提醒检查、沉默客户提醒检查已接入数据库优先链路,并按配置做基础去重与阈值判断;物流超时提醒规则已补到 service,提醒触发入口与更多业务类型仍待收口 | 待完成 | -| 报表接口 | 02-API细化设计 | 业绩统计已接入真实数据库优先聚合,支持按月/季度/年度、日期区间、产品分类统计订单金额与固定提成;导出仍为结构化占位返回 | 待完成 | -| 配置与审计接口 | 02-API细化设计 | 配置查询与更新已接入 `system_config` 数据库优先链路,并补齐欠款模式/提醒阈值等基础校验;审计列表当前仅接通订单审批日志数据源,更多业务写入仍待补 | 待完成 | +| 报表接口 | 02-API细化设计 | 业绩统计已接入真实数据库优先聚合,支持按月/季度/年度、日期区间、产品分类统计订单金额与固定提成;导出已生成真实 CSV 文件并写入附件记录,后续可再补 Excel/OSS 发布链路 | 待完成 | +| 配置与审计接口 | 02-API细化设计 | 配置查询与更新已接入 `system_config` 数据库优先链路,并补齐欠款模式/提醒阈值等基础校验;`audit_log` 已接入真实查询,并覆盖订单状态变更、配置更新、附件保存、提醒已读等关键写操作 | 待完成 | | 系统管理接口 | 02-API细化设计 | 已接入真实用户/角色/菜单查询、新增、编辑、重置密码与角色授权链路;仍待进一步补齐更细粒度审计与迁移落地 | 待完成 | ## 四、数据库 @@ -76,9 +76,9 @@ - 真实登录鉴权与权限体系 - 客户、产品、供应商、系统管理等完整后台能力 -- 真实 OSS / AI / 报表导出文件生成 +- 真实 OSS / AI / 报表导出 OSS 发布链路 - 更完整的附件上传与异常处理 -- 更完整的审计写入与提醒任务入口收口 +- 更完整的审计写入覆盖面与提醒任务入口收口 - 测试、联调、部署 ## 修正记录 @@ -124,3 +124,6 @@ | 2026-05-15 | 已继续按 `02-API细化设计.md` 推进配置接口主线:`/api/configs/{config_key}` 查询与更新已接入 `system_config` 数据库优先链路,补齐 `arrears_generate_mode`、物流超时、沉默客户周期和金额阈值等核心配置校验;审计接口仍保持“订单审批日志优先”范围。 | | 2026-05-15 | 已继续按 `02-API细化设计.md` 推进提醒接口主线:`/api/reminders` 列表、已读、欠款提醒检查、沉默客户提醒检查已接入 `system_reminder / customer_arrears` 数据库优先链路,并按配置完成基础去重和阈值判断;欠款记录自动生成与物流超时提醒仍待继续补。 | | 2026-05-15 | 已继续成组推进“欠款生成 + 物流轨迹”主线:新增订单状态变更接口,订单进入 `shipped/delivered` 等节点时会按 `arrears_generate_mode` 自动生成 `customer_arrears`;`/api/logistics/{order_id}/trace` 已改为数据库优先查询与写入,司机接单/揽货/送达会同步留下真实轨迹记录。 | +| 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 公网发布。 |