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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"aijianzhan/platform/internal/authx"
|
"aijianzhan/platform/internal/authx"
|
||||||
"aijianzhan/platform/internal/logic/applogic"
|
"aijianzhan/platform/internal/logic/applogic"
|
||||||
"aijianzhan/platform/internal/svc"
|
"aijianzhan/platform/internal/svc"
|
||||||
|
"aijianzhan/platform/internal/userstore"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"github.com/zeromicro/go-zero/rest/pathvar"
|
"github.com/zeromicro/go-zero/rest/pathvar"
|
||||||
@@ -296,20 +298,40 @@ func memberUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
OrgUnitID int64 `json:"org_unit_id"`
|
OrgUnitID int64 `json:"org_unit_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
Phone *string `json:"phone"` // 传则绑定/更换;传 "" 解绑
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l := applogic.NewMemberLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
l := applogic.NewMemberLogic(applogic.NewAuthLogic(r.Context(), svcCtx))
|
||||||
u, err := l.Update(id, body.Role, body.OrgUnitID, body.Status)
|
var u *userstore.User
|
||||||
|
var err error
|
||||||
|
if body.Phone != nil {
|
||||||
|
u, err = l.SetPhone(id, *body.Phone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
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{
|
httpx.OkJson(w, map[string]any{
|
||||||
"user_id": u.UserID,
|
"user_id": u.UserID,
|
||||||
"username": u.Username,
|
"username": u.Username,
|
||||||
|
"phone": u.Phone,
|
||||||
"display_name": u.DisplayName,
|
"display_name": u.DisplayName,
|
||||||
"role": u.Role,
|
"role": u.Role,
|
||||||
"org_unit_id": u.OrgUnitID,
|
"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)
|
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 [orgs, setOrgs] = useState<OrgUnit[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = 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<{
|
const [form] = Form.useForm<{
|
||||||
password?: string;
|
password?: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
@@ -120,6 +123,7 @@ export function MembersPage(props: {
|
|||||||
>
|
>
|
||||||
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
|
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
|
||||||
用户名由系统随机生成且全局唯一;初始密码可留空自动生成,登录后可自行「修改密码」。也可走「邀请加入」。
|
用户名由系统随机生成且全局唯一;初始密码可留空自动生成,登录后可自行「修改密码」。也可走「邀请加入」。
|
||||||
|
管理员可为成员「绑定手机」(登录/同号同步联调用);顶栏「更换手机」只改当前登录账号。
|
||||||
{session.role === "超级管理员" || session.role === "platform_admin"
|
{session.role === "超级管理员" || session.role === "platform_admin"
|
||||||
? " 超管代管修改时会要求两次确认。"
|
? " 超管代管修改时会要求两次确认。"
|
||||||
: ""}
|
: ""}
|
||||||
@@ -134,8 +138,28 @@ export function MembersPage(props: {
|
|||||||
{
|
{
|
||||||
title: "手机号",
|
title: "手机号",
|
||||||
dataIndex: "phone",
|
dataIndex: "phone",
|
||||||
width: 120,
|
width: 200,
|
||||||
render: (v: string) => v || <Typography.Text type="secondary">未绑定</Typography.Text>,
|
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 },
|
{ 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
|
<Modal
|
||||||
title="新建成员"
|
title="新建成员"
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -1557,7 +1557,7 @@ export async function bindPhone(
|
|||||||
export async function updateMember(
|
export async function updateMember(
|
||||||
session: Session,
|
session: Session,
|
||||||
id: number,
|
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}`, {
|
const res = await apiFetch(`/api/v1/admin/members/${id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|||||||
Reference in New Issue
Block a user