feat: add contract template v3 api and legacy oss migration
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import bindparam, text
|
||||
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
||||
ContractTemplateListQueryDTO,
|
||||
ContractTemplateSearchQueryDTO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
||||
ContractTemplateCategoryVO,
|
||||
ContractTemplateDetailVO,
|
||||
ContractTemplateListItemVO,
|
||||
ContractTemplatePageVO,
|
||||
ContractTemplateSearchCategoryVO,
|
||||
ContractTemplateSearchResultVO,
|
||||
)
|
||||
from fastapi_modules.fastapi_leaudit.services.contractTemplateService import IContractTemplateService
|
||||
|
||||
_ALLOWED_SORT_FIELDS = {
|
||||
"id": "t.id",
|
||||
"title": "t.title",
|
||||
"created_at": "t.created_at",
|
||||
"updated_at": "t.updated_at",
|
||||
}
|
||||
|
||||
|
||||
class ContractTemplateServiceImpl(IContractTemplateService):
|
||||
"""合同模板服务实现。"""
|
||||
|
||||
async def ListCategories(self, IncludeDisabled: bool, WithTemplateCount: bool) -> list[ContractTemplateCategoryVO]:
|
||||
count_select = "COUNT(t.id)::int AS template_count" if WithTemplateCount else "0::int AS template_count"
|
||||
sql = text(
|
||||
f"""
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.icon,
|
||||
c.description,
|
||||
COALESCE(c.sort_order, 0) AS sort_order,
|
||||
{count_select},
|
||||
TRUE AS is_enabled
|
||||
FROM contract_categories c
|
||||
LEFT JOIN contract_templates t
|
||||
ON t.category_id = c.id
|
||||
WHERE 1=1
|
||||
GROUP BY c.id, c.name, c.icon, c.description, c.sort_order
|
||||
ORDER BY COALESCE(c.sort_order, 0) ASC, c.name ASC
|
||||
"""
|
||||
)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
rows = (await session.execute(sql)).mappings().all()
|
||||
|
||||
return [self._to_category_vo(row) for row in rows]
|
||||
|
||||
async def ListTemplates(self, Query: ContractTemplateListQueryDTO) -> ContractTemplatePageVO:
|
||||
where_clause, params, needs_category_name_filter = self._build_template_filters(
|
||||
keyword=Query.keyword,
|
||||
category_id=Query.category_id,
|
||||
category_name=Query.category_name,
|
||||
file_format=Query.file_format,
|
||||
is_featured=Query.is_featured,
|
||||
)
|
||||
order_sql = self._build_order_clause(Query.sort_by, Query.sort_order, default_field="updated_at", default_order="desc")
|
||||
offset = max(Query.page - 1, 0) * Query.page_size
|
||||
params.update({"limit": Query.page_size, "offset": offset})
|
||||
|
||||
from_sql = self._build_template_from_sql(needs_category_name_filter)
|
||||
|
||||
count_sql = text(
|
||||
f"""
|
||||
SELECT COUNT(*)
|
||||
{from_sql}
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
)
|
||||
list_sql = text(
|
||||
f"""
|
||||
SELECT
|
||||
t.id,
|
||||
t.template_code,
|
||||
t.title,
|
||||
t.category_id,
|
||||
c.name AS category_name,
|
||||
c.icon AS category_icon,
|
||||
c.description AS category_description,
|
||||
t.description,
|
||||
t.file_path,
|
||||
t.pdf_file_path,
|
||||
t.file_format,
|
||||
COALESCE(t.is_featured, FALSE) AS is_featured,
|
||||
t.created_at,
|
||||
t.updated_at
|
||||
{from_sql}
|
||||
WHERE {where_clause}
|
||||
ORDER BY {order_sql}
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
)
|
||||
count_sql, list_sql = self._bind_expanding(count_sql, list_sql, params)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
total = int((await session.execute(count_sql, params)).scalar_one())
|
||||
rows = (await session.execute(list_sql, params)).mappings().all()
|
||||
|
||||
return ContractTemplatePageVO(
|
||||
total=total,
|
||||
page=Query.page,
|
||||
page_size=Query.page_size,
|
||||
total_pages=max((total + Query.page_size - 1) // Query.page_size, 1) if total else 0,
|
||||
templates=[self._to_list_item_vo(row) for row in rows],
|
||||
)
|
||||
|
||||
async def SearchTemplates(self, Query: ContractTemplateSearchQueryDTO) -> ContractTemplateSearchResultVO:
|
||||
list_query = ContractTemplateListQueryDTO(
|
||||
keyword=Query.q,
|
||||
category_id=Query.category_id,
|
||||
category_name=Query.category_name,
|
||||
page=Query.page,
|
||||
page_size=Query.page_size,
|
||||
sort_by=Query.sort_by,
|
||||
sort_order=Query.sort_order,
|
||||
)
|
||||
page_result = await self.ListTemplates(list_query)
|
||||
category_stats = await self._load_search_category_stats(Query.q)
|
||||
|
||||
return ContractTemplateSearchResultVO(
|
||||
total=page_result.total,
|
||||
page=page_result.page,
|
||||
page_size=page_result.page_size,
|
||||
total_pages=page_result.total_pages,
|
||||
templates=page_result.templates,
|
||||
category_stats=category_stats,
|
||||
)
|
||||
|
||||
async def GetTemplateDetail(self, TemplateId: int) -> ContractTemplateDetailVO | None:
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
t.id,
|
||||
t.template_code,
|
||||
t.title,
|
||||
t.category_id,
|
||||
c.name AS category_name,
|
||||
c.icon AS category_icon,
|
||||
c.description AS category_description,
|
||||
t.description,
|
||||
t.file_path,
|
||||
t.pdf_file_path,
|
||||
t.file_format,
|
||||
COALESCE(t.is_featured, FALSE) AS is_featured,
|
||||
t.created_at,
|
||||
t.updated_at
|
||||
FROM contract_templates t
|
||||
LEFT JOIN contract_categories c ON c.id = t.category_id
|
||||
WHERE t.id = :template_id
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
row = (await session.execute(sql, {"template_id": TemplateId})).mappings().first()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
return self._to_detail_vo(row)
|
||||
|
||||
async def _load_search_category_stats(
|
||||
self,
|
||||
keyword: str,
|
||||
) -> list[ContractTemplateSearchCategoryVO]:
|
||||
clean_keyword = (keyword or "").strip()
|
||||
if not clean_keyword:
|
||||
return []
|
||||
|
||||
filters = [
|
||||
"("
|
||||
"t.title ILIKE :keyword "
|
||||
"OR COALESCE(t.description, '') ILIKE :keyword "
|
||||
"OR COALESCE(t.template_code, '') ILIKE :keyword "
|
||||
"OR COALESCE(c.name, '') ILIKE :keyword"
|
||||
")"
|
||||
]
|
||||
params: dict[str, Any] = {"keyword": f"%{clean_keyword}%"}
|
||||
|
||||
sql = text(
|
||||
f"""
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
COUNT(t.id)::int AS search_count
|
||||
FROM contract_categories c
|
||||
LEFT JOIN contract_templates t ON t.category_id = c.id
|
||||
WHERE {' AND '.join(filters)}
|
||||
GROUP BY c.id, c.name
|
||||
ORDER BY c.name ASC
|
||||
"""
|
||||
)
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
rows = (await session.execute(sql, params)).mappings().all()
|
||||
|
||||
return [
|
||||
ContractTemplateSearchCategoryVO(
|
||||
id=int(row["id"]),
|
||||
name=str(row["name"] or ""),
|
||||
search_count=int(row["search_count"] or 0),
|
||||
)
|
||||
for row in rows
|
||||
if row.get("id") is not None
|
||||
]
|
||||
|
||||
def _build_template_filters(
|
||||
self,
|
||||
keyword: str | None,
|
||||
category_id: int | None,
|
||||
category_name: str | None,
|
||||
file_format: str | None,
|
||||
is_featured: bool | None,
|
||||
) -> tuple[str, dict[str, Any], bool]:
|
||||
filters = ["1=1"]
|
||||
params: dict[str, Any] = {}
|
||||
needs_category_name_filter = False
|
||||
|
||||
if category_id is not None:
|
||||
filters.append("t.category_id = :category_id")
|
||||
params["category_id"] = category_id
|
||||
elif category_name:
|
||||
filters.append("c.name = :category_name")
|
||||
params["category_name"] = category_name.strip()
|
||||
needs_category_name_filter = True
|
||||
|
||||
if file_format:
|
||||
filters.append("t.file_format = :file_format")
|
||||
params["file_format"] = file_format.strip().lower()
|
||||
|
||||
if is_featured is not None:
|
||||
filters.append("COALESCE(t.is_featured, FALSE) = :is_featured")
|
||||
params["is_featured"] = is_featured
|
||||
|
||||
clean_keyword = (keyword or "").strip()
|
||||
if clean_keyword:
|
||||
filters.append(
|
||||
"("
|
||||
"t.title ILIKE :keyword "
|
||||
"OR COALESCE(t.description, '') ILIKE :keyword "
|
||||
"OR COALESCE(t.template_code, '') ILIKE :keyword "
|
||||
"OR COALESCE(c.name, '') ILIKE :keyword"
|
||||
")"
|
||||
)
|
||||
params["keyword"] = f"%{clean_keyword}%"
|
||||
needs_category_name_filter = True
|
||||
|
||||
return " AND ".join(filters), params, needs_category_name_filter
|
||||
|
||||
def _build_template_from_sql(self, needs_category_name_filter: bool) -> str:
|
||||
_ = needs_category_name_filter
|
||||
return """
|
||||
FROM contract_templates t
|
||||
LEFT JOIN contract_categories c ON c.id = t.category_id
|
||||
"""
|
||||
|
||||
def _build_order_clause(self, sort_by: str | None, sort_order: str | None, default_field: str, default_order: str) -> str:
|
||||
field = _ALLOWED_SORT_FIELDS.get(str(sort_by or "").strip().lower(), _ALLOWED_SORT_FIELDS[default_field])
|
||||
direction = "DESC" if str(sort_order or default_order).strip().lower() == "desc" else "ASC"
|
||||
return f"{field} {direction}, t.id ASC"
|
||||
|
||||
def _bind_expanding(self, *sql_objects_and_params: Any):
|
||||
sql_objects = list(sql_objects_and_params[:-1])
|
||||
params = sql_objects_and_params[-1]
|
||||
if "category_ids" in params:
|
||||
sql_objects = [sql.bindparams(bindparam("category_ids", expanding=True)) for sql in sql_objects]
|
||||
return tuple(sql_objects)
|
||||
|
||||
def _to_category_vo(self, row: Any) -> ContractTemplateCategoryVO:
|
||||
return ContractTemplateCategoryVO(
|
||||
id=int(row["id"]),
|
||||
name=str(row["name"] or ""),
|
||||
icon=row.get("icon"),
|
||||
description=row.get("description"),
|
||||
sort_order=int(row.get("sort_order") or 0),
|
||||
template_count=int(row.get("template_count") or 0),
|
||||
is_enabled=bool(row.get("is_enabled", True)),
|
||||
)
|
||||
|
||||
def _to_list_item_vo(self, row: Any) -> ContractTemplateListItemVO:
|
||||
return ContractTemplateListItemVO(
|
||||
id=int(row["id"]),
|
||||
template_code=str(row.get("template_code") or ""),
|
||||
title=str(row.get("title") or ""),
|
||||
category_id=int(row.get("category_id") or 0),
|
||||
category_name=row.get("category_name"),
|
||||
category_icon=row.get("category_icon"),
|
||||
description=row.get("description"),
|
||||
file_path=row.get("file_path"),
|
||||
pdf_file_path=row.get("pdf_file_path"),
|
||||
file_format=str(row.get("file_format") or ""),
|
||||
is_featured=bool(row.get("is_featured", False)),
|
||||
created_at=self._stringify_time(row.get("created_at")),
|
||||
updated_at=self._stringify_time(row.get("updated_at")),
|
||||
)
|
||||
|
||||
def _to_detail_vo(self, row: Any) -> ContractTemplateDetailVO:
|
||||
base = self._to_list_item_vo(row)
|
||||
return ContractTemplateDetailVO(
|
||||
**base.model_dump(),
|
||||
category_description=row.get("category_description"),
|
||||
placeholder_schema=None,
|
||||
)
|
||||
|
||||
def _stringify_time(self, value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return str(value)
|
||||
Reference in New Issue
Block a user