feat(server): webhook 薄壳化——保留 X-Pangolin HMAC 协议,铸码走库单事务原语(#codes-lib)

This commit is contained in:
wangjia
2026-07-10 14:45:08 +08:00
parent 0822d22e2c
commit d1e2fed563
3 changed files with 78 additions and 76 deletions
+11 -12
View File
@@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -14,6 +15,7 @@ import (
"time"
"github.com/redis/go-redis/v9"
libcodes "github.com/wangjia/codes"
"github.com/wangjia/pangolin/server/internal/apierr"
)
@@ -132,25 +134,22 @@ func (h *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
hash := Hash(canonical)
// --- Idempotent code insertion ---
// --- Idempotent code insertion (via shared lib primitives) ---
ctx := r.Context()
planID, err := h.store.GetPlanID(ctx, PlanCode(strings.ToLower(payload.Plan)))
// 未知 plan 先拒(行为不变,旧 webhook.go:138-142)。
if _, err := h.store.GetPlanID(ctx, PlanCode(strings.ToLower(payload.Plan))); err != nil {
apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
return
}
ent, err := libcodes.NewDurationEntitlement(strings.ToLower(payload.Plan), payload.DurationDays)
if err != nil {
apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
return
}
// Create batch (one per webhook call; the store may aggregate externally).
batchID, err := h.store.CreateBatch(ctx, ChannelStore, "webhook", payload.Note)
if err != nil {
apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
return
}
err = h.store.CreateCode(ctx, hash, planID, payload.DurationDays, batchID)
if err == ErrDuplicate {
// Same code_hash already exists idempotent, do not error.
err = h.store.MintOne(ctx, hash, ent, ChannelStore, "webhook", payload.Note)
if errors.Is(err, ErrDuplicate) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"status": "already_exists"})