From 5bb65216a9dda0496e4eec82e3ec88bae8fc1bc6 Mon Sep 17 00:00:00 2001 From: luorijun Date: Thu, 18 Jun 2026 12:15:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=94=AF=E4=BB=98=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E6=89=A9=E5=B1=95=E4=BA=A7=E5=93=81=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E8=BF=94=E5=9B=9E=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++ web/globals/gost.go | 1 + web/handlers/resource.go | 2 +- web/handlers/trade.go | 158 +++++++++++++++++++++++------------ web/routes.go | 7 +- web/services/channel_gost.go | 1 + web/services/product.go | 45 ++++++++-- web/services/trade.go | 42 ++++++++-- web/tasks/task.go | 36 +++++--- 9 files changed, 210 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 8eba393..257506c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ ## TODO +后台交易备注 +关闭支付弹窗 +兜底手动操作 +限速 + - edge.area_id 可为空,代表节点无固定地区 - 后台展示 mac, ip:port,实际地区 diff --git a/web/globals/gost.go b/web/globals/gost.go index f2541da..5a5724e 100644 --- a/web/globals/gost.go +++ b/web/globals/gost.go @@ -89,6 +89,7 @@ type GostServiceConfig struct { Handler GostHandlerConfig `json:"handler"` Listener GostListenerConfig `json:"listener"` Recorders []GostRecorderConfig `json:"recorders,omitempty"` + Limiter string `json:"limiter,omitempty"` } type GostHandlerConfig struct { diff --git a/web/handlers/resource.go b/web/handlers/resource.go index ce09563..9b51020 100644 --- a/web/handlers/resource.go +++ b/web/handlers/resource.go @@ -422,7 +422,7 @@ func PageResourceShortOfUserByAdmin(c *fiber.Ctx) error { do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC())) } - list, total, err := q.Resource.Debug(). + list, total, err := q.Resource. Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku). Select( q.Resource.ALL, diff --git a/web/handlers/trade.go b/web/handlers/trade.go index 9512b36..3aba1d9 100644 --- a/web/handlers/trade.go +++ b/web/handlers/trade.go @@ -1,10 +1,7 @@ package handlers import ( - "bufio" - "fmt" "log/slog" - "platform/pkg/env" "platform/web/auth" "platform/web/core" g "platform/web/globals" @@ -14,7 +11,6 @@ import ( "time" "github.com/gofiber/fiber/v2" - "github.com/valyala/fasthttp" ) // PageTradeByAdmin 分页查询所有订单 @@ -221,6 +217,36 @@ type TradeCreateReq struct { // ============================================================ +// 更新订单备注 +func TradeUpdateRemarkByAdmin(c *fiber.Ctx) error { + // 检查权限 + _, err := auth.GetAuthCtx(c).PermitAdmin() + if err != nil { + return err + } + + // 解析请求参数 + var req TradeUpdateRemarkReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + // 更新订单备注 + err = s.Trade.UpdateRemark(req.TradeNo, req.Remark) + if err != nil { + return err + } + + return c.SendStatus(fiber.StatusNoContent) +} + +type TradeUpdateRemarkReq struct { + TradeNo string `json:"trade_no" validate:"required"` + Remark string `json:"remark"` +} + +// ============================================================ + // 完成订单 func TradeComplete(c *fiber.Ctx) error { // 检查权限 @@ -276,6 +302,29 @@ func TradeCompleteByAdmin(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } +// 订单补余额 +func TradeConvertByAdmin(c *fiber.Ctx) error { + // 检查权限 + authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeWrite) + if err != nil { + return err + } + + // 解析请求参数 + var req s.TradeRef + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + // 订单补余额 + err = s.Trade.ConvertTradeToBalance(authCtx.Admin, &req) + if err != nil { + return err + } + + return c.SendStatus(fiber.StatusNoContent) +} + // ============================================================ // 取消订单 @@ -287,14 +336,14 @@ func TradeCancel(c *fiber.Ctx) error { } // 解析请求参数 - req := new(TradeCancelReq) + req := new(s.TradeRef) if err := g.Validator.ParseBody(c, req); err != nil { return err } // 取消交易 - err = s.Trade.CancelTrade(&req.TradeRef) - if err != nil { + err = s.Trade.CancelTrade(req) + if err != nil && err != s.ErrTradeStatusIgnored { slog.Error("取消交易失败", "trade_no", req.TradeNo, "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "取消交易失败"}) } @@ -302,62 +351,63 @@ func TradeCancel(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } -type TradeCancelReq struct { - s.TradeRef +// ============================================================ + +// 结束订单:完成或取消订单 +func TradeFinish(c *fiber.Ctx) error { + // 检查权限 + authCtx, err := auth.GetAuthCtx(c).PermitUser() + if err != nil { + return err + } + + // 解析请求参数 + var req s.TradeRef + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + // 尝试取消交易 + err = s.Trade.CancelTrade(&req) + if err == s.ErrTradeStatusIgnored { + // 尝试完成交易 + err = s.Trade.CompleteTrade(authCtx.User, &req) + if err != nil { + return err + } + + return c.JSON(map[string]m.TradeStatus{ + "status": m.TradeStatusSuccess, + }) + } else if err != nil { + return err + } + + return c.JSON(map[string]m.TradeStatus{ + "status": m.TradeStatusCanceled, + }) } // ============================================================ // 检查订单 func TradeCheck(c *fiber.Ctx) error { - // 检查权限:sse 接口暂时不检查权限 - - // 解析请求参数 - req := new(TradeCheckReq) - if err := g.Validator.ParseQuery(c, req); err != nil { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeRead) + if err != nil { return err } - c.Set(fiber.HeaderContentType, "text/event-stream") - c.Set(fiber.HeaderCacheControl, "no-cache") - c.Set(fiber.HeaderConnection, "keep-alive") - c.Set(fiber.HeaderTransferEncoding, "chunked") - c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + // 解析请求参数 + var req s.TradeRef + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } - expire := env.TradeExpire - interval := 5 - for range expire / interval { - // 检查订单状态 - result, err := s.Trade.CheckTrade(&req.TradeRef) - if err != nil { - slog.Error("检查订单状态失败", "trade_no", req.TradeNo, "error", err) - return - } + // 检查订单状态 + result, err := s.Trade.CheckTrade(&req) + if err != nil { + return err + } - // 写入订单状态 - _, err = fmt.Fprintf(w, "data: %d\n\n", result.Status) - if err != nil { - slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err) - return - } - - err = w.Flush() - if err != nil { - return - } - - // 当订单离开支付状态后结束查询 - if result.Status != m.TradeStatusPending { - return - } - - time.Sleep(time.Duration(interval) * time.Second) - } - })) - - return nil -} - -type TradeCheckReq struct { - s.TradeRef + return c.JSON(result) } diff --git a/web/routes.go b/web/routes.go index d37d22e..8cbb7a3 100644 --- a/web/routes.go +++ b/web/routes.go @@ -66,10 +66,6 @@ func publicRouter(api fiber.Router) { resource := api.Group("/resource") resource.Post("/price", handlers.ResourcePrice) - // 交易 - trade := api.Group("/trade") - trade.Get("/check", handlers.TradeCheck) - // 前台 inquiry := api.Group("/inquiry") inquiry.Post("/create", handlers.CreateInquiry) @@ -144,6 +140,7 @@ func userRouter(api fiber.Router) { trade.Post("/create", handlers.TradeCreate) trade.Post("/complete", handlers.TradeComplete) trade.Post("/cancel", handlers.TradeCancel) + trade.Post("/finish", handlers.TradeFinish) // 账单 bill := api.Group("/bill") @@ -241,6 +238,8 @@ func adminRouter(api fiber.Router) { trade.Post("/page", handlers.PageTradeByAdmin) trade.Post("/page/of-user", handlers.PageTradeOfUserByAdmin) trade.Post("/complete", handlers.TradeCompleteByAdmin) + trade.Post("/update/remark", handlers.TradeUpdateRemarkByAdmin) + trade.Post("/check", handlers.TradeCheck) // bill 账单 var bill = api.Group("/bill") diff --git a/web/services/channel_gost.go b/web/services/channel_gost.go index 8b91b01..8aeee51 100644 --- a/web/services/channel_gost.go +++ b/web/services/channel_gost.go @@ -53,6 +53,7 @@ func (s *channelGostProvider) prepareCreate(ctx *channelCreateContext) (*channel Recorders: []g.GostRecorderConfig{ {Name: "record-http-otel", Record: "recorder.service.handler"}, }, + Limiter: "limiter-8m", } if ctx.AuthWhitelist { diff --git a/web/services/product.go b/web/services/product.go index d7c650e..a0cafde 100644 --- a/web/services/product.go +++ b/web/services/product.go @@ -6,6 +6,7 @@ import ( q "platform/web/queries" "time" + "github.com/shopspring/decimal" "gorm.io/gen/field" ) @@ -20,7 +21,7 @@ func (s *productService) AllProducts() ([]*m.Product, error) { Find() } -func (s *productService) AllProductSaleInfos() ([]*m.Product, error) { +func (s *productService) AllProductSaleInfos() ([]any, error) { products, err := q.Product. Select( q.Product.ID, @@ -43,7 +44,17 @@ func (s *productService) AllProductSaleInfos() ([]*m.Product, error) { pids[i] = p.ID } - skus, err := q.ProductSku. + type SkuInfo struct { + ID int32 `json:"id"` + ProductID int32 `json:"product_id"` + Name string `json:"name"` + Code string `json:"code"` + Price decimal.Decimal `json:"price"` + CountMin decimal.Decimal `json:"count_min"` + Discount float64 `json:"discount"` + } + var skus []*SkuInfo + err = q.ProductSku. Select( q.ProductSku.ID, q.ProductSku.ProductID, @@ -51,29 +62,47 @@ func (s *productService) AllProductSaleInfos() ([]*m.Product, error) { q.ProductSku.Code, q.ProductSku.Price, q.ProductSku.CountMin, + q.ProductDiscount.Discount, ). Where( q.ProductSku.ProductID.In(pids...), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled)), ). + LeftJoin(q.ProductDiscount, q.ProductDiscount.ID.EqCol(q.ProductSku.DiscountId)). Order(q.ProductSku.Sort). - Find() + Scan(&skus) if err != nil { return nil, err } - pmap := make(map[int32]*m.Product, len(products)) + type ProductInfo struct { + m.Product + Skus []*SkuInfo `json:"skus,omitempty"` + } + pmap := make(map[int32]*ProductInfo, len(products)) for _, p := range products { - pmap[p.ID] = p - p.Skus = make([]*m.ProductSku, 0) + pmap[p.ID] = &ProductInfo{Product: *p, Skus: make([]*SkuInfo, 0)} } for _, s := range skus { if p, ok := pmap[s.ProductID]; ok { - p.Skus = append(p.Skus, s) + p.Skus = append(p.Skus, &SkuInfo{ + ID: s.ID, + ProductID: s.ProductID, + Name: s.Name, + Code: s.Code, + Price: s.Price, + CountMin: s.CountMin, + Discount: s.Discount, + }) } } - return products, nil + plist := make([]any, 0, len(pmap)) + for _, p := range pmap { + plist = append(plist, p) + } + + return plist, nil } // 新增产品 diff --git a/web/services/trade.go b/web/services/trade.go index d271a66..d7257e9 100644 --- a/web/services/trade.go +++ b/web/services/trade.go @@ -204,12 +204,7 @@ func (s *tradeService) Create(user *m.User, tradeData *CreateTradeData, productD // 缓存产品数据 w := bytes.Buffer{} gob.NewEncoder(&w).Encode(detail) - err = g.Redis.Set( - context.Background(), - tradeProductKey(tradeNo), - w.Bytes(), - expireIn, - ).Err() + err = g.Redis.Set(context.Background(), tradeProductKey(tradeNo), w.Bytes(), 0).Err() if err != nil { return nil, core.NewServErr("保存购买信息失败", err) } @@ -271,7 +266,8 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str } // 恢复购买信息;如果反序列化失败,检查开头 init 函数中是否注册了对应的 struct 类型 - detailBytes, err := g.Redis.Get(context.Background(), tradeProductKey(interNo)).Bytes() + tradeKey := tradeProductKey(interNo) + detailBytes, err := g.Redis.Get(context.Background(), tradeKey).Bytes() if err != nil { return core.NewServErr("恢复购买信息失败", err) } @@ -344,6 +340,12 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str } } + // 删除缓存 + err = g.Redis.Del(context.Background(), tradeKey).Err() + if err != nil { + return core.NewServErr("删除缓存失败", err) + } + return nil }) if err != nil { @@ -353,6 +355,23 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str return nil } +// 转换交易 +func (s *tradeService) ConvertTradeToBalance(admin *m.Admin, ref *TradeRef) error { + trade, err := q.Trade.Where(q.Trade.InnerNo.Eq(ref.TradeNo)).First() + if err != nil { + return err + } + + user, err := q.User.Where(q.User.ID.Eq(trade.UserID)).First() + if err != nil { + return err + } + + return q.Q.Transaction(func(q *q.Query) error { + return User.UpdateBalance(q, user, trade.Payment, "管理员订单补余额", &admin.ID, nil) + }) +} + // 取消交易 func (s *tradeService) CancelTrade(ref *TradeRef) error { now := time.Now() @@ -394,7 +413,7 @@ func (s *tradeService) CancelTrade(ref *TradeRef) error { }) if err != nil { slog.Debug(fmt.Sprintf("订单无需关闭: %s", err.Error())) - return nil + return ErrTradeStatusIgnored } default: @@ -575,6 +594,12 @@ func (s *tradeService) CheckTrade(ref *TradeRef) (*CheckTradeResult, error) { return &result, nil } +// 更新备注 +func (s *tradeService) UpdateRemark(tradeNo string, remark string) error { + _, err := q.Trade.Where(q.Trade.InnerNo.Eq(tradeNo)).UpdateColumn(q.Trade.Remark, remark) + return err +} + func tradeProductKey(no string) string { return fmt.Sprintf("trade:%s:product", no) } @@ -639,4 +664,5 @@ func (e TradeErr) Error() string { var ( ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式") + ErrTradeStatusIgnored = core.NewBizErr("交易状态已忽略") ) diff --git a/web/tasks/task.go b/web/tasks/task.go index 21aa0cd..3c45a31 100644 --- a/web/tasks/task.go +++ b/web/tasks/task.go @@ -3,10 +3,11 @@ package tasks import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "platform/web/events" - q "platform/web/queries" + m "platform/web/models" s "platform/web/services" "github.com/hibiken/asynq" @@ -24,19 +25,30 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) error { Method: event.Method, } - // 尝试完成交易 - user, err := s.User.Get(q.Q, event.UserId) - if err != nil { - return fmt.Errorf("获取用户失败: %w", err) + // 关闭交易 + err := s.Trade.CancelTrade(&data) + if errors.Is(err, s.ErrTradeStatusIgnored) { + result, err := s.Trade.CheckTrade(&data) + if err != nil { + return fmt.Errorf("检查交易状态失败: %w", err) + } + + switch result.Status { + case m.TradeStatusSuccess: + if err := s.Trade.UpdateRemark(data.TradeNo, "已付款"); err != nil { + slog.Error("添加备注失败", "err", err) + } + case m.TradeStatusCanceled: + slog.Debug("交易已取消", "status", result.Status) + default: + return fmt.Errorf("意外交易状态: %v", result.Status) + } + + return nil } - if err := s.Trade.CompleteTrade(user, &data); err != nil { - slog.Debug("结束交易失败:完成交易失败", "err", err) - - // 交易无法完成,关闭交易 - if err := s.Trade.CancelTrade(&data); err != nil { - return fmt.Errorf("结束交易失败:取消交易失败: %w", err) - } + if err != nil { + return fmt.Errorf("结束交易失败:取消交易失败: %w", err) } return nil