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

442 lines
12 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"
"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,
&sectionID,
&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,
&sectionID,
&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
}