198 lines
6.1 KiB
Go
198 lines
6.1 KiB
Go
package recharge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
kratoserrors "github.com/go-kratos/kratos/v3/errors"
|
|
|
|
bizpayment "kra/internal/biz/payment"
|
|
)
|
|
|
|
const BusinessType = "live_recharge"
|
|
|
|
var (
|
|
ErrNotFound = kratoserrors.NotFound("RECHARGE_ORDER_NOT_FOUND", "recharge order not found")
|
|
ErrInvalid = kratoserrors.BadRequest("RECHARGE_INVALID", "invalid recharge data")
|
|
ErrRefundUnsupported = kratoserrors.BadRequest("RECHARGE_REFUND_UNSUPPORTED", "recharge refunds require manual balance reconciliation")
|
|
)
|
|
|
|
type Plan struct {
|
|
ID uint
|
|
Sort int
|
|
Diamonds, AmountCents int64
|
|
Enabled bool
|
|
}
|
|
|
|
// Order owns recharge facts. Payment state comes from KRA's payment order.
|
|
type Order struct {
|
|
ID uint
|
|
OrderNo string
|
|
UserID uint
|
|
Username, Nickname string
|
|
Diamonds, AmountCents int64
|
|
Provider, ProviderTradeNo string
|
|
PaymentStatus, FulfillmentStatus, RefundStatus string
|
|
CreatedAt time.Time
|
|
CreditedAt *time.Time
|
|
}
|
|
|
|
type Checkout struct {
|
|
Provider string
|
|
NotifyURL string
|
|
ReturnURL string
|
|
ClientIP string
|
|
UserAgent string
|
|
DeviceID string
|
|
Extra map[string]any
|
|
}
|
|
|
|
type Filter struct {
|
|
OrderNo, UserKeyword, PaymentStatus, Provider string
|
|
CreatedAt []time.Time
|
|
}
|
|
|
|
type ListOptions struct {
|
|
Offset, Limit int
|
|
OrderBy string
|
|
}
|
|
|
|
type Summary struct{ Count, PaidCount, AmountCents int64 }
|
|
|
|
type Repo interface {
|
|
Plans(context.Context) ([]*Plan, error)
|
|
SavePlans(context.Context, []*Plan) error
|
|
List(context.Context, Filter, ListOptions) ([]*Order, int64, *Summary, error)
|
|
Get(context.Context, string) (*Order, error)
|
|
Create(context.Context, uint, uint) (*Order, error)
|
|
MarkPaid(context.Context, string) (*Order, error)
|
|
}
|
|
|
|
type Usecase struct {
|
|
repo Repo
|
|
payments *bizpayment.PaymentUsecase
|
|
}
|
|
|
|
func NewUsecase(repo Repo, payments *bizpayment.PaymentUsecase) (*Usecase, error) {
|
|
uc := &Usecase{repo: repo, payments: payments}
|
|
if payments == nil {
|
|
return nil, fmt.Errorf("充值支付服务未接入")
|
|
}
|
|
if err := payments.RegisterBusinessModule(uc); err != nil {
|
|
return nil, err
|
|
}
|
|
return uc, nil
|
|
}
|
|
|
|
func (uc *Usecase) Plans(ctx context.Context) ([]*Plan, error) { return uc.repo.Plans(ctx) }
|
|
|
|
func (uc *Usecase) SavePlans(ctx context.Context, plans []*Plan) error {
|
|
if len(plans) < 1 || len(plans) > 8 {
|
|
return ErrInvalid
|
|
}
|
|
for i, p := range plans {
|
|
if p == nil || p.Diamonds < 1 || p.Diamonds > 99999 || p.AmountCents < 1 || p.AmountCents > 999900 {
|
|
return ErrInvalid
|
|
}
|
|
if p.Sort == 0 {
|
|
p.Sort = i + 1
|
|
}
|
|
p.Enabled = true
|
|
}
|
|
return uc.repo.SavePlans(ctx, plans)
|
|
}
|
|
|
|
func (uc *Usecase) List(ctx context.Context, f Filter, o ListOptions) ([]*Order, int64, *Summary, error) {
|
|
if o.Limit < 1 || o.Limit > 100 {
|
|
o.Limit = 10
|
|
}
|
|
return uc.repo.List(ctx, f, o)
|
|
}
|
|
|
|
func (uc *Usecase) Get(ctx context.Context, no string) (*Order, error) {
|
|
if strings.TrimSpace(no) == "" {
|
|
return nil, ErrNotFound
|
|
}
|
|
return uc.repo.Get(ctx, no)
|
|
}
|
|
|
|
func (uc *Usecase) Create(ctx context.Context, userID, planID uint, checkout Checkout) (*Order, *bizpayment.PaymentResult, error) {
|
|
checkout.Provider = strings.TrimSpace(checkout.Provider)
|
|
if userID == 0 || planID == 0 || checkout.Provider == "" || checkout.Provider == bizpayment.PaymentInternal || !bizpayment.IsSupportedPaymentProvider(checkout.Provider) {
|
|
return nil, nil, ErrInvalid
|
|
}
|
|
order, err := uc.repo.Create(ctx, userID, planID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
extra := make(map[string]any, len(checkout.Extra)+2)
|
|
for key, value := range checkout.Extra {
|
|
extra[key] = value
|
|
}
|
|
extra["userId"] = order.UserID
|
|
extra["diamonds"] = order.Diamonds
|
|
result, err := uc.payments.Create(ctx, &bizpayment.PaymentRequest{
|
|
Provider: checkout.Provider, TradeNo: order.OrderNo,
|
|
BusinessType: BusinessType, BusinessID: order.OrderNo,
|
|
NotifyURL: checkout.NotifyURL, ReturnURL: checkout.ReturnURL,
|
|
ClientIP: checkout.ClientIP, UserAgent: checkout.UserAgent, DeviceID: checkout.DeviceID,
|
|
Extra: extra,
|
|
})
|
|
if err != nil {
|
|
return order, nil, err
|
|
}
|
|
refreshed, err := uc.repo.Get(ctx, order.OrderNo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return refreshed, result, nil
|
|
}
|
|
|
|
func (uc *Usecase) Type() string { return BusinessType }
|
|
|
|
func (uc *Usecase) PreparePayment(ctx context.Context, provider, tradeNo, businessID string) (*bizpayment.PaymentIntent, error) {
|
|
if strings.TrimSpace(provider) == "" || tradeNo == "" || businessID != tradeNo {
|
|
return nil, bizpayment.ErrPaymentOrderConflict
|
|
}
|
|
order, err := uc.repo.Get(ctx, businessID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if order.OrderNo != tradeNo || order.CreditedAt != nil {
|
|
return nil, bizpayment.ErrPaymentOrderConflict
|
|
}
|
|
extra, err := json.Marshal(map[string]any{"userId": order.UserID, "diamonds": order.Diamonds})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &bizpayment.PaymentIntent{
|
|
Provider: provider, TradeNo: tradeNo, BusinessType: BusinessType, BusinessID: businessID,
|
|
Subject: fmt.Sprintf("充值 %d 钻石", order.Diamonds), Amount: order.AmountCents,
|
|
OriginalAmount: order.AmountCents, Currency: "CNY", PaymentMode: bizpayment.PaymentModeExternal,
|
|
Extra: extra,
|
|
}, nil
|
|
}
|
|
|
|
func (uc *Usecase) Fulfill(ctx context.Context, confirmation *bizpayment.PaymentConfirmation) error {
|
|
if confirmation == nil || confirmation.BusinessType != BusinessType || confirmation.BusinessID == "" || confirmation.TradeNo != confirmation.BusinessID || !strings.EqualFold(confirmation.Currency, "CNY") {
|
|
return ErrInvalid
|
|
}
|
|
order, err := uc.repo.Get(ctx, confirmation.BusinessID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if order.AmountCents != confirmation.Amount {
|
|
return bizpayment.ErrPaymentOrderConflict
|
|
}
|
|
_, err = uc.repo.MarkPaid(ctx, order.OrderNo)
|
|
return err
|
|
}
|
|
|
|
// Refunds stay disabled until diamonds can be reserved across an async refund.
|
|
func (uc *Usecase) AuthorizeRefund(context.Context, *bizpayment.PaymentOrder, int64) error {
|
|
return ErrRefundUnsupported
|
|
}
|