From 43f0ec1b156c71a34c4e9172c778bfab15ce3dcc Mon Sep 17 00:00:00 2001
From: Yvan <8574526@qq,com>
Date: Tue, 3 Nov 2026 18:10:59 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=BA=E5=B0=91?=
=?UTF-8?q?=E6=B8=A0=E9=81=93=E8=A1=A8=E6=97=B6=E5=95=86=E5=8A=A1=E5=88=97?=
=?UTF-8?q?=E8=A1=A8=E5=A4=B1=E8=B4=A5=E5=B9=B6=E8=A1=A5=E5=85=85=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E5=AE=9E=E6=B5=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
cmd/business_http_test.go | 102 ++++++++++++++++++
.../http-verification-2026-11-03.json | 64 +++++++++++
docs/sw6-to-kra/implementation.md | 4 +
internal/biz/business/account.go | 19 ++--
internal/data/business/account.go | 37 ++++++-
internal/data/business/account_test.go | 2 +-
internal/service/dto/business_account.go | 2 +-
web/src/view/sw/BusinessList.vue | 7 +-
8 files changed, 224 insertions(+), 13 deletions(-)
create mode 100644 cmd/business_http_test.go
create mode 100644 docs/sw6-to-kra/http-verification-2026-11-03.json
diff --git a/cmd/business_http_test.go b/cmd/business_http_test.go
new file mode 100644
index 0000000..4e4d5d0
--- /dev/null
+++ b/cmd/business_http_test.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/glebarez/sqlite"
+ "gorm.io/gorm"
+ businessbiz "kra/internal/biz/business"
+ "kra/internal/biz/system"
+ businessdata "kra/internal/data/business"
+ systemdata "kra/internal/data/system"
+ "kra/internal/server/handler"
+ "kra/internal/server/router"
+ businessservice "kra/internal/service/business"
+)
+
+type businessHTTPData struct{ db *gorm.DB }
+
+func (d businessHTTPData) DB() *gorm.DB { return d.db }
+
+func TestBusinessHTTPListWithSystemOnlyAndCompleteDatabase(t *testing.T) {
+ db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "http.db")), &gorm.Config{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ sqlDB, err := db.DB()
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = sqlDB.Close() })
+ for _, step := range append(systemdata.Migrations(), businessdata.Migrations()...) {
+ if err := step.Migrate(db); err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, statement := range []string{
+ "INSERT INTO sys_users(id,uuid,username,authority_id,level,id_path) VALUES (1,'admin','admin',888,0,'0'),(2,'child','child',901,1,'0'),(3,'empty','empty',901,1,'0')",
+ "INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES (2,'lx',9,'0')",
+ } {
+ if err := db.Exec(statement).Error; err != nil {
+ t.Fatal(err)
+ }
+ }
+ provider := businessHTTPData{db}
+ accounts := businessservice.NewAccountService(businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider)))
+ prefs := businessservice.NewUserPreferenceService(businessbiz.NewUserPreferenceUsecase(businessdata.NewUserPreferenceRepo(provider)))
+ engine := gin.New()
+ // 此契约测试固定已认证身份;真实登录另通过运行实例验证。
+ engine.Use(func(c *gin.Context) {
+ c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), system.Actor{UserID: 1, AuthorityID: 888}))
+ c.Next()
+ })
+ router.RegisterBusiness(engine.Group(""), handler.NewBusiness(prefs, accounts))
+ server := httptest.NewServer(engine)
+ defer server.Close()
+ requestList := func() map[string]any {
+ t.Helper()
+ req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/sw/business/list?pageNum=1&pageSize=10", nil)
+ response, err := server.Client().Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer response.Body.Close()
+ var body map[string]any
+ if err := json.NewDecoder(response.Body).Decode(&body); err != nil {
+ t.Fatal(err)
+ }
+ if response.StatusCode != 200 || body["code"] != float64(0) {
+ t.Fatalf("真实 HTTP 列表请求失败: %+v", body)
+ }
+ return body["data"].(map[string]any)
+ }
+ data := requestList()
+ rows := data["rows"].([]any)
+ if data["total"] != float64(3) || len(rows) != 3 {
+ t.Fatalf("系统数据未完整返回: %+v", data)
+ }
+ child := rows[1].(map[string]any)
+ if child["promoteCount"] != nil || child["bindCount"] != float64(1) {
+ t.Fatalf("未接入统计不能伪装为零: %+v", child)
+ }
+ if rows[2].(map[string]any)["promoteCount"] != float64(0) {
+ t.Fatal("无绑定账号的直属渠道应明确为零")
+ }
+ if err := db.Exec("CREATE TABLE tab_promote(id INTEGER,source_db TEXT,busier_id INTEGER,promote_level INTEGER)").Error; err != nil {
+ t.Fatal(err)
+ }
+ if err := db.Exec("INSERT INTO tab_promote VALUES(1,'lx',9,1),(2,'lx',9,2),(1,'hw',9,1)").Error; err != nil {
+ t.Fatal(err)
+ }
+ data = requestList()
+ child = data["rows"].([]any)[1].(map[string]any)
+ if child["promoteCount"] != float64(1) {
+ t.Fatalf("完整数据应恢复真实且按平台隔离的渠道数: %+v", child)
+ }
+}
diff --git a/docs/sw6-to-kra/http-verification-2026-11-03.json b/docs/sw6-to-kra/http-verification-2026-11-03.json
new file mode 100644
index 0000000..24c44b5
--- /dev/null
+++ b/docs/sw6-to-kra/http-verification-2026-11-03.json
@@ -0,0 +1,64 @@
+{
+ "date": "2026-11-03",
+ "timezone": "Asia/Shanghai",
+ "base_url": "http://127.0.0.1:18000",
+ "database_scope": "沿用已配置的 sw7 系统迁移测试库;未复制或迁移旧正式数据库",
+ "authentication": "通过本地服务验证码及账号密码正常登录后,携带 JWT 请求;凭据和令牌不记录在报告中",
+ "observed_failure": "原 8000 实例日志:Error 1146,sw7.tab_promote 不存在导致商务列表返回商务数据访问失败",
+ "fix": "基础商务资料和已通过绑定数继续返回;有绑定但渠道表未接入时 promoteCount 为 null;完整业务库仍按原口径计算真实渠道数",
+ "checks": [
+ {
+ "path": "/health",
+ "http": 200,
+ "elapsed_ms": 64
+ },
+ {
+ "path": "/sw/business/list?pageNum=1&pageSize=10",
+ "http": 200,
+ "elapsed_ms": 36,
+ "code": 0,
+ "msg": "成功",
+ "total": 45,
+ "rows": 10,
+ "unavailable_promote_counts": 1
+ },
+ {
+ "path": "/sw/business/list?pageNum=2&pageSize=10",
+ "http": 200,
+ "elapsed_ms": 46,
+ "code": 0,
+ "msg": "成功",
+ "total": 45,
+ "rows": 10,
+ "unavailable_promote_counts": 5
+ },
+ {
+ "path": "/sw/business/list?userName=admin&pageNum=1&pageSize=10",
+ "http": 200,
+ "elapsed_ms": 32,
+ "code": 0,
+ "msg": "成功",
+ "total": 1,
+ "rows": 1,
+ "unavailable_promote_counts": 0
+ },
+ {
+ "path": "/sw/business/10001",
+ "http": 200,
+ "elapsed_ms": 33,
+ "code": 0,
+ "msg": "成功"
+ },
+ {
+ "path": "/sw/user/preference",
+ "http": 200,
+ "elapsed_ms": 103,
+ "code": 0,
+ "msg": "成功"
+ }
+ ],
+ "limits": [
+ "验证实例与 GoLand 的 8000 进程独立;GoLand 必须重新运行才能使用新代码",
+ "本次未验证完整渠道数据,完整旧库复制和最终一致性核对仍待迁移结束后确认"
+ ]
+}
diff --git a/docs/sw6-to-kra/implementation.md b/docs/sw6-to-kra/implementation.md
index 39f4f0d..c8df480 100644
--- a/docs/sw6-to-kra/implementation.md
+++ b/docs/sw6-to-kra/implementation.md
@@ -11,6 +11,8 @@
沿用现有迁移 SQL 的 `/sw/` 路由与 `view/sw/` 菜单组件路径。认证、响应信封、权限、表格及图表采用 Kra;业务字段、统计口径、状态流转、可见范围保留旧实现。登录页、系统名称和品牌资源来自旧商务。新增代码注释与提交信息使用中文。
+用户补充:允许直接优化旧接口、SQL 和逻辑。优化时记录行为差异并验证业务结果;各批实现必须实际请求 HTTP 接口,不能只以单元测试和构建作为完成依据。
+
用户已有的 `configs/config.yaml` 修改与迁移 SQL 保留。未经最后阶段的数据库确认,不连接、复制或修改用户的旧数据库。
## 依赖顺序与完成标准
@@ -47,4 +49,6 @@
- 已生成 `source-inventory.json`:现有 SQL 的 63 个接口契约、279 个变更源文件及额外找回密码控制器。此清单是范围索引,不代表已完成实现。
- 已执行 `make generate`、相关 Go 测试、变更前端 ESLint 和生产构建。构建存在原 `arcdash` 依赖的 BigInt 浏览器目标警告。
- 第一批提交 `ce4b54a` 后 `go test ./...` 全量通过;第二批账号功能的 biz/data/service/modules/server/cmd 检查及前端生产构建通过。
+- 用户反馈实际商务列表失败后,定位为 sw7 缺少 `tab_promote`。已解耦基础列表与渠道统计:有绑定且渠道表缺失时返回 `promoteCount: null`,界面显示“未接入”,无绑定账号仍为真实零。新增通过真实 TCP 请求的 HTTP 回归测试,覆盖系统数据库及完整渠道表两种场景。
+- 已通过正常验证码登录取得测试会话,并在修复后的独立 18000 实例连接同一 sw7 执行 HTTP 请求。列表总数 45,分页、账号筛选、资料和偏好查询全部成功。详见 `http-verification-2026-11-03.json`。8000 为用户 GoLand 旧二进制,需重新运行才加载源码修复。
- 当前没有执行本轮真实数据库迁移验证。
diff --git a/internal/biz/business/account.go b/internal/biz/business/account.go
index 8d2357e..0873a92 100644
--- a/internal/biz/business/account.go
+++ b/internal/biz/business/account.go
@@ -32,14 +32,21 @@ type Account struct {
RejectReason string
AuthorityID uint
CreatedAt, UpdatedAt time.Time
- PromoteCount, BindCount int64
+ PromoteCount *int64
+ BindCount int64
+}
+
+type AccountCounts struct {
+ // 无渠道表时为 nil,避免将未接入数据误报为真实零渠道。
+ PromoteCount *int64
+ BindCount int64
}
// UserScope 表示商务树可见范围,与框架部门权限相互独立。
type UserScope struct {
- All, Empty, IncludeSelf bool
- SelfID uint
- DescendantPath string
+ All, Empty, IncludeSelf, OnlySelf bool
+ SelfID uint
+ DescendantPath string
}
type AccountFilter struct {
@@ -84,7 +91,7 @@ type AccountRepo interface {
Create(context.Context, *Account) error
Update(context.Context, *Account) error
AuthorityForLevel(context.Context, int) (uint, error)
- DirectCounts(context.Context, []uint) (map[uint][2]int64, error)
+ DirectCounts(context.Context, []uint) (map[uint]AccountCounts, error)
}
type AccountUsecase struct{ repo AccountRepo }
@@ -141,7 +148,7 @@ func (uc *AccountUsecase) List(ctx context.Context, filter AccountFilter, option
return nil, 0, err
}
for _, row := range rows {
- row.PromoteCount, row.BindCount = counts[row.ID][0], counts[row.ID][1]
+ row.PromoteCount, row.BindCount = counts[row.ID].PromoteCount, counts[row.ID].BindCount
}
return rows, total, nil
}
diff --git a/internal/data/business/account.go b/internal/data/business/account.go
index f755110..1f22c4e 100644
--- a/internal/data/business/account.go
+++ b/internal/data/business/account.go
@@ -71,6 +71,9 @@ func applyUserScope(db *gorm.DB, scope business.UserScope, columnPrefix string)
return db.Where("1=0")
}
id, path, parent := columnPrefix+"id", columnPrefix+"id_path", columnPrefix+"parent_id"
+ if scope.OnlySelf {
+ return db.Where(id+" = ?", scope.SelfID)
+ }
query := "(" + path + " = ? OR " + path + " LIKE ? OR " + parent + " = ?)"
args := []any{scope.DescendantPath, scope.DescendantPath + ",%", scope.SelfID}
if scope.IncludeSelf {
@@ -222,12 +225,16 @@ func (r *accountRepo) AuthorityForLevel(ctx context.Context, level int) (uint, e
return ids[0], nil
}
-func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint][2]int64, error) {
- result := map[uint][2]int64{}
+func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint]business.AccountCounts, error) {
+ result := map[uint]business.AccountCounts{}
if len(ids) == 0 {
return result, nil
}
db := r.data.DB().WithContext(ctx)
+ for _, id := range ids {
+ zero := int64(0)
+ result[id] = business.AccountCounts{PromoteCount: &zero}
+ }
var binds []struct {
UserID uint
Total int64
@@ -239,7 +246,28 @@ func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint][2
return result, nil
}
for _, b := range binds {
- result[b.UserID] = [2]int64{0, b.Total}
+ zero := int64(0)
+ result[b.UserID] = business.AccountCounts{PromoteCount: &zero, BindCount: b.Total}
+ }
+ // 系统数据演练库尚未包含渠道表时,基础资料与已通过绑定数仍可使用。
+ tables, err := db.Migrator().GetTables()
+ if err != nil {
+ return nil, business.ErrStorage
+ }
+ available := false
+ for _, table := range tables {
+ if table == "tab_promote" {
+ available = true
+ break
+ }
+ }
+ if !available {
+ for _, b := range binds {
+ value := result[b.UserID]
+ value.PromoteCount = nil
+ result[b.UserID] = value
+ }
+ return result, nil
}
var counts []struct {
UserID uint
@@ -250,7 +278,8 @@ func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint][2
}
for _, c := range counts {
value := result[c.UserID]
- value[0] = c.Total
+ count := c.Total
+ value.PromoteCount = &count
result[c.UserID] = value
}
return result, nil
diff --git a/internal/data/business/account_test.go b/internal/data/business/account_test.go
index bd28893..e368242 100644
--- a/internal/data/business/account_test.go
+++ b/internal/data/business/account_test.go
@@ -95,7 +95,7 @@ func TestAccountListScopesAndCrossPlatformCounts(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if counts[13] != [2]int64{3, 2} || counts[14] != [2]int64{1, 1} || counts[99] != [2]int64{} {
+ if counts[13].PromoteCount == nil || *counts[13].PromoteCount != 3 || counts[13].BindCount != 2 || counts[14].PromoteCount == nil || *counts[14].PromoteCount != 1 || counts[14].BindCount != 1 || counts[99].PromoteCount == nil || *counts[99].PromoteCount != 0 {
t.Fatalf("渠道数或绑定数口径不一致: %+v", counts)
}
rows, total, err = r.List(context.Background(), business.UserScope{}, business.ListFilter(business.AccountFilter{}))
diff --git a/internal/service/dto/business_account.go b/internal/service/dto/business_account.go
index 0d19849..8f80084 100644
--- a/internal/service/dto/business_account.go
+++ b/internal/service/dto/business_account.go
@@ -46,7 +46,7 @@ type BusinessAccountResponse struct {
AuthorityID uint `json:"authorityId"`
CreateTime string `json:"createTime"`
UpdateTime string `json:"updateTime"`
- PromoteCount int64 `json:"promoteCount"`
+ PromoteCount *int64 `json:"promoteCount"`
BindCount int64 `json:"bindCount"`
}
diff --git a/web/src/view/sw/BusinessList.vue b/web/src/view/sw/BusinessList.vue
index a5dd033..c67a0d8 100644
--- a/web/src/view/sw/BusinessList.vue
+++ b/web/src/view/sw/BusinessList.vue
@@ -28,7 +28,12 @@
-
+
+
+ 未接入
+ {{ row.promoteCount }}
+
+
{{ statuses[row.status] || row.status }}
编辑