mirror of
https://github.com/labring/sealos.git
synced 2026-10-30 18:09:56 +08:00
Corporate (#5726)
* support_pay_refund * corporate --------- Co-authored-by: wj <3638877@qq.com>
This commit is contained in:
@@ -913,6 +913,15 @@ func (c *Cockroach) updateWithAccount(userUID uuid.UUID, isDeduction, add, isAct
|
||||
return HandleUpdateResult(result, types.Account{}.TableName())
|
||||
}
|
||||
|
||||
func (c *Cockroach) UpdateWithAccount(
|
||||
userUID uuid.UUID,
|
||||
isDeduction, add, isActive bool,
|
||||
amount int64,
|
||||
db *gorm.DB,
|
||||
) error {
|
||||
return c.updateWithAccount(userUID, isDeduction, add, isActive, amount, db)
|
||||
}
|
||||
|
||||
func HandleUpdateResult(result *gorm.DB, entityName string) error {
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", entityName, result.Error)
|
||||
@@ -1735,7 +1744,7 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64,
|
||||
func (c *Cockroach) InitTables() error {
|
||||
err := CreateTableIfNotExist(c.DB, types.Account{}, types.AccountTransaction{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{},
|
||||
types.InvoicePayment{}, types.Configs{}, types.Credits{}, types.CreditsTransaction{},
|
||||
types.CardInfo{}, types.PaymentOrder{},
|
||||
types.CardInfo{}, types.PaymentOrder{}, types.PaymentRefund{}, types.Corporate{},
|
||||
types.SubscriptionPlan{}, types.Subscription{}, types.SubscriptionTransaction{},
|
||||
types.AccountRegionUserTask{}, types.UserKYC{}, types.RegionConfig{}, types.Debt{}, types.DebtStatusRecord{}, types.DebtResumeDeductionBalanceTransaction{},
|
||||
types.UserTimeRangeTraffic{})
|
||||
@@ -1808,6 +1817,17 @@ func (c *Cockroach) InitTables() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 增加状态列
|
||||
if !c.DB.Migrator().HasColumn(&types.Payment{}, "status") {
|
||||
fmt.Println("add column status to payment")
|
||||
// 注意:IF NOT EXISTS 避免重复报错
|
||||
sql := `ALTER TABLE "Payment" ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'PAID';`
|
||||
if err := c.DB.Exec(sql).Error; err != nil {
|
||||
return fmt.Errorf("failed to add payment.status column: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2032,3 +2052,49 @@ func (c *Cockroach) GetEnterpriseRealNameInfoByUserID(userID string) (*types.Ent
|
||||
}
|
||||
return &enterpriseRealNameInfo, nil
|
||||
}
|
||||
|
||||
func (c *Cockroach) CreateCorporate(account *types.Corporate) error {
|
||||
return c.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if account.UserUID == "" {
|
||||
return fmt.Errorf("corporate uid is empty")
|
||||
}
|
||||
if account.ReceiptSerialNumber == "" {
|
||||
return fmt.Errorf("corporate receiptSerialNumber is empty")
|
||||
}
|
||||
if account.PayerName == "" {
|
||||
return fmt.Errorf("corporate payerName is empty")
|
||||
}
|
||||
if account.PaymentAmount < 0 {
|
||||
return fmt.Errorf("corporate paymentAmount is zero")
|
||||
}
|
||||
id, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate payment id: %v", err)
|
||||
}
|
||||
account.ID = id
|
||||
pay := &types.Payment{}
|
||||
pay.ID = account.ID
|
||||
pay.UserUID, err = c.getUserUIDByID(account.UserUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user uid: %v", err)
|
||||
}
|
||||
pay.Method = "corporate"
|
||||
pay.TradeNO = account.ReceiptSerialNumber
|
||||
pay.Amount = account.PaymentAmount
|
||||
pay.Gift = account.GiftAmount
|
||||
pay.Status = types.PaymentStatusPAID
|
||||
if err := c.DB.Create(pay).Error; err != nil {
|
||||
return fmt.Errorf("failed to create payment: %w", err)
|
||||
}
|
||||
if err := c.DB.Create(account).Error; err != nil {
|
||||
return fmt.Errorf("failed to create corporate: %w", err)
|
||||
}
|
||||
amount := account.PaymentAmount + account.GiftAmount
|
||||
if account.PaymentAmount > 0 {
|
||||
if err := c.UpdateWithAccount(pay.UserUID, false, true, false, amount, tx); err != nil {
|
||||
return fmt.Errorf("failed to update corporate payment amount: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
)
|
||||
|
||||
@@ -115,3 +116,26 @@ func TestCockroach_InitTables(t *testing.T) {
|
||||
// t.Fatalf("AddDeductionBalanceWithCredits() error = %v", err)
|
||||
//}
|
||||
}
|
||||
|
||||
func TestCockroach_CreateCorporate(t *testing.T) {
|
||||
os.Setenv("LOCAL_REGION", "")
|
||||
ck, err := NewCockRoach("/", "")
|
||||
if err != nil {
|
||||
t.Errorf("NewCockRoach() error = %v", err)
|
||||
return
|
||||
}
|
||||
defer ck.Close()
|
||||
|
||||
cor := &types.Corporate{
|
||||
UserUID: "66EqYNUnLr",
|
||||
ReceiptSerialNumber: uuid.New().String(),
|
||||
PayerName: "payerName",
|
||||
PaymentAmount: 1_000_000,
|
||||
GiftAmount: 1_000_000,
|
||||
}
|
||||
err = ck.CreateCorporate(cor)
|
||||
if err != nil {
|
||||
t.Errorf("CreateCorporate() error = %v", err)
|
||||
}
|
||||
t.Logf("cor: %+v", cor)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
package pay
|
||||
|
||||
type RefundOption struct {
|
||||
OrderID string `json:"order_id"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Amount int64 `json:"amount"`
|
||||
OrderID string `json:"order_id"`
|
||||
RefundID string `json:"refund_id"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
|
||||
type Interface interface {
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
)
|
||||
@@ -74,10 +76,15 @@ func (w WechatPayment) RefundPayment(option RefundOption) (string, string, error
|
||||
return "", "", fmt.Errorf("order %s has not been paid or the payment time is unknown and cannot be refunded", option.TradeNo)
|
||||
}
|
||||
|
||||
_, paidAmount, err := w.GetPaymentDetails(option.TradeNo)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to query the payment order: %v", err)
|
||||
}
|
||||
|
||||
// generate a merchant refund number
|
||||
refundNo := GetRandomString(32)
|
||||
if refundNo == "" {
|
||||
return "", "", fmt.Errorf("generate refundNo failed")
|
||||
refundNo := uuid.NewString()
|
||||
if option.RefundID != "" {
|
||||
refundNo = option.RefundID
|
||||
}
|
||||
|
||||
// Amount Unit Conversion: option. Amount is the "cent", which is the same as CreatePayment (amount/10000)
|
||||
@@ -94,7 +101,7 @@ func (w WechatPayment) RefundPayment(option RefundOption) (string, string, error
|
||||
OutRefundNo: core.String(refundNo),
|
||||
Reason: core.String(fmt.Sprintf("refund for order %s", option.OrderID)),
|
||||
Amount: &refunddomestic.AmountReq{
|
||||
Total: core.Int64(refundAmt),
|
||||
Total: core.Int64(paidAmount / 10000),
|
||||
Refund: core.Int64(refundAmt),
|
||||
Currency: core.String("CNY"),
|
||||
},
|
||||
|
||||
@@ -24,9 +24,10 @@ type PaymentRaw struct {
|
||||
ActivityType ActivityType `gorm:"type:text;column:activityType"`
|
||||
Message string `gorm:"type:text;not null"`
|
||||
//TODO 初始化判断 新加字段
|
||||
CardUID *uuid.UUID `gorm:"type:uuid"`
|
||||
Type PaymentType `gorm:"type:text"` // 交易类型: AccountRecharge, Subscription,UpgradeSubscription...
|
||||
ChargeSource ChargeSource `gorm:"type:text"`
|
||||
CardUID *uuid.UUID `gorm:"type:uuid"`
|
||||
Type PaymentType `gorm:"type:text"` // 交易类型: AccountRecharge, Subscription,UpgradeSubscription...
|
||||
ChargeSource ChargeSource `gorm:"type:text"`
|
||||
Status PaymentStatus `gorm:"type:text;column:status;not null"`
|
||||
}
|
||||
|
||||
type ChargeSource string
|
||||
@@ -45,12 +46,19 @@ type PaymentOrder struct {
|
||||
}
|
||||
|
||||
type (
|
||||
PaymentStatus string
|
||||
PaymentOrderStatus string
|
||||
CardPaymentStatus string
|
||||
PaymentType string
|
||||
)
|
||||
|
||||
const (
|
||||
PaymentStatusPAID PaymentStatus = "PAID"
|
||||
PaymentStatusRefunded PaymentStatus = "REFUNDED"
|
||||
)
|
||||
|
||||
const (
|
||||
// PaymentOrderStatusPending TODO will delete
|
||||
PaymentOrderStatusPending PaymentOrderStatus = "PENDING"
|
||||
PaymentOrderStatusSuccess PaymentOrderStatus = "SUCCESS"
|
||||
PaymentOrderStatusFailed PaymentOrderStatus = "FAILED"
|
||||
@@ -218,3 +226,34 @@ func (c *CaptureResponse) Raw() []byte {
|
||||
data, _ := json.Marshal(c)
|
||||
return data
|
||||
}
|
||||
|
||||
type PaymentRefund struct {
|
||||
TradeNo string `json:"tradeNo" gorm:"type:uuid;not null"`
|
||||
ID string `json:"Id" gorm:"type:string;not null"` //外键 跟payment关联
|
||||
Method string `json:"method" gorm:"type:varchar(255);not null"` // 退款方式
|
||||
//OutTradeNo string `json:"outTradeNo" gorm:"type:uuid"`
|
||||
RefundNo string `json:"refundNo" gorm:"type:string;not null"`
|
||||
RefundAmount int64 `json:"refundAmount" gorm:"type:float;not null"`
|
||||
DeductAmount int64 `json:"deductAmount" gorm:"type:float;not null"` // 从 account的 balance里面扣款
|
||||
CreatedAt time.Time `json:"createdAt" gorm:"type:timestamp(3) with time zone;default:current_timestamp"`
|
||||
RefundReason string `json:"refundReason" gorm:"type:text"`
|
||||
}
|
||||
|
||||
func (PaymentRefund) TableName() string {
|
||||
return "PaymentRefund"
|
||||
}
|
||||
|
||||
type Corporate struct {
|
||||
UserUID string `json:"userUid" gorm:"type:string;not null"`
|
||||
ID string `json:"Id" gorm:"type:string;not null"`
|
||||
ReceiptSerialNumber string `json:"receiptSerialNumber" gorm:"type:uuid;not null"`
|
||||
PayerName string `json:"payerName" gorm:"type:varchar(255);not null"`
|
||||
PaymentAmount int64 `json:"paymentAmount" gorm:"type:float;not null"`
|
||||
GiftAmount int64 `json:"giftAmount" gorm:"type:float;not null"`
|
||||
PayDate time.Time `json:"payDate" gorm:"type:timestamp(3) with time zone;default:current_timestamp"`
|
||||
CreationDate time.Time `json:"creationDate" gorm:"type:timestamp(3) with time zone;default:current_timestamp"`
|
||||
}
|
||||
|
||||
func (Corporate) TableName() string {
|
||||
return "Corporate"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labring/sealos/controllers/pkg/pay"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -15,7 +18,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetAccount
|
||||
// AdminGetAccountWithWorkspaceID GetAccount
|
||||
// @Summary Get user account
|
||||
// @Description Get user account
|
||||
// @Tags Account
|
||||
@@ -47,7 +50,7 @@ func AdminGetAccountWithWorkspaceID(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ChargeBilling
|
||||
// AdminChargeBilling ChargeBilling
|
||||
// @Summary Charge billing
|
||||
// @Description Charge billing
|
||||
// @Tags Account
|
||||
@@ -226,3 +229,75 @@ func adminUserTrafficOperator(c *gin.Context, networkStatus string) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
func AdminPaymentRefund(c *gin.Context) {
|
||||
// 1. 管理员鉴权
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{
|
||||
Error: fmt.Sprintf("authenticate error: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 解析前端传来的 JSON
|
||||
var refundData types.PaymentRefund
|
||||
if err := c.ShouldBindJSON(&refundData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, helper.ErrorMessage{
|
||||
Error: fmt.Sprintf("invalid request body: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
postDo := func(p types.PaymentRefund) error {
|
||||
svc, err := pay.NewPayHandler(p.Method)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new payment handler failed: %v", err)
|
||||
}
|
||||
_, _, err = svc.RefundPayment(pay.RefundOption{
|
||||
TradeNo: p.TradeNo,
|
||||
Amount: p.RefundAmount,
|
||||
RefundID: p.RefundNo,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to refund payment: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 3. 调用 RefundAmount
|
||||
if err := dao.DBClient.RefundAmount(refundData, postDo); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{
|
||||
Error: fmt.Sprintf("refund processing error: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 成功返回
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
func AdminCreateCorporate(c *gin.Context) {
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{
|
||||
Error: fmt.Sprintf("authenticate error: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse interfaces coming from the frontend
|
||||
var corporateData types.Corporate
|
||||
if err := c.ShouldBindJSON(&corporateData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, helper.ErrorMessage{
|
||||
Error: fmt.Sprintf("invalid request body: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// invoke CreateCorporate
|
||||
if err := dao.DBClient.CreateCorporate(corporateData); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{
|
||||
Error: fmt.Sprintf("failed to create corporate: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package dao
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -92,6 +93,8 @@ type Interface interface {
|
||||
SetDefaultCard(cardID uuid.UUID, userUID uuid.UUID) error
|
||||
GlobalTransactionHandler(funcs ...func(tx *gorm.DB) error) error
|
||||
GetSubscriptionPlan(planName string) (*types.SubscriptionPlan, error)
|
||||
RefundAmount(ref types.PaymentRefund, postDo func(types.PaymentRefund) error) error
|
||||
CreateCorporate(corporate types.Corporate) error
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
@@ -2322,3 +2325,64 @@ func (m *MongoDB) reconcileUnsettledLLMBilling(startTime, endTime time.Time) (ma
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (g *Cockroach) RefundAmount(ref types.PaymentRefund, postDo func(types.PaymentRefund) error) error {
|
||||
//g.ck.GetGlobalDB().Transaction(func(tx *gorm.DB) error {
|
||||
// // 1. get payment with id,status设置为退款
|
||||
// // 2. 创建 paymentRefund 数据 进行关联
|
||||
// // 3. 更新用户账户余额
|
||||
//})
|
||||
return g.ck.GetGlobalDB().Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 查询原 payment 并设置状态为已退款
|
||||
var payment types.Payment
|
||||
if err := tx.
|
||||
Where("id = ? ", ref.ID).
|
||||
First(&payment).Error; err != nil {
|
||||
return fmt.Errorf("payment not found: %w", err)
|
||||
}
|
||||
|
||||
// 状态改为 "refunded"
|
||||
payment.Status = types.PaymentStatusRefunded
|
||||
if err := tx.Save(&payment).Error; err != nil {
|
||||
return fmt.Errorf("failed to update payment status: %w", err)
|
||||
}
|
||||
|
||||
// 2. 调用退款接口进行退款
|
||||
// 调用退款接口之后返回 OutTradeNo 传入payment_refund
|
||||
|
||||
if ref.RefundNo == "" {
|
||||
ref.RefundNo = uuid.NewString()
|
||||
}
|
||||
// 2. 创建一条 payment_refund 记录
|
||||
refund := types.PaymentRefund{
|
||||
TradeNo: payment.TradeNO, //自查询
|
||||
ID: payment.ID, //外键 与payment关联 前端传入
|
||||
Method: payment.Method, //前端传入
|
||||
RefundNo: ref.RefundNo, //生成传入
|
||||
RefundAmount: ref.RefundAmount, //前端传入
|
||||
DeductAmount: ref.DeductAmount, //前端传入
|
||||
RefundReason: ref.RefundReason, //前端选择传入
|
||||
}
|
||||
if err := tx.Create(&refund).Error; err != nil {
|
||||
log.Printf("创建 refund 时的字段内容: %+v", refund)
|
||||
return fmt.Errorf("failed to create payment_refund: %w", err)
|
||||
}
|
||||
|
||||
// 用公开方法调用
|
||||
if ref.DeductAmount > 0 {
|
||||
if err := g.ck.UpdateWithAccount(payment.UserUID, false, false, false, ref.DeductAmount, tx); err != nil {
|
||||
return fmt.Errorf("扣款失败:%w", err)
|
||||
}
|
||||
}
|
||||
return postDo(refund)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Cockroach) CreateCorporate(corporate types.Corporate) error {
|
||||
return g.ck.GetGlobalDB().Transaction(func(tx *gorm.DB) error {
|
||||
if err := g.ck.CreateCorporate(&corporate); err != nil {
|
||||
return fmt.Errorf("failed to create corporate: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ const (
|
||||
AdminFlushDebtResourceStatus = "/flush-debt-resource-status"
|
||||
AdminSuspendUserTraffic = "/suspend-user-traffic"
|
||||
AdminResumeUserTraffic = "/resume-user-traffic"
|
||||
|
||||
AdminRefundForms = "/refund-forms"
|
||||
AdminCreateCorporate = "/corporate"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -77,6 +77,8 @@ func RegisterPayRouter() {
|
||||
adminGroup := router.Group(helper.AdminGroup).
|
||||
GET(helper.AdminGetAccountWithWorkspace, api.AdminGetAccountWithWorkspaceID).
|
||||
GET(helper.AdminGetUserRealNameInfo, api.AdminGetUserRealNameInfo).
|
||||
POST(helper.AdminCreateCorporate, api.AdminCreateCorporate).
|
||||
POST(helper.AdminRefundForms, api.AdminPaymentRefund).
|
||||
POST(helper.AdminChargeBilling, api.AdminChargeBilling).
|
||||
POST(helper.AdminFlushDebtResourceStatus, api.AdminFlushDebtResourceStatus).
|
||||
POST(helper.AdminSuspendUserTraffic, api.AdminSuspendUserTraffic).
|
||||
|
||||
Reference in New Issue
Block a user