From 5eab324c50f180207f7b0488525a6d20c626b1c8 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Tue, 16 Dec 2025 20:03:05 +0800 Subject: [PATCH] =?UTF-8?q?Step=2012=EF=BC=9A=E5=90=8E=E7=AB=AF=E8=BD=BB?= =?UTF-8?q?=E9=87=8F=E4=BC=98=E5=8C=96=EF=BC=88API=20=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E6=80=A7=20&=20=E5=89=8D=E7=AB=AF=E5=8F=8B=E5=A5=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/constants.py | 22 +++++++++++++ backend/app/api/v1/faq_categories.py | 12 +++++-- backend/app/api/v1/faqs.py | 14 +++++++-- backend/app/api/v1/router.py | 3 +- backend/app/api/v1/users.py | 9 ++++-- backend/app/constants/ae.py | 3 ++ backend/app/constants/finance.py | 2 ++ backend/app/constants/imp.py | 10 ++++++ backend/app/constants/issue.py | 3 ++ backend/app/constants/subject.py | 2 ++ backend/app/constants/task.py | 2 ++ backend/app/core/deps.py | 11 ++++--- backend/app/core/exceptions.py | 19 +++++++++++ backend/app/main.py | 40 ++++++++++++++++++++++-- backend/app/schemas/common.py | 16 ++++++++++ backend/app/utils/pagination.py | 7 +++++ pg_data/base/16384/1249 | Bin 565248 -> 565248 bytes pg_data/base/16384/24576 | Bin 8192 -> 8192 bytes pg_data/base/16384/24584 | Bin 8192 -> 8192 bytes pg_data/base/16384/24590 | Bin 16384 -> 16384 bytes pg_data/base/16384/24597 | Bin 16384 -> 16384 bytes pg_data/base/16384/24613 | Bin 8192 -> 8192 bytes pg_data/base/16384/24618 | Bin 16384 -> 16384 bytes pg_data/base/16384/24620 | Bin 16384 -> 16384 bytes pg_data/base/16384/24632 | Bin 16384 -> 16384 bytes pg_data/base/16384/24672 | Bin 16384 -> 16384 bytes pg_data/base/16384/24672_fsm | Bin 24576 -> 24576 bytes pg_data/base/16384/24678 | Bin 16384 -> 16384 bytes pg_data/base/16384/25066 | Bin 8192 -> 8192 bytes pg_data/base/16384/25073 | Bin 16384 -> 16384 bytes pg_data/base/16384/25075 | Bin 16384 -> 16384 bytes pg_data/base/16384/25082 | Bin 16384 -> 16384 bytes pg_data/base/16384/25083 | Bin 8192 -> 8192 bytes pg_data/base/16384/25090 | Bin 16384 -> 16384 bytes pg_data/base/16384/25107 | Bin 16384 -> 16384 bytes pg_data/base/16384/25108 | Bin 16384 -> 16384 bytes pg_data/base/16384/2604 | Bin 8192 -> 8192 bytes pg_data/base/16384/2606 | Bin 49152 -> 49152 bytes pg_data/base/16384/2610 | Bin 49152 -> 49152 bytes pg_data/base/16384/2620 | Bin 40960 -> 40960 bytes pg_data/base/16384/2659 | Bin 98304 -> 98304 bytes pg_data/global/pg_control | Bin 8192 -> 8192 bytes pg_data/pg_wal/000000010000000000000001 | Bin 16777216 -> 16777216 bytes pg_data/pg_xact/0000 | Bin 8192 -> 8192 bytes 44 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 backend/app/api/v1/constants.py create mode 100644 backend/app/constants/ae.py create mode 100644 backend/app/constants/finance.py create mode 100644 backend/app/constants/imp.py create mode 100644 backend/app/constants/issue.py create mode 100644 backend/app/constants/subject.py create mode 100644 backend/app/constants/task.py create mode 100644 backend/app/core/exceptions.py create mode 100644 backend/app/schemas/common.py create mode 100644 backend/app/utils/pagination.py diff --git a/backend/app/api/v1/constants.py b/backend/app/api/v1/constants.py new file mode 100644 index 00000000..28e7b029 --- /dev/null +++ b/backend/app/api/v1/constants.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter, Depends + +from app.constants import ae, finance, imp, issue, subject, task +from app.core.deps import get_current_user + +router = APIRouter(tags=["FAQ"], dependencies=[Depends(get_current_user)]) + + +@router.get( + "/", + summary="获取前端常量字典", + description="返回各模块状态/类型常量,便于前端一次性加载。", +) +async def get_constants(): + return { + "task": {"status": task.TASK_STATUS, "priority": task.TASK_PRIORITY}, + "subject": {"status": subject.SUBJECT_STATUS, "visit_status": subject.VISIT_STATUS}, + "ae": {"seriousness": ae.AE_SERIOUSNESS, "severity": ae.AE_SEVERITY, "status": ae.AE_STATUS}, + "issue": {"category": issue.ISSUE_CATEGORY, "level": issue.ISSUE_LEVEL, "status": issue.ISSUE_STATUS}, + "finance": {"status": finance.FINANCE_STATUS, "category": finance.FINANCE_CATEGORY}, + "imp": {"tx_types": imp.IMP_TX_TYPES, "batch_status": imp.IMP_BATCH_STATUS}, + } diff --git a/backend/app/api/v1/faq_categories.py b/backend/app/api/v1/faq_categories.py index b6ee1681..349285c0 100644 --- a/backend/app/api/v1/faq_categories.py +++ b/backend/app/api/v1/faq_categories.py @@ -7,7 +7,9 @@ from app.core.deps import get_current_user, get_db_session from app.crud import audit as audit_crud from app.crud import faq_category as category_crud from app.crud import member as member_crud +from app.schemas.common import PaginatedResponse from app.schemas.faq import CategoryCreate, CategoryRead, CategoryUpdate +from app.utils.pagination import paginate router = APIRouter() @@ -25,6 +27,8 @@ def _check_permission_for_scope(study_id: uuid.UUID | None, current_user, member "/", response_model=CategoryRead, status_code=status.HTTP_201_CREATED, + summary="创建 FAQ 分类", + description="创建全局或项目内 FAQ 分类,项目 PM/ADMIN 可用,全局仅 ADMIN。", ) async def create_category( payload: CategoryCreate, @@ -54,7 +58,9 @@ async def create_category( @router.get( "/", - response_model=list[CategoryRead], + response_model=PaginatedResponse[CategoryRead], + summary="FAQ 分类列表", + description="返回全局及项目内 FAQ 分类列表,可按 study_id 过滤。", ) async def list_categories( study_id: uuid.UUID | None = None, @@ -68,12 +74,14 @@ async def list_categories( if not member and current_user.role != "ADMIN": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") categories = await category_crud.list_categories(db, study_id, include_global=include_global, is_active=is_active) - return [CategoryRead.model_validate(c) for c in categories] + return paginate([CategoryRead.model_validate(c) for c in categories], total=len(categories)) @router.patch( "/{category_id}", response_model=CategoryRead, + summary="更新 FAQ 分类", + description="更新分类名称或启停状态,项目 PM/ADMIN 或全局 ADMIN。", ) async def update_category( category_id: uuid.UUID, diff --git a/backend/app/api/v1/faqs.py b/backend/app/api/v1/faqs.py index 517f1220..de2922d0 100644 --- a/backend/app/api/v1/faqs.py +++ b/backend/app/api/v1/faqs.py @@ -8,7 +8,9 @@ from app.crud import audit as audit_crud from app.crud import faq_category as category_crud from app.crud import faq_item as faq_crud from app.crud import member as member_crud +from app.schemas.common import PaginatedResponse from app.schemas.faq import FaqCreate, FaqRead, FaqUpdate +from app.utils.pagination import paginate router = APIRouter() @@ -26,6 +28,8 @@ def _check_write_permission(study_id: uuid.UUID | None, current_user, member_rol "/", response_model=FaqRead, status_code=status.HTTP_201_CREATED, + summary="创建 FAQ", + description="创建全局或项目内 FAQ,项目 FAQ 需项目 PM/ADMIN 权限。", ) async def create_faq( payload: FaqCreate, @@ -61,7 +65,9 @@ async def create_faq( @router.get( "/", - response_model=list[FaqRead], + response_model=PaginatedResponse[FaqRead], + summary="FAQ 列表", + description="返回全局与项目 FAQ,可按关键词、分类过滤。", ) async def list_faqs( study_id: uuid.UUID | None = None, @@ -115,12 +121,14 @@ async def list_faqs( if role != "PM": continue visible.append(FaqRead.model_validate(it)) - return visible + return paginate(visible, total=len(visible)) @router.get( "/{item_id}", response_model=FaqRead, + summary="FAQ 详情", + description="获取单条 FAQ,非 PM 不能查看停用 FAQ。", ) async def get_faq( item_id: uuid.UUID, @@ -147,6 +155,8 @@ async def get_faq( @router.patch( "/{item_id}", response_model=FaqRead, + summary="更新 FAQ", + description="更新 FAQ 内容或启停状态,项目内需 PM/ADMIN 权限。", ) async def update_faq( item_id: uuid.UUID, diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 97a7dc44..96aca791 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard, faq_categories, faqs +from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard, faq_categories, faqs, constants api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -28,3 +28,4 @@ api_router.include_router(finance.router, prefix="/studies/{study_id}/finance", api_router.include_router(finance_dashboard.router, prefix="/studies/{study_id}/finance", tags=["finance"]) api_router.include_router(faq_categories.router, prefix="/faqs/categories", tags=["faq-categories"]) api_router.include_router(faqs.router, prefix="/faqs/items", tags=["faqs"]) +api_router.include_router(constants.router, prefix="/constants", tags=["faq"]) diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py index e0b55678..ea19aadd 100644 --- a/backend/app/api/v1/users.py +++ b/backend/app/api/v1/users.py @@ -4,21 +4,24 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db_session, require_roles +from app.schemas.common import PaginatedResponse from app.crud import user as user_crud +from app.utils.pagination import paginate from app.schemas.user import UserCreate, UserRead, UserUpdate router = APIRouter() -@router.get("/", response_model=list[UserRead]) +@router.get("/", response_model=PaginatedResponse[UserRead], summary="用户列表", description="仅 ADMIN 可用,支持 skip/limit。") async def list_users( skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), current_user=Depends(require_roles(["ADMIN"])), -) -> list[UserRead]: +) -> PaginatedResponse[UserRead]: users = await user_crud.list_users(db, skip=skip, limit=limit) - return list(users) + total_users = await user_crud.list_users(db, skip=0, limit=10_000_000) + return paginate(list(users), total=len(total_users)) @router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED) diff --git a/backend/app/constants/ae.py b/backend/app/constants/ae.py new file mode 100644 index 00000000..4e8e888a --- /dev/null +++ b/backend/app/constants/ae.py @@ -0,0 +1,3 @@ +AE_SERIOUSNESS = ["NON_SERIOUS", "SERIOUS"] +AE_SEVERITY = ["G1", "G2", "G3", "G4", "G5"] +AE_STATUS = ["NEW", "FOLLOW_UP", "CLOSED"] diff --git a/backend/app/constants/finance.py b/backend/app/constants/finance.py new file mode 100644 index 00000000..be96fa69 --- /dev/null +++ b/backend/app/constants/finance.py @@ -0,0 +1,2 @@ +FINANCE_STATUS = ["DRAFT", "SUBMITTED", "APPROVED", "REJECTED", "PAID"] +FINANCE_CATEGORY = ["SITE_FEE", "SUBJECT_STIPEND", "TRAVEL", "OTHER"] diff --git a/backend/app/constants/imp.py b/backend/app/constants/imp.py new file mode 100644 index 00000000..d442304c --- /dev/null +++ b/backend/app/constants/imp.py @@ -0,0 +1,10 @@ +IMP_TX_TYPES = [ + "RECEIPT", + "DISPENSE", + "RETURN", + "RECONCILE", + "DESTROY", + "TRANSFER_IN", + "TRANSFER_OUT", +] +IMP_BATCH_STATUS = ["ACTIVE", "QUARANTINED", "EXPIRED", "CLOSED"] diff --git a/backend/app/constants/issue.py b/backend/app/constants/issue.py new file mode 100644 index 00000000..10d1e760 --- /dev/null +++ b/backend/app/constants/issue.py @@ -0,0 +1,3 @@ +ISSUE_CATEGORY = ["RISK", "ISSUE", "PROTOCOL_DEVIATION"] +ISSUE_LEVEL = ["LOW", "MEDIUM", "HIGH", "CRITICAL"] +ISSUE_STATUS = ["OPEN", "MITIGATING", "CLOSED"] diff --git a/backend/app/constants/subject.py b/backend/app/constants/subject.py new file mode 100644 index 00000000..7be69108 --- /dev/null +++ b/backend/app/constants/subject.py @@ -0,0 +1,2 @@ +SUBJECT_STATUS = ["SCREENING", "ENROLLED", "COMPLETED", "DROPPED"] +VISIT_STATUS = ["PLANNED", "DONE", "MISSED", "CANCELLED"] diff --git a/backend/app/constants/task.py b/backend/app/constants/task.py new file mode 100644 index 00000000..2471f576 --- /dev/null +++ b/backend/app/constants/task.py @@ -0,0 +1,2 @@ +TASK_STATUS = ["TODO", "DOING", "DONE", "BLOCKED"] +TASK_PRIORITY = ["LOW", "MEDIUM", "HIGH"] diff --git a/backend/app/core/deps.py b/backend/app/core/deps.py index 69c64aa6..c9d5b475 100644 --- a/backend/app/core/deps.py +++ b/backend/app/core/deps.py @@ -5,6 +5,7 @@ from fastapi import Depends, HTTPException, status from pydantic import ValidationError from sqlalchemy.ext.asyncio import AsyncSession +from app.core.exceptions import AppException from app.core.security import decode_token, oauth2_scheme from app.crud import user as user_crud from app.crud import member as member_crud @@ -75,9 +76,10 @@ def require_study_member(): return current_user membership = await member_crud.get_member(db, study_id, current_user.id) if not membership or not membership.is_active: - raise HTTPException( + raise AppException( + code="FORBIDDEN", + message="Not a member of this study", status_code=status.HTTP_403_FORBIDDEN, - detail="Not a member of this study", ) return current_user @@ -96,9 +98,10 @@ def require_study_roles(roles: Iterable[str]): return current_user membership = await member_crud.get_member(db, study_id, current_user.id) if not membership or not membership.is_active or membership.role_in_study not in roles_set: - raise HTTPException( + raise AppException( + code="FORBIDDEN", + message="Insufficient study permissions", status_code=status.HTTP_403_FORBIDDEN, - detail="Insufficient study permissions", ) return current_user diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py new file mode 100644 index 00000000..98a12b99 --- /dev/null +++ b/backend/app/core/exceptions.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse + + +class AppException(Exception): + def __init__(self, code: str, message: str, status_code: int = status.HTTP_400_BAD_REQUEST): + self.code = code + self.message = message + self.status_code = status_code + super().__init__(message) + + +def register_exception_handlers(app: FastAPI) -> None: + @app.exception_handler(AppException) + async def app_exception_handler(_: Request, exc: AppException) -> JSONResponse: + return JSONResponse( + status_code=exc.status_code, + content={"code": exc.code, "message": exc.message}, + ) diff --git a/backend/app/main.py b/backend/app/main.py index 72f4d198..5f693507 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.api.v1.router import api_router from app.core.config import settings +from app.core.exceptions import register_exception_handlers from app.crud.user import ensure_admin_exists from app.db.base import Base from app.db.session import SessionLocal, engine @@ -25,10 +26,39 @@ async def lifespan(_: FastAPI): def create_app() -> FastAPI: app = FastAPI( - title="CTMS API", + title="CTMS 后端 API", + description="临床试验项目管理系统后端接口文档", version="0.1.0", debug=settings.ENV == "development", lifespan=lifespan, + openapi_tags=[ + {"name": "auth", "description": "认证与登录"}, + {"name": "users", "description": "用户管理"}, + {"name": "studies", "description": "项目管理"}, + {"name": "sites", "description": "中心管理"}, + {"name": "study-members", "description": "项目成员"}, + {"name": "comments", "description": "通用评论"}, + {"name": "attachments", "description": "通用附件"}, + {"name": "audit-logs", "description": "审计日志"}, + {"name": "milestones", "description": "里程碑"}, + {"name": "tasks", "description": "任务"}, + {"name": "dashboard", "description": "项目总览与统计"}, + {"name": "subjects", "description": "受试者"}, + {"name": "visits", "description": "访视"}, + {"name": "aes", "description": "不良事件"}, + {"name": "issues", "description": "风险 / 问题"}, + {"name": "data-queries", "description": "数据问题单"}, + {"name": "verifications", "description": "SDV/SDR 核查进度"}, + {"name": "imp-products", "description": "药品产品"}, + {"name": "imp-batches", "description": "药品批次"}, + {"name": "imp-inventory", "description": "药品库存"}, + {"name": "imp-transactions", "description": "药品台账流水"}, + {"name": "finance", "description": "费用管理"}, + {"name": "faq-categories", "description": "FAQ 分类"}, + {"name": "faqs", "description": "FAQ 条目"}, + {"name": "health", "description": "健康检查"}, + {"name": "faq", "description": "常量字典"}, + ], ) app.add_middleware( @@ -38,8 +68,14 @@ def create_app() -> FastAPI: allow_methods=["*"], allow_headers=["*"], ) + register_exception_handlers(app) - @app.get("/health", tags=["health"]) + @app.get( + "/health", + tags=["health"], + summary="健康检查", + description="返回服务存活状态。", + ) async def health() -> dict[str, str]: return {"status": "ok"} diff --git a/backend/app/schemas/common.py b/backend/app/schemas/common.py new file mode 100644 index 00000000..299734c5 --- /dev/null +++ b/backend/app/schemas/common.py @@ -0,0 +1,16 @@ +from typing import Generic, TypeVar + +from pydantic import BaseModel +from pydantic.generics import GenericModel + +T = TypeVar("T") + + +class PaginatedResponse(GenericModel, Generic[T]): + items: list[T] + total: int + + +class MessageResponse(BaseModel): + code: str + message: str diff --git a/backend/app/utils/pagination.py b/backend/app/utils/pagination.py new file mode 100644 index 00000000..cfa06266 --- /dev/null +++ b/backend/app/utils/pagination.py @@ -0,0 +1,7 @@ +from collections.abc import Iterable +from typing import Any + + +def paginate(items: Iterable[Any], total: int | None = None) -> dict[str, Any]: + items_list = list(items) + return {"items": items_list, "total": total if total is not None else len(items_list)} diff --git a/pg_data/base/16384/1249 b/pg_data/base/16384/1249 index 7ddcdfec8465a2555d74c52997148cee1095752b..b5f93d3a716c045ea13ecc1e4bec0546b08a141d 100644 GIT binary patch delta 62 zcmZoTpws|FEsQNpEzB(}EvzkUE$mx3zD{N2oc?VpM?a8in+Bq`i%sX)&&H@Z{n}h0 MS95#XJdRn60KhsG=l}o! delta 62 zcmZoTpws|FEsQNpEzB(}EvzkUE$mx3zD{N2nEq`lM?a8in+Bq`i%sX)&&H@R{n}h0 MS7UqHJdRn60Kd)^;{X5v diff --git a/pg_data/base/16384/24576 b/pg_data/base/16384/24576 index 4a9bf26a4d46db0e88cd21cbce642dcc89310c29..21a8955430b391f776f515f431ab8fe3635cd546 100644 GIT binary patch delta 35 pcmZp0XmDU>0D>QUhc~jfN-@ry+$&YjxOg+KbU6#-lF6;|)d0X93rqk2 delta 35 pcmZp0XmDU>0D_9-LmSy!r5LA7?v<)%oV=M=x}1e^(&Se8Y5=+33i$v4 diff --git a/pg_data/base/16384/24584 b/pg_data/base/16384/24584 index 56c548f28befe4ee80e988f95281cba50db1aa96..7d19d1166b6aa51c9c08939f1b46f9db05dd0ff6 100644 GIT binary patch delta 194 zcmZp0XmAi<0D=|FhZ#Y11w)6}M1chM57RC%bWFRju~1OVWeGD#1_2}(I2o8Z6^?^7 z0EHbGd+hI+cF_NL;Km#W;A`INB#xe6)kLRq#R}76cUg$W8yEz8xnx&sF!tEr rG4b5HG}!L&zu84mAm!pf@vUkcGmrIn&RTrM&{*Szj>F~$whBxDy}BHw delta 42 wcmZo@U~Fh$+`yv1&Ea&2kpTz_7#>XKR}h`7p`fxkKp{nTVt~cwC$=(70P$f9!~g&Q diff --git a/pg_data/base/16384/24597 b/pg_data/base/16384/24597 index 643613c724773c5d85a3e81b1fa1cc63b0675e94..5d1f5b3a8576839475102ea5d0f9173b449ff186 100644 GIT binary patch delta 90 zcmZo@U~Fh$+`ytB&cJz?kpT!Q7$W38$g?Pb*ah6z-94IdC<`}4JmX?y1mbkguu9gu1w=oq{ delta 42 vcmZo@U~Fh$+`ytBtl@HqkpTz_7&_!W$g?N_nUkdzA~!n8$ZhViD`5lx<4y{w diff --git a/pg_data/base/16384/24613 b/pg_data/base/16384/24613 index e5c013037a2316f760bed49a8d5fa8472e3e4b52..1054624322e15dfcc0ee9a24668e87fe275860a5 100644 GIT binary patch delta 198 zcmZp0XmAi<00M>khZsTh7KRx@69wk5f0(p^VaB8l8#g``w3x{Zl0g7<1~vvJP6>vY zd&180*m~uzaMSbqo4$4PRyB^9$9g@Z)hFP1_6!IiR)EP7wwyJT=Jl5km sYw;CBV~rO&4wD_ML{!wUNVJ!IKD+3X)4{g4^Dp*4mp`~!!|Dbr031yx>i_@% delta 38 scmZo@U~Fh$+`y{9opgwi0SMMGXh=;KP>9%EppYXuF(6{Ij@1=b0L#h?1ONa4 diff --git a/pg_data/base/16384/24632 b/pg_data/base/16384/24632 index f0dfda5c94fe7f713b16c296dc013daec6c21ab1..c0488e9a9cc6fd63fd04a0a36d2e2c9285d41b48 100644 GIT binary patch delta 87 zcmZo@U~Fh$+`yv1t?=LwBLfg@VepvDuMo+WFvozwW6tI@g?rLE>I@PLTh%yb9_#U( XwfKsmvBnD>2asAdEaIChY&w_$edQak delta 38 scmZo@U~Fh$+`yv1&5?YFkpT$SFjP$DSBTtPpm0ZeVt~cw8k-ho0MgA1lmGw# diff --git a/pg_data/base/16384/24672 b/pg_data/base/16384/24672 index 8d4111dd510b3936002c649efe662759b1d37506..066b34a416144a1adc5a69b188677ee26b579ef9 100644 GIT binary patch delta 711 zcmZo@U~Fh$+`!Ao$hDb|@td4R#`i;v3_zg6ARx=2z@h+RN6Zgkyf9CIQDa^JvYHr#V{Ed3^TdK`JY+0EHxs~X45V?Ca; z7GE(m)_9@gAe@$17@t{^n!AVFv#iF`Pi0-XkqOA_=0UuD}F1^|B{JeA)13&pbomm zwyCT=%Bg7effEJ|j}8YD?I15pgTkw6f(&`)6?^dDks$(V!80uHEQ+7jCI?dAoiYoKJIo+Dr|5{!yS+yElP))98R1Tf_`3*R7 zW}{ifh-THqy+ZEg=|0;!gbHNu%(co#35)xTtHgYk`u^>*@m4UAyZ>>rFJmyrmi-Wu E0kQn(a{vGU delta 26 hcmZo@U~Fh$+`!Ao$g!D^@tfRcGlg3+n;jICm;r5R2iE`q diff --git a/pg_data/base/16384/24672_fsm b/pg_data/base/16384/24672_fsm index 6b4fbb37b05210e9eef9f8e22d27d0ec54512772..39488e548655710ddfac8e2275b4b854b2b4e5e2 100644 GIT binary patch delta 84 zcmZoTz}Rqraf6J50wW^>BLf2%11UHSVuJwVL`8WOsVRM#haG0z@){Np=wV GgdYGE=?|&^ delta 13 UcmZoTz}Rqraf6J*Mwut_04Em(UjP6A diff --git a/pg_data/base/16384/24678 b/pg_data/base/16384/24678 index f481008d748d795bfb28ef12af22de80ce75996d..9d29828f757d7eafcb25fa0d615f9e916bb33e49 100644 GIT binary patch delta 196 zcmZo@U~Fh$+`yv1z2W;IMg}0b!w@lR9?s?({hFc(a?c2?GNo0}D|68r!C__9&;K z)dx-(G(0*S3=(GsiYtXrIA-eQue3$s#6^zj;wM1jOhEC=Od8M5C;IPO>;7~_xWI1- lkT@eyeBxdq_wsa~Z5=`dvUlcM<%8U*0~9~H`KGlx3jo8kJx>4t delta 52 zcmZo@U~Fh$+`yv1y<_DeMg|}_!Ej+Rzk=ap7X=3gO>tFf0IK7+_}L zV3@=x!7!=j5X*(Hu2HKu9+zb<>)o_fjbrAq9?w~euNWF@ywGtFeYtSw^V#d%90Q@O zr){ku%fJ=_X>I1W;`}d};Pfn@9dnTEVFKDSnNdRU?|sHqVm?cK|907UE11aL|2X-A SyaNB1jc>rZq4ebC@{s_xszYJ` delta 43 tcmZp0XmF5b00NJuLyRC=hGB&~g93{Jh`nO|hK+{X{56m~%=-4T*!^|MTFsbGc s%Z0D5QL8r|mt`*N-2_s{1Qfr|xJt}tsqfz|8*c>@x%(d{->}yJ0Ac?hCIA2c delta 43 vcmZo@U~Fh$+`ytB`l0C%BLfh~Fx-&;AkU%zV&B-TsL(AxF~DN-9eWi36i5un diff --git a/pg_data/base/16384/25075 b/pg_data/base/16384/25075 index 47668139fea7145145550641b407d2d9d8114313..403a8afedfdd4147bdea85ce82b966fa74b9f1a3 100644 GIT binary patch delta 95 zcmZo@U~Fh$+`ytB|KR;0Mg|~IVd#+mAkU%zVt33BV7M^ffnmdZhmDSz@|w&H8V+04 rIA$K}@tn2zilMQ_3mpg1mkW13pS{k_F%YB!0+=RWwU?iK#a;&hp&lMf delta 43 vcmZo@U~Fh$+`ytBx}f_6?+{32tW*B diff --git a/pg_data/base/16384/25082 b/pg_data/base/16384/25082 index 19111bb216042175f49a670588ca1e5b3d0f50c8..38c51a73e2a34ec8c5049d66ad53b444183647d5 100644 GIT binary patch delta 78 zcmZo@U~Fh$+`ytBukqm!BLfhqFzk^3AkU%zV(*x5!0=$c0>g#*3L70~%F8e_NHAX;Y=8YbVc*8l*10~Y}R delta 43 vcmZo@U~Fh$+`ytB>dqQDfbqb*07eNQ2FZf< z7dj4;Y7Vhn`05(9dgF0f=Ca;Ricj_|dp@D<+1AGAE4tkr19hMEPk6Fx2}Jbytd?hM zm%d!JL>9u=fhYq{IJcL5KD+3X)4{g4^Dp*4mp=$~Oq}>zasHP~42j7QdI8X#$ZiF? z(}9VVe(q!>;7*u(=b(irBLg=BGpE8|cBsEJPY0;X|K@aSV*hJ-xn$Mq`;4o^e3ttD z?XvM!Fp<0eQT_Sm-OqQ=f4XDGi{*_kr>}dtXol$1))h}THUI-v79yzwk!1jdAETJO cqO+c>kMEAMR~s@G95RI3lXT||IQ(EV0E1l8Jpcdz delta 12 TcmZp0XmHqQ@IrQ@g9H-*Ae#ho diff --git a/pg_data/base/16384/25090 b/pg_data/base/16384/25090 index 30e79a8db358a1f99cdffd068256e843e95a7a3a..7386db7c5799b5a0100ea5c5f6f866fc60069aa4 100644 GIT binary patch delta 148 zcmZo@U~Fh$)MNkxhu4P~L9_?M2l)^3ED9iYk|8St3y=m&far;eq8sgw$xAzYhA38G zH~?3CW4-~yf%yg-9hb`6Ffm9l?BVt-tMT+xSyyiPz~%aDZjgbDKyl5}0V?yqIo+Dr R|5{!yS+yD@4g<_U1^}U;A?^SG delta 50 zcmZo@U~Fh$lw|;d4~q^mf@lwh5Aq-6SrkC*BtupPmWhsv8|{wCZ}iwAKQX`q06dHh ARsaA1 diff --git a/pg_data/base/16384/25107 b/pg_data/base/16384/25107 index fbc0b577d1503d70a019f6e37ec8e5605bd645b2..843628a56fb6925cd9965b6979f664550fbd4709 100644 GIT binary patch delta 124 zcmZo@U~Fh$)MNmH7q1U7f@lwh5Aq-6SrkC*BtupP79b6l0MQc_MK{_Vlb7!J3{kAW za00IQ#C!vW2lEv+I@x%(eM;xNDrWB>p#9wW5? delta 50 zcmZo@U~Fh$lw|;diU$W7L9_?M2l)^3ED9iYk|8St%S1=TjdsW6H+pQ5pBUf)05fp8$xL55NEb delta 49 xcmZo@U~Xt&-Voc!$gw%Dk)07pr8O-FQj_^wmIFE4TKp}66x%+K)MUQ>p8$vx54r#V diff --git a/pg_data/base/16384/2610 b/pg_data/base/16384/2610 index cffb407433d251a723d49270d763c8c94d556872..eb94d054a35e0bc512fde402bfe7bd3e79ebeebe 100644 GIT binary patch delta 28 hcmZo@U~Xt&-mrEZBj@CG>y`t_u=PN4^S|{>0syC;3wHnj delta 28 hcmZo@U~Xt&-mrEZBgf=*>y`t_u=PN4^S|{>0syCa3v>Vg diff --git a/pg_data/base/16384/2620 b/pg_data/base/16384/2620 index 94619c7cc9242eab0feaa8dccd049ab8825145c6..ec05c67f665aa9b8d01f601d89b0dc24826dd7b4 100644 GIT binary patch delta 48 ucmZoTz|?SnX@h7xBj;qX_W6vQo42*!X9RMlb%Ho%U0{w_4_KzhOBevOnh|yY delta 48 ucmZoTz|?SnX@h7xBgbU1_W6t)o42*!X9RMlb%Ho%U0{w_4_KzhOBevO77=g& diff --git a/pg_data/base/16384/2659 b/pg_data/base/16384/2659 index 3b304d06a6dd61e750c3276e67224d1f759a1162..e8925a5d04f6a78a33c19406075f369652b4ece4 100644 GIT binary patch delta 27 hcmZo@U~2%PEsSDmoQez=+7%caIuy6dr!hX11ORxi2!8+o delta 27 hcmZo@U~2%PEsSDmoC*vV+7%caIuy3cr!hX11ORxM2z>wm diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index e7e9fba4e4396076ba45f5b646704343b1b700dc..04c4f8392ddcbf9cd79443c64ff6db10d9f4d9ad 100644 GIT binary patch delta 86 zcmZp0XmFSyp}Ex4F_QrZCh#9-1kn*t8pMHNkoeMxj_QI-po&2PAh2{|<#Lt}QOZr5 N4H?hNO?F`91pujJ5WN5Z delta 86 zcmZp0XmFSyq3Q4Ln8^SH0jmx%f@ldS4dTEsNPPN4M|D9TsA7-+2uz*w?N zLVc#-z=;btAtL%{1~7Ti8ky+jyB~d~9b2Ex;?hN-J%=#_P1RlQ-DKo9t!}Z?TuR*+&N& z@35bDIly}y7ahUh%#!n9)@FBe%;V8#A&Ivx^Bz=6$C-gJGAfFOoh*J#n8J{!4 zX-4^iF9&qrcyqe4M)A1rFu$Tgm2#XI+BfN$`VoBjT}qkKZHP4*rrthV&zKk0_fAY! z+AF3xTg=U-zBw9v*1Gxqo%TzsKPIcOxS-CQ+QZ&z&Fv6}Y*$8z11Y~JYgOaAH*wKy z%XI3WRszbH$JYMZucDM13v1Li{MG51bN-U`vr)ChYKI$JlC``nJ!jQe$M>qukeU9J z@~AN73iW;!tcVdrzGG+Jxuqo|J!QFPCJr>LPpDY2wxnp?lIpaYW$9Uw6*UcY8@16a zJ$}JiZ&haG1*g(x6icZ%!{S~?VC0y-DZ~@?LrAbPN}UOM*fmv|Kx+R;yDK?Z`M;f3 zTD5js;Y#JkxU^l(Qi9FNxcC(t-ge@M7P(h*N>g4D(XN#^gMq2+)#8imW>VMMX0xg4jyffKtu9Q#t?sZx5DZ}@<ByF5^PC*!0Vf%Lf`=A16`@hb(gJtJWa!p3>i~kG zD7$sWBdmR9cYnPmJAGPrXZ~@#$1rQkOwS__-BohbHJG}x zEJPks1L*^~27&@1p^#8WC?pgT3JHaTLP8;-kWfe{Boq<~35A40LLs4$P)H~w6cP#v zg@i&vA)$~^NGK!}5(){0ghE0gp^#8WC?pgT3JHaTLP8;-kWfe{Boq<~35A40LLs4$ MP)PVcAYqf^AAsHhYXATM delta 15075 zcmeIy)l!xL6a~=HUlh9wyIZlbJHQsZ1G~HRQDypQiDypicx*BS#rM5cis;9mN8p_Z}V@)*GOmi)?)Jkh@ zwAD^~9dy)5XI*sFO{VU8=&6_9`sgc5Km83b&>({iG1M@_jWE(Eqm40EwjAS(H^D@c zOg6<-xu%(JhM8uWZH~F-nQwuG7FleGrSdGZ+zKnLvf3JJt+U<+8*Q@L7F%tz-3~kL zvfCbe?X%wjiGvO~j3bUZ=C~71I_0!8&N}D33og3kvMa8-=DHhh%6H3ccidH=&^`A( z@X#ZVJ@M2t&%N-{E3ds#o#M}GN9jNAY*=>>`a