实现 P2 管理端:租户生命周期、套餐与运维真实 API

- 租户开通自动创建管理员账号与网页渠道,支持暂停/恢复/编辑
- 运营概览返回月收入估算、套餐分布与真实操作日志
- 套餐上下架/新建,公告与操作日志前后端打通
- 补充管理端生命周期集成测试
This commit is contained in:
yml2213
2026-07-15 11:22:37 +08:00
parent 65014d80de
commit b5203f7cbd
7 changed files with 1095 additions and 261 deletions
@@ -623,3 +623,62 @@ func TestCustomerAndKnowledgeCRUD(t *testing.T) {
t.Fatalf("创建知识条目失败: %s", createEntryRec.Body.String())
}
}
func TestAdminTenantLifecycleAndPlanToggle(t *testing.T) {
router := setupRouter(t)
platform := createUser(t, 0, "platform-ops", "platform_admin")
plan := model.Plan{Name: "测试套餐", PriceMonthly: 199, Seats: 3, StorageDays: 30, KBLimit: 20, Status: "active"}
if err := model.DB.Create(&plan).Error; err != nil {
t.Fatalf("创建套餐失败: %v", err)
}
createRec := httptest.NewRecorder()
body := fmt.Sprintf(`{"name":"新开租户A","contact_name":"王经理","contact_phone":"13800138001","plan_id":%d,"seat_count":5,"duration_months":6}`, plan.ID)
router.ServeHTTP(createRec, bearerRequest(t, http.MethodPost, "/api/admin/tenants", []byte(body), platform))
if createRec.Code != http.StatusOK {
t.Fatalf("开通租户失败: %s", createRec.Body.String())
}
var createResp struct {
Data struct {
Tenant struct {
ID uint `json:"id"`
} `json:"tenant"`
AdminUsername string `json:"admin_username"`
} `json:"data"`
}
if err := json.Unmarshal(createRec.Body.Bytes(), &createResp); err != nil || createResp.Data.Tenant.ID == 0 || createResp.Data.AdminUsername == "" {
t.Fatalf("解析开通响应失败: %v body=%s", err, createRec.Body.String())
}
suspendRec := httptest.NewRecorder()
router.ServeHTTP(suspendRec, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/admin/tenants/%d/suspend", createResp.Data.Tenant.ID), []byte(`{}`), platform))
if suspendRec.Code != http.StatusOK {
t.Fatalf("暂停失败: %s", suspendRec.Body.String())
}
var tenant model.Tenant
model.DB.First(&tenant, createResp.Data.Tenant.ID)
if tenant.Status != "suspended" {
t.Fatalf("租户状态应为 suspended: %+v", tenant)
}
resumeRec := httptest.NewRecorder()
router.ServeHTTP(resumeRec, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/admin/tenants/%d/resume", createResp.Data.Tenant.ID), []byte(`{}`), platform))
if resumeRec.Code != http.StatusOK {
t.Fatalf("恢复失败: %s", resumeRec.Body.String())
}
model.DB.First(&tenant, createResp.Data.Tenant.ID)
if tenant.Status != "normal" {
t.Fatalf("恢复后状态应为 normal: %+v", tenant)
}
planRec := httptest.NewRecorder()
router.ServeHTTP(planRec, bearerRequest(t, http.MethodPut, fmt.Sprintf("/api/admin/plans/%d", plan.ID), []byte(`{"status":"inactive"}`), platform))
if planRec.Code != http.StatusOK {
t.Fatalf("套餐下架失败: %s", planRec.Body.String())
}
var updatedPlan model.Plan
model.DB.First(&updatedPlan, plan.ID)
if updatedPlan.Status != "inactive" {
t.Fatalf("套餐状态未更新: %+v", updatedPlan)
}
}