新增客户端资产、钱包、订单、实名、设备管理等核心业务 Handler 与 DTO: - 客户端资产信息查询、套餐列表、套餐历史、资产刷新 - 客户端钱包详情、流水、充值校验、充值订单、充值记录 - 客户端订单创建、列表、详情 - 客户端实名认证链接获取 - 客户端设备卡列表、重启、恢复出厂、WiFi配置、切卡 - 客户端订单服务(含微信/支付宝支付流程) - 强充自动代购异步任务处理 - 数据库迁移 000084:充值记录增加自动代购状态字段
416 lines
12 KiB
Go
416 lines
12 KiB
Go
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
|
|
}
|