feat: implement tenant-side creator audit feature and update related tests and documentation

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-02-09 06:54:04 +08:00
parent 3126ed5e64
commit 05a0d07dbb
23 changed files with 7205 additions and 112 deletions

View File

@@ -2,9 +2,11 @@ package services
import (
"database/sql"
"errors"
"testing"
"quyun/v2/app/commands/testx"
"quyun/v2/app/errorx"
order_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/database"
"quyun/v2/database/models"
@@ -196,3 +198,137 @@ func (s *CouponTestSuite) Test_ListAvailable() {
So(list[0].CouponID, ShouldEqual, cp.ID)
})
}
func (s *CouponTestSuite) Test_Validate_DenyCrossTenantCoupon() {
Convey("Validate should deny cross-tenant coupon", s.T(), func() {
ctx := s.T().Context()
database.Truncate(
ctx,
s.DB,
models.TableNameCoupon,
models.TableNameUserCoupon,
models.TableNameUser,
)
user := &models.User{Username: "coupon_cross_validate", Phone: "13800000011"}
So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil)
tenantA := int64(11)
tenantB := int64(22)
coupon := &models.Coupon{
TenantID: tenantA,
Title: "Tenant A Coupon",
Type: consts.CouponTypeFixAmount,
Value: 200,
MinOrderAmount: 0,
}
So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil)
userCoupon := &models.UserCoupon{
UserID: user.ID,
CouponID: coupon.ID,
Status: consts.UserCouponStatusUnused,
}
So(models.UserCouponQuery.WithContext(ctx).Create(userCoupon), ShouldBeNil)
_, err := Coupon.Validate(ctx, tenantB, user.ID, userCoupon.ID, 1000)
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code)
})
}
func (s *CouponTestSuite) Test_MarkUsed_DenyCrossTenantCoupon() {
Convey("MarkUsed should deny cross-tenant coupon", s.T(), func() {
ctx := s.T().Context()
database.Truncate(
ctx,
s.DB,
models.TableNameCoupon,
models.TableNameUserCoupon,
models.TableNameOrder,
models.TableNameUser,
)
user := &models.User{Username: "coupon_cross_mark", Phone: "13800000012"}
So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil)
tenantA := int64(33)
tenantB := int64(44)
coupon := &models.Coupon{
TenantID: tenantA,
Title: "Tenant A Coupon",
Type: consts.CouponTypeFixAmount,
Value: 200,
MinOrderAmount: 0,
}
So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil)
userCoupon := &models.UserCoupon{
UserID: user.ID,
CouponID: coupon.ID,
Status: consts.UserCouponStatusUnused,
}
So(models.UserCouponQuery.WithContext(ctx).Create(userCoupon), ShouldBeNil)
order := &models.Order{
TenantID: tenantA,
UserID: user.ID,
Type: consts.OrderTypeContentPurchase,
Status: consts.OrderStatusCreated,
}
So(models.OrderQuery.WithContext(ctx).Create(order), ShouldBeNil)
err := models.Q.Transaction(func(tx *models.Query) error {
return Coupon.MarkUsed(ctx, tx, tenantB, userCoupon.ID, order.ID)
})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code)
})
}
func (s *CouponTestSuite) Test_Grant_DenyCrossTenantCoupon() {
Convey("Grant should reject coupon from another tenant", s.T(), func() {
ctx := s.T().Context()
database.Truncate(
ctx,
s.DB,
models.TableNameCoupon,
models.TableNameUserCoupon,
models.TableNameUser,
)
user := &models.User{Username: "coupon_cross_grant", Phone: "13800000013"}
So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil)
tenantA := int64(55)
tenantB := int64(66)
coupon := &models.Coupon{
TenantID: tenantA,
Title: "Tenant A Coupon",
Type: consts.CouponTypeFixAmount,
Value: 200,
MinOrderAmount: 0,
}
So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil)
granted, err := Coupon.Grant(ctx, tenantB, coupon.ID, []int64{user.ID})
So(err, ShouldNotBeNil)
So(granted, ShouldEqual, 0)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrRecordNotFound.Code)
exists, err := models.UserCouponQuery.WithContext(ctx).
Where(models.UserCouponQuery.UserID.Eq(user.ID), models.UserCouponQuery.CouponID.Eq(coupon.ID)).
Exists()
So(err, ShouldBeNil)
So(exists, ShouldBeFalse)
})
}