From 3c9fe4635958fe564e0ebddadedf1e3d6f87746a Mon Sep 17 00:00:00 2001 From: whm <973418690@qq.com> Date: Wed, 5 Aug 2026 14:41:27 +0800 Subject: [PATCH] feat: allow admins to bind member phone numbers Members page previously only displayed phone status; add bind/replace UI and admin member update phone support for login and sync identity. --- platform/internal/handler/platform.go | 34 +++++++-- platform/internal/logic/applogic/members.go | 16 +++++ web/src/MembersPage.tsx | 76 ++++++++++++++++++++- web/src/api.ts | 2 +- 4 files changed, 119 insertions(+), 9 deletions(-) diff --git a/platform/internal/handler/platform.go b/platform/internal/handler/platform.go index 9f11a95..ce8946a 100644 --- a/platform/internal/handler/platform.go +++ b/platform/internal/handler/platform.go @@ -2,12 +2,14 @@ package handler import ( "encoding/json" + "fmt" "net/http" "strconv" "aijianzhan/platform/internal/authx" "aijianzhan/platform/internal/logic/applogic" "aijianzhan/platform/internal/svc" + "aijianzhan/platform/internal/userstore" "github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/pathvar" @@ -293,23 +295,43 @@ func memberUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64) var body struct { - Role string `json:"role"` - OrgUnitID int64 `json:"org_unit_id"` - Status string `json:"status"` + Role string `json:"role"` + OrgUnitID int64 `json:"org_unit_id"` + Status string `json:"status"` + Phone *string `json:"phone"` // 传则绑定/更换;传 "" 解绑 } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { authx.WriteError(w, http.StatusBadRequest, err.Error()) return } l := applogic.NewMemberLogic(applogic.NewAuthLogic(r.Context(), svcCtx)) - u, err := l.Update(id, body.Role, body.OrgUnitID, body.Status) - if err != nil { - authx.WriteError(w, http.StatusBadRequest, err.Error()) + var u *userstore.User + var err error + if body.Phone != nil { + u, err = l.SetPhone(id, *body.Phone) + if err != nil { + authx.WriteError(w, http.StatusBadRequest, err.Error()) + return + } + _ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), + "member.bind_phone", fmt.Sprintf("成员 #%d phone=%s", id, u.Phone)) + } + // 仅改手机时不必强制带 role/status;若其它字段有值则再 Update + if body.Role != "" || body.Status != "" || body.OrgUnitID != 0 || body.Phone == nil { + u, err = l.Update(id, body.Role, body.OrgUnitID, body.Status) + if err != nil { + authx.WriteError(w, http.StatusBadRequest, err.Error()) + return + } + } + if u == nil { + authx.WriteError(w, http.StatusBadRequest, "nothing to update") return } httpx.OkJson(w, map[string]any{ "user_id": u.UserID, "username": u.Username, + "phone": u.Phone, "display_name": u.DisplayName, "role": u.Role, "org_unit_id": u.OrgUnitID, diff --git a/platform/internal/logic/applogic/members.go b/platform/internal/logic/applogic/members.go index a08d97e..7e3cb4e 100644 --- a/platform/internal/logic/applogic/members.go +++ b/platform/internal/logic/applogic/members.go @@ -64,3 +64,19 @@ func (l *MemberLogic) Update(userID int64, role string, orgUnitID int64, status } return l.svcCtx.Users.UpdateMember(l.ctx, tid, userID, role, orgUnitID, status) } + +// SetPhone 管理员为本公司成员绑定/更换/清空手机号(空字符串解绑)。 +func (l *MemberLogic) SetPhone(userID int64, phone string) (*userstore.User, error) { + tid := authx.TenantID(l.ctx) + if tid <= 0 { + return nil, fmt.Errorf("missing tenant") + } + u, err := l.svcCtx.Users.GetByID(l.ctx, userID) + if err != nil { + return nil, err + } + if u.TenantID != tid { + return nil, fmt.Errorf("成员不属于本公司") + } + return l.svcCtx.Users.BindPhone(l.ctx, userID, phone) +} diff --git a/web/src/MembersPage.tsx b/web/src/MembersPage.tsx index 941620d..6902162 100644 --- a/web/src/MembersPage.tsx +++ b/web/src/MembersPage.tsx @@ -38,6 +38,9 @@ export function MembersPage(props: { const [orgs, setOrgs] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); + const [phoneOpen, setPhoneOpen] = useState(false); + const [phoneTarget, setPhoneTarget] = useState(null); + const [phoneValue, setPhoneValue] = useState(""); const [form] = Form.useForm<{ password?: string; display_name?: string; @@ -120,6 +123,7 @@ export function MembersPage(props: { > 用户名由系统随机生成且全局唯一;初始密码可留空自动生成,登录后可自行「修改密码」。也可走「邀请加入」。 + 管理员可为成员「绑定手机」(登录/同号同步联调用);顶栏「更换手机」只改当前登录账号。 {session.role === "超级管理员" || session.role === "platform_admin" ? " 超管代管修改时会要求两次确认。" : ""} @@ -134,8 +138,28 @@ export function MembersPage(props: { { title: "手机号", dataIndex: "phone", - width: 120, - render: (v: string) => v || 未绑定, + width: 200, + render: (v: string, r: Member) => ( + + {v ? ( + {v} + ) : ( + 未绑定 + )} + + + ), }, { title: "显示名", dataIndex: "display_name", width: 120 }, { @@ -204,6 +228,54 @@ export function MembersPage(props: { ]} /> + { + setPhoneOpen(false); + setPhoneTarget(null); + }} + onOk={async () => { + if (!phoneTarget) return; + const trimmed = phoneValue.trim(); + const ok = await confirmCompanyWrite( + modal, + session, + trimmed ? "绑定/更换成员手机号" : "解绑成员手机号", + `成员:${phoneTarget.display_name || phoneTarget.username}(#${phoneTarget.user_id})→ ${trimmed || "(空)"}` + ); + if (!ok) return; + setBusy(true); + try { + await updateMember(session, phoneTarget.user_id, { phone: trimmed }); + setInfo(trimmed ? `已绑定 ${trimmed}` : "已解绑手机号"); + setPhoneOpen(false); + setPhoneTarget(null); + await refresh(); + } catch (e: any) { + message.error(e.message || String(e)); + } finally { + setBusy(false); + } + }} + confirmLoading={busy} + destroyOnHidden + okText="保存" + > + + 用于手机号登录与松离线同号绑定联调。生产宇信达请绑{" "} + 13531041944 + ;勿绑超管号 13531041945。清空并保存即解绑。 + + setPhoneValue(e.target.value)} + placeholder="例:13531041944" + maxLength={20} + allowClear + /> + +