package dao import ( "database/sql" "encoding/json" "fmt" "strings" "time" "dd_fiber_api/internal/camp" "dd_fiber_api/pkg/database" "dd_fiber_api/pkg/utils" "github.com/didi/gendry/builder" ) // TaskDAO 任务数据访问对象 type TaskDAO struct { client *database.MySQLClient } // NewTaskDAO 创建任务DAO实例 func NewTaskDAO(client *database.MySQLClient) *TaskDAO { return &TaskDAO{ client: client, } } // Create 创建任务 func (d *TaskDAO) Create(task *camp.Task) error { table := "camp_tasks" // Content 和 Condition 已经是 JSON 格式,直接使用 contentJSON := string(task.Content) if contentJSON == "" { contentJSON = "{}" } conditionJSON := string(task.Condition) if conditionJSON == "" { conditionJSON = "{}" } // 使用反引号包裹 condition 字段名(避免 MySQL 保留字冲突) data := []map[string]any{ { "id": task.ID, "camp_id": task.CampID, "section_id": task.SectionID, "task_type": convertTaskType(task.TaskType), "title": task.Title, "content": contentJSON, "`condition`": conditionJSON, // 反引号包裹保留字 "prerequisite_task_id": nullString(task.PrerequisiteTaskID), }, } 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 } // GetByID 根据ID获取任务 func (d *TaskDAO) GetByID(id string) (*camp.Task, error) { table := "camp_tasks" where := map[string]any{ "id": id, } selectFields := []string{"id", "camp_id", "section_id", "task_type", "title", "content", "`condition`", "prerequisite_task_id", "deleted_at"} cond, vals, err := builder.BuildSelect(table, where, selectFields) if err != nil { return nil, fmt.Errorf("构建查询失败: %v", err) } if strings.Contains(cond, "WHERE") { cond += " AND deleted_at IS NULL" } else { cond += " WHERE deleted_at IS NULL" } var ( taskID string campID string sectionID string taskTypeStr string title sql.NullString contentJSON sql.NullString conditionJSON sql.NullString prerequisiteTaskID sql.NullString deletedAt sql.NullTime ) err = d.client.DB.QueryRow(cond, vals...).Scan( &taskID, &campID, §ionID, &taskTypeStr, &title, &contentJSON, &conditionJSON, &prerequisiteTaskID, &deletedAt, ) if err == sql.ErrNoRows { return nil, fmt.Errorf("任务不存在: %s", id) } if err != nil { return nil, fmt.Errorf("查询任务失败: %v", err) } // 解析任务类型 taskType := parseTaskType(taskTypeStr) // 构建 Task 对象 task := &camp.Task{ ID: taskID, CampID: campID, SectionID: sectionID, TaskType: taskType, Title: title.String, PrerequisiteTaskID: prerequisiteTaskID.String, DeletedAt: utils.FormatNullTimeToStd(deletedAt), } // 处理 Content if contentJSON.Valid && contentJSON.String != "" { // 验证 JSON 格式 if json.Valid([]byte(contentJSON.String)) { task.Content = json.RawMessage(contentJSON.String) } else { task.Content = json.RawMessage("{}") } } else { task.Content = json.RawMessage("{}") } // 处理 Condition if conditionJSON.Valid && conditionJSON.String != "" { // 验证 JSON 格式 if json.Valid([]byte(conditionJSON.String)) { task.Condition = json.RawMessage(conditionJSON.String) } else { task.Condition = json.RawMessage("{}") } } else { task.Condition = json.RawMessage("{}") } return task, nil } // Update 更新任务 func (d *TaskDAO) Update(task *camp.Task) error { table := "camp_tasks" // 更新前检查是否存在 existsWhere := map[string]any{ "id": task.ID, } existsCond, existsVals, err := builder.BuildSelect(table, existsWhere, []string{"id"}) if err != nil { return fmt.Errorf("构建校验查询失败: %v", err) } existsCond += " AND deleted_at IS NULL" var dummyID string if err := d.client.DB.QueryRow(existsCond, existsVals...).Scan(&dummyID); err != nil { if err == sql.ErrNoRows { return fmt.Errorf("任务不存在: %s", task.ID) } return fmt.Errorf("查询任务失败: %v", err) } where := map[string]any{ "id": task.ID, } // Content 和 Condition 已经是 JSON 格式,直接使用 contentJSON := string(task.Content) if contentJSON == "" { contentJSON = "{}" } conditionJSON := string(task.Condition) if conditionJSON == "" { conditionJSON = "{}" } // 使用反引号包裹 condition 字段名(避免 MySQL 保留字冲突) data := map[string]any{ "camp_id": task.CampID, "section_id": task.SectionID, "task_type": convertTaskType(task.TaskType), "title": task.Title, "content": contentJSON, "`condition`": conditionJSON, // 反引号包裹保留字 "prerequisite_task_id": nullString(task.PrerequisiteTaskID), } cond, vals, err := builder.BuildUpdate(table, where, data) if err != nil { return fmt.Errorf("构建更新语句失败: %v", err) } cond += " AND deleted_at IS NULL" if _, err := d.client.DB.Exec(cond, vals...); err != nil { return fmt.Errorf("更新任务失败: %v", err) } return nil } // Delete 删除任务(软删除) func (d *TaskDAO) Delete(id string) error { table := "camp_tasks" where := map[string]any{ "id": id, } data := map[string]any{ "deleted_at": time.Now(), } cond, vals, err := builder.BuildUpdate(table, where, data) if err != nil { return fmt.Errorf("构建删除语句失败: %v", err) } cond += " AND deleted_at IS NULL" result, err := d.client.DB.Exec(cond, vals...) if err != nil { return fmt.Errorf("删除任务失败: %v", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("获取影响行数失败: %v", err) } if rows == 0 { return fmt.Errorf("任务不存在: %s", id) } return nil } // List 列出任务(支持关键词搜索、按打卡营/小节ID和任务类型筛选) func (d *TaskDAO) List(keyword, campID, sectionID string, taskType camp.TaskType, page, pageSize int) ([]*camp.Task, int, error) { table := "camp_tasks" baseWhere := "(deleted_at IS NULL OR deleted_at = '0001-01-01 00:00:00')" // 手写构建 WHERE 与参数,确保 camp_id 等筛选生效(不依赖 gendry where map) var conditions []string var args []any conditions = append(conditions, baseWhere) if campID != "" { conditions = append(conditions, "camp_id = ?") args = append(args, campID) } if sectionID != "" { conditions = append(conditions, "section_id = ?") args = append(args, sectionID) } if taskType != camp.TaskTypeUnknown { conditions = append(conditions, "task_type = ?") args = append(args, convertTaskType(taskType)) } if keyword != "" { conditions = append(conditions, "id LIKE ?") args = append(args, "%"+keyword+"%") } whereClause := strings.Join(conditions, " AND ") // 查询总数 countQuery := "SELECT COUNT(*) FROM " + table + " WHERE " + whereClause var total int err := d.client.DB.QueryRow(countQuery, args...).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("查询任务总数失败: %v", err) } // 查询数据(分页) offset := (page - 1) * pageSize dataQuery := "SELECT id, camp_id, section_id, task_type, title, content, `condition`, prerequisite_task_id, deleted_at FROM " + table + " WHERE " + whereClause + " ORDER BY id ASC LIMIT ? OFFSET ?" dataArgs := append(append([]any{}, args...), pageSize, offset) rows, err := d.client.DB.Query(dataQuery, dataArgs...) if err != nil { return nil, 0, fmt.Errorf("查询任务列表失败: %v", err) } defer rows.Close() tasks := make([]*camp.Task, 0) for rows.Next() { var ( taskID string campID string sectionID string taskTypeStr string title sql.NullString contentJSON sql.NullString conditionJSON sql.NullString prerequisiteTaskID sql.NullString deletedAt sql.NullTime ) err := rows.Scan( &taskID, &campID, §ionID, &taskTypeStr, &title, &contentJSON, &conditionJSON, &prerequisiteTaskID, &deletedAt, ) if err != nil { continue } taskType := parseTaskType(taskTypeStr) task := &camp.Task{ ID: taskID, CampID: campID, SectionID: sectionID, TaskType: taskType, Title: title.String, PrerequisiteTaskID: prerequisiteTaskID.String, DeletedAt: utils.FormatNullTimeToStd(deletedAt), } // 处理 JSON 字段 if contentJSON.Valid && contentJSON.String != "" && json.Valid([]byte(contentJSON.String)) { task.Content = json.RawMessage(contentJSON.String) } else { task.Content = json.RawMessage("{}") } if conditionJSON.Valid && conditionJSON.String != "" && json.Valid([]byte(conditionJSON.String)) { task.Condition = json.RawMessage(conditionJSON.String) } else { task.Condition = json.RawMessage("{}") } tasks = append(tasks, task) } if err = rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历任务数据失败: %v", err) } return tasks, total, nil } // CountActiveBySection 统计小节下未删除的任务数量 func (d *TaskDAO) CountActiveBySection(sectionID string) (int, error) { query := "SELECT COUNT(*) FROM camp_tasks WHERE section_id = ? AND deleted_at IS NULL" var count int err := d.client.DB.QueryRow(query, sectionID).Scan(&count) if err != nil { return 0, fmt.Errorf("统计任务数量失败: %v", err) } return count, nil } // MinTaskIDBySection 返回小节内任务 ID 最小的任务 ID(用于判断“第一个任务”) func (d *TaskDAO) MinTaskIDBySection(sectionID string) (string, error) { query := "SELECT id FROM camp_tasks WHERE section_id = ? AND deleted_at IS NULL ORDER BY id ASC LIMIT 1" var minID string err := d.client.DB.QueryRow(query, sectionID).Scan(&minID) if err != nil { if err == sql.ErrNoRows { return "", nil // 小节内无任务 } return "", fmt.Errorf("查询小节最小任务ID失败: %v", err) } return minID, nil } // CountActiveByCamp 统计打卡营下未删除的任务数量 func (d *TaskDAO) CountActiveByCamp(campID string) (int, error) { query := "SELECT COUNT(*) FROM camp_tasks WHERE camp_id = ? AND deleted_at IS NULL" var count int err := d.client.DB.QueryRow(query, campID).Scan(&count) if err != nil { return 0, fmt.Errorf("统计任务数量失败: %v", err) } return count, nil } // ========== 类型转换函数 ========== // convertTaskType 将 TaskType 转换为数据库 ENUM 字符串 func convertTaskType(taskType camp.TaskType) string { switch taskType { case camp.TaskTypeImageText: return "IMAGE_TEXT" case camp.TaskTypeVideo: return "VIDEO" case camp.TaskTypeObjective: return "OBJECTIVE" case camp.TaskTypeSubjective: return "SUBJECTIVE" case camp.TaskTypeEssay: return "ESSAY" default: return "IMAGE_TEXT" } } // parseTaskType 将数据库 ENUM 字符串转换为 TaskType func parseTaskType(taskTypeStr string) camp.TaskType { switch taskTypeStr { case "IMAGE_TEXT": return camp.TaskTypeImageText case "VIDEO": return camp.TaskTypeVideo case "OBJECTIVE": return camp.TaskTypeObjective case "SUBJECTIVE": return camp.TaskTypeSubjective case "ESSAY": return camp.TaskTypeEssay default: return camp.TaskTypeUnknown } } // nullString 空字符串返回 nil,便于写入 NULL func nullString(s string) any { if s == "" { return nil } return s }