feat: 实现平台抽成、提现审批、异步任务集成及安全审计功能

This commit is contained in:
2025-12-30 14:54:19 +08:00
parent 5e8dbec806
commit ee1acae3ed
25 changed files with 985 additions and 60 deletions

View File

@@ -177,3 +177,148 @@ func (s *CreatorTestSuite) Test_Dashboard() {
})
})
}
func (s *CreatorTestSuite) Test_PayoutAccount() {
Convey("PayoutAccount", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNamePayoutAccount, models.TableNameUser)
u := &models.User{Username: "creator5", Phone: "13700000005"}
models.UserQuery.WithContext(ctx).Create(u)
ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID)
t := &models.Tenant{UserID: u.ID, Name: "Channel 5", Code: "126", Status: consts.TenantStatusVerified}
models.TenantQuery.WithContext(ctx).Create(t)
Convey("should CRUD payout account", func() {
// Add
form := &creator_dto.PayoutAccount{
Type: string(consts.PayoutAccountTypeAlipay),
Name: "Alipay",
Account: "user@example.com",
Realname: "John Doe",
}
err := Creator.AddPayoutAccount(ctx, form)
So(err, ShouldBeNil)
// List
list, err := Creator.ListPayoutAccounts(ctx)
So(err, ShouldBeNil)
So(len(list), ShouldEqual, 1)
So(list[0].Account, ShouldEqual, "user@example.com")
// Remove
err = Creator.RemovePayoutAccount(ctx, list[0].ID)
So(err, ShouldBeNil)
// Verify Empty
list, err = Creator.ListPayoutAccounts(ctx)
So(err, ShouldBeNil)
So(len(list), ShouldEqual, 0)
})
})
}
func (s *CreatorTestSuite) Test_Withdraw() {
Convey("Withdraw", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNamePayoutAccount, models.TableNameUser, models.TableNameOrder, models.TableNameTenantLedger)
u := &models.User{Username: "creator6", Phone: "13700000006", Balance: 5000} // 50.00
models.UserQuery.WithContext(ctx).Create(u)
ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID)
t := &models.Tenant{UserID: u.ID, Name: "Channel 6", Code: "127", Status: consts.TenantStatusVerified}
models.TenantQuery.WithContext(ctx).Create(t)
pa := &models.PayoutAccount{TenantID: t.ID, UserID: u.ID, Type: "bank", Name: "Bank", Account: "123", Realname: "Creator"}
models.PayoutAccountQuery.WithContext(ctx).Create(pa)
Convey("should withdraw successfully", func() {
form := &creator_dto.WithdrawForm{
Amount: 20.00,
AccountID: cast.ToString(pa.ID),
}
err := Creator.Withdraw(ctx, form)
So(err, ShouldBeNil)
// Verify Balance Deducted
uReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(u.ID)).First()
So(uReload.Balance, ShouldEqual, 3000)
// Verify Order Created
o, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.TenantID.Eq(t.ID), models.OrderQuery.Type.Eq(consts.OrderTypeWithdrawal)).First()
So(o, ShouldNotBeNil)
So(o.AmountPaid, ShouldEqual, 2000)
// Verify Ledger
l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First()
So(l, ShouldNotBeNil)
So(l.Type, ShouldEqual, consts.TenantLedgerTypeCreditWithdrawal)
})
Convey("should fail if insufficient balance", func() {
form := &creator_dto.WithdrawForm{
Amount: 100.00,
AccountID: cast.ToString(pa.ID),
}
err := Creator.Withdraw(ctx, form)
So(err, ShouldNotBeNil)
})
})
}
func (s *CreatorTestSuite) Test_Refund() {
Convey("Refund", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB,
models.TableNameTenant, models.TableNameUser, models.TableNameOrder,
models.TableNameOrderItem, models.TableNameContentAccess, models.TableNameTenantLedger,
)
// Creator
creator := &models.User{Username: "creator7", Phone: "13700000007", Balance: 5000} // Has funds
models.UserQuery.WithContext(ctx).Create(creator)
creatorCtx := context.WithValue(ctx, consts.CtxKeyUser, creator.ID)
// Tenant
t := &models.Tenant{UserID: creator.ID, Name: "Channel 7", Code: "128", Status: consts.TenantStatusVerified}
models.TenantQuery.WithContext(ctx).Create(t)
// Buyer
buyer := &models.User{Username: "buyer7", Phone: "13900000007", Balance: 0}
models.UserQuery.WithContext(ctx).Create(buyer)
// Order (Paid -> Refunding)
o := &models.Order{
TenantID: t.ID,
UserID: buyer.ID,
AmountPaid: 1000, // 10.00
Status: consts.OrderStatusRefunding,
}
models.OrderQuery.WithContext(ctx).Create(o)
models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{OrderID: o.ID, ContentID: 100}) // Fake content
models.ContentAccessQuery.WithContext(ctx).Create(&models.ContentAccess{UserID: buyer.ID, ContentID: 100, Status: consts.ContentAccessStatusActive})
Convey("should accept refund", func() {
form := &creator_dto.RefundForm{Action: "accept", Reason: "Defective"}
err := Creator.ProcessRefund(creatorCtx, cast.ToString(o.ID), form)
So(err, ShouldBeNil)
// Verify Order
oReload, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First()
So(oReload.Status, ShouldEqual, consts.OrderStatusRefunded)
// Verify Balances
cReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(creator.ID)).First()
So(cReload.Balance, ShouldEqual, 4000) // 5000 - 1000
bReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).First()
So(bReload.Balance, ShouldEqual, 1000) // 0 + 1000
// Verify Access
acc, _ := models.ContentAccessQuery.WithContext(ctx).Where(models.ContentAccessQuery.UserID.Eq(buyer.ID)).First()
So(acc.Status, ShouldEqual, consts.ContentAccessStatusRevoked)
})
})
}