feat: 支持合同模板上传与地区隔离
This commit is contained in:
@@ -54,6 +54,7 @@ class OssPathUtils:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def BuildContractTemplateKey(
|
def BuildContractTemplateKey(
|
||||||
|
Region: str,
|
||||||
CategoryName: str,
|
CategoryName: str,
|
||||||
TemplateCode: str,
|
TemplateCode: str,
|
||||||
FileRole: str,
|
FileRole: str,
|
||||||
@@ -61,12 +62,13 @@ class OssPathUtils:
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""生成合同模板 object key。"""
|
"""生成合同模板 object key。"""
|
||||||
ext = Path(FileName).suffix or ""
|
ext = Path(FileName).suffix or ""
|
||||||
|
safe_region = OssPathUtils.BuildSafeFileStem(Region or "shared")
|
||||||
safe_category = OssPathUtils.BuildSafeFileStem(CategoryName or "uncategorized")
|
safe_category = OssPathUtils.BuildSafeFileStem(CategoryName or "uncategorized")
|
||||||
safe_template_code = OssPathUtils.BuildSafeFileStem(TemplateCode or "template")
|
safe_template_code = OssPathUtils.BuildSafeFileStem(TemplateCode or "template")
|
||||||
safe_stem = OssPathUtils.BuildSafeFileStem(FileName)
|
safe_stem = OssPathUtils.BuildSafeFileStem(FileName)
|
||||||
safe_role = OssPathUtils.BuildSafeFileStem(FileRole or "file")
|
safe_role = OssPathUtils.BuildSafeFileStem(FileRole or "file")
|
||||||
return (
|
return (
|
||||||
f"contract-templates/{safe_category}/{safe_template_code}/"
|
f"contract-templates/{safe_region}/{safe_category}/{safe_template_code}/"
|
||||||
f"{safe_role}__{safe_stem}{ext}"
|
f"{safe_role}__{safe_stem}{ext}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""合同模板控制器。"""
|
"""合同模板控制器。"""
|
||||||
|
|
||||||
from fastapi import Depends, Query
|
from fastapi import Depends, File, Form, Query, UploadFile
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from fastapi_common.fastapi_common_security.security import verify_access_token
|
from fastapi_common.fastapi_common_security.security import verify_access_token
|
||||||
from fastapi_common.fastapi_common_web.controller import BaseController
|
from fastapi_common.fastapi_common_web.controller import BaseController
|
||||||
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
||||||
|
ContractTemplateCreateDTO,
|
||||||
ContractTemplateListQueryDTO,
|
ContractTemplateListQueryDTO,
|
||||||
ContractTemplateSearchQueryDTO,
|
ContractTemplateSearchQueryDTO,
|
||||||
)
|
)
|
||||||
@@ -39,6 +40,7 @@ class ContractTemplateController(BaseController):
|
|||||||
keyword: str | None = Query(None, description="关键词"),
|
keyword: str | None = Query(None, description="关键词"),
|
||||||
category_id: int | None = Query(None, description="分类ID"),
|
category_id: int | None = Query(None, description="分类ID"),
|
||||||
category_name: str | None = Query(None, description="分类名称"),
|
category_name: str | None = Query(None, description="分类名称"),
|
||||||
|
region: str | None = Query(None, description="地区"),
|
||||||
file_format: str | None = Query(None, description="文件格式"),
|
file_format: str | None = Query(None, description="文件格式"),
|
||||||
is_featured: bool | None = Query(None, description="是否推荐"),
|
is_featured: bool | None = Query(None, description="是否推荐"),
|
||||||
page: int = Query(1, ge=1, description="页码"),
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
@@ -53,6 +55,7 @@ class ContractTemplateController(BaseController):
|
|||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
category_id=category_id,
|
category_id=category_id,
|
||||||
category_name=category_name,
|
category_name=category_name,
|
||||||
|
region=region,
|
||||||
file_format=file_format,
|
file_format=file_format,
|
||||||
is_featured=is_featured,
|
is_featured=is_featured,
|
||||||
page=page,
|
page=page,
|
||||||
@@ -60,7 +63,32 @@ class ContractTemplateController(BaseController):
|
|||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
sort_order=sort_order,
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
data = await self.ContractTemplateService.ListTemplates(query)
|
data = await self.ContractTemplateService.ListTemplates(query, int(payload["user_id"]))
|
||||||
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
||||||
|
|
||||||
|
@self.router.post("")
|
||||||
|
async def CreateContractTemplate(
|
||||||
|
title: str = Form(...),
|
||||||
|
template_code: str = Form(...),
|
||||||
|
category_id: int = Form(...),
|
||||||
|
region: str | None = Form(default=None),
|
||||||
|
description: str | None = Form(default=None),
|
||||||
|
is_featured: bool = Form(default=False),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
pdf_file: UploadFile | None = File(default=None),
|
||||||
|
payload: dict = Depends(verify_access_token),
|
||||||
|
):
|
||||||
|
if not await self._check_permission(int(payload["user_id"]), ["contract_template:create:write"]):
|
||||||
|
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有上传合同模板权限", "data": None})
|
||||||
|
body = ContractTemplateCreateDTO(
|
||||||
|
title=title,
|
||||||
|
template_code=template_code,
|
||||||
|
category_id=category_id,
|
||||||
|
region=region,
|
||||||
|
description=description,
|
||||||
|
is_featured=is_featured,
|
||||||
|
)
|
||||||
|
data = await self.ContractTemplateService.CreateTemplate(body, file, pdf_file, int(payload["user_id"]))
|
||||||
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
||||||
|
|
||||||
@self.router.get("/search")
|
@self.router.get("/search")
|
||||||
@@ -68,6 +96,7 @@ class ContractTemplateController(BaseController):
|
|||||||
q: str = Query(..., min_length=1, description="搜索关键词"),
|
q: str = Query(..., min_length=1, description="搜索关键词"),
|
||||||
category_id: int | None = Query(None, description="分类ID"),
|
category_id: int | None = Query(None, description="分类ID"),
|
||||||
category_name: str | None = Query(None, description="分类名称"),
|
category_name: str | None = Query(None, description="分类名称"),
|
||||||
|
region: str | None = Query(None, description="地区"),
|
||||||
page: int = Query(1, ge=1, description="页码"),
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
page_size: int = Query(12, ge=1, le=200, description="分页大小"),
|
page_size: int = Query(12, ge=1, le=200, description="分页大小"),
|
||||||
sort_by: str = Query("updated_at", description="排序字段"),
|
sort_by: str = Query("updated_at", description="排序字段"),
|
||||||
@@ -80,12 +109,13 @@ class ContractTemplateController(BaseController):
|
|||||||
q=q,
|
q=q,
|
||||||
category_id=category_id,
|
category_id=category_id,
|
||||||
category_name=category_name,
|
category_name=category_name,
|
||||||
|
region=region,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
sort_order=sort_order,
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
data = await self.ContractTemplateService.SearchTemplates(query)
|
data = await self.ContractTemplateService.SearchTemplates(query, int(payload["user_id"]))
|
||||||
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
||||||
|
|
||||||
@self.router.get("/{TemplateId}")
|
@self.router.get("/{TemplateId}")
|
||||||
@@ -95,11 +125,21 @@ class ContractTemplateController(BaseController):
|
|||||||
):
|
):
|
||||||
if not await self._check_permission(int(payload["user_id"]), ["contract_template:detail:read", "contract_template:list:read"]):
|
if not await self._check_permission(int(payload["user_id"]), ["contract_template:detail:read", "contract_template:list:read"]):
|
||||||
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看合同模板详情权限", "data": None})
|
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看合同模板详情权限", "data": None})
|
||||||
data = await self.ContractTemplateService.GetTemplateDetail(TemplateId)
|
data = await self.ContractTemplateService.GetTemplateDetail(TemplateId, int(payload["user_id"]))
|
||||||
if not data:
|
if not data:
|
||||||
return JSONResponse(status_code=404, content={"code": 404, "msg": "合同模板不存在", "data": None})
|
return JSONResponse(status_code=404, content={"code": 404, "msg": "合同模板不存在", "data": None})
|
||||||
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
||||||
|
|
||||||
|
@self.router.delete("/{TemplateId}")
|
||||||
|
async def DeleteContractTemplate(
|
||||||
|
TemplateId: int,
|
||||||
|
payload: dict = Depends(verify_access_token),
|
||||||
|
):
|
||||||
|
if not await self._check_permission(int(payload["user_id"]), ["contract_template:delete:delete"]):
|
||||||
|
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有删除合同模板权限", "data": None})
|
||||||
|
await self.ContractTemplateService.DeleteTemplate(TemplateId, int(payload["user_id"]))
|
||||||
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": True})
|
||||||
|
|
||||||
async def _check_permission(self, user_id: int, permission_keys: list[str]) -> bool:
|
async def _check_permission(self, user_id: int, permission_keys: list[str]) -> bool:
|
||||||
for permission_key in permission_keys:
|
for permission_key in permission_keys:
|
||||||
if await self.PermissionService.CheckPermission(user_id, permission_key):
|
if await self.PermissionService.CheckPermission(user_id, permission_key):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class ContractTemplateListQueryDTO(BaseModel):
|
|||||||
keyword: str | None = Field(None, description="关键词")
|
keyword: str | None = Field(None, description="关键词")
|
||||||
category_id: int | None = Field(None, description="分类ID")
|
category_id: int | None = Field(None, description="分类ID")
|
||||||
category_name: str | None = Field(None, description="分类名称")
|
category_name: str | None = Field(None, description="分类名称")
|
||||||
|
region: str | None = Field(None, description="地区")
|
||||||
file_format: str | None = Field(None, description="文件格式")
|
file_format: str | None = Field(None, description="文件格式")
|
||||||
is_featured: bool | None = Field(None, description="是否推荐")
|
is_featured: bool | None = Field(None, description="是否推荐")
|
||||||
page: int = Field(1, ge=1, description="页码")
|
page: int = Field(1, ge=1, description="页码")
|
||||||
@@ -21,7 +22,19 @@ class ContractTemplateSearchQueryDTO(BaseModel):
|
|||||||
q: str = Field(..., min_length=1, description="搜索关键词")
|
q: str = Field(..., min_length=1, description="搜索关键词")
|
||||||
category_id: int | None = Field(None, description="分类ID")
|
category_id: int | None = Field(None, description="分类ID")
|
||||||
category_name: str | None = Field(None, description="分类名称")
|
category_name: str | None = Field(None, description="分类名称")
|
||||||
|
region: str | None = Field(None, description="地区")
|
||||||
page: int = Field(1, ge=1, description="页码")
|
page: int = Field(1, ge=1, description="页码")
|
||||||
page_size: int = Field(12, ge=1, le=200, description="分页大小")
|
page_size: int = Field(12, ge=1, le=200, description="分页大小")
|
||||||
sort_by: str = Field("updated_at", description="排序字段")
|
sort_by: str = Field("updated_at", description="排序字段")
|
||||||
sort_order: str = Field("desc", description="排序方向")
|
sort_order: str = Field("desc", description="排序方向")
|
||||||
|
|
||||||
|
|
||||||
|
class ContractTemplateCreateDTO(BaseModel):
|
||||||
|
"""合同模板上传参数。"""
|
||||||
|
|
||||||
|
title: str = Field(..., min_length=1, max_length=200, description="模板标题")
|
||||||
|
template_code: str = Field(..., min_length=1, max_length=50, description="模板编码")
|
||||||
|
category_id: int = Field(..., description="分类ID")
|
||||||
|
region: str | None = Field(None, description="所属地区")
|
||||||
|
description: str | None = Field(None, description="模板简介")
|
||||||
|
is_featured: bool = Field(False, description="是否推荐")
|
||||||
|
|||||||
@@ -23,10 +23,17 @@ class ContractTemplateListItemVO(BaseModel):
|
|||||||
category_name: str | None = Field(None, description="分类名称")
|
category_name: str | None = Field(None, description="分类名称")
|
||||||
category_icon: str | None = Field(None, description="分类图标")
|
category_icon: str | None = Field(None, description="分类图标")
|
||||||
description: str | None = Field(None, description="模板简介")
|
description: str | None = Field(None, description="模板简介")
|
||||||
|
region: str = Field(..., description="所属地区")
|
||||||
file_path: str | None = Field(None, description="原始模板文件路径")
|
file_path: str | None = Field(None, description="原始模板文件路径")
|
||||||
pdf_file_path: str | None = Field(None, description="PDF 预览文件路径")
|
pdf_file_path: str | None = Field(None, description="PDF 预览文件路径")
|
||||||
file_format: str = Field(..., description="文件格式")
|
file_format: str = Field(..., description="文件格式")
|
||||||
|
original_file_name: str | None = Field(None, description="原始上传文件名")
|
||||||
|
mime_type: str | None = Field(None, description="MIME 类型")
|
||||||
|
file_size: int = Field(0, description="文件大小")
|
||||||
|
pdf_file_size: int | None = Field(None, description="预览 PDF 文件大小")
|
||||||
is_featured: bool = Field(False, description="是否推荐")
|
is_featured: bool = Field(False, description="是否推荐")
|
||||||
|
created_by: int | None = Field(None, description="创建人")
|
||||||
|
updated_by: int | None = Field(None, description="更新人")
|
||||||
created_at: str | None = Field(None, description="创建时间")
|
created_at: str | None = Field(None, description="创建时间")
|
||||||
updated_at: str | None = Field(None, description="更新时间")
|
updated_at: str | None = Field(None, description="更新时间")
|
||||||
|
|
||||||
@@ -48,6 +55,10 @@ class ContractTemplateDetailVO(ContractTemplateListItemVO):
|
|||||||
placeholder_schema: dict | None = Field(None, description="模板占位符结构")
|
placeholder_schema: dict | None = Field(None, description="模板占位符结构")
|
||||||
|
|
||||||
|
|
||||||
|
class ContractTemplateCreateVO(ContractTemplateDetailVO):
|
||||||
|
"""合同模板上传结果。"""
|
||||||
|
|
||||||
|
|
||||||
class ContractTemplateSearchCategoryVO(BaseModel):
|
class ContractTemplateSearchCategoryVO(BaseModel):
|
||||||
"""搜索结果分类统计。"""
|
"""搜索结果分类统计。"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
||||||
|
ContractTemplateCreateDTO,
|
||||||
ContractTemplateListQueryDTO,
|
ContractTemplateListQueryDTO,
|
||||||
ContractTemplateSearchQueryDTO,
|
ContractTemplateSearchQueryDTO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
||||||
ContractTemplateCategoryVO,
|
ContractTemplateCategoryVO,
|
||||||
|
ContractTemplateCreateVO,
|
||||||
ContractTemplateDetailVO,
|
ContractTemplateDetailVO,
|
||||||
ContractTemplatePageVO,
|
ContractTemplatePageVO,
|
||||||
ContractTemplateSearchResultVO,
|
ContractTemplateSearchResultVO,
|
||||||
@@ -20,13 +23,27 @@ class IContractTemplateService(ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def ListTemplates(self, Query: ContractTemplateListQueryDTO) -> ContractTemplatePageVO:
|
async def ListTemplates(self, Query: ContractTemplateListQueryDTO, CurrentUserId: int) -> ContractTemplatePageVO:
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def SearchTemplates(self, Query: ContractTemplateSearchQueryDTO) -> ContractTemplateSearchResultVO:
|
async def SearchTemplates(self, Query: ContractTemplateSearchQueryDTO, CurrentUserId: int) -> ContractTemplateSearchResultVO:
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def GetTemplateDetail(self, TemplateId: int) -> ContractTemplateDetailVO | None:
|
async def GetTemplateDetail(self, TemplateId: int, CurrentUserId: int) -> ContractTemplateDetailVO | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def CreateTemplate(
|
||||||
|
self,
|
||||||
|
Body: ContractTemplateCreateDTO,
|
||||||
|
File: UploadFile,
|
||||||
|
PdfFile: UploadFile | None,
|
||||||
|
CurrentUserId: int,
|
||||||
|
) -> ContractTemplateCreateVO:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def DeleteTemplate(self, TemplateId: int, CurrentUserId: int) -> None:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
from sqlalchemy import bindparam, text
|
from sqlalchemy import bindparam, text
|
||||||
|
|
||||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||||
|
from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
|
||||||
|
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||||
|
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||||
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
||||||
|
ContractTemplateCreateDTO,
|
||||||
ContractTemplateListQueryDTO,
|
ContractTemplateListQueryDTO,
|
||||||
ContractTemplateSearchQueryDTO,
|
ContractTemplateSearchQueryDTO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
||||||
ContractTemplateCategoryVO,
|
ContractTemplateCategoryVO,
|
||||||
|
ContractTemplateCreateVO,
|
||||||
ContractTemplateDetailVO,
|
ContractTemplateDetailVO,
|
||||||
ContractTemplateListItemVO,
|
ContractTemplateListItemVO,
|
||||||
ContractTemplatePageVO,
|
ContractTemplatePageVO,
|
||||||
@@ -18,6 +26,8 @@ from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
|||||||
ContractTemplateSearchResultVO,
|
ContractTemplateSearchResultVO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.services.contractTemplateService import IContractTemplateService
|
from fastapi_modules.fastapi_leaudit.services.contractTemplateService import IContractTemplateService
|
||||||
|
from fastapi_modules.fastapi_leaudit.services.ossService import IOssService
|
||||||
|
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
|
||||||
|
|
||||||
_ALLOWED_SORT_FIELDS = {
|
_ALLOWED_SORT_FIELDS = {
|
||||||
"id": "t.id",
|
"id": "t.id",
|
||||||
@@ -30,8 +40,14 @@ _ALLOWED_SORT_FIELDS = {
|
|||||||
class ContractTemplateServiceImpl(IContractTemplateService):
|
class ContractTemplateServiceImpl(IContractTemplateService):
|
||||||
"""合同模板服务实现。"""
|
"""合同模板服务实现。"""
|
||||||
|
|
||||||
|
def __init__(self, OssService: IOssService | None = None) -> None:
|
||||||
|
self.OssService = OssService or OssServiceImpl()
|
||||||
|
|
||||||
async def ListCategories(self, IncludeDisabled: bool, WithTemplateCount: bool) -> list[ContractTemplateCategoryVO]:
|
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"
|
count_select = "COUNT(t.id)::int AS template_count" if WithTemplateCount else "0::int AS template_count"
|
||||||
|
filters = ["c.deleted_at IS NULL"]
|
||||||
|
if not IncludeDisabled:
|
||||||
|
filters.append("1=1")
|
||||||
sql = text(
|
sql = text(
|
||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -45,24 +61,31 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
FROM contract_categories c
|
FROM contract_categories c
|
||||||
LEFT JOIN contract_templates t
|
LEFT JOIN contract_templates t
|
||||||
ON t.category_id = c.id
|
ON t.category_id = c.id
|
||||||
WHERE 1=1
|
AND t.deleted_at IS NULL
|
||||||
|
WHERE {' AND '.join(filters)}
|
||||||
GROUP BY c.id, c.name, c.icon, c.description, c.sort_order
|
GROUP BY c.id, c.name, c.icon, c.description, c.sort_order
|
||||||
ORDER BY COALESCE(c.sort_order, 0) ASC, c.name ASC
|
ORDER BY COALESCE(c.sort_order, 0) ASC, c.name ASC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
async with GetAsyncSession() as session:
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
rows = (await session.execute(sql)).mappings().all()
|
rows = (await session.execute(sql)).mappings().all()
|
||||||
|
|
||||||
return [self._to_category_vo(row) for row in rows]
|
return [self._to_category_vo(row) for row in rows]
|
||||||
|
|
||||||
async def ListTemplates(self, Query: ContractTemplateListQueryDTO) -> ContractTemplatePageVO:
|
async def ListTemplates(self, Query: ContractTemplateListQueryDTO, CurrentUserId: int) -> ContractTemplatePageVO:
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
where_clause, params, needs_category_name_filter = self._build_template_filters(
|
where_clause, params, needs_category_name_filter = self._build_template_filters(
|
||||||
keyword=Query.keyword,
|
keyword=Query.keyword,
|
||||||
category_id=Query.category_id,
|
category_id=Query.category_id,
|
||||||
category_name=Query.category_name,
|
category_name=Query.category_name,
|
||||||
|
region=Query.region,
|
||||||
file_format=Query.file_format,
|
file_format=Query.file_format,
|
||||||
is_featured=Query.is_featured,
|
is_featured=Query.is_featured,
|
||||||
|
currentUser=currentUser,
|
||||||
)
|
)
|
||||||
order_sql = self._build_order_clause(Query.sort_by, Query.sort_order, default_field="updated_at", default_order="desc")
|
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
|
offset = max(Query.page - 1, 0) * Query.page_size
|
||||||
@@ -87,11 +110,18 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
c.name AS category_name,
|
c.name AS category_name,
|
||||||
c.icon AS category_icon,
|
c.icon AS category_icon,
|
||||||
c.description AS category_description,
|
c.description AS category_description,
|
||||||
|
t.region,
|
||||||
t.description,
|
t.description,
|
||||||
t.file_path,
|
t.file_path,
|
||||||
t.pdf_file_path,
|
t.pdf_file_path,
|
||||||
t.file_format,
|
t.file_format,
|
||||||
|
t.original_file_name,
|
||||||
|
t.mime_type,
|
||||||
|
COALESCE(t.file_size, 0) AS file_size,
|
||||||
|
t.pdf_file_size,
|
||||||
COALESCE(t.is_featured, FALSE) AS is_featured,
|
COALESCE(t.is_featured, FALSE) AS is_featured,
|
||||||
|
t.created_by,
|
||||||
|
t.updated_by,
|
||||||
t.created_at,
|
t.created_at,
|
||||||
t.updated_at
|
t.updated_at
|
||||||
{from_sql}
|
{from_sql}
|
||||||
@@ -101,8 +131,6 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
count_sql, list_sql = self._bind_expanding(count_sql, list_sql, params)
|
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())
|
total = int((await session.execute(count_sql, params)).scalar_one())
|
||||||
rows = (await session.execute(list_sql, params)).mappings().all()
|
rows = (await session.execute(list_sql, params)).mappings().all()
|
||||||
|
|
||||||
@@ -114,18 +142,19 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
templates=[self._to_list_item_vo(row) for row in rows],
|
templates=[self._to_list_item_vo(row) for row in rows],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def SearchTemplates(self, Query: ContractTemplateSearchQueryDTO) -> ContractTemplateSearchResultVO:
|
async def SearchTemplates(self, Query: ContractTemplateSearchQueryDTO, CurrentUserId: int) -> ContractTemplateSearchResultVO:
|
||||||
list_query = ContractTemplateListQueryDTO(
|
list_query = ContractTemplateListQueryDTO(
|
||||||
keyword=Query.q,
|
keyword=Query.q,
|
||||||
category_id=Query.category_id,
|
category_id=Query.category_id,
|
||||||
category_name=Query.category_name,
|
category_name=Query.category_name,
|
||||||
|
region=Query.region,
|
||||||
page=Query.page,
|
page=Query.page,
|
||||||
page_size=Query.page_size,
|
page_size=Query.page_size,
|
||||||
sort_by=Query.sort_by,
|
sort_by=Query.sort_by,
|
||||||
sort_order=Query.sort_order,
|
sort_order=Query.sort_order,
|
||||||
)
|
)
|
||||||
page_result = await self.ListTemplates(list_query)
|
page_result = await self.ListTemplates(list_query, CurrentUserId)
|
||||||
category_stats = await self._load_search_category_stats(Query.q)
|
category_stats = await self._load_search_category_stats(Query.q, Query.region, CurrentUserId)
|
||||||
|
|
||||||
return ContractTemplateSearchResultVO(
|
return ContractTemplateSearchResultVO(
|
||||||
total=page_result.total,
|
total=page_result.total,
|
||||||
@@ -136,9 +165,14 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
category_stats=category_stats,
|
category_stats=category_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def GetTemplateDetail(self, TemplateId: int) -> ContractTemplateDetailVO | None:
|
async def GetTemplateDetail(self, TemplateId: int, CurrentUserId: int) -> ContractTemplateDetailVO | None:
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
|
params: dict[str, Any] = {"template_id": TemplateId}
|
||||||
|
scope_filters = self._build_template_scope_filters(currentUser, params, requestedRegion=None)
|
||||||
sql = text(
|
sql = text(
|
||||||
"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.template_code,
|
t.template_code,
|
||||||
@@ -147,44 +181,280 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
c.name AS category_name,
|
c.name AS category_name,
|
||||||
c.icon AS category_icon,
|
c.icon AS category_icon,
|
||||||
c.description AS category_description,
|
c.description AS category_description,
|
||||||
|
t.region,
|
||||||
t.description,
|
t.description,
|
||||||
t.file_path,
|
t.file_path,
|
||||||
t.pdf_file_path,
|
t.pdf_file_path,
|
||||||
t.file_format,
|
t.file_format,
|
||||||
|
t.original_file_name,
|
||||||
|
t.mime_type,
|
||||||
|
COALESCE(t.file_size, 0) AS file_size,
|
||||||
|
t.pdf_file_size,
|
||||||
COALESCE(t.is_featured, FALSE) AS is_featured,
|
COALESCE(t.is_featured, FALSE) AS is_featured,
|
||||||
|
t.created_by,
|
||||||
|
t.updated_by,
|
||||||
t.created_at,
|
t.created_at,
|
||||||
t.updated_at
|
t.updated_at
|
||||||
FROM contract_templates t
|
FROM contract_templates t
|
||||||
LEFT JOIN contract_categories c ON c.id = t.category_id
|
LEFT JOIN contract_categories c ON c.id = t.category_id
|
||||||
WHERE t.id = :template_id
|
WHERE t.id = :template_id
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
AND c.deleted_at IS NULL
|
||||||
|
AND {' AND '.join(scope_filters)}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
row = (await session.execute(sql, params)).mappings().first()
|
||||||
async with GetAsyncSession() as session:
|
|
||||||
row = (await session.execute(sql, {"template_id": TemplateId})).mappings().first()
|
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return self._to_detail_vo(row)
|
return self._to_detail_vo(row)
|
||||||
|
|
||||||
|
async def CreateTemplate(
|
||||||
|
self,
|
||||||
|
Body: ContractTemplateCreateDTO,
|
||||||
|
File: UploadFile,
|
||||||
|
PdfFile: UploadFile | None,
|
||||||
|
CurrentUserId: int,
|
||||||
|
) -> ContractTemplateCreateVO:
|
||||||
|
if File is None or not File.filename:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板主文件不能为空")
|
||||||
|
|
||||||
|
fileContent = await File.read()
|
||||||
|
if not fileContent:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板主文件内容不能为空")
|
||||||
|
|
||||||
|
normalizedCode = (Body.template_code or "").strip()
|
||||||
|
normalizedTitle = (Body.title or "").strip()
|
||||||
|
if not normalizedCode:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板编码不能为空")
|
||||||
|
if not normalizedTitle:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板标题不能为空")
|
||||||
|
|
||||||
|
fileExt = Path(File.filename).suffix.lstrip(".").lower()
|
||||||
|
if fileExt not in {"doc", "docx", "pdf"}:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前仅支持上传 DOC、DOCX、PDF 模板")
|
||||||
|
mimeType = File.content_type or mimetypes.guess_type(File.filename)[0] or "application/octet-stream"
|
||||||
|
|
||||||
|
pdfContent: bytes | None = None
|
||||||
|
pdfMimeType: str | None = None
|
||||||
|
pdfFileName: str | None = None
|
||||||
|
if PdfFile and PdfFile.filename:
|
||||||
|
pdfContent = await PdfFile.read()
|
||||||
|
if not pdfContent:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "预览 PDF 文件内容不能为空")
|
||||||
|
pdfExt = Path(PdfFile.filename).suffix.lstrip(".").lower()
|
||||||
|
if pdfExt != "pdf":
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "预览文件仅支持 PDF")
|
||||||
|
pdfMimeType = PdfFile.content_type or "application/pdf"
|
||||||
|
pdfFileName = PdfFile.filename
|
||||||
|
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
|
resolvedRegion = self._resolve_upload_region(currentUser, Body.region)
|
||||||
|
categoryRow = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT id, name
|
||||||
|
FROM contract_categories
|
||||||
|
WHERE id = :category_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"category_id": Body.category_id},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if not categoryRow:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "合同模板分类不存在")
|
||||||
|
|
||||||
|
duplicateRow = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_templates
|
||||||
|
WHERE region = :region
|
||||||
|
AND template_code = :template_code
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"region": resolvedRegion, "template_code": normalizedCode},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if duplicateRow:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, f"当前地区已存在模板编码 {normalizedCode}")
|
||||||
|
|
||||||
|
categoryName = str(categoryRow["name"] or "未分类")
|
||||||
|
objectKey = OssPathUtils.BuildContractTemplateKey(
|
||||||
|
Region=resolvedRegion,
|
||||||
|
CategoryName=categoryName,
|
||||||
|
TemplateCode=normalizedCode,
|
||||||
|
FileRole="source",
|
||||||
|
FileName=File.filename,
|
||||||
|
)
|
||||||
|
filePath = await self.OssService.UploadBytes(
|
||||||
|
ObjectKey=objectKey,
|
||||||
|
Content=fileContent,
|
||||||
|
ContentType=mimeType,
|
||||||
|
)
|
||||||
|
|
||||||
|
pdfPath: str | None = None
|
||||||
|
if pdfContent is not None and pdfFileName:
|
||||||
|
pdfObjectKey = OssPathUtils.BuildContractTemplateKey(
|
||||||
|
Region=resolvedRegion,
|
||||||
|
CategoryName=categoryName,
|
||||||
|
TemplateCode=normalizedCode,
|
||||||
|
FileRole="preview",
|
||||||
|
FileName=pdfFileName,
|
||||||
|
)
|
||||||
|
pdfPath = await self.OssService.UploadBytes(
|
||||||
|
ObjectKey=pdfObjectKey,
|
||||||
|
Content=pdfContent,
|
||||||
|
ContentType=pdfMimeType or "application/pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
createdRow = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO contract_templates (
|
||||||
|
template_code,
|
||||||
|
title,
|
||||||
|
category_id,
|
||||||
|
region,
|
||||||
|
description,
|
||||||
|
file_path,
|
||||||
|
pdf_file_path,
|
||||||
|
file_format,
|
||||||
|
original_file_name,
|
||||||
|
mime_type,
|
||||||
|
file_size,
|
||||||
|
pdf_file_size,
|
||||||
|
is_featured,
|
||||||
|
created_by,
|
||||||
|
updated_by,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
:template_code,
|
||||||
|
:title,
|
||||||
|
:category_id,
|
||||||
|
:region,
|
||||||
|
:description,
|
||||||
|
:file_path,
|
||||||
|
:pdf_file_path,
|
||||||
|
:file_format,
|
||||||
|
:original_file_name,
|
||||||
|
:mime_type,
|
||||||
|
:file_size,
|
||||||
|
:pdf_file_size,
|
||||||
|
:is_featured,
|
||||||
|
:created_by,
|
||||||
|
:updated_by,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"template_code": normalizedCode,
|
||||||
|
"title": normalizedTitle,
|
||||||
|
"category_id": Body.category_id,
|
||||||
|
"region": resolvedRegion,
|
||||||
|
"description": (Body.description or "").strip() or None,
|
||||||
|
"file_path": filePath,
|
||||||
|
"pdf_file_path": pdfPath,
|
||||||
|
"file_format": fileExt,
|
||||||
|
"original_file_name": File.filename,
|
||||||
|
"mime_type": mimeType,
|
||||||
|
"file_size": len(fileContent),
|
||||||
|
"pdf_file_size": len(pdfContent) if pdfContent is not None else None,
|
||||||
|
"is_featured": Body.is_featured,
|
||||||
|
"created_by": CurrentUserId,
|
||||||
|
"updated_by": CurrentUserId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
if not createdRow:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "合同模板创建失败")
|
||||||
|
|
||||||
|
detail = await self.GetTemplateDetail(int(createdRow["id"]), CurrentUserId)
|
||||||
|
if not detail:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "合同模板创建成功但详情读取失败")
|
||||||
|
return ContractTemplateCreateVO(**detail.model_dump())
|
||||||
|
|
||||||
|
async def DeleteTemplate(self, TemplateId: int, CurrentUserId: int) -> None:
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
|
params: dict[str, Any] = {"template_id": TemplateId}
|
||||||
|
scope_filters = self._build_template_scope_filters(currentUser, params, requestedRegion=None, writable=True)
|
||||||
|
row = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
f"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_templates t
|
||||||
|
WHERE t.id = :template_id
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
AND {' AND '.join(scope_filters)}
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if not row:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "合同模板不存在或无权删除")
|
||||||
|
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE contract_templates
|
||||||
|
SET deleted_at = NOW(),
|
||||||
|
updated_at = NOW(),
|
||||||
|
updated_by = :updated_by
|
||||||
|
WHERE id = :template_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"template_id": TemplateId, "updated_by": CurrentUserId},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
async def _load_search_category_stats(
|
async def _load_search_category_stats(
|
||||||
self,
|
self,
|
||||||
keyword: str,
|
keyword: str,
|
||||||
|
requestedRegion: str | None,
|
||||||
|
CurrentUserId: int,
|
||||||
) -> list[ContractTemplateSearchCategoryVO]:
|
) -> list[ContractTemplateSearchCategoryVO]:
|
||||||
clean_keyword = (keyword or "").strip()
|
clean_keyword = (keyword or "").strip()
|
||||||
if not clean_keyword:
|
if not clean_keyword:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
|
params: dict[str, Any] = {"keyword": f"%{clean_keyword}%"}
|
||||||
|
scope_filters = self._build_template_scope_filters(currentUser, params, requestedRegion=requestedRegion)
|
||||||
filters = [
|
filters = [
|
||||||
|
"c.deleted_at IS NULL",
|
||||||
|
"t.deleted_at IS NULL",
|
||||||
"("
|
"("
|
||||||
"t.title ILIKE :keyword "
|
"t.title ILIKE :keyword "
|
||||||
"OR COALESCE(t.description, '') ILIKE :keyword "
|
"OR COALESCE(t.description, '') ILIKE :keyword "
|
||||||
"OR COALESCE(t.template_code, '') ILIKE :keyword "
|
"OR COALESCE(t.template_code, '') ILIKE :keyword "
|
||||||
"OR COALESCE(c.name, '') ILIKE :keyword"
|
"OR COALESCE(c.name, '') ILIKE :keyword"
|
||||||
")"
|
")",
|
||||||
|
*scope_filters,
|
||||||
]
|
]
|
||||||
params: dict[str, Any] = {"keyword": f"%{clean_keyword}%"}
|
|
||||||
|
|
||||||
sql = text(
|
sql = text(
|
||||||
f"""
|
f"""
|
||||||
@@ -199,8 +469,6 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
ORDER BY c.name ASC
|
ORDER BY c.name ASC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
async with GetAsyncSession() as session:
|
|
||||||
rows = (await session.execute(sql, params)).mappings().all()
|
rows = (await session.execute(sql, params)).mappings().all()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -218,13 +486,17 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
keyword: str | None,
|
keyword: str | None,
|
||||||
category_id: int | None,
|
category_id: int | None,
|
||||||
category_name: str | None,
|
category_name: str | None,
|
||||||
|
region: str | None,
|
||||||
file_format: str | None,
|
file_format: str | None,
|
||||||
is_featured: bool | None,
|
is_featured: bool | None,
|
||||||
|
currentUser: dict[str, Any],
|
||||||
) -> tuple[str, dict[str, Any], bool]:
|
) -> tuple[str, dict[str, Any], bool]:
|
||||||
filters = ["1=1"]
|
filters = ["t.deleted_at IS NULL", "c.deleted_at IS NULL"]
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
needs_category_name_filter = False
|
needs_category_name_filter = False
|
||||||
|
|
||||||
|
filters.extend(self._build_template_scope_filters(currentUser, params, region))
|
||||||
|
|
||||||
if category_id is not None:
|
if category_id is not None:
|
||||||
filters.append("t.category_id = :category_id")
|
filters.append("t.category_id = :category_id")
|
||||||
params["category_id"] = category_id
|
params["category_id"] = category_id
|
||||||
@@ -234,7 +506,7 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
needs_category_name_filter = True
|
needs_category_name_filter = True
|
||||||
|
|
||||||
if file_format:
|
if file_format:
|
||||||
filters.append("t.file_format = :file_format")
|
filters.append("LOWER(t.file_format) = :file_format")
|
||||||
params["file_format"] = file_format.strip().lower()
|
params["file_format"] = file_format.strip().lower()
|
||||||
|
|
||||||
if is_featured is not None:
|
if is_featured is not None:
|
||||||
@@ -271,8 +543,8 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
def _bind_expanding(self, *sql_objects_and_params: Any):
|
def _bind_expanding(self, *sql_objects_and_params: Any):
|
||||||
sql_objects = list(sql_objects_and_params[:-1])
|
sql_objects = list(sql_objects_and_params[:-1])
|
||||||
params = sql_objects_and_params[-1]
|
params = sql_objects_and_params[-1]
|
||||||
if "category_ids" in params:
|
if "visible_regions" in params:
|
||||||
sql_objects = [sql.bindparams(bindparam("category_ids", expanding=True)) for sql in sql_objects]
|
sql_objects = [sql.bindparams(bindparam("visible_regions", expanding=True)) for sql in sql_objects]
|
||||||
return tuple(sql_objects)
|
return tuple(sql_objects)
|
||||||
|
|
||||||
def _to_category_vo(self, row: Any) -> ContractTemplateCategoryVO:
|
def _to_category_vo(self, row: Any) -> ContractTemplateCategoryVO:
|
||||||
@@ -295,10 +567,17 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
category_name=row.get("category_name"),
|
category_name=row.get("category_name"),
|
||||||
category_icon=row.get("category_icon"),
|
category_icon=row.get("category_icon"),
|
||||||
description=row.get("description"),
|
description=row.get("description"),
|
||||||
|
region=str(row.get("region") or "省级"),
|
||||||
file_path=row.get("file_path"),
|
file_path=row.get("file_path"),
|
||||||
pdf_file_path=row.get("pdf_file_path"),
|
pdf_file_path=row.get("pdf_file_path"),
|
||||||
file_format=str(row.get("file_format") or ""),
|
file_format=str(row.get("file_format") or ""),
|
||||||
|
original_file_name=row.get("original_file_name"),
|
||||||
|
mime_type=row.get("mime_type"),
|
||||||
|
file_size=int(row.get("file_size") or 0),
|
||||||
|
pdf_file_size=int(row["pdf_file_size"]) if row.get("pdf_file_size") is not None else None,
|
||||||
is_featured=bool(row.get("is_featured", False)),
|
is_featured=bool(row.get("is_featured", False)),
|
||||||
|
created_by=int(row["created_by"]) if row.get("created_by") is not None else None,
|
||||||
|
updated_by=int(row["updated_by"]) if row.get("updated_by") is not None else None,
|
||||||
created_at=self._stringify_time(row.get("created_at")),
|
created_at=self._stringify_time(row.get("created_at")),
|
||||||
updated_at=self._stringify_time(row.get("updated_at")),
|
updated_at=self._stringify_time(row.get("updated_at")),
|
||||||
)
|
)
|
||||||
@@ -315,3 +594,178 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
def _build_template_scope_filters(
|
||||||
|
self,
|
||||||
|
currentUser: dict[str, Any],
|
||||||
|
params: dict[str, Any],
|
||||||
|
requestedRegion: str | None,
|
||||||
|
writable: bool = False,
|
||||||
|
) -> list[str]:
|
||||||
|
requested = (requestedRegion or "").strip()
|
||||||
|
area = str(currentUser["area"] or "").strip()
|
||||||
|
|
||||||
|
if currentUser["is_global"]:
|
||||||
|
if requested:
|
||||||
|
params["requested_region"] = requested
|
||||||
|
return ["t.region = :requested_region"]
|
||||||
|
return ["1=1"]
|
||||||
|
|
||||||
|
if writable:
|
||||||
|
if not area:
|
||||||
|
return ["1=0"]
|
||||||
|
if requested and requested != area:
|
||||||
|
return ["1=0"]
|
||||||
|
params["scope_region"] = area
|
||||||
|
return ["t.region = :scope_region"]
|
||||||
|
|
||||||
|
if currentUser["can_manage"]:
|
||||||
|
if not area:
|
||||||
|
return ["1=0"]
|
||||||
|
if requested:
|
||||||
|
if requested == "省级":
|
||||||
|
params["requested_region"] = requested
|
||||||
|
return ["t.region = :requested_region"]
|
||||||
|
if requested != area:
|
||||||
|
return ["1=0"]
|
||||||
|
params["requested_region"] = requested
|
||||||
|
return ["t.region = :requested_region"]
|
||||||
|
params["visible_regions"] = ["省级", area]
|
||||||
|
return ["t.region IN :visible_regions"]
|
||||||
|
|
||||||
|
if requested:
|
||||||
|
if requested == "省级":
|
||||||
|
params["requested_region"] = requested
|
||||||
|
return ["t.region = :requested_region"]
|
||||||
|
if area and requested == area:
|
||||||
|
params["requested_region"] = requested
|
||||||
|
return ["t.region = :requested_region"]
|
||||||
|
return ["1=0"]
|
||||||
|
|
||||||
|
if area:
|
||||||
|
params["visible_regions"] = ["省级", area]
|
||||||
|
return ["t.region IN :visible_regions"]
|
||||||
|
params["requested_region"] = "省级"
|
||||||
|
return ["t.region = :requested_region"]
|
||||||
|
|
||||||
|
async def _getCurrentUserContext(self, CurrentUserId: int, session=None) -> dict[str, Any]:
|
||||||
|
own_session = False
|
||||||
|
if session is None:
|
||||||
|
own_session = True
|
||||||
|
session_cm = GetAsyncSession()
|
||||||
|
session = await session_cm.__aenter__()
|
||||||
|
try:
|
||||||
|
row = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
COALESCE(u.area, '') AS area,
|
||||||
|
COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin')), FALSE) AS is_global,
|
||||||
|
COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin', 'admin')), FALSE) AS can_manage
|
||||||
|
FROM sso_users u
|
||||||
|
LEFT JOIN user_role ur ON ur.user_id = u.id
|
||||||
|
LEFT JOIN roles r ON r.id = ur.role_id
|
||||||
|
WHERE u.id = :user_id
|
||||||
|
GROUP BY u.id, u.area
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"user_id": CurrentUserId},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if not row:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "当前用户不存在")
|
||||||
|
return {
|
||||||
|
"id": int(row["id"]),
|
||||||
|
"area": str(row["area"] or ""),
|
||||||
|
"is_global": bool(row["is_global"]),
|
||||||
|
"can_manage": bool(row["can_manage"]),
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if own_session:
|
||||||
|
await session_cm.__aexit__(None, None, None)
|
||||||
|
|
||||||
|
def _resolve_upload_region(self, currentUser: dict[str, Any], requestedRegion: str | None) -> str:
|
||||||
|
area = str(currentUser["area"] or "").strip()
|
||||||
|
if currentUser["can_manage"]:
|
||||||
|
if not area:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前管理员账号未配置地区,无法上传合同模板")
|
||||||
|
return area
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户没有上传合同模板权限")
|
||||||
|
|
||||||
|
async def _ensureContractTemplateSchema(self, session) -> None:
|
||||||
|
statements = [
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_categories
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_categories
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_categories
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS region VARCHAR(50) NOT NULL DEFAULT '省级'
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS pdf_file_path VARCHAR(500)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS original_file_name VARCHAR(500) NOT NULL DEFAULT ''
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS mime_type VARCHAR(200)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS file_size BIGINT NOT NULL DEFAULT 0
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS pdf_file_size BIGINT
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE contract_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
for statement in statements:
|
||||||
|
await session.execute(text(statement))
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE contract_templates
|
||||||
|
SET region = '省级'
|
||||||
|
WHERE region IS NULL OR BTRIM(region) = ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE contract_templates
|
||||||
|
SET original_file_name = COALESCE(NULLIF(original_file_name, ''), title || CASE
|
||||||
|
WHEN file_format IS NOT NULL AND BTRIM(file_format) <> '' THEN '.' || LOWER(file_format)
|
||||||
|
ELSE ''
|
||||||
|
END)
|
||||||
|
WHERE original_file_name IS NULL OR BTRIM(original_file_name) = ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
|||||||
{"permission_key": "contract_template:list:read", "display_name": "查看合同模板列表", "module": "contract_template", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates", "route_path": "/contract-template/list"},
|
{"permission_key": "contract_template:list:read", "display_name": "查看合同模板列表", "module": "contract_template", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates", "route_path": "/contract-template/list"},
|
||||||
{"permission_key": "contract_template:search:read", "display_name": "搜索合同模板", "module": "contract_template", "resource": "search", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/search", "route_path": "/contract-template/search"},
|
{"permission_key": "contract_template:search:read", "display_name": "搜索合同模板", "module": "contract_template", "resource": "search", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/search", "route_path": "/contract-template/search"},
|
||||||
{"permission_key": "contract_template:detail:read", "display_name": "查看合同模板详情", "module": "contract_template", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
|
{"permission_key": "contract_template:detail:read", "display_name": "查看合同模板详情", "module": "contract_template", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
|
||||||
|
{"permission_key": "contract_template:create:write", "display_name": "上传合同模板", "module": "contract_template", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/contract-templates", "route_path": "/contract-template/list"},
|
||||||
|
{"permission_key": "contract_template:update:write", "display_name": "更新合同模板", "module": "contract_template", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
|
||||||
|
{"permission_key": "contract_template:delete:delete", "display_name": "删除合同模板", "module": "contract_template", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
|
||||||
{"permission_key": "evaluation_group:list:read", "display_name": "评查点分组列表", "module": "evaluation_group", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
|
{"permission_key": "evaluation_group:list:read", "display_name": "评查点分组列表", "module": "evaluation_group", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
|
||||||
{"permission_key": "evaluation_group:create:write", "display_name": "创建评查点分组", "module": "evaluation_group", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
|
{"permission_key": "evaluation_group:create:write", "display_name": "创建评查点分组", "module": "evaluation_group", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"},
|
||||||
{"permission_key": "evaluation_group:update:write", "display_name": "更新评查点分组与绑定", "module": "evaluation_group", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"},
|
{"permission_key": "evaluation_group:update:write", "display_name": "更新评查点分组与绑定", "module": "evaluation_group", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"},
|
||||||
|
|||||||
@@ -164,12 +164,14 @@ def resolve_pdf_path(template: LegacyTemplate, object_keys: set[str]) -> str:
|
|||||||
|
|
||||||
def build_new_object_keys(template: LegacyTemplate, docx_path: str, pdf_path: str) -> tuple[str, str]:
|
def build_new_object_keys(template: LegacyTemplate, docx_path: str, pdf_path: str) -> tuple[str, str]:
|
||||||
docx_key = OssPathUtils.BuildContractTemplateKey(
|
docx_key = OssPathUtils.BuildContractTemplateKey(
|
||||||
|
Region="省级",
|
||||||
CategoryName=template.category_name,
|
CategoryName=template.category_name,
|
||||||
TemplateCode=template.template_code,
|
TemplateCode=template.template_code,
|
||||||
FileRole="source",
|
FileRole="source",
|
||||||
FileName=Path(docx_path).name,
|
FileName=Path(docx_path).name,
|
||||||
)
|
)
|
||||||
pdf_key = OssPathUtils.BuildContractTemplateKey(
|
pdf_key = OssPathUtils.BuildContractTemplateKey(
|
||||||
|
Region="省级",
|
||||||
CategoryName=template.category_name,
|
CategoryName=template.category_name,
|
||||||
TemplateCode=template.template_code,
|
TemplateCode=template.template_code,
|
||||||
FileRole="preview",
|
FileRole="preview",
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ BEGIN;
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- LeAudit Platform Contract Template Schema
|
-- LeAudit Platform Contract Template Schema
|
||||||
-- 目标:
|
-- 目标:
|
||||||
-- 1. 在主库 leaudit_platform 创建合同模板分类表
|
-- 1. 在主库 leaudit_platform 创建 / 升级合同模板分类表
|
||||||
-- 2. 在主库 leaudit_platform 创建合同模板表
|
-- 2. 在主库 leaudit_platform 创建 / 升级合同模板主表
|
||||||
-- 3. 补齐索引与基础约束
|
-- 3. 补齐地区字段、审计字段、软删除字段、索引与 updated_at 触发器
|
||||||
-- 说明:
|
-- 说明:
|
||||||
-- - 本脚本不依赖旧库 docauditai
|
-- - 本脚本不依赖旧库 docauditai
|
||||||
-- - 幂等脚本,可重复执行
|
-- - 幂等脚本,可重复执行
|
||||||
@@ -17,50 +17,168 @@ CREATE TABLE IF NOT EXISTS public.contract_categories (
|
|||||||
icon VARCHAR(100) NULL,
|
icon VARCHAR(100) NULL,
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_by BIGINT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_by BIGINT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_contract_categories_name
|
ALTER TABLE public.contract_categories
|
||||||
ON public.contract_categories(name);
|
ADD COLUMN IF NOT EXISTS icon VARCHAR(100),
|
||||||
|
ADD COLUMN IF NOT EXISTS description TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.contract_templates (
|
CREATE TABLE IF NOT EXISTS public.contract_templates (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
template_code VARCHAR(50) NOT NULL,
|
template_code VARCHAR(50) NOT NULL,
|
||||||
title VARCHAR(200) NOT NULL,
|
title VARCHAR(200) NOT NULL,
|
||||||
category_id INTEGER NOT NULL REFERENCES public.contract_categories(id) ON DELETE RESTRICT,
|
category_id INTEGER NOT NULL REFERENCES public.contract_categories(id) ON DELETE RESTRICT,
|
||||||
|
region VARCHAR(50) NOT NULL DEFAULT '省级',
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
file_path VARCHAR(500) NULL,
|
file_path VARCHAR(500) NULL,
|
||||||
|
pdf_file_path VARCHAR(500) NULL,
|
||||||
file_format VARCHAR(10) NOT NULL,
|
file_format VARCHAR(10) NOT NULL,
|
||||||
|
original_file_name VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
mime_type VARCHAR(200) NULL,
|
||||||
|
file_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
pdf_file_size BIGINT NULL,
|
||||||
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_by BIGINT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_by BIGINT NULL,
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
pdf_file_path VARCHAR(500) NULL
|
deleted_at TIMESTAMPTZ NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_contract_templates_code
|
ALTER TABLE public.contract_templates
|
||||||
ON public.contract_templates(template_code);
|
ADD COLUMN IF NOT EXISTS region VARCHAR(50) NOT NULL DEFAULT '省级',
|
||||||
|
ADD COLUMN IF NOT EXISTS description TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS file_path VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS pdf_file_path VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS file_format VARCHAR(10),
|
||||||
|
ADD COLUMN IF NOT EXISTS original_file_name VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN IF NOT EXISTS mime_type VARCHAR(200),
|
||||||
|
ADD COLUMN IF NOT EXISTS file_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS pdf_file_size BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_by BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_contract_templates_category_id
|
UPDATE public.contract_templates
|
||||||
ON public.contract_templates(category_id);
|
SET region = '省级'
|
||||||
|
WHERE region IS NULL OR BTRIM(region) = '';
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_contract_templates_updated_at
|
UPDATE public.contract_templates
|
||||||
ON public.contract_templates(updated_at DESC);
|
SET original_file_name = COALESCE(NULLIF(original_file_name, ''), title || CASE
|
||||||
|
WHEN file_format IS NOT NULL AND BTRIM(file_format) <> '' THEN '.' || LOWER(file_format)
|
||||||
|
ELSE ''
|
||||||
|
END)
|
||||||
|
WHERE original_file_name IS NULL OR BTRIM(original_file_name) = '';
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND indexname = 'idx_contract_categories_name'
|
||||||
|
) THEN
|
||||||
|
EXECUTE 'DROP INDEX IF EXISTS public.idx_contract_categories_name';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND indexname = 'idx_contract_templates_code'
|
||||||
|
) THEN
|
||||||
|
EXECUTE 'DROP INDEX IF EXISTS public.idx_contract_templates_code';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_contract_categories_name_active
|
||||||
|
ON public.contract_categories(name)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contract_categories_sort_active
|
||||||
|
ON public.contract_categories(sort_order)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_contract_templates_region_code_active
|
||||||
|
ON public.contract_templates(region, template_code)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contract_templates_region_active
|
||||||
|
ON public.contract_templates(region)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contract_templates_category_active
|
||||||
|
ON public.contract_templates(category_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contract_templates_updated_active
|
||||||
|
ON public.contract_templates(updated_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION update_contract_templates_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
t TEXT;
|
||||||
|
BEGIN
|
||||||
|
FOREACH t IN ARRAY ARRAY['contract_categories', 'contract_templates']
|
||||||
|
LOOP
|
||||||
|
EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_updated_at ON %I', t, t);
|
||||||
|
EXECUTE format(
|
||||||
|
'CREATE TRIGGER trg_%s_updated_at
|
||||||
|
BEFORE UPDATE ON %I
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_contract_templates_updated_at()',
|
||||||
|
t, t
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
COMMENT ON TABLE public.contract_categories IS '合同模板分类表';
|
COMMENT ON TABLE public.contract_categories IS '合同模板分类表';
|
||||||
COMMENT ON COLUMN public.contract_categories.name IS '分类名称';
|
COMMENT ON COLUMN public.contract_categories.name IS '分类名称';
|
||||||
COMMENT ON COLUMN public.contract_categories.icon IS '分类图标';
|
COMMENT ON COLUMN public.contract_categories.icon IS '分类图标';
|
||||||
COMMENT ON COLUMN public.contract_categories.description IS '分类描述';
|
COMMENT ON COLUMN public.contract_categories.description IS '分类描述';
|
||||||
COMMENT ON COLUMN public.contract_categories.sort_order IS '排序值';
|
COMMENT ON COLUMN public.contract_categories.sort_order IS '排序值';
|
||||||
|
COMMENT ON COLUMN public.contract_categories.created_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN public.contract_categories.updated_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN public.contract_categories.deleted_at IS '软删除时间';
|
||||||
|
|
||||||
COMMENT ON TABLE public.contract_templates IS '合同模板主表';
|
COMMENT ON TABLE public.contract_templates IS '合同模板主表';
|
||||||
COMMENT ON COLUMN public.contract_templates.template_code IS '模板编码';
|
COMMENT ON COLUMN public.contract_templates.template_code IS '模板编码';
|
||||||
COMMENT ON COLUMN public.contract_templates.title IS '模板标题';
|
COMMENT ON COLUMN public.contract_templates.title IS '模板标题';
|
||||||
COMMENT ON COLUMN public.contract_templates.category_id IS '所属分类ID';
|
COMMENT ON COLUMN public.contract_templates.category_id IS '所属分类ID';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.region IS '所属地区,省级模板使用“省级”';
|
||||||
COMMENT ON COLUMN public.contract_templates.description IS '模板描述';
|
COMMENT ON COLUMN public.contract_templates.description IS '模板描述';
|
||||||
COMMENT ON COLUMN public.contract_templates.file_path IS '源模板文件路径';
|
COMMENT ON COLUMN public.contract_templates.file_path IS '源模板文件路径';
|
||||||
COMMENT ON COLUMN public.contract_templates.file_format IS '文件格式';
|
|
||||||
COMMENT ON COLUMN public.contract_templates.is_featured IS '是否推荐模板';
|
|
||||||
COMMENT ON COLUMN public.contract_templates.pdf_file_path IS 'PDF预览文件路径';
|
COMMENT ON COLUMN public.contract_templates.pdf_file_path IS 'PDF预览文件路径';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.file_format IS '文件格式';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.original_file_name IS '原始上传文件名';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.mime_type IS '文件 MIME 类型';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.file_size IS '主文件大小(字节)';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.pdf_file_size IS '预览 PDF 文件大小(字节)';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.is_featured IS '是否推荐模板';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.created_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.updated_by IS '更新人';
|
||||||
|
COMMENT ON COLUMN public.contract_templates.deleted_at IS '软删除时间';
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ BEGIN;
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- LeAudit Platform Contract Template RBAC Seed
|
-- LeAudit Platform Contract Template RBAC Seed
|
||||||
-- 目标:
|
-- 目标:
|
||||||
-- 1. 补齐合同模板读权限
|
-- 1. 补齐合同模板读写删权限
|
||||||
-- 2. 给 super_admin / provincial_admin / admin 分配模板读权限
|
-- 2. 给 super_admin / provincial_admin / admin 分配模板权限
|
||||||
-- 说明:
|
-- 说明:
|
||||||
-- - 依赖 user_rbac_schema_patch.sql
|
-- - 依赖 user_rbac_schema_patch.sql
|
||||||
-- - 依赖合同模板前端路由已存在于 sys_routes
|
-- - 依赖合同模板前端路由已存在于 sys_routes
|
||||||
@@ -62,7 +62,10 @@ FROM (
|
|||||||
VALUES
|
VALUES
|
||||||
('contract_template:list:read', 'contract_template', 'list', 'read', '查看合同模板列表', '查看合同模板列表', '/contract-template/list', 310, '/api/v3/contract-templates', 'GET'),
|
('contract_template:list:read', 'contract_template', 'list', 'read', '查看合同模板列表', '查看合同模板列表', '/contract-template/list', 310, '/api/v3/contract-templates', 'GET'),
|
||||||
('contract_template:search:read', 'contract_template', 'search', 'read', '搜索合同模板', '搜索合同模板', '/contract-template/search', 311, '/api/v3/contract-templates/search','GET'),
|
('contract_template:search:read', 'contract_template', 'search', 'read', '搜索合同模板', '搜索合同模板', '/contract-template/search', 311, '/api/v3/contract-templates/search','GET'),
|
||||||
('contract_template:detail:read', 'contract_template', 'detail', 'read', '查看合同模板详情', '查看合同模板详情', '/contract-template/list', 312, '/api/v3/contract-templates/{id}', 'GET')
|
('contract_template:detail:read', 'contract_template', 'detail', 'read', '查看合同模板详情', '查看合同模板详情', '/contract-template/list', 312, '/api/v3/contract-templates/{id}', 'GET'),
|
||||||
|
('contract_template:create:write', 'contract_template', 'create', 'write', '上传合同模板', '上传合同模板', '/contract-template/list', 313, '/api/v3/contract-templates', 'POST'),
|
||||||
|
('contract_template:update:write', 'contract_template', 'update', 'write', '更新合同模板', '更新合同模板', '/contract-template/list', 314, '/api/v3/contract-templates/{id}', 'PUT'),
|
||||||
|
('contract_template:delete:delete', 'contract_template', 'delete', 'delete', '删除合同模板', '删除合同模板', '/contract-template/list', 315, '/api/v3/contract-templates/{id}', 'DELETE')
|
||||||
) AS seed(
|
) AS seed(
|
||||||
permission_key,
|
permission_key,
|
||||||
module,
|
module,
|
||||||
@@ -105,14 +108,23 @@ seed(role_key, permission_key, grant_type, data_scope) AS (
|
|||||||
('super_admin', 'contract_template:list:read', 'GRANT', 'ALL'),
|
('super_admin', 'contract_template:list:read', 'GRANT', 'ALL'),
|
||||||
('super_admin', 'contract_template:search:read', 'GRANT', 'ALL'),
|
('super_admin', 'contract_template:search:read', 'GRANT', 'ALL'),
|
||||||
('super_admin', 'contract_template:detail:read', 'GRANT', 'ALL'),
|
('super_admin', 'contract_template:detail:read', 'GRANT', 'ALL'),
|
||||||
|
('super_admin', 'contract_template:create:write', 'GRANT', 'ALL'),
|
||||||
|
('super_admin', 'contract_template:update:write', 'GRANT', 'ALL'),
|
||||||
|
('super_admin', 'contract_template:delete:delete', 'GRANT', 'ALL'),
|
||||||
|
|
||||||
('provincial_admin', 'contract_template:list:read', 'GRANT', 'ALL'),
|
('provincial_admin', 'contract_template:list:read', 'GRANT', 'ALL'),
|
||||||
('provincial_admin', 'contract_template:search:read', 'GRANT', 'ALL'),
|
('provincial_admin', 'contract_template:search:read', 'GRANT', 'ALL'),
|
||||||
('provincial_admin', 'contract_template:detail:read', 'GRANT', 'ALL'),
|
('provincial_admin', 'contract_template:detail:read', 'GRANT', 'ALL'),
|
||||||
|
('provincial_admin', 'contract_template:create:write', 'GRANT', 'ALL'),
|
||||||
|
('provincial_admin', 'contract_template:update:write', 'GRANT', 'ALL'),
|
||||||
|
('provincial_admin', 'contract_template:delete:delete', 'GRANT', 'ALL'),
|
||||||
|
|
||||||
('admin', 'contract_template:list:read', 'GRANT', 'DEPT'),
|
('admin', 'contract_template:list:read', 'GRANT', 'DEPT'),
|
||||||
('admin', 'contract_template:search:read', 'GRANT', 'DEPT'),
|
('admin', 'contract_template:search:read', 'GRANT', 'DEPT'),
|
||||||
('admin', 'contract_template:detail:read', 'GRANT', 'DEPT')
|
('admin', 'contract_template:detail:read', 'GRANT', 'DEPT'),
|
||||||
|
('admin', 'contract_template:create:write', 'GRANT', 'DEPT'),
|
||||||
|
('admin', 'contract_template:update:write', 'GRANT', 'DEPT'),
|
||||||
|
('admin', 'contract_template:delete:delete', 'GRANT', 'DEPT')
|
||||||
)
|
)
|
||||||
INSERT INTO role_permissions (
|
INSERT INTO role_permissions (
|
||||||
role_id,
|
role_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user