Files
Neo-ZQYY/.kiro/specs/tenant-admin-web/design.md

26 KiB
Raw Blame History

技术设计文档租户管理后台tenant-admin-web

概述

本设计为 NS4 租户管理后台提供完整的技术方案。系统面向租户管理员Tenant_Admin提供用户审核与管理、Excel 数据上传4 种模板)、维客线索管理三大功能模块。

前端为独立 React 应用(apps/tenant-admin/),后端复用现有 FastAPI 服务(apps/backend/)新增 4 个路由模块,所有端点注册在 /api/tenant/ 前缀下。认证体系与小程序完全隔离,使用独立的 auth.tenant_admins 表、用户名+密码登录、JWT aud=tenant-admin

关键设计决策

决策 选择 理由
认证表 独立 auth.tenant_admins 与小程序 auth.users(微信登录)完全隔离,避免 user_type 判断复杂度
JWT 隔离 aud 字段区分 复用现有 jose + bcrypt 基础设施,仅扩展 payload
Excel 写入策略 staging 表 + ETL 同步(方案 B 数据经 ETL 标准流程,一致性有保障;助教奖罚直接写 biz
FDW 多店铺查询 逐 site_id 查询合并 RLS 视图要求 SET LOCAL app.current_site_id,单次只能设一个
前端技术栈 React + Vite + Ant Design apps/admin-web/ 一致,降低维护成本
响应格式 { code: 0, data } 复用现有 ResponseWrapperMiddleware,前端统一解包

架构

高层架构图

graph TB
    subgraph "前端 apps/tenant-admin/"
        FE[React + Vite + Ant Design]
        API_LAYER[services/api.ts<br/>JWT 自动附加/刷新]
    end

    subgraph "后端 apps/backend/"
        subgraph "路由层 /api/tenant/*"
            R_AUTH[tenant_auth.py<br/>登录/刷新]
            R_USERS[tenant_users.py<br/>审核+管理]
            R_EXCEL[tenant_excel.py<br/>上传/校验/冲突]
            R_CLUES[tenant_clues.py<br/>线索管理]
        end

        AUTH_DEP[require_tenant_admin<br/>认证依赖注入]
        MIDDLEWARE[ResponseWrapperMiddleware<br/>全局响应包装]
    end

    subgraph "数据层"
        APP_DB[(zqyy_app<br/>auth + biz + public)]
        ETL_DB[(etl_feiqiu<br/>fdw_etl 视图)]
    end

    FE --> API_LAYER
    API_LAYER -->|HTTP| MIDDLEWARE
    MIDDLEWARE --> R_AUTH
    MIDDLEWARE --> R_USERS
    MIDDLEWARE --> R_EXCEL
    MIDDLEWARE --> R_CLUES
    R_USERS --> AUTH_DEP
    R_EXCEL --> AUTH_DEP
    R_CLUES --> AUTH_DEP
    R_AUTH -->|登录无需认证| APP_DB
    AUTH_DEP --> APP_DB
    R_USERS -->|FDW 人员匹配| ETL_DB
    R_EXCEL -->|FDW 助教匹配| ETL_DB
    R_CLUES -->|FDW 客户搜索| ETL_DB

认证流程

sequenceDiagram
    participant FE as Tenant Admin Web
    participant BE as Backend API
    participant DB as auth.tenant_admins

    FE->>BE: POST /api/tenant/auth/login {username, password}
    BE->>DB: SELECT password_hash WHERE username=? AND is_active=true
    alt 凭据有效
        BE->>BE: bcrypt.checkpw() 验证
        BE->>BE: 签发 JWT (aud=tenant-admin, sub=admin_id, managed_site_ids)
        BE-->>FE: {access_token, refresh_token}
    else 凭据无效
        BE-->>FE: 401 用户名或密码错误
    else 账号禁用
        BE-->>FE: 403 账号已被禁用
    end

    Note over FE,BE: 后续请求
    FE->>BE: GET /api/tenant/* (Authorization: Bearer <token>)
    BE->>BE: require_tenant_admin() 验证 aud=tenant-admin
    BE->>BE: 提取 managed_site_ids附加数据隔离条件

Excel 上传流程

flowchart TD
    A[选择模板类型 + 上传 Excel] --> B[后端解析 + 格式校验]
    B --> C{校验结果}
    C -->|格式错误| D[返回错误行号/列名/描述<br/>前端标红展示]
    D --> A
    C -->|校验通过| E[人员匹配校验<br/>仅 salary_adj / recharge_commission]
    E --> F[冲突检测<br/>按主键规则匹配]
    F --> G{有冲突?}
    G -->|是| H[返回 diff 数据<br/>前端展示 diff 表格]
    H --> I[用户逐行选择<br/>替换/保留]
    G -->|否| J[标记为待写入]
    I --> K[确认写入]
    J --> K
    K --> L{写入目标}
    L -->|助教奖罚| M[biz.salary_adjustments]
    L -->|财务支出| N[biz.stg_finance_expense]
    L -->|团购收入| O[biz.stg_platform_income]
    L -->|充值归属| P[biz.stg_recharge_commission]
    K --> Q[更新 excel_upload_log<br/>status=confirmed]

组件与接口

后端路由模块

1. tenant_auth.py — 认证路由

端点 方法 路径 认证 说明
登录 POST /api/tenant/auth/login 用户名+密码 → JWT 令牌对
刷新 POST /api/tenant/auth/refresh refresh_token → 新令牌对

2. tenant_users.py — 用户审核 + 管理路由

端点 方法 路径 说明
申请列表 GET /api/tenant/applications status 筛选 + 分页
关联建议 GET /api/tenant/applications/{id}/match-suggestions FDW 助教/员工匹配
审核通过 POST /api/tenant/applications/{id}/approve 分配角色 + 绑定
审核拒绝 POST /api/tenant/applications/{id}/reject 填写拒绝原因
用户列表 GET /api/tenant/users 角色筛选 + 搜索 + 分页
编辑用户 PATCH /api/tenant/users/{id} 修改角色/门店/状态
修改绑定 PUT /api/tenant/users/{id}/binding 更新助教/员工绑定

3. tenant_excel.py — Excel 上传路由

端点 方法 路径 说明
上传解析 POST /api/tenant/excel/upload multipart/form-data校验+冲突检测
确认写入 POST /api/tenant/excel/confirm upload_id + resolutions[]
上传记录 GET /api/tenant/excel/logs 历史记录列表 + 分页
模板下载 GET /api/tenant/excel/template/{type} 下载空白 Excel 模板

4. tenant_clues.py — 维客线索路由

端点 方法 路径 说明
客户搜索 GET /api/tenant/customers/search keyword + site_id 筛选
线索列表 GET /api/tenant/customers/{member_id}/clues 该客户全部线索
修改线索 PATCH /api/tenant/clues/{id} category + summary + detail
删除线索 DELETE /api/tenant/clues/{id} 物理删除
隐藏/显示 PATCH /api/tenant/clues/{id}/visibility is_hidden 切换

认证依赖注入

@dataclass(frozen=True)
class CurrentTenantAdmin:
    """从 JWT 解析出的租户管理员上下文。"""
    admin_id: int
    tenant_id: int
    managed_site_ids: list[int]
    display_name: str | None = None

async def require_tenant_admin(
    credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
) -> CurrentTenantAdmin:
    """
    验证 JWT aud=tenant-admin提取管理员信息。
    拒绝小程序 JWTaud 不匹配)。
    """
    payload = decode_access_token(token)  # 复用现有解码
    if payload.get("aud") != "tenant-admin":
        raise HTTPException(401, "令牌类型不匹配")
    return CurrentTenantAdmin(
        admin_id=int(payload["sub"]),
        tenant_id=payload["tenant_id"],
        managed_site_ids=payload["managed_site_ids"],
    )

数据隔离通过 managed_site_ids 实现:

def site_filter_clause(admin: CurrentTenantAdmin) -> tuple[str, tuple]:
    """生成 site_id IN (...) SQL 片段。"""
    placeholders = ",".join(["%s"] * len(admin.managed_site_ids))
    return f"site_id IN ({placeholders})", tuple(admin.managed_site_ids)

前端组件结构

apps/tenant-admin/src/
├── pages/
│   ├── Login/              # 登录页
│   ├── UserApproval/       # 用户审核(申请列表 + 关联建议 + 审核操作)
│   ├── UserManagement/     # 用户管理(列表 + 编辑 + 绑定)
│   ├── ExcelUpload/        # Excel 上传(模板选择 + 上传 + 校验 + diff + 确认)
│   └── RetentionClues/     # 维客线索(客户搜索 + 线索列表 + 编辑/删除/隐藏)
├── components/
│   ├── SiteSelector/       # 门店筛选器(页面顶部)
│   ├── DiffTable/          # 冲突 diff 交互表格
│   └── ClueEditor/         # 线索编辑表单
├── services/
│   └── api.ts              # API 调用封装JWT 自动附加/刷新)
├── hooks/
│   └── useAuth.ts          # 认证状态管理
├── utils/
│   └── format.ts           # 格式化工具(手机号脱敏等)
└── App.tsx                 # 路由配置 + 侧边栏布局

API 调用层设计

复用 apps/admin-web/src/api/client.ts 的模式:

  • axios 实例,baseURL: "/api/tenant"
  • 请求拦截器:从 localStorage 读取 access_token 附加 Authorization header
  • 响应拦截器401 时用 refresh_token 刷新,刷新失败重定向 /login
  • 并发刷新保护:多个 401 只触发一次 refresh其余排队
  • 响应解包:{ code: 0, data } → 提取 data

响应格式约定

成功响应(由 ResponseWrapperMiddleware 自动包装):

{ "code": 0, "data": { ... } }

错误响应(由异常处理器格式化):

{ "code": 401, "message": "用户名或密码错误" }

分页响应:

{
  "code": 0,
  "data": {
    "items": [...],
    "total": 100,
    "page": 1,
    "pageSize": 20
  }
}

Pydantic 模型使用 alias_generator=to_camel 实现 snake_case → camelCase 转换。


数据模型

新建表

1. auth.tenant_admins — 租户管理员表

列名 类型 约束 说明
id BIGSERIAL PK 自增主键
username VARCHAR(50) UNIQUE NOT NULL 登录用户名
password_hash VARCHAR(255) NOT NULL bcrypt 哈希
display_name VARCHAR(100) 显示名称
tenant_id BIGINT NOT NULL 所属租户
managed_site_ids BIGINT[] NOT NULL 管辖门店 ID 列表
is_active BOOLEAN DEFAULT true 账号状态
created_by BIGINT 创建者Operator ID
created_at TIMESTAMPTZ DEFAULT NOW() 创建时间
last_login_at TIMESTAMPTZ 最后登录时间

索引:idx_tenant_admin_tenant ON (tenant_id)

2. biz.excel_upload_log — Excel 上传记录表

列名 类型 约束 说明
id BIGSERIAL PK 自增主键
site_id BIGINT NOT NULL 门店 ID
upload_type VARCHAR(30) CHECK IN (expense, platform_income, salary_adj, recharge_commission) 模板类型
file_name VARCHAR(255) NOT NULL 原始文件名
uploaded_by BIGINT NOT NULL 上传人(管理员 ID
row_count INTEGER DEFAULT 0 数据行数
conflict_count INTEGER DEFAULT 0 冲突行数
resolved_count INTEGER DEFAULT 0 已解决冲突数
status VARCHAR(20) CHECK IN (pending, confirmed, failed) 批次状态
error_detail JSONB 错误详情
created_at TIMESTAMPTZ DEFAULT NOW() 上传时间
confirmed_at TIMESTAMPTZ 确认时间

索引:idx_excel_log_site ON (site_id, created_at DESC)

3. biz.salary_adjustments — 助教奖罚明细表

列名 类型 约束 说明
id BIGSERIAL PK 自增主键
site_id BIGINT NOT NULL 门店 ID
assistant_id BIGINT 匹配到的助教 ID可空
assistant_name VARCHAR(100) NOT NULL 助教姓名
assistant_number VARCHAR(50) NOT NULL 助教编号
salary_month VARCHAR(7) NOT NULL 月份 YYYY-MM
adjustment_type VARCHAR(20) CHECK IN (deduction, bonus) 类型
amount NUMERIC(12,2) CHECK > 0 金额
reason VARCHAR(200) NOT NULL 原因
upload_batch_id BIGINT FK → excel_upload_log 上传批次
created_at TIMESTAMPTZ DEFAULT NOW() 创建时间
created_by BIGINT 上传人

索引:(site_id, salary_month), (assistant_id, salary_month)

4. biz.stg_finance_expense — 财务支出暂存表

列名 类型 约束 说明
id BIGSERIAL PK 自增主键
site_id BIGINT NOT NULL 门店 ID
expense_month VARCHAR(7) NOT NULL 月份
category VARCHAR(50) NOT NULL 支出类别8 值枚举)
amount NUMERIC(12,2) NOT NULL 金额
remark TEXT 备注
upload_batch_id BIGINT FK → excel_upload_log 上传批次
synced_at TIMESTAMPTZ ETL 同步时间NULL=未同步)
created_at TIMESTAMPTZ DEFAULT NOW() 创建时间

5. biz.stg_platform_income — 团购收入暂存表

列名 类型 约束 说明
id BIGSERIAL PK 自增主键
site_id BIGINT NOT NULL 门店 ID
income_month VARCHAR(7) NOT NULL 月份
platform_name VARCHAR(100) NOT NULL 平台名称
amount NUMERIC(12,2) NOT NULL 收入金额
remark TEXT 备注
upload_batch_id BIGINT FK → excel_upload_log 上传批次
synced_at TIMESTAMPTZ ETL 同步时间
created_at TIMESTAMPTZ DEFAULT NOW() 创建时间

6. biz.stg_recharge_commission — 充值业绩归属暂存表

列名 类型 约束 说明
id BIGSERIAL PK 自增主键
site_id BIGINT NOT NULL 门店 ID
recharge_date DATE NOT NULL 充值日期
member_name VARCHAR(100) NOT NULL 会员名称
recharge_amount NUMERIC(12,2) NOT NULL 充值金额
assigned_assistant VARCHAR(100) NOT NULL 归属助教
reward_amount NUMERIC(12,2) NOT NULL 奖励金额
upload_batch_id BIGINT FK → excel_upload_log 上传批次
synced_at TIMESTAMPTZ ETL 同步时间
created_at TIMESTAMPTZ DEFAULT NOW() 创建时间

表结构变更

public.member_retention_clue — 新增 is_hidden

ALTER TABLE public.member_retention_clue
    ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT false;

COMMENT ON COLUMN public.member_retention_clue.is_hidden
    IS '是否隐藏true=管理后台保留但小程序不展示)';

已有数据通过 DEFAULT false 保证兼容。小程序端线索查询需增加 WHERE is_hidden = false

复用的现有表auth Schema

用途
auth.users 小程序用户主表(审核时更新 status
auth.user_applications 用户入驻申请(审核列表数据源)
auth.site_code_mapping 球房编号 → site_id 映射
auth.roles 角色定义coach/staff/site_admin/tenant_admin
auth.user_site_roles 用户-门店-角色关联(审核通过时写入)
auth.user_assistant_binding 用户-助教/员工绑定(审核通过时写入)

FDW 数据源ETL 库只读)

视图 用途 查询场景
fdw_etl.v_dim_assistant 助教维度表 用户审核关联建议、Excel 人员匹配
fdw_etl.v_dim_staff + v_dim_staff_ex 员工维度表 用户审核关联建议、Excel 人员匹配
fdw_etl.v_dim_member 会员维度表 维客线索客户搜索DQ-6 规则)

多店铺查询策略:逐 site_id 调用 get_etl_readonly_connection(site_id) 查询后合并结果。

Pydantic Schema 设计

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

class TenantBaseModel(BaseModel):
    """租户管理后台基础模型,统一驼峰命名。"""
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True,
    )

class PaginatedResponse(TenantBaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    page_size: int

正确性属性Correctness Properties

属性Property是在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。

Property 1: 认证正确性

For any 用户名和密码组合,如果该组合对应一个活跃的 auth.tenant_admins 记录且密码哈希匹配,则登录应返回包含 aud=tenant-admin 的有效 JWT 令牌对;如果用户名不存在或密码不匹配,则应返回 401 且错误消息不区分是用户名还是密码错误。

Validates: Requirements 1.1, 1.2

Property 2: JWT 隔离与端点保护

For any JWT 令牌和任意 /api/tenant/* 受保护端点,当且仅当令牌有效且 aud=tenant-admin 时请求应被允许;aud 不匹配(如小程序 JWT aud=xcx)、令牌过期或令牌无效时应返回 401。

Validates: Requirements 1.4, 1.6, 17.3

Property 3: 数据隔离

For any 租户管理员和任意数据查询端点,返回结果中每条记录的 site_id 必须属于该管理员的 managed_site_ids 集合;尝试访问不在 managed_site_ids 范围内的数据应返回 403。

Validates: Requirements 2.1, 2.2, 2.3

Property 4: 审核通过多表一致性

For any 状态为 pending 的用户申请,执行审核通过操作后,以下条件应同时成立:auth.users.status = 'approved'auth.user_site_roles 中存在对应角色记录、auth.user_assistant_binding 中存在绑定记录(如提供了 assistant_id/staff_idauth.user_applications.status = 'approved' 且审核人和审核时间已填写。

Validates: Requirements 3.4

Property 5: 审核拒绝状态更新

For any 状态为 pending 的用户申请和任意非空拒绝原因字符串,执行审核拒绝操作后,auth.user_applications.status 应为 rejectedreview_note 应等于提交的拒绝原因,reviewed_at 应非空。

Validates: Requirements 3.5

Property 6: 申请关联匹配

For any 用户申请中的手机号和球房编号,关联匹配逻辑应:先通过 site_code_mapping 解析 site_id,然后在 v_dim_assistantscd2_is_current=1)和 v_dim_staff + v_dim_staff_ex 中按 phone 匹配,返回的候选列表应仅包含 phone 匹配的记录。

Validates: Requirements 3.3

Property 7: 用户管理编辑

For any 已通过审核的用户和任意有效的编辑参数角色、site_id 在管辖范围内、状态),编辑操作后用户记录应反映新值;修改绑定关系后 user_assistant_binding 应更新为新的 assistant_id/staff_id

Validates: Requirements 4.2, 4.3, 4.4

Property 8: Excel 格式校验

For any 模板类型和任意数据行集合,校验器应:对符合模板列定义的行标记为通过,对不符合的行返回包含行号、列名和错误描述的错误信息;校验全部通过时应创建 excel_upload_log 记录status=pending通过行数 + 警告行数 + 错误行数 = 总行数。

Validates: Requirements 5.1, 5.2, 5.3, 5.4, 6.5

Property 9: 人员匹配校验

For any 助教姓名+编号组合和给定的助教/员工数据集,匹配逻辑应:优先在 v_dim_assistantnickname + assistant_number中匹配未匹配时再查 v_dim_staff + v_dim_staff_exname + staff_number匹配成功时填充 assistant_id,匹配失败时标记为 warning 且不阻断流程。

Validates: Requirements 6.1, 6.2, 6.3, 6.4

Property 10: 冲突检测

For any 模板类型、上传数据集和已有数据集,冲突检测应:按模板主键规则(如财务支出的"月份+支出类别")识别主键重复的行,对冲突行返回逐字段 diff旧值 vs 新值),非冲突行标记为待写入。

Validates: Requirements 7.1, 7.2

Property 11: 数据写入 round-trip

For any 确认写入的数据批次和冲突解决方案,写入后从目标表读取的数据应与提交的数据一致(选择"替换"的行使用新值,选择"保留"的行保持旧值);excel_upload_log 状态应更新为 confirmed,实际写入行数应正确记录。

Validates: Requirements 7.4, 7.5, 8.1, 8.2

Property 12: 客户搜索

For any 搜索关键词和管辖门店范围,搜索结果应满足:每条结果的 nickname 包含关键词(模糊匹配)或 mobile 等于关键词(精确匹配),且 site_id 在管辖范围内;指定门店筛选时结果应仅包含该门店的客户。

Validates: Requirements 9.1, 9.3

Property 13: 线索编辑校验

For any 有效的编辑参数category 在 6 值枚举内、summary 非空且 ≤200 字符),编辑操作后线索记录应反映新值;对于无效的 category 或空/超长 summary应拒绝操作。

Validates: Requirements 11.1, 11.2

Property 14: 线索删除

For any 存在且在管辖门店范围内的线索,执行删除操作后,该线索应不可通过任何查询接口获取(物理删除)。

Validates: Requirements 12.2

Property 15: 线索隐藏/显示 round-trip

For any 线索,将 is_hidden 设为 true 后,小程序端查询(WHERE is_hidden = false)不应返回该线索,但管理后台查询应返回;再将 is_hidden 设回 false 后,小程序端查询应恢复返回该线索。

Validates: Requirements 13.1, 13.2, 13.3, 15.3

Property 16: 响应格式一致性

For any /api/tenant/* 端点的成功响应,响应体应符合 { code: 0, data: ... } 格式;错误响应应符合 { code: <status_code>, message: <string> } 格式;分页响应的 data 应包含 itemstotalpagepageSize 字段。

Validates: Requirements 17.4


错误处理

认证错误

场景 HTTP 状态码 错误消息 处理方式
用户名或密码错误 401 "用户名或密码错误" 统一消息,不区分
账号已禁用 403 "账号已被禁用" 检查 is_active
JWT 过期 401 "令牌已过期" 前端自动刷新
JWT 无效/aud 不匹配 401 "无效的令牌" 重定向登录页
刷新令牌过期 401 "刷新令牌已过期" 清除令牌,重定向登录页

数据隔离错误

场景 HTTP 状态码 错误消息
访问非管辖门店数据 403 "无权访问该门店数据"
修改 site_id 超出管辖范围 403 "目标门店不在管辖范围内"

业务逻辑错误

场景 HTTP 状态码 错误消息
审核非 pending 状态的申请 409 "该申请已被处理"
线索 ID 不存在 404 "线索不存在"
线索不在管辖范围 404 "线索不存在"
category 枚举值无效 422 "无效的线索大类"
summary 为空或超长 422 "摘要不能为空且不超过200字符"

Excel 上传错误

场景 HTTP 状态码 错误消息
文件格式非 .xlsx/.xls 400 "请上传有效的 Excel 文件"
格式校验失败 200 返回错误行详情(不阻断,前端标红)
人员匹配失败 200 标记为 warning不阻断前端黄色高亮
写入数据库失败 500 回滚整批log status=failed记录 error_detail

数据库事务策略

  • Excel 写入:整批次在单个事务中执行,任何行写入失败则回滚全部
  • 审核通过多表写入在单个事务中执行users + user_site_roles + user_assistant_binding + user_applications
  • 线索操作:单条记录操作,无需跨表事务

测试策略

双轨测试方法

本项目采用单元测试 + 属性测试的双轨策略:

  • 单元测试:验证具体示例、边界条件和错误处理
  • 属性测试:验证跨所有输入的通用属性

两者互补:单元测试捕获具体 bug属性测试验证通用正确性。

属性测试配置

  • Python 后端使用 hypothesis(项目已有 .hypothesis/ 目录);前端使用 fast-check
  • 迭代次数:每个属性测试最少 100 次迭代
  • 标签格式Feature: tenant-admin-web, Property {number}: {property_text}
  • 每个正确性属性对应一个属性测试

单元测试覆盖

模块 测试重点
tenant_auth.py 登录成功/失败、禁用账号、JWT 签发/验证、刷新令牌
tenant_users.py 申请列表筛选、审核通过/拒绝、非 pending 状态拒绝409、用户编辑、绑定修改
tenant_excel.py 4 种模板格式校验、人员匹配、冲突检测、写入回滚、模板下载
tenant_clues.py 客户搜索、线索 CRUD、隐藏/显示、枚举校验、越权访问
前端 services/api.ts JWT 自动附加、401 刷新、并发刷新保护

属性测试覆盖

Property 测试描述 生成器
P1 认证正确性 随机用户名+密码
P2 JWT 隔离 随机 JWT payload不同 aud
P3 数据隔离 随机 managed_site_ids + 随机数据
P4 审核通过一致性 随机 pending 申请 + 角色/绑定参数
P5 审核拒绝 随机 pending 申请 + 随机拒绝原因
P6 关联匹配 随机手机号 + 随机助教/员工数据集
P7 用户编辑 随机用户 + 随机编辑参数
P8 Excel 校验 随机模板类型 + 随机数据行(含有效/无效)
P9 人员匹配 随机姓名+编号 + 随机助教/员工数据集
P10 冲突检测 随机上传数据 + 随机已有数据
P11 写入 round-trip 随机数据 + 随机冲突解决方案
P12 客户搜索 随机关键词 + 随机客户数据集
P13 线索编辑 随机 category + 随机 summary
P14 线索删除 随机线索 ID
P15 隐藏/显示 round-trip 随机线索 + 隐藏→显示→验证
P16 响应格式 随机端点调用

边界条件测试(单元测试覆盖)

  • 禁用账号登录1.3)→ 403
  • 非 pending 申请审核3.6)→ 409
  • 越权修改 site_id4.5)→ 403
  • 无效 Excel 文件格式5.5)→ 400
  • 写入数据库失败回滚8.3)→ log status=failed
  • 线索 ID 不存在/越权11.3, 12.3, 13.4)→ 404