feat(server/pay): 订单列表端点 + 分币种展示价 + 富字段详情
- GET /v1/pay/orders 订单列表(Store.ListByUser,本地台账倒序) - catalog 增 PriceUsdtMicro(USDT 一口价展示) + SettlementCurrency/DisplayAmountMinor - 下单按支付方式落展示金额/结算币种(crypto→USDT / 法币→CNY),webhook 覆盖实际 - GetOrder 详情回带 sku/plan/method/amount/currency/created_at/paid_at - 新增 List 越权/倒序/富字段 + 分币种展示金额 + 详情富字段测试 - 修 3 个 pre-existing 迁移测试(renumber 后 21→23 遗留) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -109,7 +109,8 @@ func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
if _, ok := CatalogBySKU(req.SKU); !ok || req.Method == "" {
|
||||
item, ok := CatalogBySKU(req.SKU)
|
||||
if !ok || req.Method == "" {
|
||||
apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -129,17 +130,73 @@ func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
writePayErr(w, err)
|
||||
return
|
||||
}
|
||||
if err := h.store.Insert(ctx, uid, bizRef, req.SKU, res.OrderNo, req.Method); err != nil {
|
||||
// 下单即落展示金额/结算币种(按支付方式);webhook 到账后由实际结算金额覆盖。
|
||||
amount := DisplayAmountMinor(item, req.Method)
|
||||
currency := SettlementCurrency(req.Method)
|
||||
if err := h.store.Insert(ctx, uid, bizRef, req.SKU, res.OrderNo, req.Method, amount, currency); err != nil {
|
||||
// 不吞单:webhook 会按 biz_ref 兜底补台账,这里记日志便于追查。
|
||||
slog.Error("pay: 台账写入失败(webhook 将按 biz_ref 兜底)", "order_no", res.OrderNo, "err", err)
|
||||
}
|
||||
writeJSON(w, sessionResponse{OrderNo: res.OrderNo, Session: res.Session})
|
||||
}
|
||||
|
||||
// ─── GET /v1/pay/orders (列表) ──────────────────────────────────────────────
|
||||
|
||||
// orderView 是订单的台账视图(列表/详情共用富字段;金额为结算币种 minor:CNY 分 / USDT 微元)。
|
||||
type orderView struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
SKU string `json:"sku"`
|
||||
Plan string `json:"plan"`
|
||||
Days int `json:"days"`
|
||||
Method string `json:"method"`
|
||||
Status string `json:"status"` // 本地台账:created | paid | canceled
|
||||
AmountMinor int64 `json:"amount_minor"`
|
||||
Currency string `json:"currency"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
CreatedAt string `json:"created_at"` // RFC3339
|
||||
PaidAt *string `json:"paid_at,omitempty"`
|
||||
}
|
||||
|
||||
func orderViewFromRow(row *PurchaseRow) orderView {
|
||||
v := orderView{
|
||||
OrderNo: row.OutTradeNo, SKU: row.SKU, Method: row.Method, Status: row.Status,
|
||||
AmountMinor: row.AmountMinor, Currency: row.Currency, Channel: row.Channel,
|
||||
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
if it, ok := CatalogBySKU(row.SKU); ok {
|
||||
v.Plan, v.Days = it.Plan, it.Days
|
||||
}
|
||||
if row.PaidAt.Valid {
|
||||
s := row.PaidAt.Time.UTC().Format(time.RFC3339)
|
||||
v.PaidAt = &s
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// List 列出当前用户历史订单(倒序;纯本地台账,不逐单回查 pay 上游)。
|
||||
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
uid, ok := auth.UserIDFromContext(ctx)
|
||||
if !ok {
|
||||
apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
rows, err := h.store.ListByUser(ctx, uid, 100)
|
||||
if err != nil {
|
||||
apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
|
||||
return
|
||||
}
|
||||
out := make([]orderView, 0, len(rows))
|
||||
for i := range rows {
|
||||
out = append(out, orderViewFromRow(&rows[i]))
|
||||
}
|
||||
writeJSON(w, map[string]any{"orders": out})
|
||||
}
|
||||
|
||||
// ─── GET /v1/pay/orders/{orderNo} ───────────────────────────────────────────
|
||||
|
||||
type orderStatusResponse struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
orderView
|
||||
PayStatus string `json:"pay_status"` // pay 侧状态词汇原样透传
|
||||
Activated bool `json:"activated"` // 本地台账已消费(权益已开通)——客户端轮询以此为成功判据
|
||||
ExpiresAt *string `json:"expires_at,omitempty"`
|
||||
@@ -166,7 +223,7 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
|
||||
writePayErr(w, err)
|
||||
return
|
||||
}
|
||||
resp := orderStatusResponse{OrderNo: orderNo, PayStatus: st.Status, Activated: row.Status == "paid"}
|
||||
resp := orderStatusResponse{orderView: orderViewFromRow(row), PayStatus: st.Status, Activated: row.Status == "paid"}
|
||||
if row.SubID.Valid {
|
||||
if exp, err := h.store.SubscriptionExpiry(ctx, row.SubID.Int64); err == nil {
|
||||
s := exp.UTC().Format(time.RFC3339)
|
||||
|
||||
Reference in New Issue
Block a user