Sfoglia il codice sorgente

feat(合同中心): 完善签署状态、回调判定与创建即生成签署链接

天空之城 3 settimane fa
parent
commit
a19fab1ee7

+ 1 - 1
alembic/versions/a7d4b3e21c10_create_contract_center_tables.py

@@ -32,7 +32,7 @@ def upgrade() -> None:
         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("status", sa.String(length=20), nullable=False, server_default="未签署"),
         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),

+ 1 - 1
alien_contract/db/models/bundle.py

@@ -16,5 +16,5 @@ class ContractBundle(Base, AuditMixin):
     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")
+    status: Mapped[str] = mapped_column(String(20), default="未签署")
     primary_document_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)

+ 7 - 4
alien_contract/repositories/contract_repo.py

@@ -120,13 +120,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 +143,6 @@ class ContractRepository:
 
     async def commit(self):
         await self.db.commit()
+
+    async def rollback(self):
+        await self.db.rollback()

+ 92 - 3
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),
@@ -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,6 +136,7 @@ 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
             ],
@@ -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}"}