fix: 修复缺少渠道表时商务列表失败并补充接口实测

This commit is contained in:
Yvan
2026-11-03 18:10:59 +08:00
parent 794001dfbe
commit 43f0ec1b15
8 changed files with 224 additions and 13 deletions
+102
View File
@@ -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)
}
}
@@ -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 必须重新运行才能使用新代码",
"本次未验证完整渠道数据,完整旧库复制和最终一致性核对仍待迁移结束后确认"
]
}
+4
View File
@@ -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 旧二进制,需重新运行才加载源码修复。
- 当前没有执行本轮真实数据库迁移验证。
+13 -6
View File
@@ -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
}
+33 -4
View File
@@ -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
+1 -1
View File
@@ -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{}))
+1 -1
View File
@@ -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"`
}
+6 -1
View File
@@ -28,7 +28,12 @@
<el-table-column prop="realName" label="姓名" min-width="100" />
<el-table-column prop="phoneNumber" label="手机号" min-width="130" />
<el-table-column prop="bindCount" label="SDK 商务数" width="120" />
<el-table-column prop="promoteCount" label="直属渠道数" width="120" />
<el-table-column prop="promoteCount" label="直属渠道数" width="120">
<template #default="{ row }">
<el-tooltip v-if="row.promoteCount === null" content="当前数据库尚未接入渠道数据,商务资料和绑定数仍可查看。"><span class="text-gray-400">未接入</span></el-tooltip>
<span v-else>{{ row.promoteCount }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="注册时间" sortable="custom" min-width="180" />
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statuses[row.status] || row.status }}</el-tag></template></el-table-column>
<el-table-column v-if="canEdit" label="操作" fixed="right" width="90"><template #default="{ row }"><el-button v-if="row.authorityId !== 888" type="primary" link @click="openEdit(row)">编辑</el-button></template></el-table-column>