From b9e2f8c2da6d2042aef6904db6593fff8e06246b Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 12 Jul 2026 17:18:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(server/pay):=20=E8=AE=A2=E5=8D=95=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=B1=82=E8=BF=87=E6=9C=9F=E6=80=81(created=20?= =?UTF-8?q?=E8=B6=85=20TTL=20=E6=98=BE=20expired,=E9=9B=B6=E8=BF=81?= =?UTF-8?q?=E7=A7=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- server/internal/pay/handler.go | 6 +++++ server/internal/pay/handler_test.go | 36 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/server/internal/pay/handler.go b/server/internal/pay/handler.go index 6499f34..bdd32db 100644 --- a/server/internal/pay/handler.go +++ b/server/internal/pay/handler.go @@ -157,6 +157,9 @@ type orderView struct { PaidAt *string `json:"paid_at,omitempty"` } +// 未付订单展示层过期窗口(近似 pay 会话时效;仅影响列表/详情显示,不改台账)。 +const orderPendingTTL = 30 * time.Minute + func orderViewFromRow(row *PurchaseRow) orderView { v := orderView{ OrderNo: row.OutTradeNo, SKU: row.SKU, Method: row.Method, Status: row.Status, @@ -178,6 +181,9 @@ func orderViewFromRow(row *PurchaseRow) orderView { s := row.PaidAt.Time.UTC().Format(time.RFC3339) v.PaidAt = &s } + if row.Status == "created" && row.CreatedAt.Add(orderPendingTTL).Before(time.Now()) { + v.Status = "expired" // 展示层:台账仍 created,可继续支付/取消 + } return v } diff --git a/server/internal/pay/handler_test.go b/server/internal/pay/handler_test.go index 805fd84..cc65913 100644 --- a/server/internal/pay/handler_test.go +++ b/server/internal/pay/handler_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/go-chi/chi/v5" @@ -289,3 +290,38 @@ func TestCatalog(t *testing.T) { t.Fatalf("catalog = %+v", resp.Items) } } + +// 老 created 单超 TTL → 列表视图显 expired;近期 created → 仍 created。 +func TestList_ExpiredWhenStale(t *testing.T) { + router, st := newHandlerRig(t, nil) + ctx := context.Background() + // 直接构造两条 created 单,改 created_at 到很久以前 / 刚刚 + if err := st.Insert(ctx, 1, "uuid-1", "pro_month", "old", "alipay", 2999, "CNY"); err != nil { + t.Fatal(err) + } + if err := st.Insert(ctx, 1, "uuid-1", "pro_month", "fresh", "alipay", 2999, "CNY"); err != nil { + t.Fatal(err) + } + // 把 old 的 created_at 回拨到 TTL 之前 + if _, err := st.db.ExecContext(ctx, + `UPDATE pay_purchases SET created_at = ? WHERE out_trade_no = 'old'`, + time.Now().Add(-2*orderPendingTTL).UTC()); err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/orders", nil), 1)) + var resp struct { + Orders []orderView `json:"orders"` + } + _ = json.Unmarshal(w.Body.Bytes(), &resp) + got := map[string]string{} + for _, o := range resp.Orders { + got[o.OrderNo] = o.Status + } + if got["old"] != "expired" { + t.Fatalf("old 应过期, got %q", got["old"]) + } + if got["fresh"] != "created" { + t.Fatalf("fresh 应 created, got %q", got["fresh"]) + } +}