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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ export function MembersPage(props: {
|
||||
const [orgs, setOrgs] = useState<OrgUnit[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [phoneOpen, setPhoneOpen] = useState(false);
|
||||
const [phoneTarget, setPhoneTarget] = useState<Member | null>(null);
|
||||
const [phoneValue, setPhoneValue] = useState("");
|
||||
const [form] = Form.useForm<{
|
||||
password?: string;
|
||||
display_name?: string;
|
||||
@@ -120,6 +123,7 @@ export function MembersPage(props: {
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
|
||||
用户名由系统随机生成且全局唯一;初始密码可留空自动生成,登录后可自行「修改密码」。也可走「邀请加入」。
|
||||
管理员可为成员「绑定手机」(登录/同号同步联调用);顶栏「更换手机」只改当前登录账号。
|
||||
{session.role === "超级管理员" || session.role === "platform_admin"
|
||||
? " 超管代管修改时会要求两次确认。"
|
||||
: ""}
|
||||
@@ -134,8 +138,28 @@ export function MembersPage(props: {
|
||||
{
|
||||
title: "手机号",
|
||||
dataIndex: "phone",
|
||||
width: 120,
|
||||
render: (v: string) => v || <Typography.Text type="secondary">未绑定</Typography.Text>,
|
||||
width: 200,
|
||||
render: (v: string, r: Member) => (
|
||||
<Space size={4}>
|
||||
{v ? (
|
||||
<Typography.Text copyable>{v}</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">未绑定</Typography.Text>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: 0 }}
|
||||
onClick={() => {
|
||||
setPhoneTarget(r);
|
||||
setPhoneValue(v || "");
|
||||
setPhoneOpen(true);
|
||||
}}
|
||||
>
|
||||
{v ? "更换" : "绑定"}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: "显示名", dataIndex: "display_name", width: 120 },
|
||||
{
|
||||
@@ -204,6 +228,54 @@ export function MembersPage(props: {
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={phoneTarget?.phone ? "更换手机号" : "绑定手机号"}
|
||||
open={phoneOpen}
|
||||
onCancel={() => {
|
||||
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="保存"
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
用于手机号登录与松离线同号绑定联调。生产宇信达请绑{" "}
|
||||
<Typography.Text code>13531041944</Typography.Text>
|
||||
;勿绑超管号 13531041945。清空并保存即解绑。
|
||||
</Typography.Paragraph>
|
||||
<Input
|
||||
value={phoneValue}
|
||||
onChange={(e) => setPhoneValue(e.target.value)}
|
||||
placeholder="例:13531041944"
|
||||
maxLength={20}
|
||||
allowClear
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="新建成员"
|
||||
open={open}
|
||||
|
||||
@@ -1557,7 +1557,7 @@ export async function bindPhone(
|
||||
export async function updateMember(
|
||||
session: Session,
|
||||
id: number,
|
||||
body: { role?: string; org_unit_id?: number; status?: string }
|
||||
body: { role?: string; org_unit_id?: number; status?: string; phone?: string }
|
||||
) {
|
||||
const res = await apiFetch(`/api/v1/admin/members/${id}`, {
|
||||
method: "PUT",
|
||||
|
||||
Reference in New Issue
Block a user