"""PPT 场景、计算策略与模板的不可变发布版本。""" import json from sqlalchemy import BigInteger, Column, ForeignKey, Index, Integer, String, Text, TIMESTAMP, UniqueConstraint, event, func, inspect from insurance.db.compat import db VERSION_ID_TYPE = BigInteger().with_variant(Integer, "sqlite") LIFECYCLES = {"draft", "validated", "published", "retired"} def _load_json(value, fallback): if not value: return fallback try: return json.loads(value) except (TypeError, ValueError): return fallback class PptScenarioVersion(db.Model): __tablename__ = "insurance_ppt_scenario_versions" __table_args__ = ( UniqueConstraint("scenario_code", "version", name="uq_ppt_scenario_version"), Index("idx_ppt_scenario_versions_lifecycle", "scenario_code", "lifecycle"), Index("idx_ppt_scenario_versions_hash", "definition_hash"), ) id = Column(VERSION_ID_TYPE, primary_key=True, autoincrement=True) scenario_code = Column(String(50), ForeignKey("insurance_ppt_scenarios.code"), nullable=False) version = Column(Integer, nullable=False) lifecycle = Column(String(20), nullable=False, default="draft") selector_json = Column(Text, nullable=False, default="{}") page_specs_json = Column(Text, nullable=False, default="[]") metric_specs_json = Column(Text, nullable=False, default="[]") comparison_years_json = Column(Text, nullable=False, default="[]") compatibility_rules_json = Column(Text, nullable=False, default="{}") fallback_scenario = Column(String(50), nullable=True) definition_hash = Column(String(64), nullable=False) validation_report_json = Column(Text, nullable=True) created_by = Column(String(64), nullable=False) published_by = Column(String(64), nullable=True) published_at = Column(TIMESTAMP, nullable=True) created_at = Column(TIMESTAMP, server_default=func.now()) def definition(self): return { "selector": _load_json(self.selector_json, {}), "pageSpecs": _load_json(self.page_specs_json, []), "metricSpecs": _load_json(self.metric_specs_json, []), "comparisonYears": _load_json(self.comparison_years_json, []), "compatibilityRules": _load_json(self.compatibility_rules_json, {}), "fallbackScenario": self.fallback_scenario, } def to_public_dict(self): return { "id": self.id, "scenarioCode": self.scenario_code, "version": self.version, "lifecycle": self.lifecycle, **self.definition(), "definitionHash": self.definition_hash, "validationReport": _load_json(self.validation_report_json, None), "createdBy": self.created_by, "publishedBy": self.published_by, "publishedAt": self.published_at.isoformat() if self.published_at else None, "createdAt": self.created_at.isoformat() if self.created_at else None, } class GenerationPolicyVersion(db.Model): __tablename__ = "insurance_generation_policy_versions" __table_args__ = ( UniqueConstraint("code", "version", name="uq_generation_policy_version"), Index("idx_generation_policy_versions_lifecycle", "code", "lifecycle"), ) id = Column(VERSION_ID_TYPE, primary_key=True, autoincrement=True) code = Column(String(50), nullable=False) version = Column(Integer, nullable=False) lifecycle = Column(String(20), nullable=False, default="draft") calculation_policy_json = Column(Text, nullable=False, default="{}") missing_value_policy_json = Column(Text, nullable=False, default="{}") conclusion_policy_json = Column(Text, nullable=False, default="{}") policy_hash = Column(String(64), nullable=False) validation_report_json = Column(Text, nullable=True) created_by = Column(String(64), nullable=False) published_by = Column(String(64), nullable=True) published_at = Column(TIMESTAMP, nullable=True) created_at = Column(TIMESTAMP, server_default=func.now()) def definition(self): return { "calculationPolicy": _load_json(self.calculation_policy_json, {}), "missingValuePolicy": _load_json(self.missing_value_policy_json, {}), "conclusionPolicy": _load_json(self.conclusion_policy_json, {}), } def to_public_dict(self): return { "id": self.id, "code": self.code, "version": self.version, "lifecycle": self.lifecycle, **self.definition(), "policyHash": self.policy_hash, "validationReport": _load_json(self.validation_report_json, None), "createdBy": self.created_by, "publishedBy": self.published_by, "publishedAt": self.published_at.isoformat() if self.published_at else None, "createdAt": self.created_at.isoformat() if self.created_at else None, } class PptTemplateVersion(db.Model): __tablename__ = "insurance_ppt_template_versions" __table_args__ = ( UniqueConstraint("template_id", "version", name="uq_ppt_template_version"), Index("idx_ppt_template_versions_lifecycle", "template_id", "lifecycle"), ) id = Column(VERSION_ID_TYPE, primary_key=True, autoincrement=True) template_id = Column(String(50), ForeignKey("insurance_ppt_templates.id"), nullable=False) version = Column(Integer, nullable=False) lifecycle = Column(String(20), nullable=False, default="draft") asset_id = Column(String(255), nullable=False) asset_sha256 = Column(String(64), nullable=False) page_slots_json = Column(Text, nullable=False, default="[]") theme_json = Column(Text, nullable=False, default="{}") capacity_rules_json = Column(Text, nullable=False, default="{}") supported_scenario_versions_json = Column(Text, nullable=False, default="[]") validation_report_json = Column(Text, nullable=True) definition_hash = Column(String(64), nullable=False) created_by = Column(String(64), nullable=False) published_by = Column(String(64), nullable=True) published_at = Column(TIMESTAMP, nullable=True) created_at = Column(TIMESTAMP, server_default=func.now()) def definition(self): return { "assetId": self.asset_id, "assetSha256": self.asset_sha256, "pageSlots": _load_json(self.page_slots_json, []), "theme": _load_json(self.theme_json, {}), "capacityRules": _load_json(self.capacity_rules_json, {}), "supportedScenarioVersionIds": _load_json(self.supported_scenario_versions_json, []), } def to_public_dict(self): return { "id": self.id, "templateId": self.template_id, "version": self.version, "lifecycle": self.lifecycle, **self.definition(), "definitionHash": self.definition_hash, "validationReport": _load_json(self.validation_report_json, None), "createdBy": self.created_by, "publishedBy": self.published_by, "publishedAt": self.published_at.isoformat() if self.published_at else None, "createdAt": self.created_at.isoformat() if self.created_at else None, } def _protect_immutable_definition(mapper, connection, target): """阻止绕过服务直接篡改已发布/已退役定义。""" state = inspect(target) lifecycle_history = state.attrs.lifecycle.history previous = lifecycle_history.deleted[0] if lifecycle_history.deleted else target.lifecycle if previous not in {"published", "retired"}: return mutable_metadata = {"lifecycle", "published_by", "published_at", "validation_report_json"} changed = { attribute.key for attribute in state.mapper.column_attrs if attribute.key not in mutable_metadata and state.attrs[attribute.key].history.has_changes() } if changed: raise ValueError("published version definition is immutable") for _model in (PptScenarioVersion, GenerationPolicyVersion, PptTemplateVersion): event.listen(_model, "before_update", _protect_immutable_definition)