增加支付逻辑
This commit is contained in:
1 parent
4ac786aafb
commit
dfb2c91b8d
77 files changed
+6370
-45
No files matched your search
@@ -0,0 +1,199 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments"
|
||||
)
|
||||
|
||||
type PaymentService interface {
|
||||
Options() payments.Options
|
||||
Create(context.Context, string, string, string, int64) (payments.Order, error)
|
||||
Get(context.Context, string, string) (payments.Order, error)
|
||||
List(context.Context, string) ([]payments.Order, error)
|
||||
Notify(context.Context, *http.Request) error
|
||||
}
|
||||
|
||||
type paymentHandler struct {
|
||||
authorizer *PlatformAuthorizer
|
||||
service PaymentService
|
||||
publicOrigin string
|
||||
}
|
||||
|
||||
func NewPaymentHandler(authorizer *PlatformAuthorizer, service PaymentService, publicOrigin string) http.Handler {
|
||||
return &paymentHandler{authorizer: authorizer, service: service, publicOrigin: publicOrigin}
|
||||
}
|
||||
|
||||
var paymentIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`)
|
||||
|
||||
func (h *paymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if r.URL.Path == "/api/payments/wechat/refund-notify" {
|
||||
h.refundNotify(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/api/payments/admin/") {
|
||||
h.adminPayments(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api/payments/wechat/notify" {
|
||||
if !allow(w, r, http.MethodPost) {
|
||||
return
|
||||
}
|
||||
if h.service == nil {
|
||||
writeJSON(w, 503, map[string]string{"code": "FAIL", "message": "支付服务未启用"})
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
|
||||
if err := h.service.Notify(r.Context(), r); err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, payments.ErrVerification) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
// Acknowledge only after durable settlement; never leak decrypted data.
|
||||
writeJSON(w, status, map[string]string{"code": "FAIL", "message": "支付通知处理失败"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
optionsPath := r.URL.Path == "/api/payments/options"
|
||||
ordersPath := r.URL.Path == "/api/payments/orders"
|
||||
orderID := strings.TrimPrefix(r.URL.Path, "/api/payments/orders/")
|
||||
itemPath := strings.HasPrefix(r.URL.Path, "/api/payments/orders/") && len(orderID) <= 32 && paymentIdentifier.MatchString(orderID)
|
||||
if !optionsPath && !ordersPath && !itemPath {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet && !(ordersPath && r.Method == http.MethodPost) {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
session, err := h.authorizer.Authorize(r, PlatformApp)
|
||||
if err != nil {
|
||||
writePaymentError(w, err)
|
||||
return
|
||||
}
|
||||
canRecharge := session.User.OrganizationID != "" && (session.User.Role == "organization_admin" || session.User.Role == "super_admin")
|
||||
if optionsPath {
|
||||
options := payments.Options{PresetAmountsFen: []int64{}}
|
||||
if h.service != nil {
|
||||
options = h.service.Options()
|
||||
}
|
||||
options.CanRecharge = options.Enabled && canRecharge
|
||||
writeJSON(w, http.StatusOK, options)
|
||||
return
|
||||
}
|
||||
if !canRecharge {
|
||||
writeAPIError(w, http.StatusForbidden, "需要所属组织的管理员权限。")
|
||||
return
|
||||
}
|
||||
if h.service == nil {
|
||||
writePaymentError(w, payments.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
org := session.User.OrganizationID
|
||||
if itemPath {
|
||||
order, err := h.service.Get(r.Context(), org, orderID)
|
||||
if err != nil {
|
||||
writePaymentError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"order": publicPaymentOrder(order)})
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet {
|
||||
orders, err := h.service.List(r.Context(), org)
|
||||
if err != nil {
|
||||
writePaymentError(w, err)
|
||||
return
|
||||
}
|
||||
if orders == nil {
|
||||
orders = []payments.Order{}
|
||||
}
|
||||
for i := range orders {
|
||||
orders[i] = publicPaymentOrder(orders[i])
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"orders": orders})
|
||||
return
|
||||
}
|
||||
if !h.sameOrigin(r) {
|
||||
writeAPIError(w, http.StatusForbidden, "请从本站发起充值。")
|
||||
return
|
||||
}
|
||||
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil || mediaType != "application/json" {
|
||||
writeAPIError(w, http.StatusUnsupportedMediaType, "请使用 JSON 请求。")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
AmountFen int64 `json:"amountFen"`
|
||||
RequestKey string `json:"requestKey"`
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 4096)
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&body); err != nil {
|
||||
writeAPIError(w, 400, "充值参数无效,金额必须是整数分。")
|
||||
return
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); err != io.EOF || !paymentIdentifier.MatchString(body.RequestKey) {
|
||||
writeAPIError(w, 400, "充值请求标识无效。")
|
||||
return
|
||||
}
|
||||
order, err := h.service.Create(r.Context(), org, session.User.ID, body.RequestKey, body.AmountFen)
|
||||
if err != nil {
|
||||
writePaymentError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"order": publicPaymentOrder(order)})
|
||||
}
|
||||
|
||||
func publicPaymentOrder(order payments.Order) payments.Order {
|
||||
if order.Status != payments.StatusPending {
|
||||
order.CodeURL = ""
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
func (h *paymentHandler) sameOrigin(r *http.Request) bool {
|
||||
if site := r.Header.Get("Sec-Fetch-Site"); site != "" && site != "same-origin" && site != "none" {
|
||||
return false
|
||||
}
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return true
|
||||
}
|
||||
actual, err := url.Parse(origin)
|
||||
if err != nil || actual.User != nil || actual.RawQuery != "" || actual.Fragment != "" || actual.Path != "" {
|
||||
return false
|
||||
}
|
||||
expected, err := url.Parse(h.publicOrigin)
|
||||
return err == nil && expected.Host != "" && actual.Scheme == expected.Scheme && strings.EqualFold(actual.Host, expected.Host)
|
||||
}
|
||||
|
||||
func writePaymentError(w http.ResponseWriter, err error) {
|
||||
status, message := http.StatusInternalServerError, "支付服务暂时不可用,请稍后重试。"
|
||||
var authErr *PlatformAuthError
|
||||
switch {
|
||||
case errors.As(err, &authErr):
|
||||
status, message = authErr.Status, authErr.Message
|
||||
case errors.Is(err, payments.ErrNotFound):
|
||||
status, message = 404, "充值订单不存在。"
|
||||
case errors.Is(err, payments.ErrConflict):
|
||||
status, message = 409, "充值请求与已有订单不一致,请刷新后重试。"
|
||||
case errors.Is(err, payments.ErrInvalidAmount):
|
||||
status, message = 400, "充值金额不在允许范围内。"
|
||||
case errors.Is(err, payments.ErrUnavailable):
|
||||
status, message = 503, "微信支付尚未启用。"
|
||||
}
|
||||
writeAPIError(w, status, message)
|
||||
}
|
||||
Reference in new issue
Block a user