feat: add cumulative checkin tracking mode for goals

Goals can now choose between milestone-based progress (existing) and
cumulative checkin-based progress (new). Cumulative mode supports
cross-unit conversion (e.g. kcal → g fat) via a configurable
conversion rate. New GoalCheckin model stores daily inputs; progress
auto-recalculates on every checkin C/U/D. Backup import/export covers
the new table. Frontend GoalDialog, GoalDetailPage and GoalPage cards
adapt to show cumulative progress or milestone progress based on
track_type.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
祀梦
2026-05-18 23:00:24 +08:00
parent 4ee1e39454
commit 4ce7de48c4
12 changed files with 529 additions and 20 deletions

View File

@@ -4,7 +4,7 @@ from app.models.tag import Tag, task_tags
from app.models.user_settings import UserSettings
from app.models.habit import HabitGroup, Habit, HabitCheckin
from app.models.anniversary import AnniversaryCategory, Anniversary
from app.models.goal import Goal, GoalStep, GoalReview, goal_tasks
from app.models.goal import Goal, GoalStep, GoalReview, GoalCheckin, goal_tasks
from app.models.sync_settings import SyncSettings
from app.models.certificate import Certificate, CertificateCategory
@@ -12,7 +12,7 @@ __all__ = [
"Task", "Category", "Tag", "task_tags", "UserSettings",
"HabitGroup", "Habit", "HabitCheckin",
"AnniversaryCategory", "Anniversary",
"Goal", "GoalStep", "GoalReview", "goal_tasks",
"Goal", "GoalStep", "GoalReview", "GoalCheckin", "goal_tasks",
"SyncSettings",
"Certificate", "CertificateCategory",
]

View File

@@ -1,5 +1,5 @@
import uuid as _uuid
from sqlalchemy import Column, Integer, String, Text, Date, DateTime, Boolean, ForeignKey, Table, desc
from sqlalchemy import Column, Integer, String, Text, Date, DateTime, Boolean, Float, ForeignKey, Table, desc
from sqlalchemy.orm import relationship
from app.database import Base
from app.utils.datetime import utcnow
@@ -23,7 +23,13 @@ class Goal(Base):
title = Column(String(200), nullable=False)
description = Column(Text, nullable=True)
status = Column(String(20), default="active") # active/paused/completed/abandoned
progress = Column(Integer, default=0) # 0-100从里程碑自动计算
track_type = Column(String(20), default="milestone") # "milestone" | "cumulative"
progress = Column(Integer, default=0) # 0-100里程碑模式从里程碑自动计算累计模式从打卡汇总计算
target_value = Column(Float, nullable=True) # 累计模式:目标值(目标单位)
target_unit = Column(String(20), nullable=True) # 累计模式:目标单位,如 "g"、"kg"
input_unit = Column(String(20), nullable=True) # 累计模式:打卡输入单位,如 "kcal"、"次"
conversion_rate = Column(Float, default=1.0) # 累计模式:换算率(多少输入单位 = 1 目标单位)
current_value = Column(Float, default=0) # 累计模式:累计打卡值(输入单位)
target_date = Column(Date, nullable=True)
completed_at = Column(DateTime, nullable=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
@@ -47,6 +53,11 @@ class Goal(Base):
cascade="all, delete-orphan",
order_by=lambda: desc(GoalReview.created_at),
)
checkins = relationship(
"GoalCheckin", back_populates="goal",
cascade="all, delete-orphan",
order_by=lambda: desc(GoalCheckin.checkin_date),
)
tasks = relationship("Task", secondary=goal_tasks, back_populates="goals")
@@ -88,3 +99,21 @@ class GoalReview(Base):
# 关联关系
goal = relationship("Goal", back_populates="reviews")
class GoalCheckin(Base):
"""目标累计打卡记录模型"""
__tablename__ = "goal_checkins"
id = Column(Integer, primary_key=True, index=True)
uuid = Column(String(36), default=lambda: str(_uuid.uuid4()), unique=True, index=True)
goal_id = Column(Integer, ForeignKey("goals.id"), nullable=False)
value = Column(Float, nullable=False)
note = Column(Text, nullable=True)
checkin_date = Column(Date, nullable=False)
is_deleted = Column(Boolean, default=False)
sync_version = Column(Integer, default=1)
created_at = Column(DateTime, default=utcnow)
# 关联关系
goal = relationship("Goal", back_populates="checkins")