feat(lookup): 用户前缀查找放宽至单字符——中文姓氏单字搜索(全链路)
契约 minLength 2→1 + 服务端 bounds 1..64 + admin 两处/App 一处前端门槛 同步放宽;防抖测试基线更新('s' 与合并后 'sta' 共两条)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -175,7 +175,7 @@
|
||||
async function searchUsers(prefix: string) {
|
||||
query.value = prefix
|
||||
options.value = []
|
||||
if (prefix.trim().length < 2) return
|
||||
if (prefix.trim().length < 1) return
|
||||
searching.value = true
|
||||
try {
|
||||
const result = await fetchLookupUsers(prefix.trim())
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
async function searchUsers(prefix: string) {
|
||||
query.value = prefix
|
||||
options.value = []
|
||||
if (prefix.trim().length < 2) return
|
||||
if (prefix.trim().length < 1) return
|
||||
searching.value = true
|
||||
try {
|
||||
const result = await fetchLookupUsers(prefix.trim())
|
||||
|
||||
@@ -410,7 +410,9 @@ class _AlbumInviteSheetState extends ConsumerState<AlbumInviteSheet> {
|
||||
void _onSearchChanged(String text) {
|
||||
_debounce?.cancel();
|
||||
final query = text.trim();
|
||||
if (query.length < 2) {
|
||||
// 单字符即搜(2026-09-29 与服务端 1..64 bounds 对齐——中文姓氏单字);
|
||||
// 空串清空结果。
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_searching = false;
|
||||
_results = null;
|
||||
|
||||
@@ -144,7 +144,7 @@ void main() {
|
||||
expect(find.byIcon(Icons.person_add_outlined), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('邀请搜索防抖:<2 字符不请求;连续输入合并为最后一次(300ms)',
|
||||
testWidgets('邀请搜索防抖:单字符即请求;连续输入合并为最后一次(300ms)',
|
||||
(tester) async {
|
||||
final adapter = membersAdapter();
|
||||
await pumpMembers(tester, clientOf(adapter));
|
||||
@@ -152,15 +152,15 @@ void main() {
|
||||
await openInviteSheet(tester);
|
||||
final field = find.byType(TextField);
|
||||
|
||||
// 1 个字符:无请求。
|
||||
// 单字符即发请求(2026-09-29 与服务端 1..64 bounds 对齐——中文姓氏单字)。
|
||||
await tester.enterText(field, 's');
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(
|
||||
adapter.requests.where((r) => r.path == '/v1/admin/users'),
|
||||
isEmpty,
|
||||
isNotEmpty,
|
||||
);
|
||||
|
||||
// 连续输入两次,防抖合并为最后一次。
|
||||
// 连续输入两次,防抖合并为最后一次:'st' 不产生独立请求,只有 'sta'。
|
||||
await tester.enterText(field, 'st');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.enterText(field, 'sta');
|
||||
@@ -169,8 +169,12 @@ void main() {
|
||||
final lookupsRequests = adapter.requests
|
||||
.where((r) => r.path == '/v1/admin/users')
|
||||
.toList();
|
||||
expect(lookupsRequests, hasLength(1));
|
||||
expect(lookupsRequests.single.uri.queryParameters['accountPrefix'], 'sta');
|
||||
// 前段 's' 一条 + 合并后的 'sta' 一条(单字符即搜后基线为 2)。
|
||||
expect(lookupsRequests, hasLength(2));
|
||||
expect(
|
||||
lookupsRequests.map((r) => r.uri.queryParameters['accountPrefix']).toList(),
|
||||
['s', 'sta'],
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('邀请流:搜索命中 → Add → POST userId 载荷 → 已添加态 + 列表刷新',
|
||||
|
||||
@@ -1330,10 +1330,10 @@ paths:
|
||||
parameters:
|
||||
- name: accountPrefix
|
||||
in: query
|
||||
description: 账号或姓名前缀(≥2 字符以防过宽匹配;提供即进入前缀模式;大小写敏感——中文姓名无大小写语义)。
|
||||
description: 账号或姓名前缀(≥1 字符——中文姓氏单字搜索合理,2026-09-29 放宽;提供即进入前缀模式;大小写敏感——中文姓名无大小写语义)。
|
||||
schema:
|
||||
type: string
|
||||
minLength: 2
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
- name: query
|
||||
in: query
|
||||
|
||||
+1
-1
@@ -4442,7 +4442,7 @@ export interface operations {
|
||||
listAdminUsers: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description 账号或姓名前缀(≥2 字符以防过宽匹配;提供即进入前缀模式;大小写敏感——中文姓名无大小写语义)。 */
|
||||
/** @description 账号或姓名前缀(≥1 字符——中文姓氏单字搜索合理,2026-09-29 放宽;提供即进入前缀模式;大小写敏感——中文姓名无大小写语义)。 */
|
||||
accountPrefix?: string;
|
||||
/**
|
||||
* @description 管理列表过滤:账号或展示名前缀(大小写敏感二进制前缀匹配,同
|
||||
|
||||
@@ -37,12 +37,13 @@ import { UsersLookupService, type AdminUserListResponse } from './users-lookup.s
|
||||
export const DEFAULT_LIST_LIMIT = 20;
|
||||
export const MAX_LIST_LIMIT = 100;
|
||||
|
||||
/** accountPrefix 查询参数(前缀模式):2..64(契约 bounds)。 */
|
||||
/** accountPrefix 查询参数(前缀模式):1..64(契约 bounds;2026-09-29 自 2
|
||||
* 放宽——中文姓氏单字搜索合理,用户拍板)。 */
|
||||
export function parseAccountPrefix(raw: string | undefined): string {
|
||||
// 双模式收口后 undefined 不再到达此函数(分流在 listAdminUsers);防御性
|
||||
// 保留同一校验出口。空串/1 字符/超长 → 400。
|
||||
if (raw === undefined || raw.length < 2 || raw.length > 64) {
|
||||
throw validationFailed('accountPrefix 须为 2-64 字符');
|
||||
// 保留同一校验出口。空串/超长 → 400;单字符合法。
|
||||
if (raw === undefined || raw.length < 1 || raw.length > 64) {
|
||||
throw validationFailed('accountPrefix 须为 1-64 字符');
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -258,9 +258,17 @@ describe('账号查询 GET /admin/users (e2e)', () => {
|
||||
expect(empty.status).toBe(400);
|
||||
expect(empty.body.code).toBe(ErrorCodes.VALIDATION_FAILED);
|
||||
|
||||
const short = await lookup(staffToken, { accountPrefix: 'a' });
|
||||
expect(short.status).toBe(400);
|
||||
expect(short.body.code).toBe(ErrorCodes.VALIDATION_FAILED);
|
||||
// 单字符已合法(2026-09-29 自 2 放宽——中文姓氏单字搜索):'a' 命中
|
||||
// alice/albert(displayName 前缀并入后 '爱' 亦单字可用)。
|
||||
const single = await lookup(staffToken, { accountPrefix: 'a' });
|
||||
expect(single.status).toBe(200);
|
||||
expect(single.body.items.map((i: { account: string }) => i.account)).toEqual([
|
||||
'albert',
|
||||
'alice',
|
||||
]);
|
||||
const singleName = await lookup(staffToken, { accountPrefix: '爱' });
|
||||
expect(singleName.status).toBe(200);
|
||||
expect(singleName.body.items).toHaveLength(1);
|
||||
|
||||
const long = await lookup(staffToken, { accountPrefix: 'a'.repeat(65) });
|
||||
expect(long.status).toBe(400);
|
||||
|
||||
Reference in New Issue
Block a user