442 lines
12 KiB
Go
442 lines
12 KiB
Go
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
|
||
}
|