feat(distribution): 分销站点管理支持完整 CRUD、安全删除保护与管理员密码重置
- 后端支持分销站点编辑(域名唯一性校验、收号压价点位及税率更新) - 后端支持分销站点安全删除校验(阻断存在业务订单、财务流水、结算批次或钱包余额的站点删除;对干净站点连带清理分成规则、展示账号绑定、支付通道配置及专有站长账号) - 后端支持站长账号展示与超级管理员重置站长密码 - 主管理后台与分销后台前端全面支持筛选搜索、编辑模态窗、状态快捷切换、站长密码重置与安全删除确认 - 补充后端 CRUD、删除保护与密码重置覆盖率测试用例
This commit is contained in:
@@ -21,10 +21,18 @@ type SiteDTO struct {
|
||||
TaxEnabled bool `json:"tax_enabled"`
|
||||
TaxRateBPS int64 `json:"tax_rate_bps"`
|
||||
SettlementHoldDays int `json:"settlement_hold_days"`
|
||||
AdminUserID *uint64 `json:"admin_user_id,omitempty"`
|
||||
AdminUsername string `json:"admin_username,omitempty"`
|
||||
AdminNickname string `json:"admin_nickname,omitempty"`
|
||||
MemberType string `json:"member_type,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ResetSiteAdminPasswordRequest struct {
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type CreateSiteRequest struct {
|
||||
SiteCode string `json:"site_code" binding:"required,max=64"`
|
||||
SiteName string `json:"site_name" binding:"required,max=128"`
|
||||
|
||||
@@ -83,6 +83,37 @@ func (h *Handler) UpdateSite(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteSite(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
if err := h.service.DeleteSite(c.Request.Context(), adminID, id); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ResetSiteAdminPassword(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ResetSiteAdminPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "密码格式不正确(至少6位)")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
if err := h.service.ResetSiteAdminPassword(c.Request.Context(), adminID, id, req.NewPassword); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"message": "分销商密码重置成功"})
|
||||
}
|
||||
|
||||
func (h *Handler) GetSiteSMSConfig(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -281,9 +281,46 @@ func (r *Repository) ListSites(ctx context.Context) ([]SiteDTO, error) {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]SiteDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if len(rows) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
siteIDs := make([]uint64, len(rows))
|
||||
for i, row := range rows {
|
||||
siteIDs[i] = row.ID
|
||||
items = append(items, siteDTO(row))
|
||||
}
|
||||
|
||||
type memberRow struct {
|
||||
SiteID uint64 `gorm:"column:site_id"`
|
||||
AdminUserID uint64 `gorm:"column:admin_user_id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Nickname string `gorm:"column:nickname"`
|
||||
MemberType string `gorm:"column:member_type"`
|
||||
}
|
||||
var memberRows []memberRow
|
||||
_ = r.db.WithContext(ctx).Table("distribution_site_members").
|
||||
Select("distribution_site_members.site_id, distribution_site_members.admin_user_id, admin_users.username, admin_users.nickname, distribution_site_members.member_type").
|
||||
Joins("JOIN admin_users ON admin_users.id = distribution_site_members.admin_user_id").
|
||||
Where("distribution_site_members.site_id IN ?", siteIDs).
|
||||
Scan(&memberRows).Error
|
||||
|
||||
memberMap := make(map[uint64]memberRow, len(memberRows))
|
||||
for _, m := range memberRows {
|
||||
if _, exists := memberMap[m.SiteID]; !exists {
|
||||
memberMap[m.SiteID] = m
|
||||
}
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
if m, ok := memberMap[items[i].ID]; ok {
|
||||
items[i].AdminUserID = &m.AdminUserID
|
||||
items[i].AdminUsername = m.Username
|
||||
items[i].AdminNickname = m.Nickname
|
||||
items[i].MemberType = m.MemberType
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -299,6 +336,24 @@ func (r *Repository) FindSite(ctx context.Context, adminID, id uint64) (*SiteDTO
|
||||
return nil, err
|
||||
}
|
||||
item := siteDTO(row)
|
||||
|
||||
var m struct {
|
||||
AdminUserID uint64 `gorm:"column:admin_user_id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Nickname string `gorm:"column:nickname"`
|
||||
MemberType string `gorm:"column:member_type"`
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Table("distribution_site_members").
|
||||
Select("distribution_site_members.admin_user_id, admin_users.username, admin_users.nickname, distribution_site_members.member_type").
|
||||
Joins("JOIN admin_users ON admin_users.id = distribution_site_members.admin_user_id").
|
||||
Where("distribution_site_members.site_id = ?", row.ID).
|
||||
First(&m).Error; err == nil {
|
||||
item.AdminUserID = &m.AdminUserID
|
||||
item.AdminUsername = m.Username
|
||||
item.AdminNickname = m.Nickname
|
||||
item.MemberType = m.MemberType
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
@@ -416,10 +471,23 @@ func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req Upd
|
||||
row.SiteName = strings.TrimSpace(*req.SiteName)
|
||||
}
|
||||
if req.Domain != nil {
|
||||
row.Domain = strings.TrimSpace(*req.Domain)
|
||||
newDomain := strings.TrimSpace(*req.Domain)
|
||||
if newDomain != "" && newDomain != row.Domain {
|
||||
var count int64
|
||||
if err := tx.Model(&model.DistributionSite{}).Where("domain = ? AND id != ?", newDomain, row.ID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("专属域名已被其他分销站点占用")
|
||||
}
|
||||
row.Domain = newDomain
|
||||
}
|
||||
}
|
||||
if req.Status != nil {
|
||||
row.Status = strings.TrimSpace(*req.Status)
|
||||
status := strings.TrimSpace(*req.Status)
|
||||
if status == "active" || status == "disabled" {
|
||||
row.Status = status
|
||||
}
|
||||
}
|
||||
if req.DisplayAccountEnabled != nil {
|
||||
row.DisplayAccountEnabled = *req.DisplayAccountEnabled
|
||||
@@ -473,9 +541,211 @@ func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req Upd
|
||||
}
|
||||
r.evictSiteInfoCache(ctx, &row)
|
||||
item := siteDTO(row)
|
||||
|
||||
var m struct {
|
||||
AdminUserID uint64 `gorm:"column:admin_user_id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Nickname string `gorm:"column:nickname"`
|
||||
MemberType string `gorm:"column:member_type"`
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Table("distribution_site_members").
|
||||
Select("distribution_site_members.admin_user_id, admin_users.username, admin_users.nickname, distribution_site_members.member_type").
|
||||
Joins("JOIN admin_users ON admin_users.id = distribution_site_members.admin_user_id").
|
||||
Where("distribution_site_members.site_id = ?", row.ID).
|
||||
First(&m).Error; err == nil {
|
||||
item.AdminUserID = &m.AdminUserID
|
||||
item.AdminUsername = m.Username
|
||||
item.AdminNickname = m.Nickname
|
||||
item.MemberType = m.MemberType
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// DeleteSite 删除分销站点。
|
||||
// 若分销站点已产生业务订单、财务流水或结算记录,则拒绝物理删除,保护数据与审计链路;
|
||||
// 对于未产生业务的测试站点,将级联清理成员关联、专属分销商账号、规则快照、上架关系与钱包账户。
|
||||
func (r *Repository) DeleteSite(ctx context.Context, adminID, siteID uint64) error {
|
||||
if r == nil || r.db == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
global, err := r.hasGlobalDistributionAccess(ctx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !global && adminID != 0 {
|
||||
return ErrSiteAccessDenied
|
||||
}
|
||||
|
||||
var site model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).First(&site, siteID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrSiteNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 检查是否存在订单
|
||||
var orderCount int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.RentalOrder{}).
|
||||
Where("distribution_site_id = ? OR markup_site_id = ?", siteID, siteID).
|
||||
Count(&orderCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if orderCount > 0 {
|
||||
return fmt.Errorf("分销站点【%s】已产生 %d 笔业务订单,为保障财务数据完整性无法删除;请将站点状态修改为「已停用」", site.SiteName, orderCount)
|
||||
}
|
||||
|
||||
// 2. 检查是否存在财务快照流水
|
||||
var financialCount int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.DistributionOrderFinancial{}).
|
||||
Where("site_id = ?", siteID).
|
||||
Count(&financialCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if financialCount > 0 {
|
||||
return fmt.Errorf("分销站点【%s】已存在分账财务流水,无法直接删除;请将站点状态修改为「已停用」", site.SiteName)
|
||||
}
|
||||
|
||||
// 3. 检查是否存在结算批次
|
||||
var settlementCount int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.DistributionSettlementBatch{}).
|
||||
Where("site_id = ?", siteID).
|
||||
Count(&settlementCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if settlementCount > 0 {
|
||||
return fmt.Errorf("分销站点【%s】已存在结算批次记录,无法直接删除", site.SiteName)
|
||||
}
|
||||
|
||||
// 4. 检查钱包账户余额
|
||||
var walletAccounts []model.WalletAccount
|
||||
if err := r.db.WithContext(ctx).Where("distribution_site_id = ?", siteID).Find(&walletAccounts).Error; err == nil {
|
||||
for _, wa := range walletAccounts {
|
||||
if wa.AvailableBalanceCent > 0 || wa.FrozenBalanceCent > 0 {
|
||||
return fmt.Errorf("分销站点【%s】钱包尚有未结算/未提现余额,无法删除", site.SiteName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行事务级联清理
|
||||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 查询关联管理员
|
||||
var members []model.DistributionSiteMember
|
||||
if err := tx.Where("site_id = ?", siteID).Find(&members).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理成员关系
|
||||
if err := tx.Where("site_id = ?", siteID).Delete(&model.DistributionSiteMember{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 若管理员用户仅绑定此分销站点且非系统超管,清理其账号与角色,便于用户名复用
|
||||
for _, m := range members {
|
||||
var otherMemberCount int64
|
||||
_ = tx.Model(&model.DistributionSiteMember{}).Where("admin_user_id = ?", m.AdminUserID).Count(&otherMemberCount).Error
|
||||
if otherMemberCount == 0 {
|
||||
var adminUser model.AdminUser
|
||||
if err := tx.First(&adminUser, m.AdminUserID).Error; err == nil {
|
||||
var superRoleCount int64
|
||||
_ = tx.Table("admin_user_roles").
|
||||
Joins("JOIN roles ON roles.id = admin_user_roles.role_id").
|
||||
Where("admin_user_roles.admin_user_id = ? AND roles.code IN ('super_admin', 'admin')", m.AdminUserID).
|
||||
Count(&superRoleCount).Error
|
||||
if superRoleCount == 0 {
|
||||
_ = tx.Table("admin_user_roles").Where("admin_user_id = ?", m.AdminUserID).Delete(nil).Error
|
||||
_ = tx.Delete(&adminUser).Error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理规则版本
|
||||
if err := tx.Where("site_id = ?", siteID).Delete(&model.DistributionRuleVersion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理上架货架
|
||||
if err := tx.Where("site_id = ?", siteID).Delete(&model.DistributionSiteAccount{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理支付通道绑定
|
||||
if err := tx.Where("site_id = ?", siteID).Delete(&model.DistributionSitePaymentChannel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理分销钱包与钱包账户
|
||||
_ = tx.Table("distribution_site_wallets").Where("site_id = ?", siteID).Delete(nil).Error
|
||||
_ = tx.Where("distribution_site_id = ?", siteID).Delete(&model.WalletAccount{}).Error
|
||||
|
||||
// 删除站点主体
|
||||
if err := tx.Delete(&site).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.evictSiteInfoCache(ctx, &site)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetSiteAdminPassword 重置分销站点绑定的管理员密码。
|
||||
func (r *Repository) ResetSiteAdminPassword(ctx context.Context, operatorAdminID, siteID uint64, newPassword string) error {
|
||||
if r == nil || r.db == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
global, err := r.hasGlobalDistributionAccess(ctx, operatorAdminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !global && operatorAdminID != 0 {
|
||||
return ErrSiteAccessDenied
|
||||
}
|
||||
newPassword = strings.TrimSpace(newPassword)
|
||||
if len(newPassword) < 6 {
|
||||
return errors.New("新密码长度不能少于6位")
|
||||
}
|
||||
|
||||
var site model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).First(&site, siteID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrSiteNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var members []model.DistributionSiteMember
|
||||
if err := r.db.WithContext(ctx).Where("site_id = ?", siteID).Find(&members).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(members) == 0 {
|
||||
return errors.New("该分销站点未绑定管理员账号")
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminUserIDs := make([]uint64, 0, len(members))
|
||||
for _, m := range members {
|
||||
adminUserIDs = append(adminUserIDs, m.AdminUserID)
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).Model(&model.AdminUser{}).
|
||||
Where("id IN ?", adminUserIDs).
|
||||
Updates(map[string]interface{}{
|
||||
"password_hash": string(hash),
|
||||
"token_version": gorm.Expr("token_version + 1"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ListRules(ctx context.Context, adminID uint64, siteID *uint64, mode string) ([]RuleDTO, error) {
|
||||
db := r.db.WithContext(ctx).Model(&model.DistributionRuleVersion{}).
|
||||
Order("CASE WHEN site_id IS NULL THEN 0 ELSE 1 END ASC, site_id ASC, mode ASC, version DESC, id DESC")
|
||||
|
||||
@@ -117,6 +117,18 @@ func (s *Service) UpdateSite(ctx context.Context, adminID, id uint64, req Update
|
||||
}
|
||||
return s.repo.UpdateSite(ctx, adminID, id, req)
|
||||
}
|
||||
func (s *Service) DeleteSite(ctx context.Context, adminID, siteID uint64) error {
|
||||
if err := s.available(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.DeleteSite(ctx, adminID, siteID)
|
||||
}
|
||||
func (s *Service) ResetSiteAdminPassword(ctx context.Context, operatorAdminID, siteID uint64, newPassword string) error {
|
||||
if err := s.available(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.ResetSiteAdminPassword(ctx, operatorAdminID, siteID, newPassword)
|
||||
}
|
||||
func (s *Service) ListRules(ctx context.Context, adminID uint64, siteID *uint64, mode string) ([]RuleDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestSiteCRUD_UpdateSiteAndDomainUniqueness(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
ctx := t.Context()
|
||||
|
||||
_, err := repo.CreateSite(ctx, CreateSiteRequest{
|
||||
SiteCode: "site_crud_a",
|
||||
SiteName: "站点A",
|
||||
Domain: "a.domain.test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSite A failed: %v", err)
|
||||
}
|
||||
|
||||
siteB, err := repo.CreateSite(ctx, CreateSiteRequest{
|
||||
SiteCode: "site_crud_b",
|
||||
SiteName: "站点B",
|
||||
Domain: "b.domain.test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSite B failed: %v", err)
|
||||
}
|
||||
|
||||
// 1. 尝试将站点B的域名更新为已存在的站点A域名 -> 应该报错
|
||||
dupDomain := "a.domain.test"
|
||||
_, err = repo.UpdateSite(ctx, 0, siteB.ID, UpdateSiteRequest{
|
||||
Domain: &dupDomain,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when updating to duplicate domain, got nil")
|
||||
}
|
||||
|
||||
// 2. 正常更新站点B的名称、域名、状态为 disabled、展示主站账号为 false
|
||||
newName := "站点B更新后"
|
||||
newDomain := "b-new.domain.test"
|
||||
newStatus := "disabled"
|
||||
dispFalse := false
|
||||
updated, err := repo.UpdateSite(ctx, 0, siteB.ID, UpdateSiteRequest{
|
||||
SiteName: &newName,
|
||||
Domain: &newDomain,
|
||||
Status: &newStatus,
|
||||
DisplayAccountEnabled: &dispFalse,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSite failed: %v", err)
|
||||
}
|
||||
if updated.SiteName != newName || updated.Domain != newDomain || updated.Status != "disabled" || updated.DisplayAccountEnabled != false {
|
||||
t.Fatalf("updated site values mismatch: %+v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiteCRUD_DeleteSiteProtectionAndCascade(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
ctx := t.Context()
|
||||
|
||||
// 1. 使用 OnboardSite 创建一个完整的测试分销站(包含成员与管理员)
|
||||
onboardResp, err := repo.OnboardSite(ctx, OnboardSiteRequest{
|
||||
SiteCode: "del_test_site",
|
||||
SiteName: "待删除测试分销站",
|
||||
Domain: "del.domain.test",
|
||||
AdminUsername: "del_test_admin",
|
||||
AdminPassword: "password123",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("OnboardSite failed: %v", err)
|
||||
}
|
||||
siteID := onboardResp.Site.ID
|
||||
adminUserID := onboardResp.Member.AdminID
|
||||
|
||||
// 2. 测试密码重置
|
||||
newPwd := "new_secret_pwd_666"
|
||||
if err := repo.ResetSiteAdminPassword(ctx, 0, siteID, newPwd); err != nil {
|
||||
t.Fatalf("ResetSiteAdminPassword failed: %v", err)
|
||||
}
|
||||
// 校验密码是否成功重置
|
||||
var adminUser model.AdminUser
|
||||
if err := db.First(&adminUser, adminUserID).Error; err != nil {
|
||||
t.Fatalf("failed to find admin user: %v", err)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(adminUser.PasswordHash), []byte(newPwd)); err != nil {
|
||||
t.Fatalf("password hash does not match new password: %v", err)
|
||||
}
|
||||
|
||||
// 3. 验证 ListSites 与 FindSite 能够正确携带管理员信息
|
||||
sites, err := repo.ListSites(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSites failed: %v", err)
|
||||
}
|
||||
var foundSite *SiteDTO
|
||||
for i := range sites {
|
||||
if sites[i].ID == siteID {
|
||||
foundSite = &sites[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundSite == nil {
|
||||
t.Fatalf("site not found in ListSites")
|
||||
}
|
||||
if foundSite.AdminUsername != "del_test_admin" {
|
||||
t.Fatalf("expected admin_username del_test_admin, got %s", foundSite.AdminUsername)
|
||||
}
|
||||
|
||||
// 4. 测试有订单时的删除保护
|
||||
// 创建一笔模拟关联订单
|
||||
dummyOrder := model.RentalOrder{
|
||||
OrderNo: "ORD_DEL_TEST_001",
|
||||
RenterID: 999,
|
||||
AccountID: 888,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
if err := db.Create(&dummyOrder).Error; err != nil {
|
||||
t.Fatalf("failed to create dummy order: %v", err)
|
||||
}
|
||||
|
||||
err = repo.DeleteSite(ctx, 0, siteID)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when deleting site with orders, got nil")
|
||||
}
|
||||
|
||||
// 删除此测试订单,继续测试级联删除
|
||||
if err := db.Delete(&dummyOrder).Error; err != nil {
|
||||
t.Fatalf("failed to remove dummy order: %v", err)
|
||||
}
|
||||
|
||||
// 5. 测试无订单时的干净级联删除
|
||||
if err := repo.DeleteSite(ctx, 0, siteID); err != nil {
|
||||
t.Fatalf("DeleteSite failed: %v", err)
|
||||
}
|
||||
|
||||
// 校验站点主体已删除
|
||||
var checkSite model.DistributionSite
|
||||
if err := db.First(&checkSite, siteID).Error; err == nil {
|
||||
t.Fatalf("site should be deleted from db")
|
||||
}
|
||||
|
||||
// 校验专属管理员用户已同步清理
|
||||
var checkAdmin model.AdminUser
|
||||
if err := db.First(&checkAdmin, adminUserID).Error; err == nil {
|
||||
t.Fatalf("dedicated admin user should be deleted from db")
|
||||
}
|
||||
|
||||
// 校验规则已清理
|
||||
var ruleCount int64
|
||||
db.Model(&model.DistributionRuleVersion{}).Where("site_id = ?", siteID).Count(&ruleCount)
|
||||
if ruleCount > 0 {
|
||||
t.Fatalf("expected 0 rules for deleted site, got %d", ruleCount)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user