report_config.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """
  2. Report configuration data models for the universal data report generator.
  3. Defines ReportConfig, MetricDef, PageDef, ThemeConfig, and related enums.
  4. """
  5. from dataclasses import dataclass, field
  6. from enum import Enum
  7. from datetime import date
  8. from typing import Optional
  9. class PeriodType(str, Enum):
  10. DAILY = 'daily'
  11. WEEKLY = 'weekly'
  12. MONTHLY = 'monthly'
  13. QUARTERLY = 'quarterly'
  14. CUSTOM = 'custom'
  15. class AudienceType(str, Enum):
  16. MANAGEMENT = 'management'
  17. OPERATION = 'operation'
  18. CLIENT = 'client'
  19. CUSTOM = 'custom'
  20. class ComparisonType(str, Enum):
  21. PREV_PERIOD = 'prev_period'
  22. YOY = 'yoy'
  23. NONE = 'none'
  24. class ColumnRole(str, Enum):
  25. TIME = 'time'
  26. NUMERIC = 'numeric'
  27. CATEGORY = 'category'
  28. TEXT = 'text'
  29. ID = 'id'
  30. BOOLEAN = 'boolean'
  31. UNKNOWN = 'unknown'
  32. class AggregationType(str, Enum):
  33. SUM = 'sum'
  34. COUNT = 'count'
  35. AVG = 'avg'
  36. MAX = 'max'
  37. MIN = 'min'
  38. DISTINCT_COUNT = 'distinct_count'
  39. class MetricType(str, Enum):
  40. KPI = 'kpi'
  41. TREND = 'trend'
  42. DISTRIBUTION = 'distribution'
  43. RANKING = 'ranking'
  44. FUNNEL = 'funnel'
  45. ALERT = 'alert'
  46. class ChartType(str, Enum):
  47. COLUMN = 'column'
  48. BAR = 'bar'
  49. LINE = 'line'
  50. DOUGHNUT = 'doughnut'
  51. PIE = 'pie'
  52. FUNNEL = 'funnel'
  53. TABLE = 'table'
  54. GROUPED_BAR = 'grouped_bar'
  55. class ThemePreset(str, Enum):
  56. BUSINESS_CLASSIC = 'business_classic'
  57. FRESH_SIMPLE = 'fresh_simple'
  58. DARK_PROFESSIONAL = 'dark_professional'
  59. WARM_BRAND = 'warm_brand'
  60. CUSTOM = 'custom'
  61. FROM_TEMPLATE = 'from_template'
  62. @dataclass
  63. class ColumnProfile:
  64. column_name: str
  65. dtype: str
  66. role: ColumnRole
  67. null_count: int
  68. null_rate: float
  69. unique_count: int
  70. sample_values: list = field(default_factory=list)
  71. numeric_stats: Optional[dict] = None
  72. inferred_label: str = ''
  73. @dataclass
  74. class MetricDef:
  75. name: str
  76. label: str
  77. column: str
  78. aggregation: AggregationType
  79. metric_type: MetricType = MetricType.KPI
  80. unit: str = ''
  81. format_spec: str = ',.0f'
  82. selected: bool = True
  83. is_primary: bool = False
  84. @dataclass
  85. class PageDef:
  86. page_id: str
  87. title: str
  88. page_type: str
  89. order: int
  90. selected: bool = True
  91. elements: list[dict] = field(default_factory=list)
  92. conclusion_title: str = ''
  93. @dataclass
  94. class ConfirmationSpec:
  95. """Six user confirmations required before building a report."""
  96. period_and_page_range: bool = False
  97. core_metrics: bool = False
  98. audience_and_decision: bool = False
  99. visual_style_and_palette: bool = False
  100. page_structure_and_template: bool = False
  101. data_scope_and_field_mapping: bool = False
  102. def missing_items(self) -> list[str]:
  103. labels = {
  104. 'period_and_page_range': '报告周期与页数范围',
  105. 'core_metrics': '核心指标集',
  106. 'audience_and_decision': '受众与决策场景',
  107. 'visual_style_and_palette': '视觉风格与配色方向',
  108. 'page_structure_and_template': '页面结构与模板方案',
  109. 'data_scope_and_field_mapping': '数据范围与字段映射',
  110. }
  111. return [
  112. label for field_name, label in labels.items()
  113. if not getattr(self, field_name)
  114. ]
  115. def is_complete(self) -> bool:
  116. return not self.missing_items()
  117. @dataclass
  118. class ThemeConfig:
  119. preset: ThemePreset = ThemePreset.BUSINESS_CLASSIC
  120. name: str = '商务经典'
  121. primary: str = '#1E3A5F'
  122. accent: str = '#10B981'
  123. accent_neg: str = '#EF4444'
  124. secondary: str = '#64748B'
  125. dark: str = '#1F3A5C'
  126. white: str = '#FFFFFF'
  127. gray_bg: str = '#F2F2F2'
  128. card_bg: str = '#E7F0F7'
  129. text: str = '#333333'
  130. text_gray: str = '#666666'
  131. line: str = '#D9D9D9'
  132. chart_series: list[str] = field(default_factory=lambda: [
  133. '#1E3A5F', '#10B981', '#ED7D31', '#64748B',
  134. '#EF4444', '#707070', '#4472C4', '#10B981'
  135. ])
  136. title_font: str = '微软雅黑'
  137. body_font: str = '微软雅黑'
  138. number_font: str = 'Arial'
  139. @dataclass
  140. class ReportConfig:
  141. title: str
  142. period_type: PeriodType
  143. date_range: tuple[date, date]
  144. period_str: str = ''
  145. metrics: list[MetricDef] = field(default_factory=list)
  146. pages: list[PageDef] = field(default_factory=list)
  147. audience: AudienceType = AudienceType.MANAGEMENT
  148. decision_scenario: str = ''
  149. custom_audience: str = ''
  150. theme: ThemeConfig = field(default_factory=ThemeConfig)
  151. template_path: str = ''
  152. template_profile: Optional[object] = None # TemplateProfile from template_parser
  153. use_template_theme: bool = True
  154. visual_style_direction: str = ''
  155. page_structure_template: str = ''
  156. filters: dict = field(default_factory=dict)
  157. comparison: ComparisonType = ComparisonType.PREV_PERIOD
  158. page_count_range: tuple[int, int] = (6, 15)
  159. source_label: str = '数据报告系统'
  160. data_scope: str = ''
  161. data_field_mapping: dict = field(default_factory=dict)
  162. data_profiling: Optional[dict] = None
  163. agent_recommendations: Optional[dict] = None
  164. user_confirmation: ConfirmationSpec = field(default_factory=ConfirmationSpec)
  165. require_six_confirmations: bool = True
  166. quality_threshold: int = 85
  167. max_fix_iterations: int = 5
  168. def to_dict(self) -> dict:
  169. return {
  170. 'title': self.title,
  171. 'period_type': self.period_type.value,
  172. 'period_str': self.period_str,
  173. 'page_count_range': list(self.page_count_range),
  174. 'audience': self.audience.value,
  175. 'theme_preset': self.theme.preset.value,
  176. 'metrics_count': len(self.metrics),
  177. 'pages_count': len(self.pages),
  178. 'six_confirmations_complete': self.user_confirmation.is_complete(),
  179. }
  180. def validate_six_confirmations(config: ReportConfig, data_columns: Optional[list[str]] = None) -> list[str]:
  181. """Return validation gaps for the six confirmation contract."""
  182. issues = []
  183. missing = config.user_confirmation.missing_items()
  184. if missing:
  185. issues.append('六项确认未完成:' + '、'.join(missing))
  186. if not config.period_str and not config.date_range:
  187. issues.append('缺少报告周期。')
  188. if not config.page_count_range or len(config.page_count_range) != 2:
  189. issues.append('缺少页数范围。')
  190. if not [m for m in config.metrics if m.selected]:
  191. issues.append('缺少已确认的核心指标集。')
  192. if not config.decision_scenario:
  193. issues.append('缺少受众与决策场景说明。')
  194. if not config.visual_style_direction and not config.theme:
  195. issues.append('缺少视觉风格与配色方向。')
  196. if not config.pages:
  197. issues.append('缺少页面结构与模板方案。')
  198. if not config.data_field_mapping:
  199. issues.append('缺少数据范围与字段映射。')
  200. if data_columns:
  201. missing_cols = []
  202. for metric in config.metrics:
  203. if metric.selected and metric.column and metric.column not in data_columns:
  204. missing_cols.append(f'{metric.label} -> {metric.column}')
  205. if missing_cols:
  206. issues.append('核心指标字段映射不存在:' + '、'.join(missing_cols[:8]))
  207. selected_pages = [p for p in config.pages if p.selected]
  208. if config.page_count_range and selected_pages:
  209. low, high = config.page_count_range
  210. if len(selected_pages) < low - 1 or len(selected_pages) > high + 1:
  211. issues.append(f'页面数量 {len(selected_pages)} 不在确认范围 {low}-{high} 页附近。')
  212. return issues