package app import ( "context" "strconv" "strings" "time" "github.com/break/junhong_cmp_fiber/internal/middleware" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" asset "github.com/break/junhong_cmp_fiber/internal/service/asset" clientorder "github.com/break/junhong_cmp_fiber/internal/service/client_order" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "gorm.io/gorm" ) // ClientOrderHandler C 端订单处理器 // 提供 D1~D3 下单、列表、详情接口。 type ClientOrderHandler struct { clientOrderService *clientorder.Service assetService *asset.Service orderStore *postgres.OrderStore personalDeviceStore *postgres.PersonalCustomerDeviceStore iotCardStore *postgres.IotCardStore deviceStore *postgres.DeviceStore logger *zap.Logger db *gorm.DB } // NewClientOrderHandler 创建 C 端订单处理器。 func NewClientOrderHandler( clientOrderService *clientorder.Service, assetService *asset.Service, orderStore *postgres.OrderStore, personalDeviceStore *postgres.PersonalCustomerDeviceStore, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, logger *zap.Logger, db *gorm.DB, ) *ClientOrderHandler { return &ClientOrderHandler{ clientOrderService: clientOrderService, assetService: assetService, orderStore: orderStore, personalDeviceStore: personalDeviceStore, iotCardStore: iotCardStore, deviceStore: deviceStore, logger: logger, db: db, } } // CreateOrder D1 创建订单。 // POST /api/c/v1/orders/create func (h *ClientOrderHandler) CreateOrder(c *fiber.Ctx) error { var req dto.ClientCreateOrderRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } customerID, ok := middleware.GetCustomerID(c) if !ok || customerID == 0 { return errors.New(errors.CodeUnauthorized) } resp, err := h.clientOrderService.CreateOrder(c.UserContext(), customerID, &req) if err != nil { return err } return response.Success(c, resp) } // ListOrders D2 订单列表。 // GET /api/c/v1/orders func (h *ClientOrderHandler) ListOrders(c *fiber.Ctx) error { var req dto.ClientOrderListRequest if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if req.Page < 1 { req.Page = 1 } if req.PageSize < 1 { req.PageSize = constants.DefaultPageSize } if req.PageSize > constants.MaxPageSize { req.PageSize = constants.MaxPageSize } resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier) if err != nil { return err } query := h.db.WithContext(resolved.SkipPermissionCtx). Model(&model.Order{}). Where("generation = ?", resolved.Generation) if resolved.Asset.AssetType == constants.ResourceTypeDevice { query = query.Where("order_type = ? AND device_id = ?", model.OrderTypeDevice, resolved.Asset.AssetID) } else { query = query.Where("order_type = ? AND iot_card_id = ?", model.OrderTypeSingleCard, resolved.Asset.AssetID) } if req.PaymentStatus != nil { paymentStatus, ok := clientPaymentStatusToOrderStatus(*req.PaymentStatus) if !ok { return errors.New(errors.CodeInvalidParam) } query = query.Where("payment_status = ?", paymentStatus) } var total int64 if err := query.Count(&total).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询订单总数失败") } var orders []*model.Order offset := (req.Page - 1) * req.PageSize if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&orders).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询订单列表失败") } orderIDs := make([]uint, 0, len(orders)) for _, order := range orders { if order == nil { continue } orderIDs = append(orderIDs, order.ID) } itemMap, err := h.loadOrderItemMap(resolved.SkipPermissionCtx, orderIDs) if err != nil { return err } list := make([]dto.ClientOrderListItem, 0, len(orders)) for _, order := range orders { if order == nil { continue } packageNames := make([]string, 0, len(itemMap[order.ID])) for _, item := range itemMap[order.ID] { if item == nil || item.PackageName == "" { continue } packageNames = append(packageNames, item.PackageName) } list = append(list, dto.ClientOrderListItem{ OrderID: order.ID, OrderNo: order.OrderNo, TotalAmount: order.TotalAmount, PaymentStatus: orderStatusToClientPaymentStatus(order.PaymentStatus), CreatedAt: formatClientOrderTime(order.CreatedAt), PackageNames: packageNames, }) } return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize) } // GetOrderDetail D3 订单详情。 // GET /api/c/v1/orders/:id func (h *ClientOrderHandler) GetOrderDetail(c *fiber.Ctx) error { customerID, ok := middleware.GetCustomerID(c) if !ok || customerID == 0 { return errors.New(errors.CodeUnauthorized) } orderID, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil || orderID == 0 { return errors.New(errors.CodeInvalidParam) } order, items, err := h.orderStore.GetByIDWithItems(c.UserContext(), uint(orderID)) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "订单不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "查询订单详情失败") } virtualNo, err := h.getOrderVirtualNo(c.UserContext(), order) if err != nil { return err } owned, ownErr := h.isCustomerOwnAsset(c.UserContext(), customerID, virtualNo) if ownErr != nil { return errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败") } if !owned { return errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在") } packages := make([]dto.ClientOrderPackageItem, 0, len(items)) for _, item := range items { if item == nil { continue } packages = append(packages, dto.ClientOrderPackageItem{ PackageID: item.PackageID, PackageName: item.PackageName, Price: item.UnitPrice, Quantity: item.Quantity, }) } resp := &dto.ClientOrderDetailResponse{ OrderID: order.ID, OrderNo: order.OrderNo, TotalAmount: order.TotalAmount, PaymentStatus: orderStatusToClientPaymentStatus(order.PaymentStatus), PaymentMethod: order.PaymentMethod, CreatedAt: formatClientOrderTime(order.CreatedAt), PaidAt: formatClientOrderTimePtr(order.PaidAt), CompletedAt: nil, Packages: packages, } return response.Success(c, resp) } func (h *ClientOrderHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedAssetContext, error) { customerID, ok := middleware.GetCustomerID(c) if !ok || customerID == 0 { return nil, errors.New(errors.CodeUnauthorized) } identifier = strings.TrimSpace(identifier) if identifier == "" { identifier = strings.TrimSpace(c.Query("identifier")) } if identifier == "" { return nil, errors.New(errors.CodeInvalidParam) } skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{}) assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier) if err != nil { return nil, err } owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo) if ownErr != nil { return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败") } if !owned { return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在") } generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID) if genErr != nil { return nil, genErr } return &resolvedAssetContext{ CustomerID: customerID, Identifier: identifier, Asset: assetInfo, Generation: generation, SkipPermissionCtx: skipPermissionCtx, }, nil } func (h *ClientOrderHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) { records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID) if err != nil { return false, err } for _, record := range records { if record == nil { continue } if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo { return true, nil } } return false, nil } func (h *ClientOrderHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) { switch assetType { case "card": card, err := h.iotCardStore.GetByID(ctx, assetID) if err != nil { if err == gorm.ErrRecordNotFound { return 0, errors.New(errors.CodeAssetNotFound) } return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败") } return card.Generation, nil case constants.ResourceTypeDevice: device, err := h.deviceStore.GetByID(ctx, assetID) if err != nil { if err == gorm.ErrRecordNotFound { return 0, errors.New(errors.CodeAssetNotFound) } return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败") } return device.Generation, nil default: return 0, errors.New(errors.CodeInvalidParam) } } func (h *ClientOrderHandler) loadOrderItemMap(ctx context.Context, orderIDs []uint) (map[uint][]*model.OrderItem, error) { result := make(map[uint][]*model.OrderItem) if len(orderIDs) == 0 { return result, nil } var items []*model.OrderItem if err := h.db.WithContext(ctx).Where("order_id IN ?", orderIDs).Order("id ASC").Find(&items).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单明细失败") } for _, item := range items { if item == nil { continue } result[item.OrderID] = append(result[item.OrderID], item) } return result, nil } func (h *ClientOrderHandler) getOrderVirtualNo(ctx context.Context, order *model.Order) (string, error) { if order == nil { return "", errors.New(errors.CodeNotFound, "订单不存在") } switch order.OrderType { case model.OrderTypeSingleCard: if order.IotCardID == nil || *order.IotCardID == 0 { return "", errors.New(errors.CodeInvalidParam) } card, err := h.iotCardStore.GetByID(ctx, *order.IotCardID) if err != nil { if err == gorm.ErrRecordNotFound { return "", errors.New(errors.CodeAssetNotFound) } return "", errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败") } return card.VirtualNo, nil case model.OrderTypeDevice: if order.DeviceID == nil || *order.DeviceID == 0 { return "", errors.New(errors.CodeInvalidParam) } device, err := h.deviceStore.GetByID(ctx, *order.DeviceID) if err != nil { if err == gorm.ErrRecordNotFound { return "", errors.New(errors.CodeAssetNotFound) } return "", errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败") } return device.VirtualNo, nil default: return "", errors.New(errors.CodeInvalidParam) } } func orderStatusToClientPaymentStatus(status int) int { switch status { case model.PaymentStatusPending: return 0 case model.PaymentStatusPaid: return 1 case model.PaymentStatusCancelled: return 2 default: return status } } func clientPaymentStatusToOrderStatus(status int) (int, bool) { switch status { case 0: return model.PaymentStatusPending, true case 1: return model.PaymentStatusPaid, true case 2: return model.PaymentStatusCancelled, true default: return 0, false } } func formatClientOrderTime(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) } func formatClientOrderTimePtr(t *time.Time) *string { if t == nil || t.IsZero() { return nil } formatted := formatClientOrderTime(*t) return &formatted }