From 75b8ecfaa3d0657758dbf381460865d720580b57 Mon Sep 17 00:00:00 2001 From: tbxark Date: Wed, 30 Oct 2024 10:36:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=81=E8=A3=85=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E7=BB=84=E4=BB=B6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/logic/payment/README.md | 95 ++++++++++++++++++++++++++++++++++++ pkg/logic/payment/errors.go | 20 ++++++++ pkg/logic/payment/models.go | 40 +++++++++++++++ pkg/logic/payment/payment.go | 55 +++++++++++++++++++++ pkg/logic/payment/status.go | 43 ++++++++++++++++ 5 files changed, 253 insertions(+) create mode 100644 pkg/logic/payment/README.md create mode 100644 pkg/logic/payment/errors.go create mode 100644 pkg/logic/payment/models.go create mode 100644 pkg/logic/payment/payment.go create mode 100644 pkg/logic/payment/status.go diff --git a/pkg/logic/payment/README.md b/pkg/logic/payment/README.md new file mode 100644 index 0000000..b4cc396 --- /dev/null +++ b/pkg/logic/payment/README.md @@ -0,0 +1,95 @@ +# Payment Logic + +## 交易流程 + +1. 支付服务根据支付方式(Method)选择对应的支付提供商(Provider) +2. 支付提供商验证请求参数的合法性(金额、货币等) +3. 支付提供商创建支付交易,返回支付URL或其他支付凭证 +4. 用户完成支付后,支付提供商通过CallbackURL回调通知支付结果,或者用户主动访问支付URL完成支付 +5. 支付服务验证回调参数,更新支付状态 +6. 支付状态变更记录被保存在StatusChanges中,包含状态变更的时间和原因 +7. 如需退款,通过RefundPayment接口向支付提供商发起退款请求 + +### 状态流转: +- 初始状态:pending(待支付) +- 支付完成:success(支付成功) +- 支付失败:failed(支付失败) +- 交易取消:canceled(已取消) +- 退款完成:refunded(已退款) + + +## 建议数据库结构 + +### 1. 支付主表 +- 存储支付的基本信息 +- 记录当前支付状态 +- 保存支付相关的所有核心数据 +```sql +CREATE TABLE payments ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + transaction_id VARCHAR(64) UNIQUE NOT NULL, -- 外部交易ID + amount DECIMAL(20,2) NOT NULL, -- 支付金额 + currency VARCHAR(10) NOT NULL, -- 货币类型 + method VARCHAR(32) NOT NULL, -- 支付方式 + description TEXT, -- 支付描述 + callback_url VARCHAR(255), -- 回调URL + status VARCHAR(20) NOT NULL, -- 支付状态 + payment_url VARCHAR(255), -- 支付URL + error_message TEXT, -- 错误信息 + metadata JSON, -- 元数据 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +### 2. 状态变更记录表 +- 记录支付状态的所有变更历史 +- 用于追踪支付状态的完整变更链路 +- 便于审计和问题排查 +```sql +CREATE TABLE payment_status_changes ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + payment_id BIGINT NOT NULL, -- 关联支付ID + from_status VARCHAR(20) NOT NULL, -- 原状态 + to_status VARCHAR(20) NOT NULL, -- 新状态 + reason TEXT, -- 变更原因 + changed_at TIMESTAMP NOT NULL, -- 变更时间 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (payment_id) REFERENCES payments(id) +); +``` + +### 3. 退款记录表 +- 记录退款相关的信息 +- 支持部分退款的场景 +- 记录每笔退款的处理状态 +```sql +CREATE TABLE payment_refunds ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + payment_id BIGINT NOT NULL, -- 关联支付ID + amount DECIMAL(20,2) NOT NULL, -- 退款金额 + status VARCHAR(20) NOT NULL, -- 退款状态 + reason TEXT, -- 退款原因 + metadata JSON, -- 元数据 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (payment_id) REFERENCES payments(id) +); +``` + +### 4. 回调记录表 +- 记录支付提供商的回调请求 +- 用于追踪回调处理状态 +- 便于重试和问题排查 +```sql +CREATE TABLE payment_callbacks ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + payment_id BIGINT NOT NULL, -- 关联支付ID + callback_params JSON NOT NULL, -- 回调参数 + processed_at TIMESTAMP, -- 处理时间 + is_success BOOLEAN NOT NULL, -- 处理是否成功 + error_message TEXT, -- 错误信息 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (payment_id) REFERENCES payments(id) +); +``` diff --git a/pkg/logic/payment/errors.go b/pkg/logic/payment/errors.go new file mode 100644 index 0000000..36f3446 --- /dev/null +++ b/pkg/logic/payment/errors.go @@ -0,0 +1,20 @@ +package payment + +import "errors" + +var ( + ErrorPaymentNotFound = errors.New("payment not found") + ErrorInvalidAmount = errors.New("invalid payment amount") + ErrorInvalidCurrency = errors.New("invalid currency") + ErrorProviderNotInitialized = errors.New("payment provider not initialized") + + ErrorCreateUnsupported = errors.New("create payment not supported") + ErrorQueryUnsupported = errors.New("query payment not supported") + ErrorRefundUnsupported = errors.New("refund payment not supported") + + ErrorInvalidCallbackURL = errors.New("invalid callback URL") + ErrorInvalidMetadata = errors.New("invalid metadata") + ErrorDuplicateTransaction = errors.New("duplicate transaction ID") + ErrorRefundExceedsPayment = errors.New("refund amount exceeds payment amount") + ErrorInvalidPaymentStatus = errors.New("invalid payment status for operation") +) diff --git a/pkg/logic/payment/models.go b/pkg/logic/payment/models.go new file mode 100644 index 0000000..bc8e538 --- /dev/null +++ b/pkg/logic/payment/models.go @@ -0,0 +1,40 @@ +package payment + +import ( + "encoding/json" + "time" +) + +type Amount = json.Number + +type Metadata map[string]interface{} + +type Request struct { + Amount Amount `json:"amount"` // 金额 + Currency string `json:"currency"` // 货币 + Method string `json:"method"` // 支付方式 + Description string `json:"description"` // 描述 + CallbackURL string `json:"callback_url"` // 回调URL + Metadata Metadata `json:"metadata"` // 元数据 +} + +type Response struct { + TransactionID string `json:"transaction_id"` // 外部交易ID + Status Status `json:"status"` // 支付状态 + Error string `json:"error,omitempty"` // 错误信息 + PaymentURL string `json:"payment_url,omitempty"` // 支付URL + Metadata Metadata `json:"metadata"` // 元数据 +} + +type StatusChange struct { + FromStatus Status `json:"from_status"` // 从状态 + ToStatus Status `json:"to_status"` // 到状态 + ChangedAt time.Time `json:"changed_at"` // 变更时间 + Reason string `json:"reason,omitempty"` // 变更原因 +} + +type Record struct { + Request *Request `json:"request"` // 请求 + Response *Response `json:"response"` // 最新响应 + StatusChanges []*StatusChange `json:"status_changes"` // 状态变更记录 +} diff --git a/pkg/logic/payment/payment.go b/pkg/logic/payment/payment.go new file mode 100644 index 0000000..df5de0a --- /dev/null +++ b/pkg/logic/payment/payment.go @@ -0,0 +1,55 @@ +package payment + +import ( + "context" +) + +type Provider interface { + Initialize(config map[string]interface{}) error + CreatePayment(ctx context.Context, req *Request) (*Response, error) + QueryPayment(ctx context.Context, transactionID string) (*Response, error) + RefundPayment(ctx context.Context, transactionID string, amount Amount) error + ValidateCallback(ctx context.Context, params map[string]interface{}) (*Response, error) +} + +type BaseService struct { + providers map[string]Provider +} + +func NewBaseService() *BaseService { + return &BaseService{ + providers: make(map[string]Provider), + } +} + +func (s *BaseService) RegisterProvider(method string, provider Provider) { + s.providers[method] = provider +} + +func (s *BaseService) GetProvider(method string) (Provider, bool) { + provider, ok := s.providers[method] + return provider, ok +} +func (s *BaseService) ProcessPayment(ctx context.Context, req *Request) (*Response, error) { + provider, ok := s.providers[req.Method] + if !ok { + return nil, ErrorPaymentNotFound + } + return provider.CreatePayment(ctx, req) +} + +func (s *BaseService) QueryPayment(ctx context.Context, method, transactionID string) (*Response, error) { + provider, ok := s.providers[method] + if !ok { + return nil, ErrorPaymentNotFound + } + return provider.QueryPayment(ctx, transactionID) +} + +func (s *BaseService) RefundPayment(ctx context.Context, method, transactionID string, amount Amount) error { + provider, ok := s.providers[method] + if !ok { + return ErrorPaymentNotFound + } + return provider.RefundPayment(ctx, transactionID, amount) +} diff --git a/pkg/logic/payment/status.go b/pkg/logic/payment/status.go new file mode 100644 index 0000000..5299f5a --- /dev/null +++ b/pkg/logic/payment/status.go @@ -0,0 +1,43 @@ +package payment + +type Status string + +type StatusTransitionPermission = map[Status][]Status + +const ( + StatusPending Status = "pending" + StatusSuccess Status = "success" + StatusFailed Status = "failed" + StatusCanceled Status = "canceled" + StatusRefunded Status = "refunded" +) + +var ( + DefaultStatusTransitionPermission = StatusTransitionPermission{ + StatusPending: {StatusSuccess, StatusFailed, StatusCanceled}, + StatusSuccess: {StatusRefunded}, + StatusFailed: {}, + StatusCanceled: {}, + StatusRefunded: {}, + } + RecoveryStatusTransitionPermission = StatusTransitionPermission{ + StatusPending: {StatusSuccess, StatusFailed, StatusCanceled}, + StatusSuccess: {StatusRefunded}, + StatusFailed: {StatusPending}, + StatusCanceled: {StatusPending}, + StatusRefunded: {StatusSuccess}, + } +) + +func (s Status) CanTransitionTo(permission StatusTransitionPermission, target Status) bool { + permissions, ok := permission[s] + if !ok { + return false + } + for _, status := range permissions { + if status == target { + return true + } + } + return false +}