This commit is contained in:
@@ -142,6 +142,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
|||||||
Refund: admin.NewRefundHandler(svc.Refund),
|
Refund: admin.NewRefundHandler(svc.Refund),
|
||||||
OrderPackageInvalidate: admin.NewOrderPackageInvalidateHandler(svc.OrderPackageInvalidate),
|
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),
|
SuperAdmin: admin.NewSuperAdminHandler(svc.OperationPassword),
|
||||||
AgentOpenAPI: openapiHandler.NewHandler(svc.AgentOpenAPI, validate),
|
AgentOpenAPI: openapiHandler.NewHandler(svc.AgentOpenAPI, validate),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ type Handlers struct {
|
|||||||
Refund *admin.RefundHandler
|
Refund *admin.RefundHandler
|
||||||
OrderPackageInvalidate *admin.OrderPackageInvalidateHandler
|
OrderPackageInvalidate *admin.OrderPackageInvalidateHandler
|
||||||
ClientWechat *app.ClientWechatHandler
|
ClientWechat *app.ClientWechatHandler
|
||||||
|
AdminRechargeOrder *admin.AdminRechargeOrderHandler
|
||||||
SuperAdmin *admin.SuperAdminHandler
|
SuperAdmin *admin.SuperAdminHandler
|
||||||
AgentOpenAPI *openapiHandler.Handler
|
AgentOpenAPI *openapiHandler.Handler
|
||||||
}
|
}
|
||||||
|
|||||||
212
internal/handler/admin/recharge_order.go
Normal file
212
internal/handler/admin/recharge_order.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -89,6 +89,7 @@ func (h *ClientRechargeOrderHandler) ListRechargeOrders(c *fiber.Ctx) error {
|
|||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
StatusName: getRechargeOrderStatusName(order.Status),
|
StatusName: getRechargeOrderStatusName(order.Status),
|
||||||
AutoPurchaseStatus: order.AutoPurchaseStatus,
|
AutoPurchaseStatus: order.AutoPurchaseStatus,
|
||||||
|
RejectionReason: order.RejectionReason,
|
||||||
CreatedAt: order.CreatedAt.Format(time.RFC3339),
|
CreatedAt: order.CreatedAt.Format(time.RFC3339),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -154,6 +155,7 @@ func (h *ClientRechargeOrderHandler) GetRechargeOrderDetail(c *fiber.Ctx) error
|
|||||||
StatusName: getRechargeOrderStatusName(order.Status),
|
StatusName: getRechargeOrderStatusName(order.Status),
|
||||||
AutoPurchaseStatus: order.AutoPurchaseStatus,
|
AutoPurchaseStatus: order.AutoPurchaseStatus,
|
||||||
LinkedPackageIDs: order.LinkedPackageIDs,
|
LinkedPackageIDs: order.LinkedPackageIDs,
|
||||||
|
RejectionReason: order.RejectionReason,
|
||||||
CreatedAt: order.CreatedAt.Format(time.RFC3339),
|
CreatedAt: order.CreatedAt.Format(time.RFC3339),
|
||||||
PaidAt: formatTimePtr(order.PaidAt),
|
PaidAt: formatTimePtr(order.PaidAt),
|
||||||
Payments: paymentDetails,
|
Payments: paymentDetails,
|
||||||
@@ -172,6 +174,8 @@ func getRechargeOrderStatusName(status int) string {
|
|||||||
return "已关闭"
|
return "已关闭"
|
||||||
case model.RechargeOrderStatusRefunded:
|
case model.RechargeOrderStatusRefunded:
|
||||||
return "已退款"
|
return "已退款"
|
||||||
|
case model.RechargeOrderStatusRejected:
|
||||||
|
return "已驳回"
|
||||||
default:
|
default:
|
||||||
return "未知"
|
return "未知"
|
||||||
}
|
}
|
||||||
|
|||||||
64
internal/model/dto/admin_recharge_order_dto.go
Normal file
64
internal/model/dto/admin_recharge_order_dto.go
Normal file
@@ -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字)"`
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ package dto
|
|||||||
type ClientRechargeOrderListRequest struct {
|
type ClientRechargeOrderListRequest struct {
|
||||||
Page int `json:"page" query:"page" description:"页码"`
|
Page int `json:"page" query:"page" description:"页码"`
|
||||||
PageSize int `json:"page_size" query:"page_size" 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 充值订单列表项
|
// ClientRechargeOrderListItem C6 充值订单列表项
|
||||||
@@ -12,9 +12,10 @@ type ClientRechargeOrderListItem struct {
|
|||||||
RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"`
|
RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"`
|
||||||
RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"`
|
RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"`
|
||||||
Amount int64 `json:"amount" 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:"状态名称(中文)"`
|
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||||
AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态 (pending/success/failed)"`
|
AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态 (pending/success/failed)"`
|
||||||
|
RejectionReason *string `json:"rejection_reason,omitempty" description:"驳回原因(已驳回时有值)"`
|
||||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,10 +24,11 @@ type ClientRechargeOrderDetail struct {
|
|||||||
RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"`
|
RechargeOrderID uint `json:"recharge_order_id" description:"充值订单ID"`
|
||||||
RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"`
|
RechargeOrderNo string `json:"recharge_order_no" description:"充值订单号"`
|
||||||
Amount int64 `json:"amount" 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:"状态名称(中文)"`
|
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||||
AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"`
|
AutoPurchaseStatus string `json:"auto_purchase_status" description:"自动购包状态"`
|
||||||
LinkedPackageIDs interface{} `json:"linked_package_ids" description:"关联套餐ID列表"`
|
LinkedPackageIDs interface{} `json:"linked_package_ids" description:"关联套餐ID列表"`
|
||||||
|
RejectionReason *string `json:"rejection_reason,omitempty" description:"驳回原因(已驳回时有值)"`
|
||||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||||
PaidAt string `json:"paid_at,omitempty" description:"支付时间"`
|
PaidAt string `json:"paid_at,omitempty" description:"支付时间"`
|
||||||
Payments []ClientRechargeOrderPaymentDetail `json:"payments,omitempty" description:"支付记录列表"`
|
Payments []ClientRechargeOrderPaymentDetail `json:"payments,omitempty" description:"支付记录列表"`
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type RechargeOrder struct {
|
|||||||
AutoPurchaseStatus string `gorm:"column:auto_purchase_status;type:varchar(20)" json:"auto_purchase_status,omitempty"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
|
||||||
@@ -46,6 +47,7 @@ const (
|
|||||||
RechargeOrderStatusPaid = 1 // 已支付
|
RechargeOrderStatusPaid = 1 // 已支付
|
||||||
RechargeOrderStatusClosed = 2 // 已关闭
|
RechargeOrderStatusClosed = 2 // 已关闭
|
||||||
RechargeOrderStatusRefunded = 3 // 已退款
|
RechargeOrderStatusRefunded = 3 // 已退款
|
||||||
|
RechargeOrderStatusRejected = 4 // 已驳回
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -81,6 +81,30 @@ func (s *PollingLifecycleService) OnCardStatusChanged(ctx context.Context, cardI
|
|||||||
s.enqueueCard(ctx, card)
|
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 卡删除后移除所有队列并清理缓存
|
// OnCardDeleted 卡删除后移除所有队列并清理缓存
|
||||||
func (s *PollingLifecycleService) OnCardDeleted(ctx context.Context, cardID uint) {
|
func (s *PollingLifecycleService) OnCardDeleted(ctx context.Context, cardID uint) {
|
||||||
if err := s.queueMgr.OnCardDeleted(ctx, cardID); err != nil {
|
if err := s.queueMgr.OnCardDeleted(ctx, cardID); err != nil {
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
|
|||||||
if handlers.OrderPackageInvalidate != nil {
|
if handlers.OrderPackageInvalidate != nil {
|
||||||
registerOrderPackageInvalidateRoutes(authGroup, handlers.OrderPackageInvalidate, doc, basePath)
|
registerOrderPackageInvalidateRoutes(authGroup, handlers.OrderPackageInvalidate, doc, basePath)
|
||||||
}
|
}
|
||||||
|
if handlers.AdminRechargeOrder != nil {
|
||||||
|
registerAdminRechargeOrderRoutes(authGroup, handlers.AdminRechargeOrder, doc, basePath)
|
||||||
|
}
|
||||||
if handlers.SuperAdmin != nil {
|
if handlers.SuperAdmin != nil {
|
||||||
registerSuperAdminRoutes(authGroup, handlers.SuperAdmin, doc, basePath)
|
registerSuperAdminRoutes(authGroup, handlers.SuperAdmin, doc, basePath)
|
||||||
}
|
}
|
||||||
|
|||||||
36
internal/routes/admin_recharge_order.go
Normal file
36
internal/routes/admin_recharge_order.go
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ type PollingCallback interface {
|
|||||||
OnCardCreated(ctx context.Context, card *model.IotCard)
|
OnCardCreated(ctx context.Context, card *model.IotCard)
|
||||||
// OnCardStatusChanged 卡状态变化时的回调
|
// OnCardStatusChanged 卡状态变化时的回调
|
||||||
OnCardStatusChanged(ctx context.Context, cardID uint)
|
OnCardStatusChanged(ctx context.Context, cardID uint)
|
||||||
|
// OnBatchCardsStatusChanged 批量卡状态变化时的回调(批量分配/回收场景)
|
||||||
|
OnBatchCardsStatusChanged(ctx context.Context, cardIDs []uint)
|
||||||
// OnCardDeleted 卡删除时的回调
|
// OnCardDeleted 卡删除时的回调
|
||||||
OnCardDeleted(ctx context.Context, cardID uint)
|
OnCardDeleted(ctx context.Context, cardID uint)
|
||||||
// OnCardEnabled 卡启用轮询时的回调
|
// OnCardEnabled 卡启用轮询时的回调
|
||||||
@@ -597,6 +599,7 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
|
|||||||
|
|
||||||
newStatus := constants.IotCardStatusDistributed
|
newStatus := constants.IotCardStatusDistributed
|
||||||
toShopID := req.ToShopID
|
toShopID := req.ToShopID
|
||||||
|
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
|
||||||
|
|
||||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
txIotCardStore := postgres.NewIotCardStore(tx, nil)
|
txIotCardStore := postgres.NewIotCardStore(tx, nil)
|
||||||
@@ -606,7 +609,6 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
|
|
||||||
records := s.buildAllocationRecords(cards, cardIDs, operatorShopID, toShopID, operatorID, allocationNo, req.Remark)
|
records := s.buildAllocationRecords(cards, cardIDs, operatorShopID, toShopID, operatorID, allocationNo, req.Remark)
|
||||||
return txRecordStore.BatchCreate(ctx, records)
|
return txRecordStore.BatchCreate(ctx, records)
|
||||||
})
|
})
|
||||||
@@ -631,11 +633,14 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
|
|||||||
}
|
}
|
||||||
s.iotCardStore.InvalidateListCountCache(ctx)
|
s.iotCardStore.InvalidateListCountCache(ctx)
|
||||||
|
|
||||||
// 通知轮询调度器状态变化(卡被分配后可能需要重新匹配配置)
|
// 通知轮询调度器状态变化(异步执行 + 批量操作,避免 N 次单卡 DB 查询打满连接池)
|
||||||
if s.pollingCallback != nil && len(cardIDs) > 0 {
|
if s.pollingCallback != nil && len(cardIDs) > 0 {
|
||||||
for _, cardID := range cardIDs {
|
cardIDsCopy := make([]uint, len(cardIDs))
|
||||||
s.pollingCallback.OnCardStatusChanged(ctx, cardID)
|
copy(cardIDsCopy, cardIDs)
|
||||||
}
|
cb := s.pollingCallback
|
||||||
|
go func() {
|
||||||
|
cb.OnBatchCardsStatusChanged(context.Background(), cardIDsCopy)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
shopMap := s.loadShopNames(ctx, cards)
|
shopMap := s.loadShopNames(ctx, cards)
|
||||||
@@ -672,7 +677,7 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
|
|||||||
TotalCount: len(cards),
|
TotalCount: len(cards),
|
||||||
SuccessCount: len(cardIDs),
|
SuccessCount: len(cardIDs),
|
||||||
FailCount: len(failedItems),
|
FailCount: len(failedItems),
|
||||||
AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate),
|
AllocationNo: allocationNo,
|
||||||
FailedItems: failedItems,
|
FailedItems: failedItems,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -844,11 +849,14 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
|
|||||||
}
|
}
|
||||||
s.iotCardStore.InvalidateListCountCache(ctx)
|
s.iotCardStore.InvalidateListCountCache(ctx)
|
||||||
|
|
||||||
// 通知轮询调度器状态变化(卡被回收后可能需要重新匹配配置)
|
// 通知轮询调度器状态变化(异步批量执行,避免回收大批卡时打满 DB 连接池)
|
||||||
if s.pollingCallback != nil && len(cardIDs) > 0 {
|
if s.pollingCallback != nil && len(cardIDs) > 0 {
|
||||||
for _, cardID := range cardIDs {
|
cardIDsCopy := make([]uint, len(cardIDs))
|
||||||
s.pollingCallback.OnCardStatusChanged(ctx, cardID)
|
copy(cardIDsCopy, cardIDs)
|
||||||
}
|
cb := s.pollingCallback
|
||||||
|
go func() {
|
||||||
|
cb.OnBatchCardsStatusChanged(context.Background(), cardIDsCopy)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
shopMap := s.loadShopNames(ctx, successCards)
|
shopMap := s.loadShopNames(ctx, successCards)
|
||||||
|
|||||||
@@ -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 {
|
func (s *Service) HandlePaymentCallback(ctx context.Context, paymentNo string, paymentMethod string, transactionID string) error {
|
||||||
payment, err := s.paymentStore.GetByPaymentNo(ctx, paymentNo)
|
payment, err := s.paymentStore.GetByPaymentNo(ctx, paymentNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -94,6 +94,25 @@ func (s *RechargeOrderStore) Update(ctx context.Context, order *model.RechargeOr
|
|||||||
return s.db.WithContext(ctx).Save(order).Error
|
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) {
|
func (s *RechargeOrderStore) ListByCard(ctx context.Context, iotCardID uint, generation int, offset, limit int) ([]*model.RechargeOrder, error) {
|
||||||
var orders []*model.RechargeOrder
|
var orders []*model.RechargeOrder
|
||||||
query := s.db.WithContext(ctx).
|
query := s.db.WithContext(ctx).
|
||||||
|
|||||||
Reference in New Issue
Block a user