From 8cf921f11c6711b114f841a0aaa37b4f15b670b2 Mon Sep 17 00:00:00 2001 From: break Date: Thu, 25 Jun 2026 17:10:24 +0900 Subject: [PATCH] =?UTF-8?q?=E9=A9=B3=E5=9B=9E=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/bootstrap/handlers.go | 3 +- internal/bootstrap/types.go | 1 + internal/handler/admin/recharge_order.go | 212 ++++++++++++++++++ internal/handler/app/client_recharge_order.go | 4 + .../model/dto/admin_recharge_order_dto.go | 64 ++++++ .../model/dto/client_recharge_order_dto.go | 20 +- internal/model/recharge_order.go | 2 + internal/polling/lifecycle_service.go | 24 ++ internal/routes/admin.go | 3 + internal/routes/admin_recharge_order.go | 36 +++ internal/service/iot_card/service.go | 28 ++- internal/service/recharge_order/service.go | 59 +++++ .../store/postgres/recharge_order_store.go | 19 ++ 13 files changed, 455 insertions(+), 20 deletions(-) create mode 100644 internal/handler/admin/recharge_order.go create mode 100644 internal/model/dto/admin_recharge_order_dto.go create mode 100644 internal/routes/admin_recharge_order.go diff --git a/internal/bootstrap/handlers.go b/internal/bootstrap/handlers.go index 460221c..a52880a 100644 --- a/internal/bootstrap/handlers.go +++ b/internal/bootstrap/handlers.go @@ -141,7 +141,8 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers { AgentRecharge: admin.NewAgentRechargeHandler(svc.AgentRecharge, validate), Refund: admin.NewRefundHandler(svc.Refund), OrderPackageInvalidate: admin.NewOrderPackageInvalidateHandler(svc.OrderPackageInvalidate), - ClientWechat: app.NewClientWechatHandler(svc.WechatConfig, deps.Redis, deps.Logger), + ClientWechat: app.NewClientWechatHandler(svc.WechatConfig, deps.Redis, deps.Logger), + AdminRechargeOrder: admin.NewAdminRechargeOrderHandler(rechargeOrderService, validate, deps.Logger), SuperAdmin: admin.NewSuperAdminHandler(svc.OperationPassword), AgentOpenAPI: openapiHandler.NewHandler(svc.AgentOpenAPI, validate), } diff --git a/internal/bootstrap/types.go b/internal/bootstrap/types.go index 06cad8d..4e3bfd9 100644 --- a/internal/bootstrap/types.go +++ b/internal/bootstrap/types.go @@ -65,6 +65,7 @@ type Handlers struct { Refund *admin.RefundHandler OrderPackageInvalidate *admin.OrderPackageInvalidateHandler ClientWechat *app.ClientWechatHandler + AdminRechargeOrder *admin.AdminRechargeOrderHandler SuperAdmin *admin.SuperAdminHandler AgentOpenAPI *openapiHandler.Handler } diff --git a/internal/handler/admin/recharge_order.go b/internal/handler/admin/recharge_order.go new file mode 100644 index 0000000..b9260d6 --- /dev/null +++ b/internal/handler/admin/recharge_order.go @@ -0,0 +1,212 @@ +package admin + +import ( + "strconv" + "time" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + + "github.com/break/junhong_cmp_fiber/internal/model" + "github.com/break/junhong_cmp_fiber/internal/model/dto" + rechargeOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge_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" +) + +// AdminRechargeOrderHandler 后台充值订单 Handler +type AdminRechargeOrderHandler struct { + service *rechargeOrderSvc.Service + validator *validator.Validate + logger *zap.Logger +} + +// NewAdminRechargeOrderHandler 创建后台充值订单 Handler +func NewAdminRechargeOrderHandler(service *rechargeOrderSvc.Service, validator *validator.Validate, logger *zap.Logger) *AdminRechargeOrderHandler { + return &AdminRechargeOrderHandler{service: service, validator: validator, logger: logger} +} + +// List 后台充值订单列表 +// GET /api/admin/recharge-orders +func (h *AdminRechargeOrderHandler) List(c *fiber.Ctx) error { + var req dto.AdminRechargeOrderListRequest + 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 + } + + params := &postgres.ListRechargeOrderParams{ + Page: req.Page, + PageSize: req.PageSize, + Status: req.Status, + } + if req.ResourceType != "" && req.ResourceID != nil { + params.ResourceType = &req.ResourceType + params.ResourceID = req.ResourceID + } + if req.StartTime != "" { + t, err := time.Parse(time.RFC3339, req.StartTime) + if err == nil { + params.StartTime = t + } + } + if req.EndTime != "" { + t, err := time.Parse(time.RFC3339, req.EndTime) + if err == nil { + params.EndTime = t + } + } + + orders, total, err := h.service.AdminList(c.UserContext(), params) + if err != nil { + return err + } + + items := make([]dto.AdminRechargeOrderListItem, 0, len(orders)) + for _, order := range orders { + if order == nil { + continue + } + items = append(items, dto.AdminRechargeOrderListItem{ + ID: order.ID, + RechargeOrderNo: order.RechargeOrderNo, + UserID: order.UserID, + ResourceType: order.ResourceType, + ResourceID: order.ResourceID, + Amount: order.Amount, + Status: order.Status, + StatusName: getAdminRechargeOrderStatusName(order.Status), + AutoPurchaseStatus: order.AutoPurchaseStatus, + RejectionReason: order.RejectionReason, + CreatedAt: order.CreatedAt.Format(time.RFC3339), + }) + } + + return response.SuccessWithPagination(c, items, total, req.Page, req.PageSize) +} + +// Get 后台充值订单详情 +// GET /api/admin/recharge-orders/:id +func (h *AdminRechargeOrderHandler) Get(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 64) + if err != nil { + return errors.New(errors.CodeInvalidParam, "无效的订单 ID") + } + + order, payments, err := h.service.AdminGet(c.UserContext(), uint(id)) + if err != nil { + return err + } + + var paymentDetails []dto.AdminRechargeOrderPaymentDetail + for _, p := range payments { + if p == nil { + continue + } + paymentDetails = append(paymentDetails, dto.AdminRechargeOrderPaymentDetail{ + PaymentID: p.ID, + PaymentNo: p.PaymentNo, + PaymentMethod: p.PaymentMethod, + Amount: p.Amount, + Status: p.Status, + StatusName: getAdminPaymentStatusName(p.Status), + ThirdPartyTradeNo: p.ThirdPartyTradeNo, + PaidAt: formatAdminTimePtr(p.PaidAt), + CreatedAt: p.CreatedAt.Format(time.RFC3339), + }) + } + + detail := &dto.AdminRechargeOrderDetail{ + ID: order.ID, + RechargeOrderNo: order.RechargeOrderNo, + UserID: order.UserID, + AssetWalletID: order.AssetWalletID, + ResourceType: order.ResourceType, + ResourceID: order.ResourceID, + Amount: order.Amount, + Status: order.Status, + StatusName: getAdminRechargeOrderStatusName(order.Status), + AutoPurchaseStatus: order.AutoPurchaseStatus, + LinkedPackageIDs: order.LinkedPackageIDs, + RejectionReason: order.RejectionReason, + CreatedAt: order.CreatedAt.Format(time.RFC3339), + PaidAt: formatAdminTimePtr(order.PaidAt), + Payments: paymentDetails, + } + + return response.Success(c, detail) +} + +// Reject 驳回充值订单 +// POST /api/admin/recharge-orders/:id/reject +func (h *AdminRechargeOrderHandler) Reject(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 64) + if err != nil { + return errors.New(errors.CodeInvalidParam, "无效的订单 ID") + } + + var req dto.AdminRechargeOrderRejectRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + if err := h.validator.Struct(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + + if err := h.service.Reject(c.UserContext(), uint(id), req.RejectionReason); err != nil { + return err + } + + return response.Success(c, nil) +} + +func getAdminRechargeOrderStatusName(status int) string { + switch status { + case model.RechargeOrderStatusPending: + return "待支付" + case model.RechargeOrderStatusPaid: + return "已支付" + case model.RechargeOrderStatusClosed: + return "已关闭" + case model.RechargeOrderStatusRefunded: + return "已退款" + case model.RechargeOrderStatusRejected: + return "已驳回" + default: + return "未知" + } +} + +func getAdminPaymentStatusName(status int) string { + switch status { + case model.PaymentRecordStatusPending: + return "待支付" + case model.PaymentRecordStatusPaid: + return "已支付" + case model.PaymentRecordStatusFailed: + return "已失败" + case model.PaymentRecordStatusRefunded: + return "已退款" + default: + return "未知" + } +} + +func formatAdminTimePtr(t *time.Time) string { + if t == nil { + return "" + } + return t.Format(time.RFC3339) +} diff --git a/internal/handler/app/client_recharge_order.go b/internal/handler/app/client_recharge_order.go index f565dc7..1549302 100644 --- a/internal/handler/app/client_recharge_order.go +++ b/internal/handler/app/client_recharge_order.go @@ -89,6 +89,7 @@ func (h *ClientRechargeOrderHandler) ListRechargeOrders(c *fiber.Ctx) error { Status: order.Status, StatusName: getRechargeOrderStatusName(order.Status), AutoPurchaseStatus: order.AutoPurchaseStatus, + RejectionReason: order.RejectionReason, CreatedAt: order.CreatedAt.Format(time.RFC3339), }) } @@ -154,6 +155,7 @@ func (h *ClientRechargeOrderHandler) GetRechargeOrderDetail(c *fiber.Ctx) error StatusName: getRechargeOrderStatusName(order.Status), AutoPurchaseStatus: order.AutoPurchaseStatus, LinkedPackageIDs: order.LinkedPackageIDs, + RejectionReason: order.RejectionReason, CreatedAt: order.CreatedAt.Format(time.RFC3339), PaidAt: formatTimePtr(order.PaidAt), Payments: paymentDetails, @@ -172,6 +174,8 @@ func getRechargeOrderStatusName(status int) string { return "已关闭" case model.RechargeOrderStatusRefunded: return "已退款" + case model.RechargeOrderStatusRejected: + return "已驳回" default: return "未知" } diff --git a/internal/model/dto/admin_recharge_order_dto.go b/internal/model/dto/admin_recharge_order_dto.go new file mode 100644 index 0000000..889cb3c --- /dev/null +++ b/internal/model/dto/admin_recharge_order_dto.go @@ -0,0 +1,64 @@ +package dto + +// AdminRechargeOrderListRequest 后台充值订单列表请求 +type AdminRechargeOrderListRequest struct { + Page int `json:"page" query:"page" description:"页码"` + PageSize int `json:"page_size" query:"page_size" description:"每页数量"` + Status *int `json:"status" query:"status" description:"状态过滤 (0:待支付, 1:已支付, 2:已关闭, 3:已退款, 4:已驳回)"` + ResourceType string `json:"resource_type" query:"resource_type" description:"资产类型 (iot_card/device)"` + ResourceID *uint `json:"resource_id" query:"resource_id" description:"资产 ID"` + StartTime string `json:"start_time" query:"start_time" description:"创建时间起始(RFC3339)"` + EndTime string `json:"end_time" query:"end_time" description:"创建时间结束(RFC3339)"` +} + +// AdminRechargeOrderListItem 后台充值订单列表项 +type AdminRechargeOrderListItem struct { + ID uint `json:"id" description:"充值订单ID"` + RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` + UserID uint `json:"user_id" description:"用户ID"` + ResourceType string `json:"resource_type" description:"资产类型 (iot_card/device)"` + ResourceID uint `json:"resource_id" description:"资产ID"` + Amount int64 `json:"amount" description:"充值金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款, 4:已驳回)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态 (pending/success/failed)"` + RejectionReason *string `json:"rejection_reason,omitempty" description:"驳回原因(已驳回时有值)"` + CreatedAt string `json:"created_at" description:"创建时间"` +} + +// AdminRechargeOrderDetail 后台充值订单详情 +type AdminRechargeOrderDetail struct { + ID uint `json:"id" description:"充值订单ID"` + RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` + UserID uint `json:"user_id" description:"用户ID"` + AssetWalletID uint `json:"asset_wallet_id" description:"资产钱包ID"` + ResourceType string `json:"resource_type" description:"资产类型 (iot_card/device)"` + ResourceID uint `json:"resource_id" description:"资产ID"` + Amount int64 `json:"amount" description:"充值金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款, 4:已驳回)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"` + LinkedPackageIDs interface{} `json:"linked_package_ids" description:"关联套餐ID列表"` + RejectionReason *string `json:"rejection_reason,omitempty" description:"驳回原因(已驳回时有值)"` + CreatedAt string `json:"created_at" description:"创建时间"` + PaidAt string `json:"paid_at,omitempty" description:"支付时间"` + Payments []AdminRechargeOrderPaymentDetail `json:"payments,omitempty" description:"支付记录列表"` +} + +// AdminRechargeOrderPaymentDetail 后台充值订单支付记录 +type AdminRechargeOrderPaymentDetail struct { + PaymentID uint `json:"payment_id" description:"支付记录ID"` + PaymentNo string `json:"payment_no" description:"支付单号"` + PaymentMethod string `json:"payment_method" description:"支付方式 (wechat/alipay/wallet)"` + Amount int64 `json:"amount" description:"支付金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已失败, 3:已退款)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + ThirdPartyTradeNo string `json:"third_party_trade_no,omitempty" description:"第三方交易号"` + PaidAt string `json:"paid_at,omitempty" description:"支付时间"` + CreatedAt string `json:"created_at" description:"创建时间"` +} + +// AdminRechargeOrderRejectRequest 后台驳回充值订单请求 +type AdminRechargeOrderRejectRequest struct { + RejectionReason string `json:"rejection_reason" validate:"required,max=500" description:"驳回原因(必填,最多500字)"` +} diff --git a/internal/model/dto/client_recharge_order_dto.go b/internal/model/dto/client_recharge_order_dto.go index 0e45e43..d572e8f 100644 --- a/internal/model/dto/client_recharge_order_dto.go +++ b/internal/model/dto/client_recharge_order_dto.go @@ -4,18 +4,19 @@ package dto type ClientRechargeOrderListRequest struct { Page int `json:"page" query:"page" description:"页码"` PageSize int `json:"page_size" query:"page_size" description:"每页数量"` - Status int `json:"status" query:"status" description:"状态过滤 (0:待支付, 1:已支付, 2:已关闭, 3:已退款)"` + Status int `json:"status" query:"status" description:"状态过滤 (0:待支付, 1:已支付, 2:已关闭, 3:已退款, 4:已驳回)"` } // ClientRechargeOrderListItem C6 充值订单列表项 type ClientRechargeOrderListItem struct { - RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"` - RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` - Amount int64 `json:"amount" description:"充值金额(分)"` - Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款)"` - StatusName string `json:"status_name" description:"状态名称(中文)"` - AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态 (pending/success/failed)"` - CreatedAt string `json:"created_at" description:"创建时间"` + RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"` + RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` + Amount int64 `json:"amount" description:"充值金额(分)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款, 4:已驳回)"` + StatusName string `json:"status_name" description:"状态名称(中文)"` + AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态 (pending/success/failed)"` + RejectionReason *string `json:"rejection_reason,omitempty" description:"驳回原因(已驳回时有值)"` + CreatedAt string `json:"created_at" description:"创建时间"` } // ClientRechargeOrderDetail C7 充值订单详情 @@ -23,10 +24,11 @@ type ClientRechargeOrderDetail struct { RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"` RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"` Amount int64 `json:"amount" description:"充值金额(分)"` - Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款)"` + Status int `json:"status" description:"状态 (0:待支付, 1:已支付, 2:已关闭, 3:已退款, 4:已驳回)"` StatusName string `json:"status_name" description:"状态名称(中文)"` AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"` LinkedPackageIDs interface{} `json:"linked_package_ids" description:"关联套餐ID列表"` + RejectionReason *string `json:"rejection_reason,omitempty" description:"驳回原因(已驳回时有值)"` CreatedAt string `json:"created_at" description:"创建时间"` PaidAt string `json:"paid_at,omitempty" description:"支付时间"` Payments []ClientRechargeOrderPaymentDetail `json:"payments,omitempty" description:"支付记录列表"` diff --git a/internal/model/recharge_order.go b/internal/model/recharge_order.go index 7ded3ec..790c357 100644 --- a/internal/model/recharge_order.go +++ b/internal/model/recharge_order.go @@ -31,6 +31,7 @@ type RechargeOrder struct { AutoPurchaseStatus string `gorm:"column:auto_purchase_status;type:varchar(20)" json:"auto_purchase_status,omitempty"` AccumulatedRechargeSeries int64 `gorm:"column:accumulated_recharge_series;not null;default:0" json:"accumulated_recharge_series"` PaymentConfigID *uint `gorm:"column:payment_config_id;index" json:"payment_config_id,omitempty"` + RejectionReason *string `gorm:"column:rejection_reason;type:varchar(500)" json:"rejection_reason,omitempty"` CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"` @@ -46,6 +47,7 @@ const ( RechargeOrderStatusPaid = 1 // 已支付 RechargeOrderStatusClosed = 2 // 已关闭 RechargeOrderStatusRefunded = 3 // 已退款 + RechargeOrderStatusRejected = 4 // 已驳回 ) const ( diff --git a/internal/polling/lifecycle_service.go b/internal/polling/lifecycle_service.go index fbdb417..28c9b4f 100644 --- a/internal/polling/lifecycle_service.go +++ b/internal/polling/lifecycle_service.go @@ -81,6 +81,30 @@ func (s *PollingLifecycleService) OnCardStatusChanged(ctx context.Context, cardI s.enqueueCard(ctx, card) } +// OnBatchCardsStatusChanged 批量卡状态变化:单次 Redis pipeline 清理 + 单次 DB 批量查询 +// 替代 N 次 OnCardStatusChanged 调用,避免批量分配/回收时打满 DB 连接池 +func (s *PollingLifecycleService) OnBatchCardsStatusChanged(ctx context.Context, cardIDs []uint) { + if len(cardIDs) == 0 { + return + } + // 单次 pipeline:批量从当前分片队列移除并清理缓存 + if err := s.queueMgr.RemoveBatchFromCurrentShardQueues(ctx, cardIDs); err != nil { + s.logger.Warn("批量卡状态变化:从队列移除失败", zap.Int("count", len(cardIDs)), zap.Error(err)) + } + // 单次 DB 查询:批量加载所有卡信息 + cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs) + if err != nil { + s.logger.Error("批量卡状态变化:加载卡信息失败", zap.Int("count", len(cardIDs)), zap.Error(err)) + return + } + for _, card := range cards { + if !s.shouldEnqueue(ctx, card) { + continue + } + s.enqueueCard(ctx, card) + } +} + // OnCardDeleted 卡删除后移除所有队列并清理缓存 func (s *PollingLifecycleService) OnCardDeleted(ctx context.Context, cardID uint) { if err := s.queueMgr.OnCardDeleted(ctx, cardID); err != nil { diff --git a/internal/routes/admin.go b/internal/routes/admin.go index ac26298..9c30b1b 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -128,6 +128,9 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd if handlers.OrderPackageInvalidate != nil { registerOrderPackageInvalidateRoutes(authGroup, handlers.OrderPackageInvalidate, doc, basePath) } + if handlers.AdminRechargeOrder != nil { + registerAdminRechargeOrderRoutes(authGroup, handlers.AdminRechargeOrder, doc, basePath) + } if handlers.SuperAdmin != nil { registerSuperAdminRoutes(authGroup, handlers.SuperAdmin, doc, basePath) } diff --git a/internal/routes/admin_recharge_order.go b/internal/routes/admin_recharge_order.go new file mode 100644 index 0000000..24144f1 --- /dev/null +++ b/internal/routes/admin_recharge_order.go @@ -0,0 +1,36 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/break/junhong_cmp_fiber/internal/handler/admin" + "github.com/break/junhong_cmp_fiber/internal/model/dto" + "github.com/break/junhong_cmp_fiber/pkg/openapi" +) + +// registerAdminRechargeOrderRoutes 注册后台充值订单路由 +func registerAdminRechargeOrderRoutes(router fiber.Router, handler *admin.AdminRechargeOrderHandler, doc *openapi.Generator, basePath string) { + Register(router, doc, basePath, "GET", "/recharge-orders", handler.List, RouteSpec{ + Summary: "后台充值订单列表", + Tags: []string{"充值订单管理"}, + Input: new(dto.AdminRechargeOrderListRequest), + Output: new(dto.AdminRechargeOrderListItem), + Auth: true, + }) + + Register(router, doc, basePath, "GET", "/recharge-orders/:id", handler.Get, RouteSpec{ + Summary: "后台充值订单详情", + Tags: []string{"充值订单管理"}, + Input: nil, + Output: new(dto.AdminRechargeOrderDetail), + Auth: true, + }) + + Register(router, doc, basePath, "POST", "/recharge-orders/:id/reject", handler.Reject, RouteSpec{ + Summary: "驳回充值订单", + Tags: []string{"充值订单管理"}, + Input: new(dto.AdminRechargeOrderRejectRequest), + Output: nil, + Auth: true, + }) +} diff --git a/internal/service/iot_card/service.go b/internal/service/iot_card/service.go index afe7428..1027548 100644 --- a/internal/service/iot_card/service.go +++ b/internal/service/iot_card/service.go @@ -29,6 +29,8 @@ type PollingCallback interface { OnCardCreated(ctx context.Context, card *model.IotCard) // OnCardStatusChanged 卡状态变化时的回调 OnCardStatusChanged(ctx context.Context, cardID uint) + // OnBatchCardsStatusChanged 批量卡状态变化时的回调(批量分配/回收场景) + OnBatchCardsStatusChanged(ctx context.Context, cardIDs []uint) // OnCardDeleted 卡删除时的回调 OnCardDeleted(ctx context.Context, cardID uint) // OnCardEnabled 卡启用轮询时的回调 @@ -597,6 +599,7 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone newStatus := constants.IotCardStatusDistributed toShopID := req.ToShopID + allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate) err = s.db.Transaction(func(tx *gorm.DB) error { txIotCardStore := postgres.NewIotCardStore(tx, nil) @@ -606,7 +609,6 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone return err } - allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate) records := s.buildAllocationRecords(cards, cardIDs, operatorShopID, toShopID, operatorID, allocationNo, req.Remark) return txRecordStore.BatchCreate(ctx, records) }) @@ -631,11 +633,14 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone } s.iotCardStore.InvalidateListCountCache(ctx) - // 通知轮询调度器状态变化(卡被分配后可能需要重新匹配配置) + // 通知轮询调度器状态变化(异步执行 + 批量操作,避免 N 次单卡 DB 查询打满连接池) if s.pollingCallback != nil && len(cardIDs) > 0 { - for _, cardID := range cardIDs { - s.pollingCallback.OnCardStatusChanged(ctx, cardID) - } + cardIDsCopy := make([]uint, len(cardIDs)) + copy(cardIDsCopy, cardIDs) + cb := s.pollingCallback + go func() { + cb.OnBatchCardsStatusChanged(context.Background(), cardIDsCopy) + }() } shopMap := s.loadShopNames(ctx, cards) @@ -672,7 +677,7 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone TotalCount: len(cards), SuccessCount: len(cardIDs), FailCount: len(failedItems), - AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate), + AllocationNo: allocationNo, FailedItems: failedItems, }, nil } @@ -844,11 +849,14 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard } s.iotCardStore.InvalidateListCountCache(ctx) - // 通知轮询调度器状态变化(卡被回收后可能需要重新匹配配置) + // 通知轮询调度器状态变化(异步批量执行,避免回收大批卡时打满 DB 连接池) if s.pollingCallback != nil && len(cardIDs) > 0 { - for _, cardID := range cardIDs { - s.pollingCallback.OnCardStatusChanged(ctx, cardID) - } + cardIDsCopy := make([]uint, len(cardIDs)) + copy(cardIDsCopy, cardIDs) + cb := s.pollingCallback + go func() { + cb.OnBatchCardsStatusChanged(context.Background(), cardIDsCopy) + }() } shopMap := s.loadShopNames(ctx, successCards) diff --git a/internal/service/recharge_order/service.go b/internal/service/recharge_order/service.go index 613e07f..7865bf2 100644 --- a/internal/service/recharge_order/service.go +++ b/internal/service/recharge_order/service.go @@ -60,6 +60,65 @@ func New( } } +// AdminList 后台分页查询充值订单列表 +func (s *Service) AdminList(ctx context.Context, params *postgres.ListRechargeOrderParams) ([]*model.RechargeOrder, int64, error) { + orders, total, err := s.rechargeOrderStore.List(ctx, params) + if err != nil { + return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单列表失败") + } + return orders, total, nil +} + +// AdminGet 后台查询充值订单详情(含支付记录) +func (s *Service) AdminGet(ctx context.Context, id uint) (*model.RechargeOrder, []*model.Payment, error) { + order, err := s.rechargeOrderStore.GetByID(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil, errors.New(errors.CodeNotFound, "充值订单不存在") + } + return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") + } + payments, err := s.paymentStore.ListByOrderID(ctx, id, model.PaymentOrderTypeRecharge) + if err != nil { + return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") + } + return order, payments, nil +} + +// Reject 驳回充值订单 +// 仅待支付订单可驳回,驳回后状态变为已驳回(终态) +func (s *Service) Reject(ctx context.Context, id uint, rejectionReason string) error { + order, err := s.rechargeOrderStore.GetByID(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return errors.New(errors.CodeNotFound, "充值订单不存在") + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") + } + + if order.Status != model.RechargeOrderStatusPending { + return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回") + } + + if err := s.rechargeOrderStore.UpdateStatusWithRejection(ctx, id, rejectionReason); err != nil { + if err == gorm.ErrRecordNotFound { + // 并发驳回:重查状态给出明确错误 + latest, queryErr := s.rechargeOrderStore.GetByID(ctx, id) + if queryErr == nil && latest.Status != model.RechargeOrderStatusPending { + return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回") + } + return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回") + } + return errors.Wrap(errors.CodeDatabaseError, err, "驳回充值订单失败") + } + + s.logger.Info("充值订单驳回成功", + zap.Uint("order_id", id), + zap.String("rejection_reason", rejectionReason), + ) + return nil +} + func (s *Service) HandlePaymentCallback(ctx context.Context, paymentNo string, paymentMethod string, transactionID string) error { payment, err := s.paymentStore.GetByPaymentNo(ctx, paymentNo) if err != nil { diff --git a/internal/store/postgres/recharge_order_store.go b/internal/store/postgres/recharge_order_store.go index f39272c..ce00680 100644 --- a/internal/store/postgres/recharge_order_store.go +++ b/internal/store/postgres/recharge_order_store.go @@ -94,6 +94,25 @@ func (s *RechargeOrderStore) Update(ctx context.Context, order *model.RechargeOr return s.db.WithContext(ctx).Save(order).Error } +// UpdateStatusWithRejection 条件更新订单状态为已驳回并写入驳回原因 +// 仅当 status = 0(待支付)时更新,RowsAffected=0 说明订单不存在或状态已变更 +func (s *RechargeOrderStore) UpdateStatusWithRejection(ctx context.Context, id uint, rejectionReason string) error { + result := s.db.WithContext(ctx). + Model(&model.RechargeOrder{}). + Where("id = ? AND status = ?", id, model.RechargeOrderStatusPending). + Updates(map[string]interface{}{ + "status": model.RechargeOrderStatusRejected, + "rejection_reason": rejectionReason, + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + func (s *RechargeOrderStore) ListByCard(ctx context.Context, iotCardID uint, generation int, offset, limit int) ([]*model.RechargeOrder, error) { var orders []*model.RechargeOrder query := s.db.WithContext(ctx).