Skip to content

Commit 1fdc28b

Browse files
committed
improve: Support similarity sorting across account, name, and email fields
1 parent fcf858c commit 1fdc28b

1 file changed

Lines changed: 34 additions & 15 deletions

File tree

backend/apps/system/api/user.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,14 @@ async def pager(
8080
if order_by and order_by != 'account':
8181
select_columns.append(sort_field)
8282

83-
# 相似度排序:精确匹配 > 前缀匹配 > 包含匹配
84-
# 当有 keyword 时,将 similarity_score 加入 SELECT 列以满足 DISTINCT 约束
85-
# 对 account、name、email 三个字段分别计算相似度,取最高(最小值)
86-
similarity_score = None
83+
# 相似度排序:综合考虑匹配字段数量和相似度分数
84+
# 当有 keyword 时,将 match_count 和 total_score 加入 SELECT 列以满足 DISTINCT 约束
85+
# 匹配字段越多越靠前;相同匹配字段数时,总分越低(相似度越高)越靠前
86+
match_count = None
87+
total_score = None
8788
if keyword:
8889
from sqlalchemy import func
89-
# 每个字段的相似度分数
90+
# 每个字段的相似度分数 (0=精确匹配, 1=前缀匹配, 2=包含匹配, 3=无匹配)
9091
account_score = case(
9192
(UserModel.account == keyword, 0),
9293
(UserModel.account.startswith(keyword), 1),
@@ -105,9 +106,16 @@ async def pager(
105106
(UserModel.email.contains(keyword), 2),
106107
else_=3
107108
)
108-
# 取三个字段中的最小值(最高匹配度)
109-
similarity_score = func.LEAST(account_score, name_score, email_score)
110-
select_columns.append(similarity_score.label('similarity_score'))
109+
# 计算匹配字段数量(score < 3 表示有匹配):匹配字段越多越靠前
110+
match_count = (
111+
case((account_score < 3, 1), else_=0) +
112+
case((name_score < 3, 1), else_=0) +
113+
case((email_score < 3, 1), else_=0)
114+
)
115+
# 总相似度分数:三个字段分数之和,越低越好
116+
total_score = account_score + name_score + email_score
117+
select_columns.append(match_count.label('match_count'))
118+
select_columns.append(total_score.label('total_score'))
111119

112120
origin_stmt = (
113121
select(*select_columns)
@@ -117,7 +125,8 @@ async def pager(
117125
)
118126
# 根据是否有 keyword 决定排序方式
119127
if keyword:
120-
origin_stmt = origin_stmt.order_by(similarity_score, sort_clause)
128+
# 按匹配字段数降序、总分升序、再按用户选择的排序字段
129+
origin_stmt = origin_stmt.order_by(match_count.desc(), total_score.asc(), sort_clause)
121130
else:
122131
origin_stmt = origin_stmt.order_by(sort_clause)
123132

@@ -128,12 +137,14 @@ async def pager(
128137
if status is not None:
129138
origin_stmt = origin_stmt.where(UserModel.status == status)
130139
if keyword:
131-
keyword_pattern = f"%{keyword}%"
140+
# 转义 SQL LIKE 特殊字符(_ 匹配单个字符,% 匹配任意字符串)
141+
escaped_keyword = keyword.replace('\\', '\\\\').replace('_', '\\_').replace('%', '\\%')
142+
keyword_pattern = f"%{escaped_keyword}%"
132143
origin_stmt = origin_stmt.where(
133144
or_(
134-
UserModel.account.ilike(keyword_pattern),
135-
UserModel.name.ilike(keyword_pattern),
136-
UserModel.email.ilike(keyword_pattern)
145+
UserModel.account.ilike(keyword_pattern, escape='\\'),
146+
UserModel.name.ilike(keyword_pattern, escape='\\'),
147+
UserModel.email.ilike(keyword_pattern, escape='\\')
137148
)
138149
)
139150

@@ -169,8 +180,16 @@ async def pager(
169180
(UserModel.email.contains(keyword), 2),
170181
else_=3
171182
)
172-
similarity_score = func.LEAST(account_score, name_score, email_score)
173-
stmt = stmt.order_by(similarity_score, sort_clause)
183+
# 计算匹配字段数量(score < 3 表示有匹配):匹配字段越多越靠前
184+
match_count = (
185+
case((account_score < 3, 1), else_=0) +
186+
case((name_score < 3, 1), else_=0) +
187+
case((email_score < 3, 1), else_=0)
188+
)
189+
# 总相似度分数:三个字段分数之和,越低越好
190+
total_score = account_score + name_score + email_score
191+
# 排序:匹配字段数降序、总分升序、再按用户选择的排序字段
192+
stmt = stmt.order_by(match_count.desc(), total_score.asc(), sort_clause)
174193
else:
175194
stmt = stmt.order_by(sort_clause)
176195
user_workspaces = session.exec(stmt).all()

0 commit comments

Comments
 (0)