package dao import ( "database/sql" "fmt" "strings" "time" "dd_fiber_api/internal/camp" "dd_fiber_api/pkg/database" "dd_fiber_api/pkg/utils" "github.com/didi/gendry/builder" ) // SectionDAO 小节数据访问对象 type SectionDAO struct { client *database.MySQLClient } // NewSectionDAO 创建小节DAO实例 func NewSectionDAO(client *database.MySQLClient) *SectionDAO { return &SectionDAO{ client: client, } } // Create 创建小节 func (d *SectionDAO) Create(section *camp.Section) error { table := "camp_sections" data := []map[string]any{ { "id": section.ID, "camp_id": section.CampID, "title": section.Title, "section_number": section.SectionNumber, "price_fen": section.PriceFen, "require_previous_section": section.RequirePreviousSection, "time_interval_type": convertTimeIntervalType(section.TimeIntervalType), "time_interval_value": section.TimeIntervalValue, }, } 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 *SectionDAO) GetByID(id string) (*camp.Section, error) { table := "camp_sections" where := map[string]any{ "id": id, } selectFields := []string{"id", "camp_id", "title", "section_number", "price_fen", "require_previous_section", "time_interval_type", "time_interval_value", "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 section camp.Section var timeIntervalTypeStr string var deletedAt sql.NullTime err = d.client.DB.QueryRow(cond, vals...).Scan( §ion.ID, §ion.CampID, §ion.Title, §ion.SectionNumber, §ion.PriceFen, §ion.RequirePreviousSection, &timeIntervalTypeStr, §ion.TimeIntervalValue, &deletedAt, ) if err == sql.ErrNoRows { return nil, fmt.Errorf("小节不存在: %s", id) } if err != nil { return nil, fmt.Errorf("查询小节失败: %v", err) } section.TimeIntervalType = parseTimeIntervalType(timeIntervalTypeStr) section.DeletedAt = utils.FormatNullTimeToStd(deletedAt) return §ion, nil } // Update 更新小节 func (d *SectionDAO) Update(section *camp.Section) error { table := "camp_sections" where := map[string]any{ "id": section.ID, } data := map[string]any{ "camp_id": section.CampID, "title": section.Title, "section_number": section.SectionNumber, "price_fen": section.PriceFen, "require_previous_section": section.RequirePreviousSection, "time_interval_type": convertTimeIntervalType(section.TimeIntervalType), "time_interval_value": section.TimeIntervalValue, } 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", section.ID) } return nil } // Delete 删除小节(软删除) func (d *SectionDAO) Delete(id string) error { table := "camp_sections" 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 *SectionDAO) List(keyword, campID string, page, pageSize int) ([]*camp.Section, int, error) { table := "camp_sections" // 构建查询条件 where := map[string]any{} if campID != "" { where["camp_id"] = campID } if keyword != "" { where["_or"] = []map[string]any{ {"title like": "%" + keyword + "%"}, {"id like": "%" + keyword + "%"}, } } // 查询总数 countCond, countVals, err := builder.BuildSelect(table, where, []string{"count(*) as total"}) if err != nil { return nil, 0, fmt.Errorf("构建统计查询失败: %v", err) } if strings.Contains(countCond, "WHERE") { countCond += " AND deleted_at IS NULL" } else { countCond += " WHERE deleted_at IS NULL" } var total int err = d.client.DB.QueryRow(countCond, countVals...).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("查询小节总数失败: %v", err) } // 查询数据 selectFields := []string{"id", "camp_id", "title", "section_number", "price_fen", "require_previous_section", "time_interval_type", "time_interval_value", "deleted_at"} cond, vals, err := builder.BuildSelect(table, where, selectFields) if err != nil { return nil, 0, fmt.Errorf("构建查询失败: %v", err) } if strings.Contains(cond, "WHERE") { cond += " AND deleted_at IS NULL" } else { cond += " WHERE deleted_at IS NULL" } // 添加排序和分页 offset := (page - 1) * pageSize cond += " ORDER BY section_number ASC 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() sections := make([]*camp.Section, 0) for rows.Next() { var section camp.Section var timeIntervalTypeStr string var deletedAt sql.NullTime err := rows.Scan( §ion.ID, §ion.CampID, §ion.Title, §ion.SectionNumber, §ion.PriceFen, §ion.RequirePreviousSection, &timeIntervalTypeStr, §ion.TimeIntervalValue, &deletedAt, ) if err != nil { continue } section.TimeIntervalType = parseTimeIntervalType(timeIntervalTypeStr) section.DeletedAt = utils.FormatNullTimeToStd(deletedAt) sections = append(sections, §ion) } if err = rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历小节数据失败: %v", err) } return sections, total, nil } // CountActiveByCamp 统计打卡营下未删除的小节数量(与 List 条件一致,兼容 deleted_at 为 NULL 或 0001-01-01) func (d *SectionDAO) CountActiveByCamp(campID string) (int, error) { query := "SELECT COUNT(*) FROM camp_sections WHERE camp_id = ? AND (deleted_at IS NULL OR deleted_at = '0001-01-01 00:00:00')" var count int err := d.client.DB.QueryRow(query, campID).Scan(&count) if err != nil { return 0, fmt.Errorf("统计小节数量失败: %v", err) } return count, nil } // CountByCampIDs 批量统计多个打卡营下未删除的小节数量,返回 map[campID]count(用于列表展示时校正 section_count) func (d *SectionDAO) CountByCampIDs(campIDs []string) (map[string]int, error) { if len(campIDs) == 0 { return map[string]int{}, nil } placeholders := strings.Repeat("?,", len(campIDs)) placeholders = placeholders[:len(placeholders)-1] query := "SELECT camp_id, COUNT(*) FROM camp_sections WHERE (deleted_at IS NULL OR deleted_at = '0001-01-01 00:00:00') AND camp_id IN (" + placeholders + ") GROUP BY camp_id" args := make([]any, len(campIDs)) for i, id := range campIDs { args[i] = id } rows, err := d.client.DB.Query(query, args...) if err != nil { return nil, fmt.Errorf("批量统计小节数量失败: %v", err) } defer rows.Close() result := make(map[string]int) for _, id := range campIDs { result[id] = 0 } for rows.Next() { var campID string var count int if err := rows.Scan(&campID, &count); err != nil { continue } result[campID] = count } if err = rows.Err(); err != nil { return nil, fmt.Errorf("遍历小节统计结果失败: %v", err) } return result, nil } // GetFirstSectionID 获取打卡营中 section_number 最小的小节ID func (d *SectionDAO) GetFirstSectionID(campID string) (string, error) { query := "SELECT id FROM camp_sections WHERE camp_id = ? AND deleted_at IS NULL ORDER BY section_number ASC LIMIT 1" var sectionID string err := d.client.DB.QueryRow(query, campID).Scan(§ionID) if err != nil { if err == sql.ErrNoRows { return "", nil // 没有小节,返回空字符串 } return "", fmt.Errorf("获取第一个小节ID失败: %v", err) } return sectionID, nil } // convertTimeIntervalType 将 TimeIntervalType 转换为数据库字符串 func convertTimeIntervalType(timeIntervalType camp.TimeIntervalType) string { switch timeIntervalType { case camp.TimeIntervalTypeHour: return "HOUR_INTERVAL" case camp.TimeIntervalTypeNaturalDay: return "NATURAL_DAY" case camp.TimeIntervalTypePaid: return "PAID" default: return "NONE" } } // parseTimeIntervalType 将数据库字符串转换为 TimeIntervalType(大小写不敏感) func parseTimeIntervalType(timeIntervalTypeStr string) camp.TimeIntervalType { s := strings.TrimSpace(strings.ToUpper(timeIntervalTypeStr)) switch s { case "HOUR_INTERVAL", "HOUR": return camp.TimeIntervalTypeHour case "NATURAL_DAY": return camp.TimeIntervalTypeNaturalDay case "PAID": return camp.TimeIntervalTypePaid default: return camp.TimeIntervalTypeNone } }