Jelajahi Sumber

Merge branch 'sit' into dev: 保留 sit 上的合同业务功能

天空之城 2 minggu lalu
induk
melakukan
47e7c3a867

+ 40 - 40
alembic/versions/a7d4b3e21c10_create_contract_center_tables.py

@@ -23,20 +23,20 @@ def upgrade() -> None:
     op.execute("DROP TABLE IF EXISTS contract_bundle")
     op.create_table(
         "contract_bundle",
-        sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
-        sa.Column("subject_type", sa.String(length=20), nullable=False),
-        sa.Column("subject_id", sa.BigInteger(), nullable=False),
-        sa.Column("subject_name", sa.String(length=120), nullable=False),
-        sa.Column("business_segment", sa.String(length=100), nullable=False),
-        sa.Column("contact_name", sa.String(length=100), nullable=False),
-        sa.Column("contact_phone", sa.String(length=20), nullable=False),
-        sa.Column("ord_id", sa.String(length=40), nullable=False),
-        sa.Column("bundle_type", sa.String(length=50), nullable=False),
-        sa.Column("status", sa.String(length=20), nullable=False, server_default="pending"),
-        sa.Column("primary_document_id", sa.BigInteger(), nullable=True),
-        sa.Column("created_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
-        sa.Column("delete_flag", sa.Integer(), server_default="0", nullable=False),
+        sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False, comment="合同包唯一标识"),
+        sa.Column("subject_type", sa.String(length=20), nullable=False, comment="签约主体类型(store/lawyer)"),
+        sa.Column("subject_id", sa.BigInteger(), nullable=False, comment="签约主体ID,关联门店/律师业务主键"),
+        sa.Column("subject_name", sa.String(length=120), nullable=False, comment="签约主体名称"),
+        sa.Column("business_segment", sa.String(length=100), nullable=False, comment="业务板块"),
+        sa.Column("contact_name", sa.String(length=100), nullable=False, comment="联系人姓名"),
+        sa.Column("contact_phone", sa.String(length=20), nullable=False, comment="联系人手机号"),
+        sa.Column("ord_id", sa.String(length=40), nullable=False, comment="组织标识(统一社会信用代码)"),
+        sa.Column("bundle_type", sa.String(length=50), nullable=False, comment="合同包类型(STORE_STANDARD/LAWYER_STANDARD)"),
+        sa.Column("status", sa.String(length=20), nullable=False, server_default="未签署", comment="整体状态: 未签署/审核中/已签署"),
+        sa.Column("primary_document_id", sa.BigInteger(), nullable=True, comment="主合同文档ID"),
+        sa.Column("created_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False, comment="创建时间"),
+        sa.Column("updated_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False, comment="更新时间"),
+        sa.Column("delete_flag", sa.Integer(), server_default="0", nullable=False, comment="逻辑删除(0未删/1已删)"),
         sa.PrimaryKeyConstraint("id"),
     )
     op.create_index("idx_contract_bundle_subject", "contract_bundle", ["subject_type", "subject_id"], unique=False)
@@ -44,23 +44,23 @@ def upgrade() -> None:
 
     op.create_table(
         "contract_document",
-        sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
-        sa.Column("bundle_id", sa.BigInteger(), nullable=False),
-        sa.Column("contract_type", sa.String(length=50), nullable=False),
-        sa.Column("contract_name", sa.String(length=100), nullable=False),
-        sa.Column("is_primary", sa.Integer(), nullable=False, server_default="0"),
-        sa.Column("status", sa.Integer(), nullable=False, server_default="0"),
-        sa.Column("sign_flow_id", sa.String(length=64), nullable=False),
-        sa.Column("file_id", sa.String(length=64), nullable=False),
-        sa.Column("template_url", mysql.LONGTEXT(), nullable=False),
-        sa.Column("sign_url", mysql.LONGTEXT(), nullable=False),
-        sa.Column("download_url", mysql.LONGTEXT(), nullable=False),
-        sa.Column("signing_time", sa.DateTime(), nullable=True),
-        sa.Column("effective_time", sa.DateTime(), nullable=True),
-        sa.Column("expiry_time", sa.DateTime(), nullable=True),
-        sa.Column("created_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
-        sa.Column("delete_flag", sa.Integer(), server_default="0", nullable=False),
+        sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False, comment="合同文档唯一标识"),
+        sa.Column("bundle_id", sa.BigInteger(), nullable=False, comment="所属合同包ID"),
+        sa.Column("contract_type", sa.String(length=50), nullable=False, comment="合同类型编码(store_agreement/alipay_auth等)"),
+        sa.Column("contract_name", sa.String(length=100), nullable=False, comment="合同展示名称"),
+        sa.Column("is_primary", sa.Integer(), nullable=False, server_default="0", comment="是否主合同(1是/0否)"),
+        sa.Column("status", sa.Integer(), nullable=False, server_default="0", comment="签署状态(0未签署/1已签署)"),
+        sa.Column("sign_flow_id", sa.String(length=64), nullable=False, comment="e签宝签署流程ID"),
+        sa.Column("file_id", sa.String(length=64), nullable=False, comment="e签宝文件ID"),
+        sa.Column("template_url", mysql.LONGTEXT(), nullable=False, comment="合同模板预览URL"),
+        sa.Column("sign_url", mysql.LONGTEXT(), nullable=False, comment="签署链接URL"),
+        sa.Column("download_url", mysql.LONGTEXT(), nullable=False, comment="签署完成后的下载URL"),
+        sa.Column("signing_time", sa.DateTime(), nullable=True, comment="签署时间"),
+        sa.Column("effective_time", sa.DateTime(), nullable=True, comment="合同生效时间"),
+        sa.Column("expiry_time", sa.DateTime(), nullable=True, comment="合同到期时间"),
+        sa.Column("created_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False, comment="创建时间"),
+        sa.Column("updated_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False, comment="更新时间"),
+        sa.Column("delete_flag", sa.Integer(), server_default="0", nullable=False, comment="逻辑删除(0未删/1已删)"),
         sa.ForeignKeyConstraint(["bundle_id"], ["contract_bundle.id"]),
         sa.PrimaryKeyConstraint("id"),
     )
@@ -70,15 +70,15 @@ def upgrade() -> None:
 
     op.create_table(
         "contract_event",
-        sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
-        sa.Column("bundle_id", sa.BigInteger(), nullable=True),
-        sa.Column("document_id", sa.BigInteger(), nullable=True),
-        sa.Column("sign_flow_id", sa.String(length=64), nullable=False),
-        sa.Column("event_type", sa.String(length=50), nullable=False),
-        sa.Column("payload_json", mysql.LONGTEXT(), nullable=False),
-        sa.Column("created_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
-        sa.Column("delete_flag", sa.Integer(), server_default="0", nullable=False),
+        sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False, comment="事件唯一标识"),
+        sa.Column("bundle_id", sa.BigInteger(), nullable=True, comment="关联合同包ID"),
+        sa.Column("document_id", sa.BigInteger(), nullable=True, comment="关联合同文档ID"),
+        sa.Column("sign_flow_id", sa.String(length=64), nullable=False, comment="e签宝签署流程ID"),
+        sa.Column("event_type", sa.String(length=50), nullable=False, comment="事件类型(esign_callback:{action})"),
+        sa.Column("payload_json", mysql.LONGTEXT(), nullable=False, comment="e签宝回调原始JSON报文"),
+        sa.Column("created_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False, comment="创建时间"),
+        sa.Column("updated_time", sa.DateTime(), server_default=sa.text("now()"), nullable=False, comment="更新时间"),
+        sa.Column("delete_flag", sa.Integer(), server_default="0", nullable=False, comment="逻辑删除(0未删/1已删)"),
         sa.PrimaryKeyConstraint("id"),
     )
     op.create_index("idx_contract_event_sign_flow_id", "contract_event", ["sign_flow_id"], unique=False)

+ 86 - 0
alembic/versions/d8a7a6a4581c_add_column_comments_to_contract_tables.py

@@ -0,0 +1,86 @@
+"""add column comments to contract tables
+
+Revision ID: d8a7a6a4581c
+Revises: a7d4b3e21c10
+Create Date: 2026-04-13 00:00:00.000000
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+
+revision: str = "d8a7a6a4581c"
+down_revision: Union[str, Sequence[str], None] = "a7d4b3e21c10"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+BUNDLE_COLUMNS = [
+    ("id",                   "BIGINT NOT NULL AUTO_INCREMENT", "合同包唯一标识"),
+    ("subject_type",         "VARCHAR(20) NOT NULL",           "签约主体类型(store/lawyer)"),
+    ("subject_id",           "BIGINT NOT NULL",                "签约主体ID,关联门店/律师业务主键"),
+    ("subject_name",         "VARCHAR(120) NOT NULL",          "签约主体名称"),
+    ("business_segment",     "VARCHAR(100) NOT NULL",          "业务板块"),
+    ("contact_name",         "VARCHAR(100) NOT NULL",          "联系人姓名"),
+    ("contact_phone",        "VARCHAR(20) NOT NULL",           "联系人手机号"),
+    ("ord_id",               "VARCHAR(40) NOT NULL",           "组织标识(统一社会信用代码)"),
+    ("bundle_type",          "VARCHAR(50) NOT NULL",           "合同包类型(STORE_STANDARD/LAWYER_STANDARD)"),
+    ("status",               "VARCHAR(20) NOT NULL DEFAULT '未签署'", "整体状态: 未签署/审核中/已签署"),
+    ("primary_document_id",  "BIGINT NULL",                    "主合同文档ID"),
+    ("created_time",         "DATETIME NOT NULL DEFAULT NOW()", "创建时间"),
+    ("updated_time",         "DATETIME NOT NULL DEFAULT NOW()", "更新时间"),
+    ("delete_flag",          "INT NOT NULL DEFAULT 0",         "逻辑删除(0未删/1已删)"),
+]
+
+DOCUMENT_COLUMNS = [
+    ("id",              "BIGINT NOT NULL AUTO_INCREMENT",  "合同文档唯一标识"),
+    ("bundle_id",       "BIGINT NOT NULL",                 "所属合同包ID"),
+    ("contract_type",   "VARCHAR(50) NOT NULL",            "合同类型编码(store_agreement/alipay_auth等)"),
+    ("contract_name",   "VARCHAR(100) NOT NULL",           "合同展示名称"),
+    ("is_primary",      "INT NOT NULL DEFAULT 0",          "是否主合同(1是/0否)"),
+    ("status",          "INT NOT NULL DEFAULT 0",          "签署状态(0未签署/1已签署)"),
+    ("sign_flow_id",    "VARCHAR(64) NOT NULL",            "e签宝签署流程ID"),
+    ("file_id",         "VARCHAR(64) NOT NULL",            "e签宝文件ID"),
+    ("template_url",    "LONGTEXT NOT NULL",               "合同模板预览URL"),
+    ("sign_url",        "LONGTEXT NOT NULL",               "签署链接URL"),
+    ("download_url",    "LONGTEXT NOT NULL",               "签署完成后的下载URL"),
+    ("signing_time",    "DATETIME NULL",                   "签署时间"),
+    ("effective_time",  "DATETIME NULL",                   "合同生效时间"),
+    ("expiry_time",     "DATETIME NULL",                   "合同到期时间"),
+    ("created_time",    "DATETIME NOT NULL DEFAULT NOW()", "创建时间"),
+    ("updated_time",    "DATETIME NOT NULL DEFAULT NOW()", "更新时间"),
+    ("delete_flag",     "INT NOT NULL DEFAULT 0",          "逻辑删除(0未删/1已删)"),
+]
+
+EVENT_COLUMNS = [
+    ("id",            "BIGINT NOT NULL AUTO_INCREMENT",  "事件唯一标识"),
+    ("bundle_id",     "BIGINT NULL",                     "关联合同包ID"),
+    ("document_id",   "BIGINT NULL",                     "关联合同文档ID"),
+    ("sign_flow_id",  "VARCHAR(64) NOT NULL",            "e签宝签署流程ID"),
+    ("event_type",    "VARCHAR(50) NOT NULL",            "事件类型(esign_callback:{action})"),
+    ("payload_json",  "LONGTEXT NOT NULL",               "e签宝回调原始JSON报文"),
+    ("created_time",  "DATETIME NOT NULL DEFAULT NOW()", "创建时间"),
+    ("updated_time",  "DATETIME NOT NULL DEFAULT NOW()", "更新时间"),
+    ("delete_flag",   "INT NOT NULL DEFAULT 0",          "逻辑删除(0未删/1已删)"),
+]
+
+
+def _alter_comments(table: str, columns: list[tuple[str, str, str]]) -> None:
+    for col, definition, comment in columns:
+        op.execute(f"ALTER TABLE `{table}` MODIFY COLUMN `{col}` {definition} COMMENT '{comment}'")
+
+
+def _remove_comments(table: str, columns: list[tuple[str, str, str]]) -> None:
+    for col, definition, _ in columns:
+        op.execute(f"ALTER TABLE `{table}` MODIFY COLUMN `{col}` {definition}")
+
+
+def upgrade() -> None:
+    _alter_comments("contract_bundle", BUNDLE_COLUMNS)
+    _alter_comments("contract_document", DOCUMENT_COLUMNS)
+    _alter_comments("contract_event", EVENT_COLUMNS)
+
+
+def downgrade() -> None:
+    _remove_comments("contract_event", EVENT_COLUMNS)
+    _remove_comments("contract_document", DOCUMENT_COLUMNS)
+    _remove_comments("contract_bundle", BUNDLE_COLUMNS)

+ 1 - 0
alien_contract/README.md

@@ -0,0 +1 @@
+e签宝功能

+ 8 - 3
alien_contract/api/router.py

@@ -1,4 +1,4 @@
-from typing import Union
+from typing import Optional, Union, Literal
 
 from fastapi import APIRouter, Depends, Query
 
@@ -26,6 +26,7 @@ async def create_bundle(
     request: BundleCreateRequest,
     service: ContractCenterService = Depends(get_contract_service),
 ) -> Union[BundleCreateResponse, ErrorResponse]:
+    """创建合同包"""
     result = await service.create_bundle(request)
     if not result.get("success"):
         return ErrorResponse(**result)
@@ -34,13 +35,15 @@ async def create_bundle(
 
 @router.get("/bundles", response_model=PaginatedBundleResponse)
 async def list_bundles(
-    subject_type: str = Query(...),
+    subject_type: Literal["store", "lawyer"] = Query(...),
     subject_id: int = Query(..., gt=0),
+    status: Optional[int] = Query(None, ge=0, le=1, description="文档签署状态过滤: 0=未签署, 1=已签署"),
     page: int = Query(1, ge=1),
     page_size: int = Query(10, ge=1, le=100),
     service: ContractCenterService = Depends(get_contract_service),
 ) -> PaginatedBundleResponse:
-    result = await service.list_bundles(subject_type, subject_id, page, page_size)
+    """"查询合同列表"""
+    result = await service.list_bundles(subject_type, subject_id, page, page_size, doc_status=status)
     return PaginatedBundleResponse(**result)
 
 
@@ -49,6 +52,7 @@ async def get_document_detail(
     sign_flow_id: str,
     service: ContractCenterService = Depends(get_contract_service),
 ) -> Union[dict, ErrorResponse]:
+    """查询合同详情"""
     result = await service.get_document_detail(sign_flow_id)
     if not result.get("success", True):
         return ErrorResponse(**result)
@@ -60,6 +64,7 @@ async def esign_callback(
     payload: dict,
     service: ContractCenterService = Depends(get_contract_service),
 ) -> Union[SuccessResponse, ErrorResponse]:
+    """e签宝回调"""
     result = await service.process_esign_callback(payload)
     if not result.get("success"):
         return ErrorResponse(**result)

+ 12 - 11
alien_contract/db/models/bundle.py

@@ -5,16 +5,17 @@ from alien_database.base import Base, AuditMixin
 
 
 class ContractBundle(Base, AuditMixin):
+    """合同包"""
     __tablename__ = "contract_bundle"
 
-    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
-    subject_type: Mapped[str] = mapped_column(String(20))
-    subject_id: Mapped[int] = mapped_column(BigInteger)
-    subject_name: Mapped[str] = mapped_column(String(120))
-    business_segment: Mapped[str] = mapped_column(String(100))
-    contact_name: Mapped[str] = mapped_column(String(100))
-    contact_phone: Mapped[str] = mapped_column(String(20))
-    ord_id: Mapped[str] = mapped_column(String(40))
-    bundle_type: Mapped[str] = mapped_column(String(50))
-    status: Mapped[str] = mapped_column(String(20), default="pending")
-    primary_document_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
+    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True, comment="合同包唯一标识")
+    subject_type: Mapped[str] = mapped_column(String(20), comment="签约主体类型(store/lawyer)")
+    subject_id: Mapped[int] = mapped_column(BigInteger, comment="签约主体ID,关联门店/律师业务主键")
+    subject_name: Mapped[str] = mapped_column(String(120), comment="签约主体名称")
+    business_segment: Mapped[str] = mapped_column(String(100), comment="业务板块")
+    contact_name: Mapped[str] = mapped_column(String(100), comment="联系人姓名")
+    contact_phone: Mapped[str] = mapped_column(String(20), comment="联系人手机号")
+    ord_id: Mapped[str] = mapped_column(String(40), comment="组织标识(统一社会信用代码)")
+    bundle_type: Mapped[str] = mapped_column(String(50), comment="合同包类型(STORE_STANDARD/LAWYER_STANDARD)")
+    status: Mapped[str] = mapped_column(String(20), default="未签署", comment="整体状态: 未签署/审核中/已签署")
+    primary_document_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, comment="主合同文档ID")

+ 15 - 14
alien_contract/db/models/document.py

@@ -8,19 +8,20 @@ from alien_database.base import Base, AuditMixin
 
 
 class ContractDocument(Base, AuditMixin):
+    """合同文档"""
     __tablename__ = "contract_document"
 
-    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
-    bundle_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("contract_bundle.id"), index=True)
-    contract_type: Mapped[str] = mapped_column(String(50))
-    contract_name: Mapped[str] = mapped_column(String(100))
-    is_primary: Mapped[int] = mapped_column(Integer, default=0)
-    status: Mapped[int] = mapped_column(Integer, default=0)
-    sign_flow_id: Mapped[str] = mapped_column(String(64), unique=True)
-    file_id: Mapped[str] = mapped_column(String(64))
-    template_url: Mapped[str] = mapped_column(LONGTEXT)
-    sign_url: Mapped[str] = mapped_column(LONGTEXT)
-    download_url: Mapped[str] = mapped_column(LONGTEXT)
-    signing_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
-    effective_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
-    expiry_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True, comment="合同文档唯一标识")
+    bundle_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("contract_bundle.id"), index=True, comment="所属合同包ID")
+    contract_type: Mapped[str] = mapped_column(String(50), comment="合同类型编码(store_agreement/alipay_auth等)")
+    contract_name: Mapped[str] = mapped_column(String(100), comment="合同展示名称")
+    is_primary: Mapped[int] = mapped_column(Integer, default=0, comment="是否主合同(1是/0否)")
+    status: Mapped[int] = mapped_column(Integer, default=0, comment="签署状态(0未签署/1已签署)")
+    sign_flow_id: Mapped[str] = mapped_column(String(64), unique=True, comment="e签宝签署流程ID")
+    file_id: Mapped[str] = mapped_column(String(64), comment="e签宝文件ID")
+    template_url: Mapped[str] = mapped_column(LONGTEXT, comment="合同模板预览URL")
+    sign_url: Mapped[str] = mapped_column(LONGTEXT, comment="签署链接URL")
+    download_url: Mapped[str] = mapped_column(LONGTEXT, comment="签署完成后的下载URL")
+    signing_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment="签署时间")
+    effective_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment="合同生效时间")
+    expiry_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment="合同到期时间")

+ 7 - 6
alien_contract/db/models/event.py

@@ -6,11 +6,12 @@ from alien_database.base import Base, AuditMixin
 
 
 class ContractEvent(Base, AuditMixin):
+    """合同事件"""
     __tablename__ = "contract_event"
 
-    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
-    bundle_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
-    document_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
-    sign_flow_id: Mapped[str] = mapped_column(String(64), index=True)
-    event_type: Mapped[str] = mapped_column(String(50))
-    payload_json: Mapped[str] = mapped_column(LONGTEXT)
+    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True, comment="事件唯一标识")
+    bundle_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, comment="关联合同包ID")
+    document_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, comment="关联合同文档ID")
+    sign_flow_id: Mapped[str] = mapped_column(String(64), index=True, comment="e签宝签署流程ID")
+    event_type: Mapped[str] = mapped_column(String(50), comment="事件类型(esign_callback:{action})")
+    payload_json: Mapped[str] = mapped_column(LONGTEXT, comment="e签宝回调原始JSON报文")

+ 12 - 6
alien_contract/repositories/contract_repo.py

@@ -68,10 +68,13 @@ class ContractRepository:
         bundles = result.scalars().all()
         return bundles, total
 
-    async def list_documents_by_bundle_ids(self, bundle_ids: list[int]) -> dict[int, list[ContractDocument]]:
+    async def list_documents_by_bundle_ids(self, bundle_ids: list[int], *, doc_status: int | None = None) -> dict[int, list[ContractDocument]]:
         if not bundle_ids:
             return {}
-        stmt = select(ContractDocument).where(ContractDocument.bundle_id.in_(bundle_ids), ContractDocument.delete_flag == 0)
+        conditions = [ContractDocument.bundle_id.in_(bundle_ids), ContractDocument.delete_flag == 0]
+        if doc_status is not None:
+            conditions.append(ContractDocument.status == doc_status)
+        stmt = select(ContractDocument).where(*conditions)
         result = await self.db.execute(stmt)
         documents = result.scalars().all()
         grouped: dict[int, list[ContractDocument]] = {}
@@ -120,13 +123,13 @@ class ContractRepository:
         result = await self.db.execute(stmt)
         statuses = [row[0] for row in result.fetchall()]
         if not statuses:
-            status = "pending"
+            status = "未签署"
         elif all(s == 1 for s in statuses):
-            status = "all_signed"
+            status = "已签署"
         elif any(s == 1 for s in statuses):
-            status = "partially_signed"
+            status = "审核中"
         else:
-            status = "pending"
+            status = "未签署"
         update_stmt = ContractBundle.__table__.update().where(ContractBundle.id == bundle_id).values(status=status)
         await self.db.execute(update_stmt)
         return status
@@ -143,3 +146,6 @@ class ContractRepository:
 
     async def commit(self):
         await self.db.commit()
+
+    async def rollback(self):
+        await self.db.rollback()

+ 0 - 8
alien_contract/schemas/request/contract.py

@@ -12,7 +12,6 @@ class BundleCreateRequest(BaseModel):
     contact_name: str = Field(description="联系人姓名")
     contact_phone: str = Field(description="联系电话")
     ord_id: str = Field(description="统一社会信用代码")
-    bundle_type: str | None = Field(default=None, description="合同包类型")  # LAWYER_STANDARD/STORE_STANDARD
 
     @field_validator("contact_phone")
     @classmethod
@@ -20,10 +19,3 @@ class BundleCreateRequest(BaseModel):
         if not re.fullmatch(r"^1\d{10}$", value):
             raise ValueError("contact_phone 格式错误,应为11位手机号")
         return value
-
-    @field_validator("ord_id")
-    @classmethod
-    def validate_ord_id(cls, value: str) -> str:
-        if not re.fullmatch(r"^[0-9A-Z]{18}$", value):
-            raise ValueError("ord_id 格式错误,应为18位大写字母或数字")
-        return value

+ 43 - 43
alien_contract/schemas/response/contract.py

@@ -1,67 +1,67 @@
 from datetime import datetime
 from typing import Optional, List, Any
 
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
 
 
 class ContractDocumentResponse(BaseModel):
-    id: int
-    contract_type: str
-    contract_name: str
-    is_primary: int
-    status: int
-    sign_flow_id: str
-    file_id: str
-    template_url: str
-    sign_url: str
-    download_url: str
-    signing_time: Optional[datetime] = None
-    effective_time: Optional[datetime] = None
-    expiry_time: Optional[datetime] = None
+    id: int = Field(description="合同文档ID")
+    contract_type: str = Field(description="合同类型编码")
+    contract_name: str = Field(description="合同名称")
+    is_primary: int = Field(description="是否主合同,1是0否")
+    status: int = Field(description="签署状态,0未签1已签")
+    sign_flow_id: str = Field(description="e签宝签署流程ID")
+    file_id: str = Field(description="e签宝文件ID")
+    template_url: str = Field(description="合同模板文件链接")
+    sign_url: str = Field(description="签署链接")
+    download_url: str = Field(description="已签文件下载链接")
+    signing_time: Optional[datetime] = Field(default=None, description="签署时间")
+    effective_time: Optional[datetime] = Field(default=None, description="生效时间")
+    expiry_time: Optional[datetime] = Field(default=None, description="到期时间")
 
 
 class ContractBundleResponse(BaseModel):
-    id: int
-    subject_type: str
-    subject_id: int
-    subject_name: str
-    business_segment: str
-    contact_name: str
-    contact_phone: str
-    ord_id: str
-    bundle_type: str
-    status: str
-    primary_document_id: Optional[int] = None
-    documents: List[ContractDocumentResponse]
+    id: int = Field(description="合同包ID")
+    subject_type: str = Field(description="主体类型,store或lawyer")
+    subject_id: int = Field(description="主体ID")
+    subject_name: str = Field(description="主体名称")
+    business_segment: str = Field(description="业务板块")
+    contact_name: str = Field(description="联系人姓名")
+    contact_phone: str = Field(description="联系人手机号")
+    ord_id: str = Field(description="统一社会信用代码")
+    bundle_type: str = Field(description="合同包类型")
+    status: str = Field(description="合同包状态")
+    primary_document_id: Optional[int] = Field(default=None, description="主合同文档ID")
+    documents: List[ContractDocumentResponse] = Field(description="合同文档列表")
 
 
 class BundleCreateResponse(BaseModel):
-    success: bool
-    message: str
-    bundle_id: Optional[int] = None
-    primary_sign_flow_id: Optional[str] = None
-    created_contracts: Optional[List[dict]] = None
+    success: bool = Field(description="是否成功")
+    message: str = Field(description="响应消息")
+    bundle_id: Optional[int] = Field(default=None, description="合同包ID")
+    primary_sign_flow_id: Optional[str] = Field(default=None, description="主合同签署流程ID")
+    created_contracts: Optional[List[dict]] = Field(default=None, description="本次创建的合同列表")
 
 
 class ErrorResponse(BaseModel):
-    success: bool = False
-    message: str
-    raw: Optional[Any] = None
+    success: bool = Field(default=False, description="是否成功")
+    message: str = Field(description="错误消息")
+    raw: Optional[Any] = Field(default=None, description="原始错误数据")
 
 
 class SuccessResponse(BaseModel):
-    code: str
-    msg: str
+    code: str = Field(description="响应代码")
+    msg: str = Field(description="响应消息")
 
 
 class PaginatedBundleResponse(BaseModel):
-    items: List[ContractBundleResponse]
-    total: int
-    page: int
-    page_size: int
-    total_pages: int
+    items: List[ContractBundleResponse] = Field(description="合同包列表")
+    total: int = Field(description="总记录数")
+    page: int = Field(description="当前页码")
+    page_size: int = Field(description="每页条数")
+    total_pages: int = Field(description="总页数")
 
 
 class ModuleStatusResponse(BaseModel):
-    module: str
-    status: str
+    module: str = Field(description="模块名称")
+    status: str = Field(description="模块状态")

+ 95 - 6
alien_contract/services/contract_server.py

@@ -34,6 +34,17 @@ def _init_logger():
 
 logger = _init_logger()
 
+SIGN_SUCCESS_ACTIONS = {
+    "OPERATOR_COMPLETE_SIGN",
+    "SIGN_FLOW_FINISH",
+    "SIGN_FLOW_COMPLETE",
+}
+
+NON_SIGNING_ACTIONS = {
+    "OPERATOR_READ",
+    "OPERATOR_VIEW",
+}
+
 BUNDLE_CONFIGS = {
     "STORE_STANDARD": [
         ("store_agreement", "店铺入驻协议", 1),
@@ -58,7 +69,7 @@ class ContractCenterService:
         self.repo = ContractRepository(db)
 
     async def create_bundle(self, req: BundleCreateRequest) -> dict:
-        bundle_type = req.bundle_type or DEFAULT_BUNDLE_BY_SUBJECT[req.subject_type]
+        bundle_type = DEFAULT_BUNDLE_BY_SUBJECT[req.subject_type]
         configs = BUNDLE_CONFIGS.get(bundle_type)
         if not configs:
             return {"success": False, "message": "不支持的合同包类型", "raw": {"bundle_type": bundle_type}}
@@ -84,10 +95,32 @@ class ContractCenterService:
                 "contact_phone": req.contact_phone,
                 "ord_id": req.ord_id,
                 "bundle_type": bundle_type,
-                "status": "pending",
+                "status": "未签署",
             }
         )
         documents = await self.repo.create_documents(bundle.id, items)
+        for document in documents:
+            try:
+                sign_resp = sign_url(document.sign_flow_id, req.contact_phone)
+                sign_json = json.loads(sign_resp)
+                sign_data = sign_json.get("data") if isinstance(sign_json, dict) else None
+                result_sign_url = sign_data.get("url") if isinstance(sign_data, dict) else None
+            except Exception:
+                await self.repo.rollback()
+                return {
+                    "success": False,
+                    "message": f"{document.contract_name}创建成功但签署链接获取失败",
+                    "raw": {"contract_type": document.contract_type, "sign_flow_id": document.sign_flow_id},
+                }
+            if not result_sign_url:
+                await self.repo.rollback()
+                return {
+                    "success": False,
+                    "message": f"{document.contract_name}创建成功但签署链接缺失",
+                    "raw": {"contract_type": document.contract_type, "sign_flow_id": document.sign_flow_id, "resp": sign_json},
+                }
+            await self.repo.update_document_urls(document.id, sign_url=result_sign_url)
+            document.sign_url = result_sign_url
         primary_doc = next((doc for doc in documents if doc.is_primary == 1), documents[0])
         await self.repo.set_primary_document(bundle.id, primary_doc.id)
         await self.repo.commit()
@@ -103,15 +136,16 @@ class ContractCenterService:
                     "sign_flow_id": d.sign_flow_id,
                     "file_id": d.file_id,
                     "contract_url": d.template_url,
+                    "sign_url": d.sign_url,
                 }
                 for d in documents
             ],
         }
 
-    async def list_bundles(self, subject_type: str, subject_id: int, page: int, page_size: int) -> dict:
+    async def list_bundles(self, subject_type: str, subject_id: int, page: int, page_size: int, *, doc_status: int | None = None) -> dict:
         bundles, total = await self.repo.list_bundles(subject_type, subject_id, page, page_size)
         ids = [b.id for b in bundles]
-        docs_map = await self.repo.list_documents_by_bundle_ids(ids)
+        docs_map = await self.repo.list_documents_by_bundle_ids(ids, doc_status=doc_status)
         items = []
         for b in bundles:
             docs = docs_map.get(b.id, [])
@@ -211,16 +245,69 @@ class ContractCenterService:
     async def process_esign_callback(self, payload: dict) -> dict:
         sign_result = payload.get("signResult")
         sign_flow_id = payload.get("signFlowId")
+        action = payload.get("action")
+        operator_mobile = (
+            payload.get("operator", {})
+            .get("psnAccount", {})
+            .get("accountMobile")
+        )
         if not sign_flow_id:
+            logger.info(
+                "esign_callback_event %s",
+                json.dumps(
+                    {
+                        "result": "ignored",
+                        "reason": "missing_signFlowId",
+                        "action": action,
+                        "sign_result": sign_result,
+                        "operator_mobile": operator_mobile,
+                    },
+                    ensure_ascii=False,
+                ),
+            )
             return {"success": True, "code": "200", "msg": "ignored_missing_signFlowId"}
 
         document, bundle = await self.repo.get_document_and_bundle(sign_flow_id)
         if not document:
+            logger.info(
+                "esign_callback_event %s",
+                json.dumps(
+                    {
+                        "result": "ignored",
+                        "reason": "unknown_signFlowId",
+                        "sign_flow_id": sign_flow_id,
+                        "action": action,
+                        "sign_result": sign_result,
+                        "operator_mobile": operator_mobile,
+                    },
+                    ensure_ascii=False,
+                ),
+            )
             return {"success": True, "code": "200", "msg": "ignored_unknown_signFlowId"}
 
-        await self.repo.create_event(bundle.id, document.id, sign_flow_id, "esign_callback", payload)
+        event_type = f"esign_callback:{action or 'UNKNOWN'}"
+        await self.repo.create_event(bundle.id, document.id, sign_flow_id, event_type[:50], payload)
+
+        mark_signed = bool(sign_result == 2 or (action in SIGN_SUCCESS_ACTIONS and action not in NON_SIGNING_ACTIONS))
+
+        logger.info(
+            "esign_callback_event %s",
+            json.dumps(
+                {
+                    "result": "mark_signed" if mark_signed else "ignored",
+                    "sign_flow_id": sign_flow_id,
+                    "bundle_id": bundle.id,
+                    "document_id": document.id,
+                    "contract_type": document.contract_type,
+                    "action": action,
+                    "sign_result": sign_result,
+                    "operator_mobile": operator_mobile,
+                },
+                ensure_ascii=False,
+            ),
+        )
 
-        if sign_result == 2:
+        if mark_signed:
             ts_ms = payload.get("operateTime") or payload.get("timestamp")
             signing_dt = None
             if ts_ms:
@@ -245,4 +332,6 @@ class ContractCenterService:
             return {"success": True, "code": "200", "msg": "success"}
 
         await self.repo.commit()
+        if action:
+            return {"success": True, "code": "200", "msg": f"ignored_action_{action}"}
         return {"success": True, "code": "200", "msg": f"ignored_signResult_{sign_result}"}

+ 0 - 1
alien_lawyer/api/router.py

@@ -56,7 +56,6 @@ async def create_esign_templates(
         contact_name=templates_data.contact_name,
         contact_phone=templates_data.contact_phone,
         ord_id=templates_data.ord_id,
-        bundle_type="LAWYER_STANDARD",
     )
     result = await templates_server.create_bundle(request)
     if not result.get("success"):

+ 0 - 1
alien_store/api/router.py

@@ -64,7 +64,6 @@ async def create_esign_templates(
         contact_name=templates_data.merchant_name,
         contact_phone=templates_data.contact_phone,
         ord_id=templates_data.ord_id,
-        bundle_type="STORE_STANDARD",
     )
     result = await templates_server.create_bundle(request)
     if not result.get("success"):

+ 1 - 1
alien_store/db/models/contract_store.py

@@ -16,7 +16,7 @@ class ContractStore(Base, AuditMixin):
     business_segment: Mapped[str] = mapped_column(String(100), comment="经营板块")
     merchant_name: Mapped[str] = mapped_column(String(100), comment="商家姓名")
     contact_phone: Mapped[str] = mapped_column(String(20), comment="联系电话")
-    signing_status: Mapped[str] = mapped_column(String(20), default="未签署", comment="签署状态(已签署,未签署,已到期)")
+    signing_status: Mapped[str] = mapped_column(String(20), default="未签署", comment="签署状态(已签署,未签署,已到期,审核中)")
     contract_url: Mapped[str] = mapped_column(LONGTEXT, comment='合同URL')
     ord_id: Mapped[str] = mapped_column(LONGTEXT, comment='入驻商家的社会同一信用代码')
     signing_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="签署时间")