fix(分组): 候选人排除已在本组的成员
反馈:某人明明已经在成员名单里,点「添加成员」却还能再选他一次 ——
界面自相矛盾,看着就像分组没生效。
member-candidates 新增 group_id 参数:传了就把该组已有成员排除掉。
⚠️ MOM 与 Track 是**两个独立的库**,没法 JOIN,所以先在本库查出该组成员
集合,再在内存里排掉。LICA 只有十几人,这个取舍是划算的(已加注释)。
前端配合:
· openAddMember 传 selectedId —— 面板一打开,候选就是干净的
· 添加成功后**保持面板打开并重拉候选**:刚加进去的人立刻从方块里消失,
既能连续加人,也不会出现「刚加完还能再选他一次」
实测:
生产小组(成员=魏会军):不带 group_id 17 人含他;带 group_id=3 → 16 人已排除
维修大组同理排除齐海建
加「李婧」后她立即从候选方块消失;测试数据已清理
This commit is contained in:
@ -231,9 +231,11 @@ async def list_phase_options(current_user: dict = Depends(get_current_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/member-candidates", response_model=list[MemberCandidate])
|
@router.get("/member-candidates", response_model=list[MemberCandidate])
|
||||||
def list_member_candidates(
|
async def list_member_candidates(
|
||||||
keyword: str = Query("", description="按姓名/账号模糊搜索"),
|
keyword: str = Query("", description="按姓名/账号模糊搜索"),
|
||||||
limit: int = Query(500, ge=1, le=1000),
|
limit: int = Query(500, ge=1, le=1000),
|
||||||
|
group_id: int | None = Query(None, description="传了则把已在该组的成员排除掉"),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""候选人下拉 —— 复用与 users.py 一致的 MOM 查询口径(部门已钉死为 ORG_DEPARTMENT)。
|
"""候选人下拉 —— 复用与 users.py 一致的 MOM 查询口径(部门已钉死为 ORG_DEPARTMENT)。
|
||||||
@ -244,10 +246,25 @@ def list_member_candidates(
|
|||||||
· 若再给他打组长标记,会产生「组长却不受组范围限制」的矛盾状态。
|
· 若再给他打组长标记,会产生「组长却不受组范围限制」的矛盾状态。
|
||||||
所以既不显示在候选里,也不允许加入(add_member 会再挡一道)。
|
所以既不显示在候选里,也不允许加入(add_member 会再挡一道)。
|
||||||
|
|
||||||
|
⚠️ 传 `group_id` 时会**排除已在该组的成员** —— 否则会出现「这个人明明已经在
|
||||||
|
成员名单里,选人时却还能再选一次」的矛盾,用户会以为系统没生效。
|
||||||
|
|
||||||
|
⚠️ MOM 与 Track 是**两个独立的库**,没法 JOIN,所以先在本库查出该组的成员
|
||||||
|
集合,再在内存里排掉。LICA 只有十几人,这个做法完全够用。
|
||||||
|
|
||||||
注意这里**不复用 users.py 的端点函数**:那个函数与 FastAPI 的 Query 默认值
|
注意这里**不复用 users.py 的端点函数**:那个函数与 FastAPI 的 Query 默认值
|
||||||
耦合,直接调用拿到的是 Query 对象而非值。所以照抄同一条 SQL 的写法,
|
耦合,直接调用拿到的是 Query 对象而非值。所以照抄同一条 SQL 的写法,
|
||||||
但部门条件取自同一处 settings.ORG_DEPARTMENT,口径不会漂移。
|
但部门条件取自同一处 settings.ORG_DEPARTMENT,口径不会漂移。
|
||||||
"""
|
"""
|
||||||
|
# 先取本组成员(Track 库)
|
||||||
|
exclude: set[str] = set()
|
||||||
|
if group_id is not None:
|
||||||
|
rows = await db.execute(
|
||||||
|
select(BusinessGroupMember.user_id)
|
||||||
|
.where(BusinessGroupMember.group_id == group_id)
|
||||||
|
)
|
||||||
|
exclude = {r[0] for r in rows.all()}
|
||||||
|
|
||||||
db = MomSessionLocal()
|
db = MomSessionLocal()
|
||||||
try:
|
try:
|
||||||
base_sql = """
|
base_sql = """
|
||||||
@ -266,13 +283,15 @@ def list_member_candidates(
|
|||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
rows = db.execute(text(sql_text), params).fetchall()
|
rows = db.execute(text(sql_text), params).fetchall()
|
||||||
return [
|
out: list[MemberCandidate] = []
|
||||||
MemberCandidate(
|
for row in rows:
|
||||||
username=row.username.split("/")[-1] if "/" in row.username else row.username,
|
acct = row.username.split("/")[-1] if "/" in row.username else row.username
|
||||||
full_name=row.full_name or row.username,
|
if acct in exclude: # 已在组内 —— 不再作为候选
|
||||||
)
|
continue
|
||||||
for row in rows
|
out.append(MemberCandidate(
|
||||||
]
|
username=acct, full_name=row.full_name or row.username,
|
||||||
|
))
|
||||||
|
return out
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_502_BAD_GATEWAY, f"MOM 用户查询失败: {str(e)}"
|
status.HTTP_502_BAD_GATEWAY, f"MOM 用户查询失败: {str(e)}"
|
||||||
|
|||||||
@ -207,7 +207,8 @@ export default function AdminGroupsPage() {
|
|||||||
async function openAddMember() {
|
async function openAddMember() {
|
||||||
if (selectedId == null) return;
|
if (selectedId == null) return;
|
||||||
try {
|
try {
|
||||||
setCandidates(await fetchMemberCandidates());
|
// 传 selectedId:后端会把已在本组的人排除掉,避免"已经加了还能再选"
|
||||||
|
setCandidates(await fetchMemberCandidates(selectedId));
|
||||||
setPicked([]);
|
setPicked([]);
|
||||||
setPickedLeader(false);
|
setPickedLeader(false);
|
||||||
setCandidateKeyword("");
|
setCandidateKeyword("");
|
||||||
@ -234,7 +235,9 @@ export default function AdminGroupsPage() {
|
|||||||
}
|
}
|
||||||
setPicked([]);
|
setPicked([]);
|
||||||
setPickedLeader(false);
|
setPickedLeader(false);
|
||||||
setCandidates([]);
|
// 面板保持打开并重拉候选 —— 刚加进去的人会从方块里消失,
|
||||||
|
// 这样既能连续加人,也不会出现"刚加完还能再选他一次"
|
||||||
|
setCandidates(await fetchMemberCandidates(selectedId));
|
||||||
await Promise.all([loadDetail(selectedId), load()]);
|
await Promise.all([loadDetail(selectedId), load()]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error(extractErrorMessage(err, "添加失败"));
|
message.error(extractErrorMessage(err, "添加失败"));
|
||||||
|
|||||||
@ -131,10 +131,21 @@ export async function setGroupLeader(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 候选人下拉 —— 数据源是 MOM 的 LICA 部门人员 */
|
/**
|
||||||
export async function fetchMemberCandidates(keyword?: string): Promise<MemberCandidate[]> {
|
* 候选人列表 —— 数据源是 MOM 的 LICA 部门人员。
|
||||||
|
*
|
||||||
|
* ⚠️ 传 groupId 时后端会**排除已在该组的人** —— 否则会出现
|
||||||
|
* 「他明明已经在成员名单里,选人时却还能再选一次」的矛盾。
|
||||||
|
*/
|
||||||
|
export async function fetchMemberCandidates(
|
||||||
|
groupId?: number,
|
||||||
|
keyword?: string
|
||||||
|
): Promise<MemberCandidate[]> {
|
||||||
const { data } = await api.get<MemberCandidate[]>("/groups/member-candidates", {
|
const { data } = await api.get<MemberCandidate[]>("/groups/member-candidates", {
|
||||||
params: keyword ? { keyword } : {},
|
params: {
|
||||||
|
...(groupId != null ? { group_id: groupId } : {}),
|
||||||
|
...(keyword ? { keyword } : {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user