- Add company module (3-tier org, CEO planning, parallel departments) - Add company orchestrator, knowledge extractor, presets, scheduler - Add company API endpoints, models, and frontend views - Add 今天吃啥 PWA app (69 dishes, real images, offline support) - Add team_projects output directory structure - Add unified manage.ps1 for service lifecycle - Add Windows startup guide v1.0 - Add TTS troubleshooting doc - Update frontend (AgentChat UX overhaul, new views) - Update backend (voice engine fix, multi-tenant, RBAC) - Remove deprecated startup scripts and old docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
7.2 KiB
Python
171 lines
7.2 KiB
Python
# ============================================================================
|
||
# 家庭阅读激励工具 - 积分规则配置
|
||
# ============================================================================
|
||
"""
|
||
可配置的积分规则引擎配置模块。
|
||
|
||
设计原则:
|
||
1. 所有积分参数可调,家长端可通过 API 修改
|
||
2. 支持多种计分维度:基础打卡、连续天数加成、时长加成、页数加成
|
||
3. 配置持久化在 points_config 表中(首次运行时自动创建)
|
||
4. 默认配置开箱即用,无需手动设置
|
||
|
||
积分公式:
|
||
total = base_points
|
||
+ streak_bonus(consecutive_days)
|
||
+ duration_bonus(minutes)
|
||
+ pages_bonus(pages)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional, Dict, Any
|
||
from db_config import get_connection, transaction
|
||
|
||
|
||
# ============================================================================
|
||
# 配置表 DDL(自动迁移)
|
||
# ============================================================================
|
||
|
||
CONFIG_TABLE_DDL = """
|
||
CREATE TABLE IF NOT EXISTS points_config (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL,
|
||
description TEXT,
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||
)
|
||
"""
|
||
|
||
|
||
def _ensure_config_table() -> None:
|
||
"""确保配置表存在(幂等操作)。"""
|
||
conn = get_connection()
|
||
conn.execute(CONFIG_TABLE_DDL)
|
||
conn.execute("""
|
||
INSERT OR IGNORE INTO points_config (key, value, description)
|
||
VALUES
|
||
('base_points', '10', '每日基础打卡积分'),
|
||
('streak_enabled', 'true', '是否启用连续打卡加成'),
|
||
('streak_threshold_7', '5', '连续7天额外加分'),
|
||
('streak_threshold_14', '10', '连续14天额外加分'),
|
||
('streak_threshold_30', '20', '连续30天额外加分'),
|
||
('duration_enabled', 'true', '是否启用阅读时长加成'),
|
||
('duration_unit_minutes', '10', '每多少分钟加一次分'),
|
||
('duration_points_per_unit', '2', '每单位时长加多少分'),
|
||
('duration_max_bonus', '20', '时长加成每日上限'),
|
||
('pages_enabled', 'true', '是否启用页数加成'),
|
||
('pages_per_point', '20', '每多少页加1分'),
|
||
('pages_max_bonus', '10', '页数加成每日上限'),
|
||
('max_daily_points', '50', '每日积分获取硬上限(含所有加成)')
|
||
""")
|
||
conn.commit()
|
||
|
||
|
||
# ============================================================================
|
||
# 积分配置数据类
|
||
# ============================================================================
|
||
|
||
@dataclass
|
||
class PointsConfig:
|
||
"""积分规则配置(从数据库读取,提供默认值)。"""
|
||
|
||
base_points: int = 10
|
||
streak_enabled: bool = True
|
||
streak_threshold_7: int = 5 # 连续7天额外 +5
|
||
streak_threshold_14: int = 10 # 连续14天额外 +10
|
||
streak_threshold_30: int = 20 # 连续30天额外 +20
|
||
duration_enabled: bool = True
|
||
duration_unit_minutes: int = 10 # 每10分钟
|
||
duration_points_per_unit: int = 2 # 得2分
|
||
duration_max_bonus: int = 20 # 每日上限
|
||
pages_enabled: bool = True
|
||
pages_per_point: int = 20 # 每20页1分
|
||
pages_max_bonus: int = 10 # 每日上限
|
||
max_daily_points: int = 50 # 每日硬上限
|
||
|
||
@classmethod
|
||
def load(cls) -> "PointsConfig":
|
||
"""
|
||
从数据库加载配置,合并默认值。
|
||
|
||
配置表中存在的 key 覆盖默认值,不存在的保留默认值。
|
||
"""
|
||
_ensure_config_table()
|
||
rows = get_connection().execute(
|
||
"SELECT key, value FROM points_config"
|
||
).fetchall()
|
||
db_config: Dict[str, str] = {r["key"]: r["value"] for r in rows}
|
||
|
||
def _get_int(key: str, default: int) -> int:
|
||
return int(db_config.get(key, str(default)))
|
||
|
||
def _get_bool(key: str, default: bool) -> bool:
|
||
val = db_config.get(key, str(default).lower())
|
||
return val.lower() in ("true", "1", "yes")
|
||
|
||
return cls(
|
||
base_points=_get_int("base_points", 10),
|
||
streak_enabled=_get_bool("streak_enabled", True),
|
||
streak_threshold_7=_get_int("streak_threshold_7", 5),
|
||
streak_threshold_14=_get_int("streak_threshold_14", 10),
|
||
streak_threshold_30=_get_int("streak_threshold_30", 20),
|
||
duration_enabled=_get_bool("duration_enabled", True),
|
||
duration_unit_minutes=_get_int("duration_unit_minutes", 10),
|
||
duration_points_per_unit=_get_int("duration_points_per_unit", 2),
|
||
duration_max_bonus=_get_int("duration_max_bonus", 20),
|
||
pages_enabled=_get_bool("pages_enabled", True),
|
||
pages_per_point=_get_int("pages_per_point", 20),
|
||
pages_max_bonus=_get_int("pages_max_bonus", 10),
|
||
max_daily_points=_get_int("max_daily_points", 50),
|
||
)
|
||
|
||
def save(self) -> None:
|
||
"""将当前配置持久化到数据库。"""
|
||
_ensure_config_table()
|
||
|
||
fields = {
|
||
"base_points": str(self.base_points),
|
||
"streak_enabled": str(self.streak_enabled).lower(),
|
||
"streak_threshold_7": str(self.streak_threshold_7),
|
||
"streak_threshold_14": str(self.streak_threshold_14),
|
||
"streak_threshold_30": str(self.streak_threshold_30),
|
||
"duration_enabled": str(self.duration_enabled).lower(),
|
||
"duration_unit_minutes": str(self.duration_unit_minutes),
|
||
"duration_points_per_unit": str(self.duration_points_per_unit),
|
||
"duration_max_bonus": str(self.duration_max_bonus),
|
||
"pages_enabled": str(self.pages_enabled).lower(),
|
||
"pages_per_point": str(self.pages_per_point),
|
||
"pages_max_bonus": str(self.pages_max_bonus),
|
||
"max_daily_points": str(self.max_daily_points),
|
||
}
|
||
|
||
with transaction() as conn:
|
||
for key, value in fields.items():
|
||
conn.execute(
|
||
"""INSERT INTO points_config (key, value, updated_at)
|
||
VALUES (?, ?, datetime('now', 'localtime'))
|
||
ON CONFLICT(key) DO UPDATE SET
|
||
value = excluded.value,
|
||
updated_at = excluded.updated_at""",
|
||
(key, value),
|
||
)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""导出为字典,供前端展示。"""
|
||
return {
|
||
"base_points": self.base_points,
|
||
"streak_enabled": self.streak_enabled,
|
||
"streak_threshold_7": self.streak_threshold_7,
|
||
"streak_threshold_14": self.streak_threshold_14,
|
||
"streak_threshold_30": self.streak_threshold_30,
|
||
"duration_enabled": self.duration_enabled,
|
||
"duration_unit_minutes": self.duration_unit_minutes,
|
||
"duration_points_per_unit": self.duration_points_per_unit,
|
||
"duration_max_bonus": self.duration_max_bonus,
|
||
"pages_enabled": self.pages_enabled,
|
||
"pages_per_point": self.pages_per_point,
|
||
"pages_max_bonus": self.pages_max_bonus,
|
||
"max_daily_points": self.max_daily_points,
|
||
}
|