354 lines
9.6 KiB
Go
354 lines
9.6 KiB
Go
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
|
||
}
|
||
}
|
||
|