169 lines
7.9 KiB
Python
169 lines
7.9 KiB
Python
|
|
"""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)
|