report_config.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. @dataclass
  62. class ColumnProfile:
  63. column_name: str
  64. dtype: str
  65. role: ColumnRole
  66. null_count: int
  67. null_rate: float
  68. unique_count: int
  69. sample_values: list = field(default_factory=list)
  70. numeric_stats: Optional[dict] = None
  71. inferred_label: str = ''
  72. @dataclass
  73. class MetricDef:
  74. name: str
  75. label: str
  76. column: str
  77. aggregation: AggregationType
  78. metric_type: MetricType = MetricType.KPI
  79. unit: str = ''
  80. format_spec: str = ',.0f'
  81. selected: bool = True
  82. is_primary: bool = False
  83. @dataclass
  84. class PageDef:
  85. page_id: str
  86. title: str
  87. page_type: str
  88. order: int
  89. selected: bool = True
  90. elements: list[dict] = field(default_factory=list)
  91. conclusion_title: str = ''
  92. @dataclass
  93. class ConfirmationSpec:
  94. """Six user confirmations required before building a report."""
  95. period_and_page_range: bool = False
  96. core_metrics: bool = False
  97. audience_and_decision: bool = False
  98. visual_style_and_palette: bool = False
  99. page_structure_and_template: bool = False
  100. data_scope_and_field_mapping: bool = False
  101. def missing_items(self) -> list[str]:
  102. labels = {
  103. 'period_and_page_range': '报告周期与页数范围',
  104. 'core_metrics': '核心指标集',
  105. 'audience_and_decision': '受众与决策场景',
  106. 'visual_style_and_palette': '视觉风格与配色方向',
  107. 'page_structure_and_template': '页面结构与模板方案',
  108. 'data_scope_and_field_mapping': '数据范围与字段映射',
  109. }
  110. return [
  111. label for field_name, label in labels.items()
  112. if not getattr(self, field_name)
  113. ]
  114. def is_complete(self) -> bool:
  115. return not self.missing_items()
  116. @dataclass
  117. class ThemeConfig:
  118. preset: ThemePreset = ThemePreset.BUSINESS_CLASSIC
  119. name: str = '商务经典'
  120. primary: str = '#1E3A5F'
  121. accent: str = '#10B981'
  122. accent_neg: str = '#EF4444'
  123. secondary: str = '#64748B'
  124. dark: str = '#1F3A5C'
  125. white: str = '#FFFFFF'
  126. gray_bg: str = '#F2F2F2'
  127. card_bg: str = '#E7F0F7'
  128. text: str = '#333333'
  129. text_gray: str = '#666666'
  130. line: str = '#D9D9D9'
  131. chart_series: list[str] = field(default_factory=lambda: [
  132. '#1E3A5F', '#10B981', '#ED7D31', '#64748B',
  133. '#EF4444', '#707070', '#4472C4', '#10B981'
  134. ])
  135. title_font: str = '微软雅黑'
  136. body_font: str = '微软雅黑'
  137. number_font: str = 'Arial'
  138. @dataclass
  139. class ReportConfig:
  140. title: str
  141. period_type: PeriodType
  142. date_range: tuple[date, date]
  143. period_str: str = ''
  144. metrics: list[MetricDef] = field(default_factory=list)
  145. pages: list[PageDef] = field(default_factory=list)
  146. audience: AudienceType = AudienceType.MANAGEMENT
  147. decision_scenario: str = ''
  148. custom_audience: str = ''
  149. theme: ThemeConfig = field(default_factory=ThemeConfig)
  150. template_path: str = ''
  151. visual_style_direction: str = ''
  152. page_structure_template: str = ''
  153. filters: dict = field(default_factory=dict)
  154. comparison: ComparisonType = ComparisonType.PREV_PERIOD
  155. page_count_range: tuple[int, int] = (6, 15)
  156. source_label: str = '数据报告系统'
  157. data_scope: str = ''
  158. data_field_mapping: dict = field(default_factory=dict)
  159. data_profiling: Optional[dict] = None
  160. agent_recommendations: Optional[dict] = None
  161. user_confirmation: ConfirmationSpec = field(default_factory=ConfirmationSpec)
  162. require_six_confirmations: bool = True
  163. quality_threshold: int = 85
  164. max_fix_iterations: int = 5
  165. def to_dict(self) -> dict:
  166. return {
  167. 'title': self.title,
  168. 'period_type': self.period_type.value,
  169. 'period_str': self.period_str,
  170. 'page_count_range': list(self.page_count_range),
  171. 'audience': self.audience.value,
  172. 'theme_preset': self.theme.preset.value,
  173. 'metrics_count': len(self.metrics),
  174. 'pages_count': len(self.pages),
  175. 'six_confirmations_complete': self.user_confirmation.is_complete(),
  176. }
  177. def validate_six_confirmations(config: ReportConfig, data_columns: Optional[list[str]] = None) -> list[str]:
  178. """Return validation gaps for the six confirmation contract."""
  179. issues = []
  180. missing = config.user_confirmation.missing_items()
  181. if missing:
  182. issues.append('六项确认未完成:' + '、'.join(missing))
  183. if not config.period_str and not config.date_range:
  184. issues.append('缺少报告周期。')
  185. if not config.page_count_range or len(config.page_count_range) != 2:
  186. issues.append('缺少页数范围。')
  187. if not [m for m in config.metrics if m.selected]:
  188. issues.append('缺少已确认的核心指标集。')
  189. if not config.decision_scenario:
  190. issues.append('缺少受众与决策场景说明。')
  191. if not config.visual_style_direction and not config.theme:
  192. issues.append('缺少视觉风格与配色方向。')
  193. if not config.pages:
  194. issues.append('缺少页面结构与模板方案。')
  195. if not config.data_field_mapping:
  196. issues.append('缺少数据范围与字段映射。')
  197. if data_columns:
  198. missing_cols = []
  199. for metric in config.metrics:
  200. if metric.selected and metric.column and metric.column not in data_columns:
  201. missing_cols.append(f'{metric.label} -> {metric.column}')
  202. if missing_cols:
  203. issues.append('核心指标字段映射不存在:' + '、'.join(missing_cols[:8]))
  204. selected_pages = [p for p in config.pages if p.selected]
  205. if config.page_count_range and selected_pages:
  206. low, high = config.page_count_range
  207. if len(selected_pages) < low - 1 or len(selected_pages) > high + 1:
  208. issues.append(f'页面数量 {len(selected_pages)} 不在确认范围 {low}-{high} 页附近。')
  209. return issues