3df85fdb04
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
348 lines
13 KiB
Python
348 lines
13 KiB
Python
from typing import Optional
|
|
from urllib.parse import quote
|
|
|
|
from fastapi import APIRouter, Depends, File, Form, Header, HTTPException, Request, Response, UploadFile
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.response import success_response
|
|
from app.db import get_db_session
|
|
from app.services.audit_log_service import AuditLogService
|
|
from app.services.auth_service import AuthService
|
|
from app.services.review_risk_action_service import ReviewRiskActionService
|
|
from app.services.review_service import ReviewService
|
|
from app.services.tenant_service import TenantService
|
|
|
|
router = APIRouter(prefix="/reviews", tags=["reviews"])
|
|
ROUTE_PERMISSION_SCOPE = "review_workflow"
|
|
ROUTE_PERMISSION_ACTIONS = {
|
|
"create": "reviews:create",
|
|
"list": "reviews:list",
|
|
"detail": "reviews:detail",
|
|
"result": "reviews:result",
|
|
"report": "reviews:report:get",
|
|
"download_report": "reviews:report:download",
|
|
"retry": "reviews:retry",
|
|
"risk_actions": "reviews:risk_actions:list",
|
|
"risk_action_update": "reviews:risk_actions:update",
|
|
"risk_action_apply": "reviews:risk_actions:apply",
|
|
}
|
|
|
|
|
|
class ReviewRiskActionUpdateRequest(BaseModel):
|
|
action_status: Optional[str] = None
|
|
user_modified_text: Optional[str] = None
|
|
skip_reason: Optional[str] = None
|
|
applied_contract_version_id: Optional[str] = None
|
|
|
|
|
|
class ReviewRiskActionApplyRequest(BaseModel):
|
|
contract_id: Optional[str] = None
|
|
action_mode: str = "adopt"
|
|
replacement_text: Optional[str] = None
|
|
match_text: Optional[str] = None
|
|
change_summary: Optional[str] = None
|
|
|
|
|
|
def get_review_service(db: Session = Depends(get_db_session)) -> ReviewService:
|
|
return ReviewService(db)
|
|
|
|
|
|
def get_audit_log_service(db: Session = Depends(get_db_session)) -> AuditLogService:
|
|
return AuditLogService(db)
|
|
|
|
|
|
def get_review_risk_action_service(db: Session = Depends(get_db_session)) -> ReviewRiskActionService:
|
|
return ReviewRiskActionService(db)
|
|
|
|
|
|
def get_tenant_service(db: Session = Depends(get_db_session)) -> TenantService:
|
|
return TenantService(db)
|
|
|
|
|
|
def require_current_user(
|
|
authorization: Optional[str] = Header(default=None),
|
|
db: Session = Depends(get_db_session),
|
|
):
|
|
return AuthService(db).require_current_user(authorization)
|
|
|
|
|
|
@router.post("")
|
|
async def create_review(
|
|
request: Request,
|
|
file: Optional[UploadFile] = File(default=None),
|
|
text: Optional[str] = Form(default=None),
|
|
contract_type: Optional[str] = Form(default=None),
|
|
review_as: str = Form(default="party_a"),
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
audit_log_service: AuditLogService = Depends(get_audit_log_service),
|
|
tenant_service: TenantService = Depends(get_tenant_service),
|
|
current_user: dict = Depends(require_current_user),
|
|
):
|
|
tenant_service.assert_billing_allows(current_user, "review")
|
|
normalized_text = text.strip() if text else None
|
|
file_name = file.filename if file else None
|
|
file_text: Optional[str] = None
|
|
extraction: Optional[dict] = None
|
|
|
|
if file is not None:
|
|
file_bytes = await file.read()
|
|
review_service.validate_review_upload(file_name=file_name, mime_type=file.content_type, file_bytes=file_bytes)
|
|
extraction = review_service.extract_review_upload_text(file_bytes=file_bytes, file_name=file_name)
|
|
file_text = extraction["text"]
|
|
|
|
combined_text = review_service.merge_review_text(text=normalized_text, file_text=file_text)
|
|
|
|
if file is None and not combined_text:
|
|
return {"code": 40001, "message": "file 和 text 至少提供一个", "data": None}
|
|
|
|
if file is not None and not file_text and not normalized_text:
|
|
return {
|
|
"code": 40002,
|
|
"message": extraction["failure_message"],
|
|
"data": None,
|
|
}
|
|
|
|
result = review_service.create_review_task(
|
|
tenant_id=current_user["tenant_id"],
|
|
user_id=current_user["id"],
|
|
review_as=review_as,
|
|
contract_type=contract_type,
|
|
text=combined_text,
|
|
file_name=file_name,
|
|
extracted_text=file_text,
|
|
extraction_metadata=extraction["metadata"] if extraction else None,
|
|
)
|
|
audit_log_service.record_event(
|
|
tenant_id=current_user.get("tenant_id"),
|
|
user_id=current_user.get("id"),
|
|
action="create_review",
|
|
resource_type="review",
|
|
resource_id=result["id"],
|
|
detail_json={
|
|
"contract_type": contract_type,
|
|
"review_as": review_as,
|
|
"source_type": "file" if file is not None else "text",
|
|
"file_name": file_name,
|
|
"text_length": len(combined_text or ""),
|
|
"status": result.get("status"),
|
|
},
|
|
ip_address=request.client.host if request.client else None,
|
|
user_agent=request.headers.get("user-agent"),
|
|
)
|
|
return success_response(result)
|
|
|
|
|
|
@router.get("")
|
|
def list_reviews(
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
_current_user: dict = Depends(require_current_user),
|
|
):
|
|
return success_response(review_service.list_reviews(_current_user))
|
|
|
|
|
|
@router.get("/{review_id}")
|
|
def get_review_detail(
|
|
review_id: str,
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
_current_user: dict = Depends(require_current_user),
|
|
):
|
|
detail = review_service.get_review_detail(_current_user, review_id)
|
|
|
|
if detail is None:
|
|
raise HTTPException(status_code=404, detail="review not found")
|
|
|
|
return success_response(detail)
|
|
|
|
|
|
@router.post("/{review_id}/retry")
|
|
def retry_review(
|
|
review_id: str,
|
|
request: Request,
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
audit_log_service: AuditLogService = Depends(get_audit_log_service),
|
|
tenant_service: TenantService = Depends(get_tenant_service),
|
|
current_user: dict = Depends(require_current_user),
|
|
):
|
|
tenant_service.assert_billing_allows(current_user, "review")
|
|
result = review_service.retry_review_task(current_user, review_id)
|
|
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="review not found")
|
|
|
|
audit_log_service.record_event(
|
|
tenant_id=current_user.get("tenant_id"),
|
|
user_id=current_user.get("id"),
|
|
action="retry_review",
|
|
resource_type="review",
|
|
resource_id=result["id"],
|
|
detail_json={
|
|
"original_review_id": review_id,
|
|
"new_review_id": result["id"],
|
|
"status": result.get("status"),
|
|
"current_stage": result.get("current_stage"),
|
|
"next_action": result.get("next_action"),
|
|
},
|
|
ip_address=request.client.host if request.client else None,
|
|
user_agent=request.headers.get("user-agent"),
|
|
)
|
|
return success_response(result, message="审查任务已重新发起")
|
|
|
|
|
|
@router.get("/{review_id}/risk-actions")
|
|
def list_review_risk_actions(
|
|
review_id: str,
|
|
review_risk_action_service: ReviewRiskActionService = Depends(get_review_risk_action_service),
|
|
current_user: dict = Depends(require_current_user),
|
|
):
|
|
result = review_risk_action_service.list_actions(current_user, review_id)
|
|
return success_response(result)
|
|
|
|
|
|
@router.put("/{review_id}/risk-actions/{action_id}")
|
|
def update_review_risk_action(
|
|
review_id: str,
|
|
action_id: str,
|
|
payload: ReviewRiskActionUpdateRequest,
|
|
request: Request,
|
|
review_risk_action_service: ReviewRiskActionService = Depends(get_review_risk_action_service),
|
|
audit_log_service: AuditLogService = Depends(get_audit_log_service),
|
|
current_user: dict = Depends(require_current_user),
|
|
):
|
|
result = review_risk_action_service.update_action(
|
|
current_user,
|
|
review_id,
|
|
action_id,
|
|
action_status=payload.action_status,
|
|
user_modified_text=payload.user_modified_text,
|
|
skip_reason=payload.skip_reason,
|
|
applied_contract_version_id=payload.applied_contract_version_id,
|
|
)
|
|
audit_log_service.record_event(
|
|
tenant_id=current_user.get("tenant_id"),
|
|
user_id=current_user.get("id"),
|
|
action="update_review_risk_action",
|
|
resource_type="review_risk_action",
|
|
resource_id=result["id"],
|
|
detail_json={
|
|
"review_id": review_id,
|
|
"risk_item_id": result["risk_item_id"],
|
|
"action_status": result["action_status"],
|
|
"has_user_modified_text": bool(result.get("user_modified_text")),
|
|
"has_skip_reason": bool(result.get("skip_reason")),
|
|
"applied_contract_version_id": result.get("applied_contract_version_id"),
|
|
},
|
|
ip_address=request.client.host if request.client else None,
|
|
user_agent=request.headers.get("user-agent"),
|
|
)
|
|
return success_response(result)
|
|
|
|
|
|
@router.post("/{review_id}/risk-actions/{action_id}/apply")
|
|
def apply_review_risk_action(
|
|
review_id: str,
|
|
action_id: str,
|
|
payload: ReviewRiskActionApplyRequest,
|
|
request: Request,
|
|
review_risk_action_service: ReviewRiskActionService = Depends(get_review_risk_action_service),
|
|
audit_log_service: AuditLogService = Depends(get_audit_log_service),
|
|
current_user: dict = Depends(require_current_user),
|
|
):
|
|
result = review_risk_action_service.apply_action(
|
|
current_user,
|
|
review_id,
|
|
action_id,
|
|
contract_id=payload.contract_id,
|
|
action_mode=payload.action_mode,
|
|
replacement_text=payload.replacement_text,
|
|
match_text=payload.match_text,
|
|
change_summary=payload.change_summary,
|
|
)
|
|
audit_log_service.record_event(
|
|
tenant_id=current_user.get("tenant_id"),
|
|
user_id=current_user.get("id"),
|
|
action="apply_review_risk_action",
|
|
resource_type="review_risk_action",
|
|
resource_id=result["action"]["id"],
|
|
detail_json={
|
|
"review_id": review_id,
|
|
"risk_item_id": result["action"]["risk_item_id"],
|
|
"action_status": result["action"]["action_status"],
|
|
"contract_id": (result.get("version") or {}).get("contract_id") or payload.contract_id,
|
|
"contract_version_id": (result.get("version") or {}).get("id"),
|
|
"version_label": (result.get("version") or {}).get("version_label"),
|
|
"locate_status": result.get("locate_status"),
|
|
"manual_confirmation_required": result.get("manual_confirmation_required", False),
|
|
},
|
|
ip_address=request.client.host if request.client else None,
|
|
user_agent=request.headers.get("user-agent"),
|
|
)
|
|
return success_response(result)
|
|
|
|
|
|
@router.get("/{review_id}/result")
|
|
def get_review_result(
|
|
review_id: str,
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
_current_user: dict = Depends(require_current_user),
|
|
):
|
|
result = review_service.get_review_result(_current_user, review_id)
|
|
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="review not found")
|
|
|
|
return success_response(result)
|
|
|
|
|
|
@router.get("/{review_id}/report")
|
|
def get_review_report(
|
|
review_id: str,
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
_current_user: dict = Depends(require_current_user),
|
|
):
|
|
report = review_service.get_review_report(_current_user, review_id)
|
|
|
|
if report is None:
|
|
raise HTTPException(status_code=404, detail="review not found")
|
|
|
|
return success_response(report)
|
|
|
|
|
|
@router.get("/{review_id}/report/download")
|
|
def download_review_report(
|
|
review_id: str,
|
|
request: Request,
|
|
review_service: ReviewService = Depends(get_review_service),
|
|
audit_log_service: AuditLogService = Depends(get_audit_log_service),
|
|
current_user: dict = Depends(require_current_user),
|
|
):
|
|
report = review_service.get_review_report(current_user, review_id)
|
|
|
|
if report is None:
|
|
raise HTTPException(status_code=404, detail="review not found")
|
|
|
|
content = review_service.build_report_pdf(current_user, review_id)
|
|
if content is None:
|
|
raise HTTPException(status_code=404, detail="review not found")
|
|
|
|
audit_log_service.record_event(
|
|
tenant_id=current_user.get("tenant_id"),
|
|
user_id=current_user.get("id"),
|
|
action="download_report",
|
|
resource_type="review",
|
|
resource_id=review_id,
|
|
detail_json={
|
|
"file_name": report["file_name"],
|
|
"download_url": report["download_url"],
|
|
"legal_article_ref_count": report.get("legal_article_ref_count", 0),
|
|
"legal_article_ref_ids": (
|
|
((report.get("report_metadata") or {}).get("legal_article_refs") or {}).get("legal_article_ref_ids")
|
|
or []
|
|
),
|
|
},
|
|
ip_address=request.client.host if request.client else None,
|
|
user_agent=request.headers.get("user-agent"),
|
|
)
|
|
headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{quote(report['file_name'])}"}
|
|
return Response(content=content, media_type="application/pdf", headers=headers)
|