package dao import ( "database/sql" "fmt" "time" "dd_fiber_api/internal/order" "dd_fiber_api/pkg/database" "dd_fiber_api/pkg/utils" "github.com/didi/gendry/builder" ) // OrderDAO 订单数据访问对象 type OrderDAO struct { client *database.MySQLClient } // NewOrderDAO 创建订单DAO实例 func NewOrderDAO(client *database.MySQLClient) *OrderDAO { return &OrderDAO{client: client} } // CreateOrder 创建订单(使用新的 orders 表) func (d *OrderDAO) CreateOrder( orderID, userID string, orderType order.OrderType, originalAmount, discountAmount, actualAmount int32, couponID string, status order.OrderStatus, paymentMethod order.PaymentMethod, paymentTime *time.Time, ) error { table := "orders" data := []map[string]any{{ "order_id": orderID, "user_id": userID, "order_type": string(orderType), "original_amount": originalAmount, "discount_amount": discountAmount, "actual_amount": actualAmount, "coupon_id": couponID, "status": string(status), }} // 处理支付方式:如果是空字符串(PaymentMethodUnknown),使用 NULL if paymentMethod == order.PaymentMethodUnknown || paymentMethod == "" { data[0]["payment_method"] = nil } else { data[0]["payment_method"] = string(paymentMethod) } // 如果提供了支付时间(0元订单直接完成),添加到数据中 if paymentTime != nil { data[0]["payment_time"] = *paymentTime } cond, vals, err := builder.BuildInsert(table, data) if err != nil { return fmt.Errorf("构建插入失败: %v", err) } // 添加幂等性处理(如果订单ID已存在,不更新) cond += " ON DUPLICATE KEY UPDATE order_id=order_id" if _, err := d.client.DB.Exec(cond, vals...); err != nil { return fmt.Errorf("创建订单失败: %v", err) } return nil } // CreateOrderBusinessData 创建订单业务数据(关联订单和小节) func (d *OrderDAO) CreateOrderBusinessData(orderID, campID, sectionID string) error { table := "order_business_data" data := []map[string]any{{ "order_id": orderID, "camp_id": campID, "section_id": sectionID, }} cond, vals, err := builder.BuildInsert(table, data) if err != nil { return fmt.Errorf("构建插入失败: %v", err) } // 添加幂等性处理 cond += " ON DUPLICATE KEY UPDATE order_id=order_id" if _, err := d.client.DB.Exec(cond, vals...); err != nil { // 如果表不存在,忽略错误(向后兼容) return nil } return nil } // CheckUserHasSection 检查用户是否已拥有某个小节(通过查询已支付的订单) func (d *OrderDAO) CheckUserHasSection(userID, sectionID string) (bool, error) { // 先尝试查询 order_business_data 表 table := "order_business_data" where := map[string]any{ "section_id": sectionID, } // 查询该小节的所有订单ID cond, vals, err := builder.BuildSelect(table, where, []string{"order_id"}) if err != nil { // 如果表不存在,返回 false(向后兼容) return false, nil } rows, err := d.client.DB.Query(cond, vals...) if err != nil { // 如果表不存在,返回 false(向后兼容) return false, nil } defer rows.Close() var orderIDs []string for rows.Next() { var orderID string if err := rows.Scan(&orderID); err != nil { continue } orderIDs = append(orderIDs, orderID) } if len(orderIDs) == 0 { return false, nil } // 查询这些订单中是否有该用户的已支付订单 ordersTable := "orders" ordersWhere := map[string]any{ "user_id": userID, "status": string(order.OrderStatusPaid), } // 构建 IN 查询 ordersCond, ordersVals, err := builder.BuildSelect(ordersTable, ordersWhere, []string{"COUNT(*) as count"}) if err != nil { return false, nil } // 添加 order_id IN (...) 条件 if len(orderIDs) > 0 { placeholders := "" for i, id := range orderIDs { if i > 0 { placeholders += "," } placeholders += "?" ordersVals = append(ordersVals, id) } ordersCond += " AND order_id IN (" + placeholders + ")" } var count int if err := d.client.DB.QueryRow(ordersCond, ordersVals...).Scan(&count); err != nil { return false, nil } return count > 0, nil } // GetUserSectionAccessTime 获取用户某小节的「开启」时间(上一节开启时的起点:已支付订单的 payment_time,若无则 created_at) func (d *OrderDAO) GetUserSectionAccessTime(userID, sectionID string) (*time.Time, error) { table := "order_business_data" where := map[string]any{"section_id": sectionID} cond, vals, err := builder.BuildSelect(table, where, []string{"order_id"}) if err != nil { return nil, nil } rows, err := d.client.DB.Query(cond, vals...) if err != nil { return nil, nil } defer rows.Close() var orderIDs []string for rows.Next() { var orderID string if err := rows.Scan(&orderID); err != nil { continue } orderIDs = append(orderIDs, orderID) } if len(orderIDs) == 0 { return nil, nil } placeholders := "" for i := range orderIDs { if i > 0 { placeholders += "," } placeholders += "?" } args := make([]any, 0, len(orderIDs)+2) args = append(args, userID, string(order.OrderStatusPaid)) for _, id := range orderIDs { args = append(args, id) } q := "SELECT COALESCE(payment_time, created_at) FROM orders WHERE user_id=? AND status=? AND order_id IN (" + placeholders + ") ORDER BY COALESCE(payment_time, created_at) DESC LIMIT 1" var accessAt sql.NullTime err = d.client.DB.QueryRow(q, args...).Scan(&accessAt) if err != nil || !accessAt.Valid { return nil, nil } t := accessAt.Time return &t, nil } // GetOrderByID 根据订单ID查询订单 func (d *OrderDAO) GetOrderByID(orderID string) (*order.Order, error) { return d.getOrderByCondition(map[string]any{"order_id": orderID}) } // GetOrderByOrderNo 根据订单号查询订单(兼容方法,新表使用 order_id) func (d *OrderDAO) GetOrderByOrderNo(orderNo string) (*order.Order, error) { return d.getOrderByCondition(map[string]any{"order_id": orderNo}) } // getOrderByCondition 根据条件查询订单(内部方法,使用新的 orders 表) // 使用 Go 代码实现业务数据关联,避免 JOIN 查询 func (d *OrderDAO) getOrderByCondition(where map[string]any) (*order.Order, error) { table := "orders" // 先查询订单表(不使用 JOIN) selectFields := []string{ "order_id", "user_id", "order_type", "original_amount", "discount_amount", "actual_amount", "coupon_id", "status", "payment_method", "transaction_id", "payment_time", "created_at", "updated_at", } cond, vals, err := builder.BuildSelect(table, where, selectFields) if err != nil { return nil, fmt.Errorf("构建查询失败: %v", err) } cond += " LIMIT 1" var ( orderID, userID, orderTypeStr, couponID, statusStr, paymentMethodStr, transactionID sql.NullString originalAmount, discountAmount, actualAmount int32 createdAt, updatedAt, paymentTime sql.NullTime ) err = d.client.DB.QueryRow(cond, vals...).Scan( &orderID, &userID, &orderTypeStr, &originalAmount, &discountAmount, &actualAmount, &couponID, &statusStr, &paymentMethodStr, &transactionID, &paymentTime, &createdAt, &updatedAt, ) if err != nil { if err == sql.ErrNoRows { return nil, nil // 订单不存在 } return nil, fmt.Errorf("查询订单失败: %v", err) } result := &order.Order{ OrderID: orderID.String, UserID: userID.String, OrderType: order.OrderType(orderTypeStr.String), OriginalAmount: originalAmount, DiscountAmount: discountAmount, ActualAmount: actualAmount, CouponID: couponID.String, Status: order.OrderStatus(statusStr.String), PaymentMethod: order.PaymentMethod(paymentMethodStr.String), TransactionID: transactionID.String, PaymentTime: utils.FormatNullTimeToStd(paymentTime), CreatedAt: utils.FormatNullTimeToStd(createdAt), UpdatedAt: utils.FormatNullTimeToStd(updatedAt), } // 使用 Go 代码查询业务数据并关联 if orderID.Valid { businessDataMap, err := d.getOrderBusinessDataBatch([]string{orderID.String}) if err == nil { if data, ok := businessDataMap[orderID.String]; ok { result.CampID = data.CampID result.SectionID = data.SectionID } } } return result, nil } // ListOrders 查询订单列表(支持多条件筛选和分页,使用新的 orders 表) func (d *OrderDAO) ListOrders( userID, campID, sectionID string, orderStatus order.OrderStatus, paymentMethod order.PaymentMethod, page, pageSize int, ) ([]*order.Order, int, error) { table := "orders" where := make(map[string]any) if userID != "" { where["user_id"] = userID } // 注意:新的 orders 表没有 camp_id 和 section_id 字段,这些信息需要从业务数据中获取 // 如果传入这些参数,可以通过 order_type = 'CAMP_SECTION' 来筛选打卡营小节订单 if campID != "" || sectionID != "" { where["order_type"] = string(order.OrderTypeCampSection) } if orderStatus != order.OrderStatusUnknown && orderStatus != "" { where["status"] = string(orderStatus) } if paymentMethod != order.PaymentMethodUnknown && paymentMethod != "" { where["payment_method"] = string(paymentMethod) } // count - 直接查询 orders 表(不使用 JOIN) countCond, countVals, err := builder.BuildSelect(table, where, []string{"COUNT(*) as total"}) if err != nil { return nil, 0, fmt.Errorf("构建统计查询失败: %v", err) } var total int if err := d.client.DB.QueryRow(countCond, countVals...).Scan(&total); err != nil { return nil, 0, fmt.Errorf("查询总数失败: %v", err) } // select - 先查询订单表(不使用 JOIN) selectFields := []string{ "order_id", "user_id", "order_type", "original_amount", "discount_amount", "actual_amount", "coupon_id", "status", "payment_method", "transaction_id", "payment_time", "created_at", "updated_at", } cond, vals, err := builder.BuildSelect(table, where, selectFields) if err != nil { return nil, 0, fmt.Errorf("构建查询失败: %v", err) } offset := (page - 1) * pageSize cond += " ORDER BY created_at DESC 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() list := make([]*order.Order, 0) orderIDs := make([]string, 0) // 第一遍:读取所有订单数据 for rows.Next() { var ( orderID, userID, orderTypeStr, couponID, statusStr, paymentMethodStr, transactionID sql.NullString originalAmount, discountAmount, actualAmount int32 createdAt, updatedAt, paymentTime sql.NullTime ) if err := rows.Scan( &orderID, &userID, &orderTypeStr, &originalAmount, &discountAmount, &actualAmount, &couponID, &statusStr, &paymentMethodStr, &transactionID, &paymentTime, &createdAt, &updatedAt, ); err != nil { continue } orderObj := &order.Order{ OrderID: orderID.String, UserID: userID.String, OrderType: order.OrderType(orderTypeStr.String), OriginalAmount: originalAmount, DiscountAmount: discountAmount, ActualAmount: actualAmount, CouponID: couponID.String, Status: order.OrderStatus(statusStr.String), PaymentMethod: order.PaymentMethod(paymentMethodStr.String), TransactionID: transactionID.String, PaymentTime: utils.FormatNullTimeToStd(paymentTime), CreatedAt: utils.FormatNullTimeToStd(createdAt), UpdatedAt: utils.FormatNullTimeToStd(updatedAt), } list = append(list, orderObj) orderIDs = append(orderIDs, orderID.String) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历订单列表失败: %v", err) } // 第二遍:批量查询业务数据并关联(使用 Go 代码实现,避免 JOIN) if len(orderIDs) > 0 { businessDataMap, err := d.getOrderBusinessDataBatch(orderIDs) if err == nil { // 将业务数据关联到订单对象 for _, orderObj := range list { if data, ok := businessDataMap[orderObj.OrderID]; ok { orderObj.CampID = data.CampID orderObj.SectionID = data.SectionID } } } } return list, total, nil } // OrderBusinessData 订单业务数据 type OrderBusinessData struct { OrderID string CampID string SectionID string } // getOrderBusinessDataBatch 批量查询订单业务数据 func (d *OrderDAO) getOrderBusinessDataBatch(orderIDs []string) (map[string]*OrderBusinessData, error) { if len(orderIDs) == 0 { return make(map[string]*OrderBusinessData), nil } table := "order_business_data" // 构建 IN 查询 placeholders := "" vals := make([]any, 0) for i, id := range orderIDs { if i > 0 { placeholders += "," } placeholders += "?" vals = append(vals, id) } cond := "SELECT order_id, camp_id, section_id FROM " + table + " WHERE order_id IN (" + placeholders + ")" rows, err := d.client.DB.Query(cond, vals...) if err != nil { // 如果表不存在,返回空映射(向后兼容) return make(map[string]*OrderBusinessData), nil } defer rows.Close() result := make(map[string]*OrderBusinessData) for rows.Next() { var ( orderID, campID, sectionID sql.NullString ) if err := rows.Scan(&orderID, &campID, §ionID); err != nil { continue } if orderID.Valid { result[orderID.String] = &OrderBusinessData{ OrderID: orderID.String, CampID: campID.String, SectionID: sectionID.String, } } } return result, nil } // UpdateOrderStatus 更新订单状态(使用新的 orders 表) func (d *OrderDAO) UpdateOrderStatus( orderID, orderNo string, orderStatus order.OrderStatus, paymentMethod order.PaymentMethod, thirdPartyOrderNo string, paymentTime *time.Time, ) error { table := "orders" where := make(map[string]any) if orderID != "" { where["order_id"] = orderID } else if orderNo != "" { where["order_id"] = orderNo // 新表使用 order_id } else { return fmt.Errorf("订单ID和订单号不能同时为空") } data := make(map[string]any) if orderStatus != order.OrderStatusUnknown && orderStatus != "" { data["status"] = string(orderStatus) } if paymentMethod != order.PaymentMethodUnknown && paymentMethod != "" { data["payment_method"] = string(paymentMethod) } if thirdPartyOrderNo != "" { data["transaction_id"] = thirdPartyOrderNo } if paymentTime != nil { data["payment_time"] = *paymentTime } if len(data) == 0 { return fmt.Errorf("没有需要更新的字段") } cond, vals, err := builder.BuildUpdate(table, where, 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 } // RefundOrder 退款订单(使用新的 orders 表,注意:新表没有 refund_amount_fen, refund_reason, refund_time 字段) // 退款操作只需要更新订单状态为 REFUNDED func (d *OrderDAO) RefundOrder( orderID, orderNo string, refundAmountFen int32, refundReason string, ) error { table := "orders" where := make(map[string]any) if orderID != "" { where["order_id"] = orderID } else if orderNo != "" { where["order_id"] = orderNo // 新表使用 order_id } else { return fmt.Errorf("订单ID和订单号不能同时为空") } // 新表只有 status 字段,退款时更新为 REFUNDED // 注意:refund_amount_fen, refund_reason, refund_time 字段在新表中不存在 // 如果需要记录这些信息,可能需要额外的退款记录表 data := map[string]any{ "status": string(order.OrderStatusRefunded), } cond, vals, err := builder.BuildUpdate(table, where, 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 } // ========== 辅助函数 ========== // convertOrderStatus 转换订单状态枚举为数据库值 func convertOrderStatus(status order.OrderStatus) *string { switch status { case order.OrderStatusPending: s := "PENDING" return &s case order.OrderStatusPaid: s := "PAID" return &s case order.OrderStatusFailed: s := "FAILED" return &s case order.OrderStatusRefunded: s := "REFUNDED" return &s case order.OrderStatusCancelled: s := "CANCELLED" return &s default: return nil // NULL } } // parseOrderStatus 解析订单状态字符串为枚举 func parseOrderStatus(s string) order.OrderStatus { switch s { case "PENDING": return order.OrderStatusPending case "PAID": return order.OrderStatusPaid case "FAILED": return order.OrderStatusFailed case "REFUNDED": return order.OrderStatusRefunded case "CANCELLED": return order.OrderStatusCancelled default: return order.OrderStatusUnknown } } // convertPaymentMethod 转换支付方式枚举为数据库值 func convertPaymentMethod(method order.PaymentMethod) *string { switch method { case order.PaymentMethodWechat: s := "WECHAT" return &s case order.PaymentMethodAlipay: s := "ALIPAY" return &s case order.PaymentMethodBalance: s := "BALANCE" return &s default: return nil // NULL } } // parsePaymentMethod 解析支付方式字符串为枚举 func parsePaymentMethod(s string) order.PaymentMethod { switch s { case "WECHAT": return order.PaymentMethodWechat case "ALIPAY": return order.PaymentMethodAlipay case "BALANCE": return order.PaymentMethodBalance default: return order.PaymentMethodUnknown } } // parseAccessSource 解析访问来源字符串为枚举 func parseAccessSource(s string) order.AccessSource { switch s { case "GRANT": return order.AccessSourceGrant case "PURCHASE": return order.AccessSourcePurchase default: return order.AccessSourceUnknown } }