622 lines
18 KiB
Go
622 lines
18 KiB
Go
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
|
||
}
|
||
}
|