已继续按 02-API细化设计.md 推进 AI 接口主线:/api/ai/recognize 与 /api/ai/recognize/{log_id}/correct 已补齐请求模型、数据库优先 service/repository 链路、识别记录落库、人工修正落库与审计留痕;当前识别结果仍为后端 mock provider,未接真实阿里云 OCR。
This commit is contained in:
parent
05434cbe82
commit
2a36276793
@ -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))
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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)
|
||||
|
||||
16
backend/app/repositories/ai_repository.py
Normal file
16
backend/app/repositories/ai_repository.py
Normal file
@ -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()
|
||||
33
backend/app/repositories/audit_repository.py
Normal file
33
backend/app/repositories/audit_repository.py
Normal file
@ -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())
|
||||
11
backend/app/schemas/ai.py
Normal file
11
backend/app/schemas/ai.py
Normal file
@ -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
|
||||
136
backend/app/services/ai_service.py
Normal file
136
backend/app/services/ai_service.py
Normal file
@ -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()
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
43
backend/app/services/export_service.py
Normal file
43
backend/app/services/export_service.py
Normal file
@ -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()
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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 公网发布。 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user