duidui_fiber/internal/camp/dao/progress_dao.go
2026-03-27 10:34:03 +08:00

918 lines
30 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dao
import (
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"dd_fiber_api/internal/camp"
"dd_fiber_api/pkg/database"
"dd_fiber_api/pkg/utils"
"github.com/didi/gendry/builder"
)
// ProgressDAO 用户进度数据访问对象
type ProgressDAO struct {
client *database.MySQLClient
}
// NewProgressDAO 创建进度DAO实例
func NewProgressDAO(client *database.MySQLClient) *ProgressDAO {
return &ProgressDAO{
client: client,
}
}
// marshalAnswerImages 将进度答案序列化为 answer_images JSON申论题为 [][]string否则 []string
func marshalAnswerImages(progress *camp.UserProgress) ([]byte, error) {
if len(progress.EssayAnswerImages) > 0 {
return json.Marshal(progress.EssayAnswerImages)
}
return json.Marshal(progress.AnswerImages)
}
// unmarshalAnswerImages 从 answer_images JSON 反序列化:若为二维数组则填 EssayAnswerImages否则填 AnswerImages
func unmarshalAnswerImages(raw string, progress *camp.UserProgress) {
if raw == "" {
return
}
// 先尝试按申论格式 [["url"],["url"]] 解析
var essay [][]string
if err := json.Unmarshal([]byte(raw), &essay); err == nil && len(essay) > 0 {
progress.EssayAnswerImages = essay
return
}
// 再按扁平格式 ["url","url"] 解析
var flat []string
if err := json.Unmarshal([]byte(raw), &flat); err == nil {
for _, img := range flat {
if strings.TrimSpace(img) != "" {
progress.AnswerImages = append(progress.AnswerImages, img)
}
}
}
}
// marshalEssayReviewStatuses 将申论每题审核状态序列化为 JSON 数组字符串
func marshalEssayReviewStatuses(st []string) (string, error) {
if len(st) == 0 {
return "", nil
}
b, err := json.Marshal(st)
if err != nil {
return "", err
}
return string(b), nil
}
// unmarshalEssayReviewStatuses 从 essay_review_statuses JSON 反序列化
func unmarshalEssayReviewStatuses(raw string) []string {
if raw == "" {
return nil
}
var st []string
if err := json.Unmarshal([]byte(raw), &st); err != nil {
return nil
}
return st
}
// Create 创建用户进度
func (d *ProgressDAO) Create(progress *camp.UserProgress) error {
table := "camp_user_progress"
// 通过 task_id 查询 camp_id、section_id
campID, sectionID, needReview, err := d.getTaskInfo(progress.TaskID)
if err != nil {
return fmt.Errorf("获取任务信息失败: %v", err)
}
progress.CampID = campID
progress.NeedReview = needReview
// 序列化审核图片数组
reviewImagesJSON, err := json.Marshal(progress.ReviewImages)
if err != nil {
return fmt.Errorf("序列化审核图片失败: %v", err)
}
answerImagesJSON, err := marshalAnswerImages(progress)
if err != nil {
return fmt.Errorf("序列化答案图片失败: %v", err)
}
essayReviewStatusesJSON, _ := marshalEssayReviewStatuses(progress.EssayReviewStatuses)
data := []map[string]any{
{
"id": progress.ID,
"user_id": progress.UserID,
"task_id": progress.TaskID,
"camp_id": campID,
"section_id": sectionID,
"is_completed": progress.IsCompleted,
"completed_at": normalizeCompletedAt(progress.CompletedAt),
"review_status": convertReviewStatus(progress.ReviewStatus),
"review_comment": progress.ReviewComment,
"review_images": string(reviewImagesJSON),
"answer_images": string(answerImagesJSON),
"essay_review_statuses": essayReviewStatusesJSON,
"objective_best_correct_count": progress.ObjectiveBestCorrectCount,
"objective_best_total_count": progress.ObjectiveBestTotalCount,
},
}
cond, vals, err := builder.BuildInsert(table, data)
if err != nil {
return fmt.Errorf("构建插入语句失败: %v", err)
}
_, err = d.client.DB.Exec(cond, vals...)
if err != nil {
return fmt.Errorf("创建用户进度失败: %v", err)
}
return nil
}
// Update 更新用户进度(使用 UPSERT 逻辑)
func (d *ProgressDAO) Update(progress *camp.UserProgress) error {
table := "camp_user_progress"
// 先检查是否存在
checkWhere := map[string]any{
"user_id": progress.UserID,
"task_id": progress.TaskID,
}
checkCond, checkVals, err := builder.BuildSelect(table, checkWhere, []string{"id"})
if err != nil {
return fmt.Errorf("构建查询语句失败: %v", err)
}
var existingID string
checkErr := d.client.DB.QueryRow(checkCond, checkVals...).Scan(&existingID)
// 通过 task_id 查询 camp_id、section_id
campID, sectionID, needReview, err := d.getTaskInfo(progress.TaskID)
if err != nil {
return fmt.Errorf("获取任务信息失败: %v", err)
}
progress.CampID = campID
progress.NeedReview = needReview
// 序列化审核图片数组
reviewImagesJSON, err := json.Marshal(progress.ReviewImages)
if err != nil {
return fmt.Errorf("序列化审核图片失败: %v", err)
}
answerImagesJSON, err := marshalAnswerImages(progress)
if err != nil {
return fmt.Errorf("序列化答案图片失败: %v", err)
}
essayReviewStatusesJSON, _ := marshalEssayReviewStatuses(progress.EssayReviewStatuses)
if checkErr == sql.ErrNoRows {
// 不存在,执行插入
return d.Create(progress)
} else if checkErr != nil {
// 查询出错
return fmt.Errorf("检查用户进度是否存在失败: %v", checkErr)
} else {
// 已存在,执行更新
where := map[string]any{
"user_id": progress.UserID,
"task_id": progress.TaskID,
}
data := map[string]any{
"is_completed": progress.IsCompleted,
"completed_at": normalizeCompletedAt(progress.CompletedAt),
"review_status": convertReviewStatus(progress.ReviewStatus),
"review_comment": progress.ReviewComment,
"review_images": string(reviewImagesJSON),
"answer_images": string(answerImagesJSON),
"essay_review_statuses": essayReviewStatusesJSON,
"camp_id": campID,
"section_id": sectionID,
"objective_best_correct_count": progress.ObjectiveBestCorrectCount,
"objective_best_total_count": progress.ObjectiveBestTotalCount,
}
cond, vals, err := builder.BuildUpdate(table, where, data)
if err != nil {
return fmt.Errorf("构建更新语句失败: %v", err)
}
_, err = d.client.DB.Exec(cond, vals...)
if err != nil {
return fmt.Errorf("更新用户进度失败: %v", err)
}
}
return nil
}
// GetByUserAndTask 根据用户ID和任务ID获取进度
func (d *ProgressDAO) GetByUserAndTask(userID, taskID string) (*camp.UserProgress, error) {
table := "camp_user_progress"
where := map[string]any{
"user_id": userID,
"task_id": taskID,
}
selectFields := []string{"id", "user_id", "task_id", "camp_id", "is_completed", "completed_at", "review_status", "review_comment", "review_images", "answer_images", "essay_review_statuses", "objective_best_correct_count", "objective_best_total_count"}
cond, vals, err := builder.BuildSelect(table, where, selectFields)
if err != nil {
return nil, fmt.Errorf("构建查询失败: %v", err)
}
var (
id string
userIDResult string
taskIDResult string
campIDResult string
isCompleted bool
completedAtTs sql.NullTime
reviewStatusStr string
reviewComment sql.NullString
reviewImagesJSON sql.NullString
answerImagesJSON sql.NullString
essayReviewStatusesJSON sql.NullString
objectiveBestCorrectCount sql.NullInt64
objectiveBestTotalCount sql.NullInt64
)
err = d.client.DB.QueryRow(cond, vals...).Scan(
&id,
&userIDResult,
&taskIDResult,
&campIDResult,
&isCompleted,
&completedAtTs,
&reviewStatusStr,
&reviewComment,
&reviewImagesJSON,
&answerImagesJSON,
&essayReviewStatusesJSON,
&objectiveBestCorrectCount,
&objectiveBestTotalCount,
)
if err == sql.ErrNoRows {
// 没有进度记录是正常情况,返回 nil, nil 而不是错误
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("查询用户进度失败: %v", err)
}
progress := &camp.UserProgress{
ID: id,
UserID: userIDResult,
TaskID: taskIDResult,
CampID: campIDResult,
IsCompleted: isCompleted,
CompletedAt: utils.FormatNullTimeToStd(completedAtTs),
ReviewStatus: parseReviewStatus(reviewStatusStr),
ReviewComment: reviewComment.String,
ObjectiveBestCorrectCount: int(objectiveBestCorrectCount.Int64),
ObjectiveBestTotalCount: int(objectiveBestTotalCount.Int64),
}
// 反序列化审核图片数组(过滤空字符串)
if reviewImagesJSON.Valid && reviewImagesJSON.String != "" {
var reviewImages []string
if err := json.Unmarshal([]byte(reviewImagesJSON.String), &reviewImages); err == nil {
filtered := make([]string, 0, len(reviewImages))
for _, img := range reviewImages {
if strings.TrimSpace(img) != "" {
filtered = append(filtered, img)
}
}
progress.ReviewImages = filtered
}
}
unmarshalAnswerImages(answerImagesJSON.String, progress)
if essayReviewStatusesJSON.Valid && essayReviewStatusesJSON.String != "" {
progress.EssayReviewStatuses = unmarshalEssayReviewStatuses(essayReviewStatusesJSON.String)
}
if _, _, needReview, err := d.getTaskInfo(taskIDResult); err == nil {
progress.NeedReview = needReview
}
return progress, nil
}
// List 列出用户进度支持按用户ID、用户关键词、任务ID筛选
// userKeyword 同时支持用户ID 模糊匹配user_id LIKE、手机号匹配从 users 表解析 phone/mobile 再筛进度)
func (d *ProgressDAO) List(userID, userKeyword, taskID, sectionID, campID, reviewStatus string, page, pageSize int) ([]*camp.UserProgress, int, error) {
table := "camp_user_progress"
// 构建查询条件
where := map[string]any{}
if userKeyword != "" {
// 用户关键词:尝试从 users 表按手机号解析出 user_id 列表(表不存在则忽略)
phoneUserIDs, _ := d.findUserIDsByPhoneKeyword(userKeyword)
if len(phoneUserIDs) > 0 {
return d.listWithUserKeywordOrPhone(table, userKeyword, phoneUserIDs, taskID, sectionID, campID, reviewStatus, page, pageSize)
}
where["user_id like"] = "%" + userKeyword + "%"
} else if userID != "" {
where["user_id"] = userID
}
if taskID != "" {
where["task_id"] = taskID
}
if sectionID != "" {
where["section_id"] = sectionID
}
if campID != "" {
where["camp_id"] = campID
}
if reviewStatus != "" {
where["review_status"] = reviewStatus
}
// 查询总数
countFields := []string{"COUNT(*) as total"}
countCond, countVals, err := builder.BuildSelect(table, where, countFields)
if err != nil {
return nil, 0, fmt.Errorf("构建统计查询失败: %v", err)
}
var total int
err = d.client.DB.QueryRow(countCond, countVals...).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("查询用户进度总数失败: %v", err)
}
// 查询数据(含 section_id 供管理端展示所属小节)
selectFields := []string{"id", "user_id", "task_id", "camp_id", "section_id", "is_completed", "completed_at", "review_status", "review_comment", "review_images", "answer_images", "essay_review_statuses", "objective_best_correct_count", "objective_best_total_count"}
cond, vals, err := builder.BuildSelect(table, where, selectFields)
if err != nil {
return nil, 0, fmt.Errorf("构建查询失败: %v", err)
}
// 按时间顺序排序(完成时间或创建时间倒序)
offset := (page - 1) * pageSize
cond += " ORDER BY COALESCE(completed_at, created_at) DESC LIMIT ? OFFSET ?"
vals = append(vals, pageSize, offset)
rows, err := d.client.DB.Query(cond, vals...)
if err != nil {
return nil, 0, fmt.Errorf("查询用户进度列表失败: %v", err)
}
defer rows.Close()
progressList := make([]*camp.UserProgress, 0)
taskNeedReviewCache := make(map[string]bool)
taskIDs := make(map[string]struct{})
for rows.Next() {
var (
id string
userIDResult string
taskIDResult string
campIDResult string
sectionIDResult string
isCompleted bool
completedAtTs sql.NullTime
reviewStatusStr string
reviewComment sql.NullString
reviewImagesJSON sql.NullString
answerImagesJSON sql.NullString
essayReviewStatusesJSON sql.NullString
objectiveBestCorrectCount sql.NullInt64
objectiveBestTotalCount sql.NullInt64
)
err := rows.Scan(
&id,
&userIDResult,
&taskIDResult,
&campIDResult,
&sectionIDResult,
&isCompleted,
&completedAtTs,
&reviewStatusStr,
&reviewComment,
&reviewImagesJSON,
&answerImagesJSON,
&essayReviewStatusesJSON,
&objectiveBestCorrectCount,
&objectiveBestTotalCount,
)
if err != nil {
continue
}
progress := &camp.UserProgress{
ID: id,
UserID: userIDResult,
TaskID: taskIDResult,
CampID: campIDResult,
SectionID: sectionIDResult,
IsCompleted: isCompleted,
CompletedAt: utils.FormatNullTimeToStd(completedAtTs),
ReviewStatus: parseReviewStatus(reviewStatusStr),
ReviewComment: reviewComment.String,
ObjectiveBestCorrectCount: int(objectiveBestCorrectCount.Int64),
ObjectiveBestTotalCount: int(objectiveBestTotalCount.Int64),
}
taskIDs[taskIDResult] = struct{}{}
// 反序列化审核图片数组(过滤空字符串)
if reviewImagesJSON.Valid && reviewImagesJSON.String != "" {
var reviewImages []string
if err := json.Unmarshal([]byte(reviewImagesJSON.String), &reviewImages); err == nil {
filtered := make([]string, 0, len(reviewImages))
for _, img := range reviewImages {
if strings.TrimSpace(img) != "" {
filtered = append(filtered, img)
}
}
progress.ReviewImages = filtered
}
}
unmarshalAnswerImages(answerImagesJSON.String, progress)
if essayReviewStatusesJSON.Valid && essayReviewStatusesJSON.String != "" {
progress.EssayReviewStatuses = unmarshalEssayReviewStatuses(essayReviewStatusesJSON.String)
}
progressList = append(progressList, progress)
}
if err = rows.Err(); err != nil {
return nil, 0, fmt.Errorf("遍历用户进度数据失败: %v", err)
}
// 批量查询任务是否需要审核
for taskID := range taskIDs {
_, _, needReview, err := d.getTaskInfo(taskID)
if err == nil {
taskNeedReviewCache[taskID] = needReview
}
}
for _, progress := range progressList {
if val, ok := taskNeedReviewCache[progress.TaskID]; ok {
progress.NeedReview = val
}
}
return progressList, total, nil
}
// ListByUserIDsAndCamp 按用户 ID 列表与打卡营查询进度(不分页,用于管理端矩阵:一页用户的所有进度)
func (d *ProgressDAO) ListByUserIDsAndCamp(userIDs []string, campID, sectionID, taskID, reviewStatus string) ([]*camp.UserProgress, error) {
if len(userIDs) == 0 {
return nil, nil
}
table := "camp_user_progress"
placeholders := strings.Repeat("?,", len(userIDs))
placeholders = placeholders[:len(placeholders)-1]
query := `SELECT id, user_id, task_id, camp_id, section_id, is_completed, completed_at, review_status, review_comment, review_images, answer_images, essay_review_statuses, objective_best_correct_count, objective_best_total_count FROM ` + table + ` WHERE user_id IN (` + placeholders + `) AND camp_id = ?`
args := make([]any, 0, len(userIDs)+4)
for _, u := range userIDs {
args = append(args, u)
}
args = append(args, campID)
if sectionID != "" {
query += ` AND section_id = ?`
args = append(args, sectionID)
}
if taskID != "" {
query += ` AND task_id = ?`
args = append(args, taskID)
}
if reviewStatus != "" {
query += ` AND review_status = ?`
args = append(args, reviewStatus)
}
query += ` ORDER BY section_id, task_id`
rows, err := d.client.DB.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("查询进度失败: %v", err)
}
defer rows.Close()
progressList, taskNeedReviewCache, _, err := d.scanProgressRows(rows)
if err != nil {
return nil, err
}
for _, progress := range progressList {
if val, ok := taskNeedReviewCache[progress.TaskID]; ok {
progress.NeedReview = val
}
}
return progressList, nil
}
// findUserIDsByPhoneKeyword 根据手机号关键词从 users 表查询 user_id 列表(表需含 id、phone 或 mobile 列)
// 若 users 表不存在或查询失败,返回 nil, nil调用方仅用 user_id LIKE 即可
func (d *ProgressDAO) findUserIDsByPhoneKeyword(keyword string) ([]string, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return nil, nil
}
// 兼容表名为 users列名为 phone 或 mobile常见 C 端用户表)
query := `SELECT id FROM users WHERE (phone LIKE ? OR mobile LIKE ?) LIMIT 500`
rows, err := d.client.DB.Query(query, "%"+keyword+"%", "%"+keyword+"%")
if err != nil {
return nil, nil // 表不存在或列名不同时不报错,让上层只用 user_id LIKE
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
continue
}
if id != "" {
ids = append(ids, id)
}
}
return ids, nil
}
// listWithUserKeywordOrPhone 使用 (user_id LIKE ? OR user_id IN (...)) 条件查询进度列表与总数
func (d *ProgressDAO) listWithUserKeywordOrPhone(table, userKeyword string, phoneUserIDs []string, taskID, sectionID, campID, reviewStatus string, page, pageSize int) ([]*camp.UserProgress, int, error) {
inPlaceholders := strings.Repeat("?,", len(phoneUserIDs))
if len(inPlaceholders) > 0 {
inPlaceholders = inPlaceholders[:len(inPlaceholders)-1]
}
userCond := "(user_id LIKE ? OR user_id IN (" + inPlaceholders + "))"
var conditions []string
var vals []interface{}
vals = append(vals, "%"+userKeyword+"%")
for _, id := range phoneUserIDs {
vals = append(vals, id)
}
conditions = append(conditions, userCond)
if taskID != "" {
conditions = append(conditions, "task_id = ?")
vals = append(vals, taskID)
}
if sectionID != "" {
conditions = append(conditions, "section_id = ?")
vals = append(vals, sectionID)
}
if campID != "" {
conditions = append(conditions, "camp_id = ?")
vals = append(vals, campID)
}
if reviewStatus != "" {
conditions = append(conditions, "review_status = ?")
vals = append(vals, reviewStatus)
}
whereSQL := strings.Join(conditions, " AND ")
countQuery := "SELECT COUNT(*) as total FROM " + table + " WHERE " + whereSQL
var total int
if err := d.client.DB.QueryRow(countQuery, vals...).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("查询用户进度总数失败: %v", err)
}
selectFields := "id, user_id, task_id, camp_id, section_id, is_completed, completed_at, review_status, review_comment, review_images, answer_images, essay_review_statuses, objective_best_correct_count, objective_best_total_count"
offset := (page - 1) * pageSize
listQuery := "SELECT " + selectFields + " FROM " + table + " WHERE " + whereSQL + " ORDER BY COALESCE(completed_at, created_at) DESC LIMIT ? OFFSET ?"
listVals := append(vals, pageSize, offset)
rows, err := d.client.DB.Query(listQuery, listVals...)
if err != nil {
return nil, 0, fmt.Errorf("查询用户进度列表失败: %v", err)
}
defer rows.Close()
progressList, taskNeedReviewCache, taskIDs, err := d.scanProgressRows(rows)
if err != nil {
return nil, 0, err
}
for taskID := range taskIDs {
_, _, needReview, err := d.getTaskInfo(taskID)
if err != nil {
continue
}
taskNeedReviewCache[taskID] = needReview
}
for _, progress := range progressList {
if val, ok := taskNeedReviewCache[progress.TaskID]; ok {
progress.NeedReview = val
}
}
return progressList, total, nil
}
// scanProgressRows 将 progress 查询结果扫描为列表,并返回 taskNeedReviewCache、taskIDs
func (d *ProgressDAO) scanProgressRows(rows *sql.Rows) ([]*camp.UserProgress, map[string]bool, map[string]struct{}, error) {
progressList := make([]*camp.UserProgress, 0)
taskNeedReviewCache := make(map[string]bool)
taskIDs := make(map[string]struct{})
for rows.Next() {
var (
id string
userIDResult string
taskIDResult string
campIDResult string
sectionIDResult string
isCompleted bool
completedAtTs sql.NullTime
reviewStatusStr string
reviewComment sql.NullString
reviewImagesJSON sql.NullString
answerImagesJSON sql.NullString
essayReviewStatusesJSON sql.NullString
objectiveBestCorrectCount sql.NullInt64
objectiveBestTotalCount sql.NullInt64
)
err := rows.Scan(
&id,
&userIDResult,
&taskIDResult,
&campIDResult,
&sectionIDResult,
&isCompleted,
&completedAtTs,
&reviewStatusStr,
&reviewComment,
&reviewImagesJSON,
&answerImagesJSON,
&essayReviewStatusesJSON,
&objectiveBestCorrectCount,
&objectiveBestTotalCount,
)
if err != nil {
continue
}
progress := &camp.UserProgress{
ID: id,
UserID: userIDResult,
TaskID: taskIDResult,
CampID: campIDResult,
SectionID: sectionIDResult,
IsCompleted: isCompleted,
CompletedAt: utils.FormatNullTimeToStd(completedAtTs),
ReviewStatus: parseReviewStatus(reviewStatusStr),
ReviewComment: reviewComment.String,
ObjectiveBestCorrectCount: int(objectiveBestCorrectCount.Int64),
ObjectiveBestTotalCount: int(objectiveBestTotalCount.Int64),
}
taskIDs[taskIDResult] = struct{}{}
if reviewImagesJSON.Valid && reviewImagesJSON.String != "" {
var reviewImages []string
if err := json.Unmarshal([]byte(reviewImagesJSON.String), &reviewImages); err == nil {
filtered := make([]string, 0, len(reviewImages))
for _, img := range reviewImages {
if strings.TrimSpace(img) != "" {
filtered = append(filtered, img)
}
}
progress.ReviewImages = filtered
}
}
unmarshalAnswerImages(answerImagesJSON.String, progress)
if essayReviewStatusesJSON.Valid && essayReviewStatusesJSON.String != "" {
progress.EssayReviewStatuses = unmarshalEssayReviewStatuses(essayReviewStatusesJSON.String)
}
progressList = append(progressList, progress)
}
if err := rows.Err(); err != nil {
return nil, nil, nil, fmt.Errorf("遍历用户进度数据失败: %v", err)
}
return progressList, taskNeedReviewCache, taskIDs, nil
}
// DeleteByUserAndCamp 删除用户在某打卡营下的所有进度
func (d *ProgressDAO) DeleteByUserAndCamp(userID, campID string) (int64, error) {
query := `DELETE FROM camp_user_progress WHERE user_id = ? AND camp_id = ?`
res, err := d.client.DB.Exec(query, userID, campID)
if err != nil {
return 0, fmt.Errorf("删除用户营内进度失败: %v", err)
}
rows, _ := res.RowsAffected()
return rows, nil
}
// DeleteByUserAndTask 删除用户在某任务下的进度记录
func (d *ProgressDAO) DeleteByUserAndTask(userID, taskID string) error {
query := `DELETE FROM camp_user_progress WHERE user_id = ? AND task_id = ?`
_, err := d.client.DB.Exec(query, userID, taskID)
if err != nil {
return fmt.Errorf("删除用户任务进度失败: %v", err)
}
return nil
}
// HasProgressForTask 判断任务是否已有用户进度
func (d *ProgressDAO) HasProgressForTask(taskID string) (bool, error) {
query := "SELECT 1 FROM camp_user_progress WHERE task_id = ? LIMIT 1"
var dummy int
err := d.client.DB.QueryRow(query, taskID).Scan(&dummy)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, fmt.Errorf("查询任务进度失败: %v", err)
}
return true, nil
}
// HasProgressForSection 判断小节是否已有用户进度
func (d *ProgressDAO) HasProgressForSection(sectionID string) (bool, error) {
query := "SELECT 1 FROM camp_user_progress WHERE section_id = ? LIMIT 1"
var dummy int
err := d.client.DB.QueryRow(query, sectionID).Scan(&dummy)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, fmt.Errorf("查询小节进度失败: %v", err)
}
return true, nil
}
// HasProgressForCamp 判断打卡营是否已有用户进度
func (d *ProgressDAO) HasProgressForCamp(campID string) (bool, error) {
query := `SELECT 1 FROM camp_user_progress WHERE camp_id = ? LIMIT 1`
var dummy int
err := d.client.DB.QueryRow(query, campID).Scan(&dummy)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, fmt.Errorf("查询打卡营进度失败: %v", err)
}
return true, nil
}
// CountCompletedBySection 统计用户在小节下已完成的任务数
func (d *ProgressDAO) CountCompletedBySection(userID, sectionID string) (int, error) {
query := `SELECT COUNT(*) FROM camp_user_progress
WHERE user_id = ? AND section_id = ? AND is_completed = 1`
var count int
err := d.client.DB.QueryRow(query, userID, sectionID).Scan(&count)
if err != nil {
return 0, fmt.Errorf("统计用户小节完成任务数失败: %v", err)
}
return count, nil
}
// GetUserSectionCompletedAt 获取用户在某小节「全部任务完成」的时间(该小节下已完成任务的 completed_at 最大值)
// 用于时间间隔解锁的起点:间隔从「上一小节完成时刻」开始计算
func (d *ProgressDAO) GetUserSectionCompletedAt(userID, sectionID string) (*time.Time, error) {
query := `SELECT MAX(completed_at) FROM camp_user_progress
WHERE user_id = ? AND section_id = ? AND is_completed = 1 AND completed_at IS NOT NULL`
var completedAt sql.NullTime
err := d.client.DB.QueryRow(query, userID, sectionID).Scan(&completedAt)
if err != nil {
return nil, fmt.Errorf("获取小节完成时间失败: %v", err)
}
if !completedAt.Valid {
return nil, nil
}
t := completedAt.Time
return &t, nil
}
// GetUserSectionStartedAt 获取用户在某小节「首次产生进度」的时间(该小节下任意进度记录的 created_at 最小值)
// 用于时间间隔解锁的起点:间隔从「上一小节开启时」开始计算(用户第一次进入/开始该小节的时间)
func (d *ProgressDAO) GetUserSectionStartedAt(userID, sectionID string) (*time.Time, error) {
query := `SELECT MIN(created_at) FROM camp_user_progress
WHERE user_id = ? AND section_id = ?`
var createdAt sql.NullTime
err := d.client.DB.QueryRow(query, userID, sectionID).Scan(&createdAt)
if err != nil {
return nil, fmt.Errorf("获取小节开启时间失败: %v", err)
}
if !createdAt.Valid {
return nil, nil
}
t := createdAt.Time
return &t, nil
}
// CountTasksAndCompletedByCamp 统计打卡营下的总任务数和用户已完成的任务数
// 用于轻量计算打卡营整体完成状态,避免加载全部小节/任务数据
func (d *ProgressDAO) CountTasksAndCompletedByCamp(userID, campID string) (totalTasks int, completedTasks int, err error) {
// 1. 查询打卡营下的总任务数(关联 camp_sections 确保只统计未删除的小节下的任务)
totalQuery := `SELECT COUNT(*) FROM camp_tasks t
INNER JOIN camp_sections s ON t.section_id = s.id AND (s.deleted_at IS NULL OR s.deleted_at = '0001-01-01 00:00:00')
WHERE t.camp_id = ? AND (t.deleted_at IS NULL OR t.deleted_at = '0001-01-01 00:00:00')`
err = d.client.DB.QueryRow(totalQuery, campID).Scan(&totalTasks)
if err != nil {
return 0, 0, fmt.Errorf("统计打卡营总任务数失败: %v", err)
}
// 2. 查询用户在该打卡营下已完成的任务数
completedQuery := `SELECT COUNT(*) FROM camp_user_progress
WHERE user_id = ? AND camp_id = ? AND is_completed = 1`
err = d.client.DB.QueryRow(completedQuery, userID, campID).Scan(&completedTasks)
if err != nil {
return totalTasks, 0, fmt.Errorf("统计用户已完成任务数失败: %v", err)
}
return totalTasks, completedTasks, nil
}
// ========== 辅助函数 ==========
// convertReviewStatus 将 ReviewStatus 转换为数据库 ENUM 字符串
func convertReviewStatus(status camp.ReviewStatus) string {
switch status {
case camp.ReviewStatusApproved:
return "APPROVED"
case camp.ReviewStatusRejected:
return "REJECTED"
default:
return "PENDING"
}
}
// parseReviewStatus 将数据库 ENUM 字符串转换为 ReviewStatus
func parseReviewStatus(statusStr string) camp.ReviewStatus {
switch statusStr {
case "APPROVED":
return camp.ReviewStatusApproved
case "REJECTED":
return camp.ReviewStatusRejected
default:
return camp.ReviewStatusPending
}
}
// getTaskInfo 通过任务ID获取打卡营ID、小节ID与审核标记
func (d *ProgressDAO) getTaskInfo(taskID string) (string, string, bool, error) {
var (
campID string
sectionID string
taskTypeStr string
conditionJSON sql.NullString
)
query := "SELECT camp_id, section_id, task_type, `condition` FROM camp_tasks WHERE id = ? AND deleted_at IS NULL"
err := d.client.DB.QueryRow(query, taskID).Scan(&campID, &sectionID, &taskTypeStr, &conditionJSON)
if err == sql.ErrNoRows {
return "", "", false, fmt.Errorf("任务不存在: %s", taskID)
}
if err != nil {
return "", "", false, err
}
needReview := false
if conditionJSON.Valid && conditionJSON.String != "" {
var raw map[string]any
if err := json.Unmarshal([]byte(conditionJSON.String), &raw); err == nil {
if val, ok := raw["need_review"]; ok {
if boolVal, ok := val.(bool); ok {
needReview = boolVal
}
} else if subjectiveRaw, ok := raw["subjective"].(map[string]any); ok {
if val, ok := subjectiveRaw["need_review"]; ok {
if boolVal, ok := val.(bool); ok {
needReview = boolVal
}
}
} else if essayRaw, ok := raw["essay"].(map[string]any); ok {
if val, ok := essayRaw["need_review"]; ok {
if boolVal, ok := val.(bool); ok {
needReview = boolVal
}
}
}
}
}
return campID, sectionID, needReview, nil
}
// normalizeCompletedAt 将 completed_at 字符串统一转换为数据库可接受的时间值
func normalizeCompletedAt(s string) any {
if s == "" {
return nil
}
// 尝试 Unix 秒
if sec, err := strconv.ParseInt(s, 10, 64); err == nil {
return time.Unix(sec, 0).In(time.Local).Format("2006-01-02 15:04:05")
}
// 尝试 RFC3339 / RFC3339Nano
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t.In(time.Local).Format("2006-01-02 15:04:05")
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return t.In(time.Local).Format("2006-01-02 15:04:05")
}
// 尝试已是标准格式
if _, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local); err == nil {
return s
}
// 无法解析,回退为 NULL
return nil
}