fix: 同步原生账号启停与商务审核状态

This commit is contained in:
Yvan
2026-11-03 22:02:51 +08:00
parent 075d94a51e
commit fc17b384ee
10 changed files with 423 additions and 7 deletions
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
businessbiz "kra/internal/biz/business"
"kra/internal/biz/system"
"kra/internal/config"
businessdata "kra/internal/data/business"
systemdata "kra/internal/data/system"
"kra/internal/server/handler"
"kra/internal/server/router"
businessservice "kra/internal/service/business"
systemservice "kra/internal/service/system"
)
type nativeUserHTTPData struct {
businessHTTPData
runtime *config.Store
}
func (d nativeUserHTTPData) Database(name string) (*gorm.DB, error) {
if name == "" {
return d.db, nil
}
return nil, fmt.Errorf("未配置")
}
func (d nativeUserHTTPData) DatabaseReady() bool { return true }
func (d nativeUserHTTPData) Runtime() *config.Store { return d.runtime }
func TestNativeUserEnableAndBusinessStatusOverHTTP(t *testing.T) {
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "native-status.db")), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
pool, _ := db.DB()
t.Cleanup(func() { _ = pool.Close() })
for _, step := range append(systemdata.Migrations(), businessdata.Migrations()...) {
if err := step.Migrate(db); err != nil {
t.Fatal(err)
}
}
if err := db.Exec("INSERT INTO sys_users(id,uuid,username,nick_name,authority_id,level,id_path,enable,business_status) VALUES(1,'root','root','root',888,0,'0',1,0),(2,'normal','normal','normal',901,1,'0',1,0),(3,'pending','pending','pending',903,3,'0,2',0,3),(4,'rejected','rejected','rejected',903,3,'0,2',0,2)").Error; err != nil {
t.Fatal(err)
}
provider := nativeUserHTTPData{businessHTTPData{db}, config.NewStore(&config.Config{Admin: &config.Admin{}})}
users := handler.NewUser(systemservice.NewUserService(system.NewUserUsecase(systemdata.NewUserRepo(provider)), nil), nil)
accounts := handler.NewBusiness(nil, businessservice.NewAccountService(businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider))), nil)
engine := gin.New()
engine.Use(func(c *gin.Context) {
id := uint64(1)
if raw := c.GetHeader("X-Test-User"); raw != "" {
id, _ = strconv.ParseUint(raw, 10, 64)
}
aid := uint(903)
if id == 1 {
aid = 888
}
c.Set("admin_claims", &system.AuthClaims{ID: uint(id), AuthorityID: aid})
c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), system.Actor{UserID: uint(id), AuthorityID: aid}))
c.Next()
})
router.RegisterUser(engine.Group(""), users)
engine.GET("/sw/business/:userId", accounts.Account)
server := httptest.NewServer(engine)
defer server.Close()
call := func(user int, method, path, body string, expected int) map[string]any {
t.Helper()
req, _ := http.NewRequest(method, server.URL+path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Test-User", strconv.Itoa(user))
res, err := server.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
var result map[string]any
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
t.Fatal(err)
}
if result["code"] != float64(expected) {
t.Fatalf("%s %s 返回错误: %+v", method, path, result)
}
return result["data"].(map[string]any)
}
check := func(id int, status string, enable int) {
t.Helper()
data := call(1, "GET", fmt.Sprintf("/sw/business/%d", id), "", 0)
var actual int
db.Table("sys_users").Select("enable").Where("id = ?", id).Scan(&actual)
if data["status"] != status || actual != enable {
t.Fatalf("账号%d状态不同步: %+v enable=%d", id, data, actual)
}
}
call(1, "PUT", "/user/setUserInfo", `{"ID":2,"nickName":"停用后","enable":2}`, 0)
check(2, "1", 2)
call(1, "PUT", "/user/setUserInfo", `{"ID":2,"nickName":"启用后","enable":1}`, 0)
check(2, "0", 1)
call(1, "PUT", "/user/setUserInfo", `{"ID":3,"nickName":"修改待审资料","enable":0}`, 0)
check(3, "3", 0)
call(1, "PUT", "/user/setUserInfo", `{"ID":4,"nickName":"修改驳回资料","enable":2}`, 0)
check(4, "2", 2)
call(1, "PUT", "/user/setUserInfo", `{"ID":3,"nickName":"审核后启用","enable":1}`, 0)
check(3, "0", 1)
// 模拟鉴权之后管理员又修改状态的竞争,个人资料不能用 enable 绕过审核。
call(4, "PUT", "/user/setSelfInfo", `{"enable":1,"nickName":"试图自行启用"}`, 7)
check(4, "2", 2)
call(2, "PUT", "/user/setSelfInfo", `{"enable":2}`, 0)
check(2, "1", 2)
call(2, "PUT", "/user/setSelfInfo", `{"enable":1}`, 7)
check(2, "1", 2)
call(1, "PUT", "/user/setUserInfo", `{"ID":99999,"enable":1}`, 7)
}
+2
View File
@@ -78,4 +78,6 @@
- 游戏分布只列当前渠道已通过的推广游戏,读取 `tab_sw_report_day` / `tab_sw_report_day_key`,人数按当前去重模式去重,金额固定模式 0。累计起点保留旧实现从当前人数口径的最早日汇总取得、缺省当天的规则,截止上海当天;不能与列表原始注册/充值口径混用。金额先保留两位再排序,同金额按游戏 ID。
- 新增渠道由 biz 决定一级、待审核 `-3`、默认注册类型和七天结算周期;data 在对应源平台库同事务写渠道、结算记录、商务渠道关系。密码复用旧 cmf 格式和 `SW_SDK_AUTH_CODE`。MySQL 创建使用固定连接持有账号命名锁直到事务提交,商务关系在行锁内追加;命名锁的真实并发验证留待最终隔离平台库联调。没有创建真实平台测试渠道,也没有伪造主库同步副本。
- 渠道批次通过全量 Go 检查、前端 ESLint/生产构建、独立主库和源平台 SQLite 的 TCP HTTP 验证。`channel-http-2026-11-03.json` 记录实际 sw7 会话请求,当前主库渠道数据未接入,lx/hw/yq 正常绑定选项分别为 17/20/20 条。`channel-page-2026-11-03.json` 核对一级商务新增入口、平台切换清空绑定、跨平台同号、零游戏数入口、缺失指标提示及新增提交参数。画像入口继续随画像用例迁移,当前不将整个渠道菜单标为完成。
- 核对剩余用户接口的基线差异及 Kra 原生能力,记录于 `system-api-audit.md`;保留历史别名未注册、Excel 协议未兼容等差异,不把能力对应表当成接口兼容完成证明。商务页面仍不增加原页面没有的编辑/删除/导入/导出入口。
- 修复原生用户启停只更新 enable、商务状态不同步的问题:由 biz 决定状态联动,在 data 行锁事务中一起写入;未启用时保留待审/驳回,个人资料请求不能自行解除管理员限制。当前 45 个真实用户只读检查未发现已有冲突,没有批量更新真实账号;正常启停、审核状态保留、失败回滚和越权通过隔离 HTTP/仓储/领域测试。全量 Go 测试通过,实际服务检查记录为 `user-status-http-2026-11-03.json`。
- 当前没有执行最终旧数据库复制与迁移验收。
+42
View File
@@ -0,0 +1,42 @@
# 系统接口承接核对
核对基线:旧后端 `d31082f4a7816d4c83544b8f54e6f42a3b2132b1` 至 `main`。原 SQL 的接口表按控制器收集接口,因此包含基线前已有的 RuoYi 用户管理接口;不能把所有记录都当成新商务新增功能,也不能只凭新路由存在就宣布功能完成。
## 商务新增与改动
`SysUserController` 的 Git 差异主要是商务列表语义、添加子商务,以及直属渠道、平台绑定和两种商务画像。用户列表查询新增商务树、姓名/等级过滤、直属渠道与正常绑定数量;添加子商务新增层级、归属与偏好初始化。当前列表、子商务、偏好和平台绑定已迁移;商务/平台商务画像仍未完成。
原用户控制器中的以下方法在基线前已经存在,旧 `BusMgmt/BusinessList.vue` 没有为它们增加实际操作入口。新商务页面不据此增加编辑、删除、导入、导出、角色分配等按钮。
| 旧目标别名 | 当前 Kra 能力 / 核对结果 |
| --- | --- |
| `GET /sw/business`(新增表单初始化) | 原生用户表单通过用户、角色、部门接口加载;商务新增表单由后端决定商务层级,不开放角色选择 |
| `DELETE /sw/business/:userIds` | 原生 `DELETE /user/deleteUser`;旧商务列表没有删除入口 |
| `GET /sw/business/optionselect` | 原生用户列表/部门用户查询;商务与报表实际使用各自的可见范围接口 |
| `PUT /sw/business/resetPwd` | 原生 `POST /user/resetPassword`;商务自助找回密码使用已迁移的独立流程 |
| `PUT /sw/business/changeStatus` | 原生 `PUT /user/setUserInfo`;本批补齐 enable 与 business_status 联动 |
| `GET /sw/business/authRole/:userId` | 原生用户角色资料及角色列表 |
| `PUT /sw/business/authRole` | 原生 `POST /user/setUserAuthorities` |
| `GET /sw/business/deptTree` | 原生 `POST /department/getDepartmentList` |
| `GET /sw/business/list/dept/:deptId` | 原生 `GET /department/getDepartmentUsers` |
| `POST /sw/business/export` | RuoYi Excel 导出协议尚未增加别名;旧导出调用共享查询构造器,会继承商务过滤,不能把原生通用导出直接算成相同协议已实现 |
| `POST /sw/business/importData` | RuoYi Excel 导入协议尚未实现;当前商务模板没有导入入口 |
| `GET /sw/business/unlock/:userId` | 原先删除 RuoYi Redis 密码错误键;新系统使用 Kra 鉴权/限制机制,不读取旧 Redis 键。旧协议别名尚未注册 |
以上记录说明框架能力的对应关系,并非宣称旧别名已可调用。别名与原生接口的请求/响应并不相同;最终接口核对仍须保留这项差异,尤其不能把导入/导出列为已经通过兼容验收。当前不删除用户 SQL 中的这些历史接口元数据。
## 账号启停与商务状态
原 RuoYi 用 `status` 同时表示正常、停用、驳回和待审核。迁移后有 Kra 的 `enable` 和保留语义的 `business_status`。原生用户启停此前只改前者,可能导致商务列表仍显示正常,或启用后仍因旧状态无法登录。
本批处理:
- 新建原生账号时,默认启用与禁用状态同步到商务状态。
- 管理员明确启用时恢复正常;关闭启用时,普通状态变为停用,待审核/驳回继续保留,避免普通资料编辑丢失审核状态。
- 读取当前业务状态与写入两个字段在同一行锁事务内完成,规则由 biz 回调决定,data 只做持久化。
- 个人资料不传 enable 时仍按原有字段更新规则处理;传 enable 时不能越过管理员已经写入的停用、待审或驳回状态。
- 角色范围检查继续执行,不增加商务列表的编辑入口。
隔离 TCP HTTP 测试覆盖原生启停后商务接口返回状态、待审/驳回资料编辑、自行启用被拒绝和不存在用户;仓储测试覆盖失败回滚与越权。现有鉴权测试继续检查停用/待审/驳回会拒绝密码登录和已有会话。
当前 sw7 的 45 个用户已做只读一致性检查,未发现 enable 与 business_status 冲突,因此没有批量改写真实账号。
@@ -0,0 +1,35 @@
{
"scope": "真实 sw7 登录会话,只读检查;未启停任何真实账号。正向启停和审核状态保留由隔离 TCP HTTP 测试覆盖。",
"checks": [
{
"method": "POST",
"path": "/user/getUserList",
"http": 200,
"code": 0,
"msg": "获取成功",
"passed": true
},
{
"method": "GET",
"path": "/sw/business/list?pageNum=1&pageSize=500",
"http": 200,
"code": 0,
"msg": "成功",
"passed": true
},
{
"method": "PUT",
"path": "/user/setUserInfo",
"http": 200,
"code": 7,
"msg": "ID值不能为空",
"passed": true
}
],
"native_total": 45,
"business_total": 45,
"checked_users": 45,
"inconsistent_states": 0,
"response_mismatches": 0,
"passed": true
}
+29 -1
View File
@@ -52,7 +52,9 @@ type UserRepo interface {
ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
CreateUserWithAuthorities(context.Context, *User, []uint) (*User, error)
UpdateUser(context.Context, *User) error
UpdateUserWithStatus(context.Context, *User, func(int) (int, error)) error
UpdateSelfUser(context.Context, *User) error
UpdateSelfUserWithStatus(context.Context, *User, func(int) (int, error)) error
DeleteUser(context.Context, uint) error
UpdatePassword(context.Context, uint, string, bool) error
SetUserAuthorities(context.Context, uint, []uint) error
@@ -108,6 +110,10 @@ func (uc *UserUsecase) ListUsers(ctx context.Context, page, pageSize int, filter
return uc.repo.ListUsers(ctx, page, pageSize, filter)
}
func (uc *UserUsecase) CreateUser(ctx context.Context, user *User, authorityIDs []uint) (*User, error) {
if user.Enable == 0 {
user.Enable = 1
}
user.BusinessStatus = businessStatusForEnable(0, user.Enable)
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
@@ -125,10 +131,32 @@ func (uc *UserUsecase) UpdateUser(ctx context.Context, user *User, authorityIDs
return err
}
}
return uc.repo.UpdateUser(ctx, user)
return uc.repo.UpdateUserWithStatus(ctx, user, func(current int) (int, error) {
return businessStatusForEnable(current, user.Enable), nil
})
}
// 启用代表恢复正常;未启用时保留待审核/驳回,避免编辑资料丢失业务审核状态。
func businessStatusForEnable(current, enable int) int {
if enable == 1 {
return 0
}
if current == 2 || current == 3 {
return current
}
return 1
}
func (uc *UserUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
if user.Enable != 0 {
return uc.repo.UpdateSelfUserWithStatus(ctx, user, func(current int) (int, error) {
// 个人资料请求不能越过管理员刚写入的停用或审核状态。
if current != 0 {
return current, ErrUserDisabled
}
return businessStatusForEnable(current, user.Enable), nil
})
}
return uc.repo.UpdateSelfUser(ctx, user)
}
+51
View File
@@ -59,3 +59,54 @@ func TestSetUserAuthoritiesRejectsEmptyListAsBadRequest(t *testing.T) {
t.Fatalf("SetUserAuthorities() error = %v, want ErrAuthoritiesRequired", err)
}
}
type statusUpdateRepo struct {
UserRepo
current, next int
created *User
}
func (r *statusUpdateRepo) UpdateUserWithStatus(_ context.Context, _ *User, policy func(int) (int, error)) error {
var err error
r.next, err = policy(r.current)
return err
}
func (r *statusUpdateRepo) UpdateSelfUserWithStatus(ctx context.Context, user *User, policy func(int) (int, error)) error {
return r.UpdateUserWithStatus(ctx, user, policy)
}
func (r *statusUpdateRepo) CreateUserWithAuthorities(_ context.Context, user *User, _ []uint) (*User, error) {
r.created = user
return user, nil
}
func TestNativeEnablePreservesBusinessReviewStates(t *testing.T) {
for _, tc := range []struct{ current, enable, want int }{{0, 2, 1}, {1, 1, 0}, {2, 0, 2}, {3, 2, 3}, {2, 1, 0}, {3, 1, 0}} {
repo := &statusUpdateRepo{current: tc.current}
if err := NewUserUsecase(repo).UpdateUser(context.Background(), &User{ID: 1, Enable: tc.enable}, nil); err != nil || repo.next != tc.want {
t.Fatalf("商务状态与启停不同步: %+v next=%d err=%v", tc, repo.next, err)
}
}
}
func TestNativeCreationStatusMatchesDefaultAndDisabledEnable(t *testing.T) {
for _, tc := range []struct{ enable, wantEnable, wantStatus int }{{0, 1, 0}, {1, 1, 0}, {2, 2, 1}} {
repo := &statusUpdateRepo{}
_, err := NewUserUsecase(repo).CreateUser(context.Background(), &User{Username: "test", Password: "test-password", Enable: tc.enable}, nil)
if err != nil || repo.created.Enable != tc.wantEnable || repo.created.BusinessStatus != tc.wantStatus {
t.Fatalf("新账号状态错误: %+v %v", repo.created, err)
}
}
}
func TestSelfProfileCannotApproveOrReenableBusinessState(t *testing.T) {
for _, status := range []int{1, 2, 3} {
repo := &statusUpdateRepo{current: status}
if err := NewUserUsecase(repo).UpdateSelfUser(context.Background(), &User{ID: 1, Enable: 1}); err != ErrUserDisabled || repo.next != status {
t.Fatal("个人资料修改越过了停用/审核状态")
}
}
repo := &statusUpdateRepo{current: 0}
if err := NewUserUsecase(repo).UpdateSelfUser(context.Background(), &User{ID: 1, Enable: 2}); err != nil || repo.next != 1 {
t.Fatal("个人资料停用字段没有同步商务状态")
}
}
+47 -6
View File
@@ -342,7 +342,7 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *system.U
user.NickName = "系统用户"
}
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, BusinessStatus: user.BusinessStatus, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
var createdAuthorities []system.Authority
if err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// The compatible registration rejects an existing non-deleted username before creating the
@@ -397,6 +397,34 @@ func (r *userRepo) UpdateUser(ctx context.Context, user *system.User) error {
return r.updateUser(r.data.DB().WithContext(ctx), user)
}
func (r *userRepo) UpdateUserWithStatus(ctx context.Context, user *system.User, updateStatus func(int) (int, error)) error {
if err := r.checkUserUpdateAuth(ctx, user); err != nil {
return err
}
return r.updateUserAndStatus(ctx, user.ID, userUpdateFields(user), updateStatus)
}
func (r *userRepo) updateUserAndStatus(ctx context.Context, userID uint, updates map[string]any, updateStatus func(int) (int, error)) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var current userPO
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id,business_status").First(&current, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return system.ErrUserNotFound
}
return err
}
// 使用锁内的最新审核状态运行领域策略,与 enable 在同一次更新中提交。
if updateStatus != nil {
status, err := updateStatus(current.BusinessStatus)
if err != nil {
return err
}
updates["business_status"] = status
}
return tx.Model(&userPO{}).Where("id = ?", userID).Updates(updates).Error
})
}
func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *system.User) error {
access := &authorityAccessRepo{data: r.data}
if err := access.checkUserIDAuth(ctx, user.ID, false); err != nil {
@@ -409,6 +437,18 @@ func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *system.User) e
}
func (r *userRepo) UpdateSelfUser(ctx context.Context, user *system.User) error {
updates := selfUserUpdateFields(user)
if len(updates) == 0 {
return nil
}
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *userRepo) UpdateSelfUserWithStatus(ctx context.Context, user *system.User, updateStatus func(int) (int, error)) error {
return r.updateUserAndStatus(ctx, user.ID, selfUserUpdateFields(user), updateStatus)
}
func selfUserUpdateFields(user *system.User) map[string]any {
updates := make(map[string]any)
if user.NickName != "" {
updates["nick_name"] = user.NickName
@@ -425,10 +465,7 @@ func (r *userRepo) UpdateSelfUser(ctx context.Context, user *system.User) error
if user.Enable != 0 {
updates["enable"] = user.Enable
}
if len(updates) == 0 {
return nil
}
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
return updates
}
func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *system.User) error {
@@ -473,11 +510,15 @@ func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *system.Use
}
func (r *userRepo) updateUser(tx *gorm.DB, user *system.User) error {
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(userUpdateFields(user)).Error
}
func userUpdateFields(user *system.User) map[string]any {
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
if user.AuthorityID != 0 {
updates["authority_id"] = user.AuthorityID
}
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
return updates
}
func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
@@ -0,0 +1,43 @@
package system
import (
"context"
"testing"
"kra/internal/biz/system"
)
func TestUserStatusPolicyReadsLockedStateAndWritesTogether(t *testing.T) {
data := newTransactionTestData(t)
db := data.gormDB.WithContext(context.Background())
user := userPO{Username: "status-user", AuthorityID: 888, Enable: 0, BusinessStatus: 3, NickName: "old"}
if err := db.Create(&user).Error; err != nil {
t.Fatal(err)
}
repo := &userRepo{data: data}
seen := -1
err := repo.UpdateUserWithStatus(context.Background(), &system.User{ID: user.ID, NickName: "new", Enable: 1}, func(current int) (int, error) { seen = current; return 0, nil })
if err != nil {
t.Fatal(err)
}
var stored userPO
if err := db.First(&stored, user.ID).Error; err != nil {
t.Fatal(err)
}
if seen != 3 || stored.Enable != 1 || stored.BusinessStatus != 0 || stored.NickName != "new" {
t.Fatalf("状态策略和更新不同步: %+v seen=%d", stored, seen)
}
if err := db.Exec("CREATE TRIGGER fail_status_update BEFORE UPDATE OF business_status ON sys_users BEGIN SELECT RAISE(ABORT, 'test rejection'); END").Error; err != nil {
t.Fatal(err)
}
err = repo.UpdateUserWithStatus(context.Background(), &system.User{ID: user.ID, NickName: "failed", Enable: 2}, func(int) (int, error) { return 1, nil })
if err == nil {
t.Fatal("失败的状态更新应回滚")
}
if err := db.First(&stored, user.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Enable != 1 || stored.BusinessStatus != 0 || stored.NickName != "new" {
t.Fatal("留下部分状态或资料更新")
}
}
+3
View File
@@ -40,6 +40,9 @@ func TestStrictUserMutationsRejectOutsideTargetAndAllowSelfPassword(t *testing.T
if err := repo.UpdateUser(ctx, &system.User{ID: users[2].ID, NickName: "changed"}); err == nil {
t.Fatal("UpdateUser() accepted an out-of-scope target")
}
if err := repo.UpdateUserWithStatus(ctx, &system.User{ID: users[2].ID, Enable: 1}, func(int) (int, error) { t.Fatal("越权时不应执行状态策略"); return 0, nil }); err == nil {
t.Fatal("启停同步接口放行了角色范围外用户")
}
if err := repo.DeleteUser(ctx, users[2].ID); err == nil {
t.Fatal("DeleteUser() accepted an out-of-scope target")
}
+48
View File
@@ -0,0 +1,48 @@
"""只读核对原生账号与商务状态;写接口只发送缺失 ID 的无效请求。"""
import argparse
import json
import urllib.request
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default="http://127.0.0.1:18000")
parser.add_argument("--token-file", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
token = args.token_file.read_text(encoding="utf-8").strip()
checks = []
def request(method, path, body=None, expected=0):
req = urllib.request.Request(args.base_url + path, method=method,
headers={"x-token": token, "Content-Type": "application/json"},
data=json.dumps(body).encode() if body is not None else None)
with urllib.request.urlopen(req, timeout=30) as response:
result = json.load(response)
checks.append({"method": method, "path": path, "http": response.status,
"code": result.get("code"), "msg": result.get("msg"),
"passed": response.status == 200 and result.get("code") == expected})
return result.get("data", {})
native = request("POST", "/user/getUserList", {"page": 1, "pageSize": 500})
business = request("GET", "/sw/business/list?pageNum=1&pageSize=500")
native_rows = native.get("list", [])
by_id = {row["userId"]: row for row in business.get("rows", [])}
conflicts = sum((row.get("enable") == 1) != (row.get("businessStatus") == 0) for row in native_rows)
mismatches = sum(str(row.get("businessStatus")) != by_id.get(row["ID"], {}).get("status") for row in native_rows)
request("PUT", "/user/setUserInfo", {"ID": 0, "enable": 1}, expected=7)
report = {"scope": "真实 sw7 登录会话,只读检查;未启停任何真实账号。正向启停和审核状态保留由隔离 TCP HTTP 测试覆盖。",
"checks": checks, "native_total": native.get("total"), "business_total": business.get("total"),
"checked_users": len(native_rows), "inconsistent_states": conflicts, "response_mismatches": mismatches,
"passed": all(item["passed"] for item in checks) and conflicts == 0 and mismatches == 0
and len(native_rows) == native.get("total") == business.get("total")}
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, ensure_ascii=False))
if not report["passed"]:
raise SystemExit(1)
if __name__ == "__main__":
main()