支付相关

This commit is contained in:
Yvan
2026-08-19 18:06:03 +08:00
parent 1ef8ecb7b2
commit e54cbd1207
56 changed files with 8453 additions and 42 deletions
+137
View File
@@ -0,0 +1,137 @@
package handler
import (
"io"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"net/http"
"github.com/gin-gonic/gin"
)
type Payment struct{ service *service.PaymentService }
func NewPayment(service *service.PaymentService) *Payment { return &Payment{service: service} }
func (h *Payment) Configs(c *gin.Context) {
v, err := h.service.Configs(c.Request.Context())
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, v)
}
func (h *Payment) SaveConfig(c *gin.Context) {
var req dto.PaymentConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
if err := h.service.SaveConfig(c.Request.Context(), &biz.PaymentConfig{Provider: req.Provider, Enabled: req.Enabled, Values: req.Config}); err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OK(c)
}
func (h *Payment) Order(c *gin.Context) {
var req dto.PaymentQueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
order, err := h.service.Order(c.Request.Context(), req.Provider, req.TradeNo)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, order)
}
func (h *Payment) Create(c *gin.Context) {
var req dto.PaymentRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
req.ClientIP = c.ClientIP()
result, err := h.service.Create(c.Request.Context(), &biz.PaymentRequest{
Provider: req.Provider, TradeNo: req.TradeNo, Subject: req.Subject,
Amount: req.Amount, OriginalAmount: req.OriginalAmount, Currency: req.Currency, NotifyURL: req.NotifyURL,
ReturnURL: req.ReturnURL, ClientIP: req.ClientIP, Extra: req.Extra,
BusinessType: req.BusinessType, BusinessID: req.BusinessID, PaymentMode: req.PaymentMode,
})
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, result)
}
func (h *Payment) Query(c *gin.Context) {
var req dto.PaymentQueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
result, err := h.service.Query(c.Request.Context(), req.Provider, req.TradeNo)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, result)
}
func (h *Payment) Refund(c *gin.Context) {
var req dto.PaymentRefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
result, err := h.service.Refund(c.Request.Context(), req.Provider, req.TradeNo, req.Amount)
if err != nil {
httpx.Fail(c, err.Error())
return
}
httpx.OKWithData(c, result)
}
func (h *Payment) Callback(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 8<<20)
raw, err := io.ReadAll(c.Request.Body)
if err != nil {
writePaymentCallbackAck(c, biz.DefaultPaymentCallbackAck(c.Param("provider"), false))
return
}
headers := map[string]string{}
for k, v := range c.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
query := map[string]string{}
for k, v := range c.Request.URL.Query() {
if len(v) > 0 {
query[k] = v[0]
}
}
result, err := h.service.Callback(c.Request.Context(), &biz.PaymentCallback{Provider: c.Param("provider"), Headers: headers, Body: raw, Query: query})
if err != nil {
writePaymentCallbackAck(c, biz.CallbackFailure(err, biz.DefaultPaymentCallbackAck(c.Param("provider"), false)))
return
}
ack := biz.DefaultPaymentCallbackAck(c.Param("provider"), true)
if result != nil && result.SuccessAck.StatusCode != 0 {
ack = result.SuccessAck
}
writePaymentCallbackAck(c, ack)
}
func writePaymentCallbackAck(c *gin.Context, ack biz.PaymentCallbackAck) {
if ack.StatusCode == 0 {
ack.StatusCode = http.StatusInternalServerError
}
if ack.ContentType != "" {
c.Header("Content-Type", ack.ContentType)
}
c.Status(ack.StatusCode)
if len(ack.Body) > 0 {
_, _ = c.Writer.Write(ack.Body)
}
}
+29
View File
@@ -0,0 +1,29 @@
package handler
import (
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"kra/internal/biz"
)
func TestWritePaymentCallbackAckDoesNotExposeError(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
writePaymentCallbackAck(context, biz.PaymentCallbackAck{
StatusCode: 500,
ContentType: "text/plain; charset=utf-8",
Body: []byte("failure"),
})
if recorder.Code != 500 {
t.Fatalf("status = %d, want 500", recorder.Code)
}
if recorder.Body.String() != "failure" {
t.Fatalf("body = %q, want fixed failure ack", recorder.Body.String())
}
if recorder.Body.String() == "internal order resolver details" {
t.Fatal("internal error was exposed in callback response")
}
}
+3 -2
View File
@@ -8,6 +8,7 @@ type Set struct {
Organization *Organization
Announcement *Announcement
Email *Email
Payment *Payment
Task *Task
Media *Media
Audit *Audit
@@ -23,6 +24,6 @@ type Set struct {
Session *Session
}
func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session) *Set {
return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session}
func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, payment *Payment, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session) *Set {
return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Payment: payment, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session}
}