feat: 完善IM用户管理与治理操作
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
// ListReq 查询IM用户列表
|
||||
@@ -23,6 +24,41 @@ type ListRes struct {
|
||||
List []*sysin.ImUserListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
type CreateReq struct {
|
||||
g.Meta `path:"/imUser/create" method:"post" tags:"IM用户" summary:"后台创建IM用户"`
|
||||
sysin.ImUserCreateInp
|
||||
}
|
||||
|
||||
type CreateRes struct {
|
||||
*sysin.ImUserCreateModel
|
||||
}
|
||||
|
||||
type BatchCreateReq struct {
|
||||
g.Meta `path:"/imUser/batchCreate" method:"post" tags:"IM用户" summary:"批量创建IM用户"`
|
||||
sysin.ImUserBatchCreateInp
|
||||
}
|
||||
|
||||
type BatchCreateRes struct {
|
||||
*sysin.ImUserBatchCreateModel
|
||||
}
|
||||
|
||||
type ImportReq struct {
|
||||
g.Meta `path:"/imUser/import" method:"post" tags:"IM用户" summary:"Excel导入IM用户"`
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
GroupId int64 `json:"groupId" dc:"默认用户策略分组ID"`
|
||||
File *ghttp.UploadFile `json:"file" type:"file" v:"required#请选择Excel文件" dc:"Excel文件"`
|
||||
}
|
||||
|
||||
type ImportRes struct {
|
||||
*sysin.ImUserBatchCreateModel
|
||||
}
|
||||
|
||||
type DownloadTemplateReq struct {
|
||||
g.Meta `path:"/imUser/downloadTemplate" method:"get" tags:"IM用户" summary:"下载IM用户导入模板"`
|
||||
}
|
||||
|
||||
type DownloadTemplateRes struct{}
|
||||
|
||||
// ViewReq 获取IM用户指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imUser/view" method:"get" tags:"IM用户" summary:"获取IM用户指定信息"`
|
||||
@@ -56,6 +92,29 @@ type StatusReq struct {
|
||||
|
||||
type StatusRes struct{}
|
||||
|
||||
type AdjustGroupReq struct {
|
||||
g.Meta `path:"/imUser/adjustGroup" method:"post" tags:"IM用户" summary:"调整IM用户分组"`
|
||||
sysin.ImUserAdjustGroupInp
|
||||
}
|
||||
|
||||
type AdjustGroupRes struct{}
|
||||
|
||||
type IPWhitelistReq struct {
|
||||
g.Meta `path:"/imUser/ipWhitelist" method:"get" tags:"IM用户" summary:"获取IM用户IP白名单"`
|
||||
sysin.ImUserIPWhitelistInp
|
||||
}
|
||||
|
||||
type IPWhitelistRes struct {
|
||||
*sysin.ImUserIPWhitelistModel
|
||||
}
|
||||
|
||||
type IPWhitelistSaveReq struct {
|
||||
g.Meta `path:"/imUser/ipWhitelistSave" method:"post" tags:"IM用户" summary:"保存IM用户IP白名单"`
|
||||
sysin.ImUserIPWhitelistInp
|
||||
}
|
||||
|
||||
type IPWhitelistSaveRes struct{}
|
||||
|
||||
type DevicesReq struct {
|
||||
g.Meta `path:"/imUser/devices" method:"get" tags:"IM用户" summary:"获取IM用户设备列表"`
|
||||
sysin.ImUserDeviceListInp
|
||||
|
||||
@@ -12,6 +12,19 @@ import (
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("ImUserStateOptions", "IM用户状态选项", ImUserStateOptions)
|
||||
dict.RegisterEnums("ImUserRegisterTypeOptions", "IM用户注册方式选项", ImUserRegisterTypeOptions)
|
||||
}
|
||||
|
||||
const (
|
||||
ImUserRegisterTypeAdmin = "admin"
|
||||
ImUserRegisterTypeMobile = "mobile"
|
||||
ImUserRegisterTypeInviteCode = "invite_code"
|
||||
)
|
||||
|
||||
var ImUserRegisterTypeOptions = []*model.Option{
|
||||
dict.GenPrimaryOption(ImUserRegisterTypeAdmin, "后台创建"),
|
||||
dict.GenSuccessOption(ImUserRegisterTypeMobile, "手机号注册"),
|
||||
dict.GenInfoOption(ImUserRegisterTypeInviteCode, "邀请码注册"),
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"hotgo/api/admin/imuser"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -18,6 +20,40 @@ var (
|
||||
|
||||
type cImUser struct{}
|
||||
|
||||
func (c *cImUser) Create(ctx context.Context, req *imuser.CreateReq) (res *imuser.CreateRes, err error) {
|
||||
data, err := service.SysImUser().Create(ctx, &req.ImUserCreateInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &imuser.CreateRes{ImUserCreateModel: data}, nil
|
||||
}
|
||||
|
||||
func (c *cImUser) BatchCreate(ctx context.Context, req *imuser.BatchCreateReq) (res *imuser.BatchCreateRes, err error) {
|
||||
data, err := service.SysImUser().BatchCreate(ctx, &req.ImUserBatchCreateInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &imuser.BatchCreateRes{ImUserBatchCreateModel: data}, nil
|
||||
}
|
||||
|
||||
func (c *cImUser) Import(ctx context.Context, req *imuser.ImportReq) (res *imuser.ImportRes, err error) {
|
||||
file, err := req.File.Open()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "打开Excel文件失败")
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := service.SysImUser().ImportExcel(ctx, req.TenantId, req.GroupId, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &imuser.ImportRes{ImUserBatchCreateModel: data}, nil
|
||||
}
|
||||
|
||||
func (c *cImUser) DownloadTemplate(ctx context.Context, req *imuser.DownloadTemplateReq) (res *imuser.DownloadTemplateRes, err error) {
|
||||
err = service.SysImUser().DownloadTemplate(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// List 查看IM用户列表
|
||||
func (c *cImUser) List(ctx context.Context, req *imuser.ListReq) (res *imuser.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImUser().List(ctx, &req.ImUserListInp)
|
||||
@@ -67,6 +103,24 @@ func (c *cImUser) Status(ctx context.Context, req *imuser.StatusReq) (res *imuse
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImUser) AdjustGroup(ctx context.Context, req *imuser.AdjustGroupReq) (res *imuser.AdjustGroupRes, err error) {
|
||||
err = service.SysImUser().AdjustGroup(ctx, &req.ImUserAdjustGroupInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImUser) IPWhitelist(ctx context.Context, req *imuser.IPWhitelistReq) (res *imuser.IPWhitelistRes, err error) {
|
||||
data, err := service.SysImUser().IPWhitelist(ctx, &req.ImUserIPWhitelistInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &imuser.IPWhitelistRes{ImUserIPWhitelistModel: data}, nil
|
||||
}
|
||||
|
||||
func (c *cImUser) IPWhitelistSave(ctx context.Context, req *imuser.IPWhitelistSaveReq) (res *imuser.IPWhitelistSaveRes, err error) {
|
||||
err = service.SysImUser().IPWhitelistSave(ctx, &req.ImUserIPWhitelistInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImUser) Devices(ctx context.Context, req *imuser.DevicesReq) (res *imuser.DevicesRes, err error) {
|
||||
list, totalCount, err := service.SysImUser().Devices(ctx, &req.ImUserDeviceListInp)
|
||||
if err != nil {
|
||||
|
||||
@@ -58,6 +58,39 @@ func TestListCodesWithInviteCodeUsesSignedSearchPost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalPostUsesInternalSignatureAndBody(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/internal/admin-query/tenants/default/users" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
if string(body) != `{"username":"alice"}` {
|
||||
t.Fatalf("unexpected body %s", body)
|
||||
}
|
||||
if r.Header.Get("X-Service-Name") != "hotgo-admin" || r.Header.Get("X-Signature") == "" {
|
||||
t.Fatalf("missing internal authentication headers")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":{"user_id":123,"username":"alice"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(Config{InternalBaseUrl: server.URL, InternalServiceName: "hotgo-admin", InternalServiceSecret: "secret", Timeout: time.Second})
|
||||
var out struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
if err := client.InternalPost(context.Background(), "/internal/admin-query/tenants/default/users", map[string]string{"username": "alice"}, &out); err != nil {
|
||||
t.Fatalf("InternalPost returned error: %v", err)
|
||||
}
|
||||
if out.UserID != 123 || out.Username != "alice" {
|
||||
t.Fatalf("unexpected response: %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func signedTestServer(t *testing.T, method string, path string, inspect func(t *testing.T, r *http.Request, body []byte)) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -18,7 +18,19 @@ import (
|
||||
)
|
||||
|
||||
func (c *Client) InternalGet(ctx context.Context, path string, out any) error {
|
||||
req, err := c.newInternalRequest(ctx, http.MethodGet, path, nil)
|
||||
return c.internalJSON(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) InternalPost(ctx context.Context, path string, in any, out any) error {
|
||||
body, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "序列化IM内部请求失败")
|
||||
}
|
||||
return c.internalJSON(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
|
||||
func (c *Client) internalJSON(ctx context.Context, method, path string, body []byte, out any) error {
|
||||
req, err := c.newInternalRequest(ctx, method, path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -34,7 +46,7 @@ func (c *Client) InternalGet(ctx context.Context, path string, out any) error {
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
|
||||
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取IM内部查询响应失败")
|
||||
}
|
||||
@@ -44,13 +56,13 @@ func (c *Client) InternalGet(ctx context.Context, path string, out any) error {
|
||||
Reason string `json:"reason"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &envelope); err == nil && len(envelope.Data) > 0 {
|
||||
if err = json.Unmarshal(responseBody, &envelope); err == nil && len(envelope.Data) > 0 {
|
||||
if envelope.Code != 0 {
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
return json.Unmarshal(envelope.Data, out)
|
||||
}
|
||||
return json.Unmarshal(body, out)
|
||||
return json.Unmarshal(responseBody, out)
|
||||
}
|
||||
|
||||
func (c *Client) newInternalRequest(ctx context.Context, method string, path string, bodyBytes []byte) (*http.Request, error) {
|
||||
|
||||
@@ -13,12 +13,17 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"hotgo/utility/excel"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,8 +31,11 @@ import (
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -41,6 +49,141 @@ func init() {
|
||||
service.RegisterSysImUser(NewSysImUser())
|
||||
}
|
||||
|
||||
func (s *sSysImUser) Create(ctx context.Context, in *sysin.ImUserCreateInp) (res *sysin.ImUserCreateModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.GroupId > 0 {
|
||||
if err = validateImUserPolicyGroup(ctx, tenant.Id, in.GroupId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
payload := map[string]any{
|
||||
"username": in.Username,
|
||||
"nickname": in.Nickname,
|
||||
"phone": in.Phone,
|
||||
"password": in.Password,
|
||||
}
|
||||
var wire struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/users"
|
||||
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, &wire); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &sysin.ImUserCreateModel{UserId: wire.UserId, Username: wire.Username, Nickname: wire.Nickname, Phone: wire.Phone}
|
||||
if in.GroupId > 0 {
|
||||
if err = service.SysImUserPolicyGroup().MemberAdd(ctx, &sysin.ImUserPolicyGroupMemberAddInp{GroupId: in.GroupId, UserId: res.UserId}); err != nil {
|
||||
return nil, gerror.Wrap(err, "用户已创建,但加入用户分组失败")
|
||||
}
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_user_create", "user", res.UserId, "success", map[string]any{"username": res.Username, "group_id": in.GroupId})
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) BatchCreate(ctx context.Context, in *sysin.ImUserBatchCreateInp) (res *sysin.ImUserBatchCreateModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]*sysin.ImUserCreateInp, 0, in.Count)
|
||||
for i := 1; i <= in.Count; i++ {
|
||||
nickname := in.NicknamePrefix
|
||||
if nickname == "" {
|
||||
nickname = "用户"
|
||||
}
|
||||
rows = append(rows, &sysin.ImUserCreateInp{TenantId: in.TenantId, Nickname: fmt.Sprintf("%s%d", nickname, i), Password: in.Password, GroupId: in.GroupId})
|
||||
}
|
||||
return s.Import(ctx, &sysin.ImUserImportInp{TenantId: in.TenantId, GroupId: in.GroupId, Rows: rows})
|
||||
}
|
||||
|
||||
func (s *sSysImUser) Import(ctx context.Context, in *sysin.ImUserImportInp) (res *sysin.ImUserBatchCreateModel, err error) {
|
||||
if len(in.Rows) == 0 {
|
||||
return nil, gerror.New("没有可导入的用户数据")
|
||||
}
|
||||
if len(in.Rows) > 500 {
|
||||
return nil, gerror.New("单次最多导入500个用户")
|
||||
}
|
||||
res = &sysin.ImUserBatchCreateModel{Items: make([]*sysin.ImUserBatchItemModel, 0, len(in.Rows))}
|
||||
for index, row := range in.Rows {
|
||||
row.TenantId = in.TenantId
|
||||
if row.GroupId == 0 {
|
||||
row.GroupId = in.GroupId
|
||||
}
|
||||
item := &sysin.ImUserBatchItemModel{Row: index + 2, Username: row.Username}
|
||||
created, createErr := s.Create(ctx, row)
|
||||
if createErr != nil {
|
||||
item.Reason = createErr.Error()
|
||||
res.FailedCount++
|
||||
} else {
|
||||
item.Success = true
|
||||
item.UserId = created.UserId
|
||||
item.Username = created.Username
|
||||
res.SuccessCount++
|
||||
}
|
||||
res.Items = append(res.Items, item)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) ImportExcel(ctx context.Context, tenantId, groupId int64, reader io.Reader) (res *sysin.ImUserBatchCreateModel, err error) {
|
||||
workbook, err := excelize.OpenReader(reader)
|
||||
if err != nil {
|
||||
return nil, gerror.New("Excel文件格式不正确")
|
||||
}
|
||||
defer workbook.Close()
|
||||
sheets := workbook.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return nil, gerror.New("Excel文件没有工作表")
|
||||
}
|
||||
rows, err := workbook.GetRows(sheets[0])
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取Excel文件失败")
|
||||
}
|
||||
input := &sysin.ImUserImportInp{TenantId: tenantId, GroupId: groupId}
|
||||
for index, row := range rows {
|
||||
if index == 0 || len(row) == 0 {
|
||||
continue
|
||||
}
|
||||
cell := func(i int) string {
|
||||
if i >= len(row) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(row[i])
|
||||
}
|
||||
rowGroupId := groupId
|
||||
if raw := cell(4); raw != "" {
|
||||
if _, scanErr := fmt.Sscan(raw, &rowGroupId); scanErr != nil {
|
||||
rowGroupId = groupId
|
||||
}
|
||||
}
|
||||
input.Rows = append(input.Rows, &sysin.ImUserCreateInp{
|
||||
TenantId: tenantId, Username: cell(0), Nickname: cell(1), Phone: cell(2), Password: cell(3), GroupId: rowGroupId,
|
||||
})
|
||||
}
|
||||
return s.Import(ctx, input)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) DownloadTemplate(ctx context.Context) error {
|
||||
type templateRow struct {
|
||||
Username string
|
||||
Nickname string
|
||||
Phone string
|
||||
Password string
|
||||
GroupId string
|
||||
}
|
||||
return excel.ExportByStructs(ctx,
|
||||
[]string{"用户名(可空,自动生成)", "昵称(必填)", "手机号", "密码(必填)", "用户分组ID"},
|
||||
[]templateRow{{Username: "", Nickname: "示例用户", Phone: "", Password: "123456", GroupId: ""}},
|
||||
"用户导入模板", "用户")
|
||||
}
|
||||
|
||||
// List 获取IM用户列表
|
||||
func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
@@ -51,6 +194,16 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
||||
return
|
||||
}
|
||||
values := addPageQuery(url.Values{}, in.Page, in.PerPage)
|
||||
if in.GroupId > 0 {
|
||||
userIds, groupErr := imUserPolicyGroupUserIds(ctx, tenant.Id, in.GroupId)
|
||||
if groupErr != nil {
|
||||
return nil, 0, groupErr
|
||||
}
|
||||
if len(userIds) == 0 {
|
||||
return []*sysin.ImUserListModel{}, 0, nil
|
||||
}
|
||||
values.Set("user_ids", strings.Join(userIds, ","))
|
||||
}
|
||||
if in.UserId != "" {
|
||||
values.Set("user_id", in.UserId)
|
||||
}
|
||||
@@ -60,12 +213,15 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
||||
if in.AuthName != "" {
|
||||
values.Set("auth_name", in.AuthName)
|
||||
}
|
||||
if in.Keyword != "" {
|
||||
values.Set("keyword", in.Keyword)
|
||||
}
|
||||
if in.State != nil {
|
||||
values.Set("state", intToString(*in.State))
|
||||
}
|
||||
addTimeRangeQuery(values, "created_at", in.CreatedAt)
|
||||
addTimeRangeQuery(values, "last_seen", in.LastSeen)
|
||||
return imAdminQueryList[sysin.ImUserListModel](ctx, tenant,
|
||||
list, totalCount, err = imAdminQueryList[sysin.ImUserListModel](ctx, tenant,
|
||||
"/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users",
|
||||
values,
|
||||
map[string]string{
|
||||
@@ -74,6 +230,116 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
||||
"access": "accessJson",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err = fillImUserPolicyGroups(ctx, tenant.Id, list); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err = fillImUserRegisterTypes(ctx, tenant.Id, list); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return list, totalCount, nil
|
||||
}
|
||||
|
||||
func fillImUserRegisterTypes(ctx context.Context, tenantId int64, list []*sysin.ImUserListModel) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]int64, 0, len(list))
|
||||
byID := make(map[int64]*sysin.ImUserListModel, len(list))
|
||||
for _, item := range list {
|
||||
id, err := strconv.ParseInt(item.UserId, 10, 64)
|
||||
if err == nil {
|
||||
ids = append(ids, id)
|
||||
byID[id] = item
|
||||
}
|
||||
if item.RegisterType == "" {
|
||||
item.RegisterType = consts.ImUserRegisterTypeAdmin
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
rows, err := imtenant.RegistryDB().Model(imAdminOperationAuditTable).Safe().Ctx(ctx).
|
||||
Fields("target_id").Where("tenant_id", tenantId).Where("target_type", "user").
|
||||
Where("action", "im_user_create").Where("result", "success").WhereIn("target_id", ids).All()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取用户注册方式失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if item := byID[row["target_id"].Int64()]; item != nil {
|
||||
item.RegisterType = consts.ImUserRegisterTypeAdmin
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateImUserPolicyGroup(ctx context.Context, tenantId, groupId int64) error {
|
||||
c := dao.ImUserPolicyGroups.Columns()
|
||||
count, err := imtenant.RegistryDB().Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx).
|
||||
Where(c.Id, groupId).Where(c.TenantId, tenantId).Where(c.Status, consts.StatusEnabled).WhereNull(c.DeletedAt).Count()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "检查用户分组失败")
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("用户分组不存在或已停用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func imUserPolicyGroupUserIds(ctx context.Context, tenantId, groupId int64) ([]string, error) {
|
||||
if err := validateImUserPolicyGroup(ctx, tenantId, groupId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := dao.ImUserPolicyGroupMembers.Columns()
|
||||
rows, err := imtenant.RegistryDB().Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).
|
||||
Fields(c.UserId).Where(c.TenantId, tenantId).Where(c.GroupId, groupId).Where(c.Status, consts.StatusEnabled).WhereNull(c.DeletedAt).All()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取用户分组成员失败")
|
||||
}
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, strconv.FormatInt(row[c.UserId].Int64(), 10))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func fillImUserPolicyGroups(ctx context.Context, tenantId int64, list []*sysin.ImUserListModel) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]int64, 0, len(list))
|
||||
byId := make(map[int64]*sysin.ImUserListModel, len(list))
|
||||
for _, item := range list {
|
||||
id, err := strconv.ParseInt(item.UserId, 10, 64)
|
||||
if err == nil {
|
||||
ids = append(ids, id)
|
||||
byId[id] = item
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
mc := dao.ImUserPolicyGroupMembers.Columns()
|
||||
gc := dao.ImUserPolicyGroups.Columns()
|
||||
rows, err := imtenant.RegistryDB().Model(dao.ImUserPolicyGroupMembers.Table()+" m").Safe().Ctx(ctx).
|
||||
LeftJoin(dao.ImUserPolicyGroups.Table()+" g", "g."+gc.Id+"=m."+mc.GroupId).
|
||||
Fields("m."+mc.UserId, "g."+gc.Id+" policy_group_id", "g."+gc.GroupName+" policy_group_name").
|
||||
Where("m."+mc.TenantId, tenantId).WhereIn("m."+mc.UserId, ids).
|
||||
Where("m."+mc.Status, consts.StatusEnabled).Where("g."+gc.Status, consts.StatusEnabled).
|
||||
WhereNull("m." + mc.DeletedAt).WhereNull("g." + gc.DeletedAt).OrderDesc("g." + gc.Priority).All()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取用户分组信息失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
item := byId[row[mc.UserId].Int64()]
|
||||
if item != nil && item.PolicyGroupId == 0 {
|
||||
item.PolicyGroupId = row["policy_group_id"].Int64()
|
||||
item.PolicyGroupName = row["policy_group_name"].String()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
@@ -285,6 +551,123 @@ func (s *sSysImUser) Status(ctx context.Context, in *sysin.ImUserStatusInp) (err
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysImUser) AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGroupInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = validateImUserPolicyGroup(ctx, scope.TenantId, in.GroupId); err != nil {
|
||||
return err
|
||||
}
|
||||
db := imtenant.RegistryDB()
|
||||
c := dao.ImUserPolicyGroupMembers.Columns()
|
||||
now := gtime.Now()
|
||||
operatorID := contexts.GetUserId(ctx)
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, updateErr := tx.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx).
|
||||
Where(c.TenantId, scope.TenantId).Where(c.UserId, in.UserId).WhereNull(c.DeletedAt).
|
||||
Data(g.Map{c.Status: consts.StatusDisable, c.DeletedAt: now, c.DeletedBy: operatorID, c.UpdatedAt: now, c.UpdatedBy: operatorID}).Update()
|
||||
if updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
_, insertErr := tx.Exec(`INSERT INTO `+dao.ImUserPolicyGroupMembers.Table()+`
|
||||
(tenant_id,group_id,user_id,status,valid_from,remark,created_at,created_by,updated_at,updated_by,deleted_at,deleted_by)
|
||||
VALUES(?,?,?,1,?,'后台调整分组',?,?,?, ?,NULL,NULL)
|
||||
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id),status=1,valid_from=VALUES(valid_from),valid_until=NULL,
|
||||
remark=VALUES(remark),updated_at=VALUES(updated_at),updated_by=VALUES(updated_by),deleted_at=NULL,deleted_by=NULL`,
|
||||
scope.TenantId, in.GroupId, in.UserId, now, now, operatorID, now, operatorID)
|
||||
return insertErr
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "调整用户分组失败")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "im_user_adjust_group", "user", in.UserId, "success", g.Map{"group_id": in.GroupId})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) IPWhitelist(ctx context.Context, in *sysin.ImUserIPWhitelistInp) (*sysin.ImUserIPWhitelistModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := db.Model("im_user_ip_whitelists").Safe().Ctx(ctx).
|
||||
Fields("ip_cidr").Where("user_id", in.UserId).
|
||||
Where("status", consts.StatusEnabled).WhereNull("deleted_at").OrderAsc("id").All()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取用户IP白名单失败")
|
||||
}
|
||||
result := &sysin.ImUserIPWhitelistModel{IPList: make([]string, 0, len(rows))}
|
||||
for _, row := range rows {
|
||||
result.IPList = append(result.IPList, row["ip_cidr"].String())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) IPWhitelistSave(ctx context.Context, in *sysin.ImUserIPWhitelistInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unique := make([]string, 0, len(in.IPList))
|
||||
seen := make(map[string]struct{}, len(in.IPList))
|
||||
for _, raw := range in.IPList {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if net.ParseIP(value) == nil {
|
||||
if _, _, parseErr := net.ParseCIDR(value); parseErr != nil {
|
||||
return gerror.Newf("IP或CIDR格式不合法:%s", value)
|
||||
}
|
||||
}
|
||||
if _, ok := seen[value]; !ok {
|
||||
seen[value] = struct{}{}
|
||||
unique = append(unique, value)
|
||||
}
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := gtime.Now()
|
||||
operatorID := contexts.GetUserId(ctx)
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if _, updateErr := tx.Model("im_user_ip_whitelists").Safe().Ctx(ctx).
|
||||
Where("user_id", in.UserId).WhereNull("deleted_at").
|
||||
Data(g.Map{"status": consts.StatusDisable, "deleted_at": now, "deleted_by": operatorID, "updated_at": now, "updated_by": operatorID}).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
for _, value := range unique {
|
||||
if _, insertErr := tx.Exec(`INSERT INTO im_user_ip_whitelists
|
||||
(user_id,ip_cidr,status,created_at,created_by,updated_at,updated_by,deleted_at,deleted_by)
|
||||
VALUES(?,?,1,?,?,?,?,NULL,NULL)
|
||||
ON DUPLICATE KEY UPDATE status=1,updated_at=VALUES(updated_at),updated_by=VALUES(updated_by),deleted_at=NULL,deleted_by=NULL`,
|
||||
in.UserId, value, now, operatorID, now, operatorID); insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "保存用户IP白名单失败")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "im_user_ip_whitelist_save", "user", in.UserId, "success", g.Map{"ip_list": unique})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) Devices(ctx context.Context, in *sysin.ImUserDeviceListInp) (list []*sysin.ImUserDeviceListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ package sysin
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/form"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
@@ -18,6 +19,8 @@ type ImUserListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId string `json:"userId" dc:"用户ID"`
|
||||
GroupId int64 `json:"groupId" dc:"用户策略分组ID"`
|
||||
Keyword string `json:"keyword" dc:"用户名、昵称或手机号"`
|
||||
Nickname string `json:"nickname" dc:"昵称"`
|
||||
AuthName string `json:"authName" dc:"登录标识"`
|
||||
State *int `json:"state" dc:"账号状态"`
|
||||
@@ -26,6 +29,7 @@ type ImUserListInp struct {
|
||||
}
|
||||
|
||||
func (in *ImUserListInp) Filter(ctx context.Context) (err error) {
|
||||
in.Keyword = strings.TrimSpace(in.Keyword)
|
||||
if in.UserId != "" {
|
||||
if _, err = ParseOptionalInt64(in.UserId); err != nil {
|
||||
return err
|
||||
@@ -53,6 +57,79 @@ type ImUserListModel struct {
|
||||
UserAgent string `json:"userAgent" dc:"客户端"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
|
||||
Avatar string `json:"avatar" dc:"头像地址"`
|
||||
PolicyGroupId int64 `json:"policyGroupId" dc:"用户策略分组ID"`
|
||||
PolicyGroupName string `json:"policyGroupName" dc:"用户策略分组名称"`
|
||||
RegisterType string `json:"registerType" dc:"注册方式"`
|
||||
}
|
||||
|
||||
type ImUserCreateInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
Username string `json:"username" dc:"用户名"`
|
||||
Nickname string `json:"nickname" v:"required#昵称不能为空" dc:"昵称"`
|
||||
Phone string `json:"phone" dc:"手机号"`
|
||||
Password string `json:"password" v:"required#密码不能为空" dc:"密码"`
|
||||
GroupId int64 `json:"groupId" dc:"用户策略分组ID"`
|
||||
}
|
||||
|
||||
func (in *ImUserCreateInp) Filter(ctx context.Context) error {
|
||||
in.Username = strings.ToLower(strings.TrimSpace(in.Username))
|
||||
in.Nickname = strings.TrimSpace(in.Nickname)
|
||||
in.Phone = strings.TrimSpace(in.Phone)
|
||||
if in.Nickname == "" {
|
||||
return gerror.New("昵称不能为空")
|
||||
}
|
||||
if len([]rune(in.Password)) < 3 || len([]byte(in.Password)) > 72 {
|
||||
return gerror.New("密码长度需为3至72字节")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImUserCreateModel struct {
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
Username string `json:"username" dc:"用户名"`
|
||||
Nickname string `json:"nickname" dc:"昵称"`
|
||||
Phone string `json:"phone" dc:"手机号"`
|
||||
}
|
||||
|
||||
type ImUserBatchCreateInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
Count int `json:"count" v:"required|min:1|max:100#创建数量不能为空|创建数量最少为1|单次最多创建100个用户" dc:"创建数量"`
|
||||
Password string `json:"password" v:"required#默认密码不能为空" dc:"默认密码"`
|
||||
NicknamePrefix string `json:"nicknamePrefix" dc:"昵称前缀"`
|
||||
GroupId int64 `json:"groupId" dc:"用户策略分组ID"`
|
||||
}
|
||||
|
||||
func (in *ImUserBatchCreateInp) Filter(ctx context.Context) error {
|
||||
in.NicknamePrefix = strings.TrimSpace(in.NicknamePrefix)
|
||||
if in.Count < 1 || in.Count > 100 {
|
||||
return gerror.New("单次创建数量需为1至100")
|
||||
}
|
||||
if len([]rune(in.Password)) < 3 || len([]byte(in.Password)) > 72 {
|
||||
return gerror.New("密码长度需为3至72字节")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImUserBatchItemModel struct {
|
||||
Row int `json:"row" dc:"行号"`
|
||||
Username string `json:"username" dc:"用户名"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
Success bool `json:"success" dc:"是否成功"`
|
||||
Reason string `json:"reason" dc:"失败原因"`
|
||||
}
|
||||
|
||||
type ImUserBatchCreateModel struct {
|
||||
SuccessCount int `json:"successCount" dc:"成功数量"`
|
||||
FailedCount int `json:"failedCount" dc:"失败数量"`
|
||||
Items []*ImUserBatchItemModel `json:"items" dc:"处理明细"`
|
||||
}
|
||||
|
||||
type ImUserImportInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
GroupId int64 `json:"groupId" dc:"默认用户策略分组ID"`
|
||||
Rows []*ImUserCreateInp `json:"rows" dc:"导入用户"`
|
||||
}
|
||||
|
||||
// ImUserViewInp 获取指定IM用户信息
|
||||
@@ -167,12 +244,39 @@ func (in *ImUserGovernanceInp) Filter(ctx context.Context) (err error) {
|
||||
if in.UserId <= 0 {
|
||||
return gerror.New("用户ID不能为空")
|
||||
}
|
||||
if in.ActorUserId <= 0 {
|
||||
return gerror.New("操作IM用户ID不能为空")
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImUserAdjustGroupInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId int64 `json:"userId" v:"required|min:1#用户ID不能为空|用户ID不合法" dc:"用户ID"`
|
||||
GroupId int64 `json:"groupId" v:"required|min:1#用户分组不能为空|用户分组不合法" dc:"用户策略分组ID"`
|
||||
}
|
||||
|
||||
func (in *ImUserAdjustGroupInp) Filter(ctx context.Context) error {
|
||||
if in.UserId <= 0 || in.GroupId <= 0 {
|
||||
return gerror.New("用户和用户分组不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImUserIPWhitelistInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId int64 `json:"userId" v:"required|min:1#用户ID不能为空|用户ID不合法" dc:"用户ID"`
|
||||
IPList []string `json:"ipList" dc:"IP或CIDR列表"`
|
||||
}
|
||||
|
||||
func (in *ImUserIPWhitelistInp) Filter(ctx context.Context) error {
|
||||
if in.UserId <= 0 {
|
||||
return gerror.New("用户ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImUserIPWhitelistModel struct {
|
||||
IPList []string `json:"ipList" dc:"IP或CIDR列表"`
|
||||
}
|
||||
|
||||
func (in *ImUserGovernanceInp) FilterDevice(ctx context.Context) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
func TestImUserGovernanceAllowsAdminActor(t *testing.T) {
|
||||
in := &ImUserGovernanceInp{TenantId: 1, UserId: 2}
|
||||
if err := in.Filter(gctx.New()); err != nil {
|
||||
t.Fatalf("admin governance should not require an IM actor: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImUserActionInputs(t *testing.T) {
|
||||
ctx := gctx.New()
|
||||
if err := (&ImUserAdjustGroupInp{UserId: 2, GroupId: 3}).Filter(ctx); err != nil {
|
||||
t.Fatalf("valid group adjustment rejected: %v", err)
|
||||
}
|
||||
if err := (&ImUserAdjustGroupInp{UserId: 2}).Filter(ctx); err == nil {
|
||||
t.Fatal("missing group must be rejected")
|
||||
}
|
||||
if err := (&ImUserIPWhitelistInp{UserId: 2}).Filter(ctx); err != nil {
|
||||
t.Fatalf("valid whitelist input rejected: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/utility/tree"
|
||||
"io"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/database/gredis"
|
||||
@@ -214,6 +215,11 @@ type (
|
||||
Export(ctx context.Context, in *sysin.ImInviteListInp) (err error)
|
||||
}
|
||||
ISysImUser interface {
|
||||
Create(ctx context.Context, in *sysin.ImUserCreateInp) (res *sysin.ImUserCreateModel, err error)
|
||||
BatchCreate(ctx context.Context, in *sysin.ImUserBatchCreateInp) (res *sysin.ImUserBatchCreateModel, err error)
|
||||
Import(ctx context.Context, in *sysin.ImUserImportInp) (res *sysin.ImUserBatchCreateModel, err error)
|
||||
ImportExcel(ctx context.Context, tenantId, groupId int64, reader io.Reader) (res *sysin.ImUserBatchCreateModel, err error)
|
||||
DownloadTemplate(ctx context.Context) (err error)
|
||||
// List 获取IM用户列表
|
||||
List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error)
|
||||
// View 获取IM用户指定信息
|
||||
@@ -224,6 +230,9 @@ type (
|
||||
RevokeSessions(ctx context.Context, in *sysin.ImUserRevokeSessionsInp) (err error)
|
||||
// Status 更新IM用户状态
|
||||
Status(ctx context.Context, in *sysin.ImUserStatusInp) (err error)
|
||||
AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGroupInp) (err error)
|
||||
IPWhitelist(ctx context.Context, in *sysin.ImUserIPWhitelistInp) (res *sysin.ImUserIPWhitelistModel, err error)
|
||||
IPWhitelistSave(ctx context.Context, in *sysin.ImUserIPWhitelistInp) (err error)
|
||||
// Devices 获取IM用户设备列表
|
||||
Devices(ctx context.Context, in *sysin.ImUserDeviceListInp) (list []*sysin.ImUserDeviceListModel, totalCount int, err error)
|
||||
// Ban 通过Kafka封禁IM用户
|
||||
|
||||
@@ -26,10 +26,48 @@ WHERE @userListId IS NULL;
|
||||
|
||||
SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' LIMIT 1;
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '创建用户', 'imUserCreate', '', '', '3', '', '/imUser/create', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '1', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserCreate');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '批量创建用户', 'imUserBatchCreate', '', '', '3', '', '/imUser/batchCreate', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '2', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserBatchCreate');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '导入用户', 'imUserImport', '', '', '3', '', '/imUser/import', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '3', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserImport');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '下载用户模板', 'imUserDownloadTemplate', '', '', '3', '', '/imUser/downloadTemplate', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '4', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserDownloadTemplate');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userDirId, '用户列表', 'imUserIndex', 'index', '', '2', '', '/imUser/list', '', '/imUser/index', '1', 'imUser', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @userListId IS NULL;
|
||||
|
||||
SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' LIMIT 1;
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '用户详情', 'imUserView', '', '', '3', '', '/imUser/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserView');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '重置用户密码', 'imUserResetPassword', '', '', '3', '', '/imUser/resetPassword', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '11', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserResetPassword');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '查看用户IP白名单', 'imUserIPWhitelist', '', '', '3', '', '/imUser/ipWhitelist', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '12', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserIPWhitelist');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '保存用户IP白名单', 'imUserIPWhitelistSave', '', '', '3', '', '/imUser/ipWhitelistSave', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '13', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserIPWhitelistSave');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '调整用户分组', 'imUserAdjustGroup', '', '', '3', '', '/imUser/adjustGroup', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '14', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserAdjustGroup');
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '封禁用户', 'imUserBan', '', '', '3', '', '/imUser/ban', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '20', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserBan');
|
||||
@@ -61,3 +99,12 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserAccountCan
|
||||
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @userListId, '删除账号', 'imUserAccountDelete', '', '', '3', '', '/imUser/accountDelete', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '90', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserAccountDelete');
|
||||
|
||||
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT parent_role.`role_id`, child.`id`
|
||||
FROM `hg_admin_role_menu` parent_role
|
||||
JOIN `hg_admin_menu` parent ON parent.`id` = parent_role.`menu_id` AND parent.`name` = 'imUserIndex'
|
||||
JOIN `hg_admin_menu` child ON child.`pid` = parent.`id`
|
||||
WHERE child.`name` IN ('imUserCreate', 'imUserBatchCreate', 'imUserImport', 'imUserDownloadTemplate',
|
||||
'imUserView', 'imUserResetPassword', 'imUserIPWhitelist', 'imUserIPWhitelistSave', 'imUserAdjustGroup',
|
||||
'imUserBan', 'imUserUnban', 'imUserLogoutAll', 'imUserAccountCancel');
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
import { http, jumpExport } from '@/utils/http/axios';
|
||||
import type { UploadFileParams } from '@/utils/http/axios/types';
|
||||
|
||||
export function Create(params) {
|
||||
return http.request({ url: '/imUser/create', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function BatchCreate(params) {
|
||||
return http.request({ url: '/imUser/batchCreate', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function ImportUsers(params: UploadFileParams) {
|
||||
return http.uploadFile({ url: '/imUser/import', method: 'POST' }, params);
|
||||
}
|
||||
|
||||
export function DownloadTemplate() {
|
||||
jumpExport('/imUser/downloadTemplate', {});
|
||||
}
|
||||
|
||||
// 获取IM用户列表
|
||||
export function List(params) {
|
||||
@@ -45,6 +62,18 @@ export function Status(params) {
|
||||
});
|
||||
}
|
||||
|
||||
export function AdjustGroup(params) {
|
||||
return http.request({ url: '/imUser/adjustGroup', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function IPWhitelist(params) {
|
||||
return http.request({ url: '/imUser/ipWhitelist', method: 'GET', params });
|
||||
}
|
||||
|
||||
export function IPWhitelistSave(params) {
|
||||
return http.request({ url: '/imUser/ipWhitelistSave', method: 'POST', params });
|
||||
}
|
||||
|
||||
// 获取IM用户设备列表
|
||||
export function Devices(params) {
|
||||
return http.request({
|
||||
|
||||
+457
-31
@@ -19,26 +19,226 @@
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
/>
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-space>
|
||||
<n-button type="primary" v-if="hasPermission(['/imUser/create'])" @click="openCreate">
|
||||
<template #icon
|
||||
><n-icon><PlusOutlined /></n-icon
|
||||
></template>
|
||||
创建用户
|
||||
</n-button>
|
||||
<n-button v-if="hasPermission(['/imUser/batchCreate'])" @click="openBatchCreate">
|
||||
批量创建
|
||||
</n-button>
|
||||
<n-button v-if="hasPermission(['/imUser/import'])" @click="openImport">
|
||||
<template #icon
|
||||
><n-icon><UploadOutlined /></n-icon
|
||||
></template>
|
||||
导入Excel
|
||||
</n-button>
|
||||
<n-button v-if="hasPermission(['/imUser/downloadTemplate'])" @click="DownloadTemplate">
|
||||
<template #icon
|
||||
><n-icon><DownloadOutlined /></n-icon
|
||||
></template>
|
||||
下载模板
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<ViewModal ref="viewRef" />
|
||||
<DevicesModal ref="devicesRef" />
|
||||
<ResetPasswordModal ref="resetPasswordRef" @reload="reloadTable" />
|
||||
|
||||
<n-modal
|
||||
v-model:show="ipVisible"
|
||||
preset="dialog"
|
||||
title="IP白名单"
|
||||
:show-icon="false"
|
||||
class="user-dialog"
|
||||
>
|
||||
<n-form label-placement="left" label-width="90">
|
||||
<n-form-item label="用户"
|
||||
>{{ operationUser?.username }}({{ operationUser?.userId }})</n-form-item
|
||||
>
|
||||
<n-form-item label="IP/CIDR">
|
||||
<n-input
|
||||
v-model:value="ipWhitelistText"
|
||||
type="textarea"
|
||||
:rows="7"
|
||||
placeholder="每行一个 IP 或 CIDR,例如 192.168.1.10 或 10.0.0.0/8"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-button @click="ipVisible = false">取消</n-button>
|
||||
<n-button
|
||||
v-if="hasPermission(['/imUser/ipWhitelistSave'])"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submitIPWhitelist"
|
||||
>保存</n-button
|
||||
>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="groupVisible"
|
||||
preset="dialog"
|
||||
title="调整分组"
|
||||
:show-icon="false"
|
||||
class="user-dialog"
|
||||
>
|
||||
<n-form label-placement="left" label-width="90">
|
||||
<n-form-item label="用户"
|
||||
>{{ operationUser?.username }}({{ operationUser?.userId }})</n-form-item
|
||||
>
|
||||
<n-form-item label="用户分组" required>
|
||||
<n-select
|
||||
v-model:value="adjustGroupId"
|
||||
:options="policyGroupOptions"
|
||||
placeholder="请选择用户分组"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-button @click="groupVisible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="submitting" @click="submitAdjustGroup">保存</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="createVisible"
|
||||
preset="dialog"
|
||||
title="创建用户"
|
||||
:show-icon="false"
|
||||
class="user-dialog"
|
||||
>
|
||||
<n-form
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
label-placement="left"
|
||||
label-width="90"
|
||||
>
|
||||
<n-form-item label="用户名" path="username"
|
||||
><n-input v-model:value="createForm.username" placeholder="可空,系统自动生成"
|
||||
/></n-form-item>
|
||||
<n-form-item label="昵称" path="nickname"
|
||||
><n-input v-model:value="createForm.nickname" placeholder="请输入昵称"
|
||||
/></n-form-item>
|
||||
<n-form-item label="手机号" path="phone"
|
||||
><n-input v-model:value="createForm.phone" placeholder="可选"
|
||||
/></n-form-item>
|
||||
<n-form-item label="密码" path="password"
|
||||
><n-input v-model:value="createForm.password" type="password" show-password-on="click"
|
||||
/></n-form-item>
|
||||
<n-form-item label="用户分组"
|
||||
><n-select
|
||||
v-model:value="createForm.groupId"
|
||||
:options="policyGroupOptions"
|
||||
clearable
|
||||
placeholder="请选择用户分组"
|
||||
/></n-form-item>
|
||||
</n-form>
|
||||
<template #action
|
||||
><n-button @click="createVisible = false">取消</n-button
|
||||
><n-button type="primary" :loading="submitting" @click="submitCreate"
|
||||
>确定</n-button
|
||||
></template
|
||||
>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="batchVisible"
|
||||
preset="dialog"
|
||||
title="批量创建用户"
|
||||
:show-icon="false"
|
||||
class="user-dialog"
|
||||
>
|
||||
<n-form
|
||||
ref="batchFormRef"
|
||||
:model="batchForm"
|
||||
:rules="batchRules"
|
||||
label-placement="left"
|
||||
label-width="100"
|
||||
>
|
||||
<n-form-item label="创建数量" path="count"
|
||||
><n-input-number v-model:value="batchForm.count" :min="1" :max="100"
|
||||
/></n-form-item>
|
||||
<n-form-item label="昵称前缀"
|
||||
><n-input v-model:value="batchForm.nicknamePrefix" placeholder="默认:用户"
|
||||
/></n-form-item>
|
||||
<n-form-item label="默认密码" path="password"
|
||||
><n-input v-model:value="batchForm.password" type="password" show-password-on="click"
|
||||
/></n-form-item>
|
||||
<n-form-item label="默认分组"
|
||||
><n-select v-model:value="batchForm.groupId" :options="policyGroupOptions" clearable
|
||||
/></n-form-item>
|
||||
</n-form>
|
||||
<template #action
|
||||
><n-button @click="batchVisible = false">取消</n-button
|
||||
><n-button type="primary" :loading="submitting" @click="submitBatchCreate"
|
||||
>确定</n-button
|
||||
></template
|
||||
>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="importVisible"
|
||||
preset="dialog"
|
||||
title="导入Excel"
|
||||
:show-icon="false"
|
||||
class="user-dialog"
|
||||
>
|
||||
<n-form label-placement="left" label-width="90">
|
||||
<n-form-item label="默认分组"
|
||||
><n-select v-model:value="importGroupId" :options="policyGroupOptions" clearable
|
||||
/></n-form-item>
|
||||
<n-form-item label="Excel文件">
|
||||
<n-upload :max="1" accept=".xlsx" :custom-request="handleImport" :show-file-list="true">
|
||||
<n-upload-dragger><n-text>点击或拖动 .xlsx 文件到此处</n-text></n-upload-dragger>
|
||||
</n-upload>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { UploadCustomRequestOptions, useDialog, useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { List, RevokeSessions } from '@/api/imUser';
|
||||
import { columns, loadOptions, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
||||
import {
|
||||
AccountCancel,
|
||||
AdjustGroup,
|
||||
Ban,
|
||||
BatchCreate,
|
||||
Create,
|
||||
DownloadTemplate,
|
||||
ImportUsers,
|
||||
IPWhitelist,
|
||||
IPWhitelistSave,
|
||||
List,
|
||||
LogoutAll,
|
||||
Unban,
|
||||
} from '@/api/imUser';
|
||||
import {
|
||||
columns,
|
||||
loadOptions,
|
||||
loadPolicyGroupOptions,
|
||||
loadTenantOptions,
|
||||
policyGroupOptions,
|
||||
schemas,
|
||||
State,
|
||||
tenantOptions,
|
||||
} from './model';
|
||||
import { DownloadOutlined, PlusOutlined, UploadOutlined } from '@vicons/antd';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import ViewModal from './view.vue';
|
||||
import DevicesModal from './DevicesModal.vue';
|
||||
import ResetPasswordModal from './ResetPasswordModal.vue';
|
||||
|
||||
const message = useMessage();
|
||||
@@ -49,8 +249,40 @@
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const viewRef = ref();
|
||||
const devicesRef = ref();
|
||||
const resetPasswordRef = ref();
|
||||
const createFormRef = ref();
|
||||
const batchFormRef = ref();
|
||||
const createVisible = ref(false);
|
||||
const batchVisible = ref(false);
|
||||
const importVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
const ipVisible = ref(false);
|
||||
const groupVisible = ref(false);
|
||||
const operationUser = ref<State | null>(null);
|
||||
const ipWhitelistText = ref('');
|
||||
const adjustGroupId = ref<number | null>(null);
|
||||
const importGroupId = ref<number | null>(null);
|
||||
const createForm = reactive({
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
groupId: null as number | null,
|
||||
});
|
||||
const batchForm = reactive({
|
||||
count: 10,
|
||||
nicknamePrefix: '用户',
|
||||
password: '',
|
||||
groupId: null as number | null,
|
||||
});
|
||||
const createRules = {
|
||||
nickname: { required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
password: { required: true, min: 3, message: '密码至少3位', trigger: 'blur' },
|
||||
};
|
||||
const batchRules = {
|
||||
count: { required: true, type: 'number', message: '请输入创建数量', trigger: 'change' },
|
||||
password: { required: true, min: 3, message: '密码至少3位', trigger: 'blur' },
|
||||
};
|
||||
|
||||
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
|
||||
if (tenantSchema) {
|
||||
@@ -58,7 +290,7 @@
|
||||
}
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 330,
|
||||
width: 600,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
@@ -68,25 +300,51 @@
|
||||
actions: [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'default',
|
||||
onClick: handleView.bind(null, record),
|
||||
auth: ['/imUser/view'],
|
||||
},
|
||||
{
|
||||
label: '设备',
|
||||
onClick: handleDevices.bind(null, record),
|
||||
auth: ['/imUser/devices'],
|
||||
},
|
||||
{
|
||||
label: '撤销会话',
|
||||
type: 'error',
|
||||
onClick: handleRevokeSessions.bind(null, record),
|
||||
auth: ['/imUser/revokeSessions'],
|
||||
},
|
||||
{
|
||||
label: '重置密码',
|
||||
type: 'warning',
|
||||
type: 'default',
|
||||
onClick: handleResetPassword.bind(null, record),
|
||||
auth: ['/imUser/resetPassword'],
|
||||
ifShow: record.state !== 20,
|
||||
},
|
||||
{
|
||||
label: 'IP白名单',
|
||||
type: 'default',
|
||||
onClick: openIPWhitelist.bind(null, record),
|
||||
auth: ['/imUser/ipWhitelist'],
|
||||
ifShow: record.state !== 20,
|
||||
},
|
||||
{
|
||||
label: '调整分组',
|
||||
type: 'default',
|
||||
onClick: openAdjustGroup.bind(null, record),
|
||||
auth: ['/imUser/adjustGroup'],
|
||||
ifShow: record.state !== 20,
|
||||
},
|
||||
{
|
||||
label: record.state === 0 ? '封禁' : '解封',
|
||||
type: record.state === 0 ? 'error' : 'default',
|
||||
onClick: handleBanToggle.bind(null, record),
|
||||
auth: [record.state === 0 ? '/imUser/ban' : '/imUser/unban'],
|
||||
ifShow: record.state !== 20,
|
||||
},
|
||||
{
|
||||
label: '强制下线',
|
||||
type: 'error',
|
||||
onClick: handleForceOffline.bind(null, record),
|
||||
auth: ['/imUser/logoutAll'],
|
||||
ifShow: record.state !== 20,
|
||||
},
|
||||
{
|
||||
label: '强制注销',
|
||||
type: 'error',
|
||||
onClick: handleForceCancel.bind(null, record),
|
||||
auth: ['/imUser/accountCancel'],
|
||||
ifShow: record.state !== 20,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -122,19 +380,19 @@
|
||||
viewRef.value.openModal(record);
|
||||
}
|
||||
|
||||
function handleDevices(record: State) {
|
||||
devicesRef.value.openModal(record);
|
||||
}
|
||||
|
||||
function handleRevokeSessions(record: State) {
|
||||
function handleForceOffline(record: State) {
|
||||
dialog.warning({
|
||||
title: '撤销用户会话',
|
||||
content: '将递增该用户认证版本,并通知 IM 服务清退在线会话,是否确认?',
|
||||
title: '强制下线',
|
||||
content: `确定强制用户“${record.username}”的全部设备下线吗?`,
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await RevokeSessions({ tenantId: record.tenantId, userId: record.userId });
|
||||
message.success('会话撤销事件已提交');
|
||||
await LogoutAll({
|
||||
tenantId: record.tenantId,
|
||||
userId: Number(record.userId),
|
||||
reason: '后台强制下线',
|
||||
});
|
||||
message.success('强制下线命令已提交');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
@@ -144,6 +402,169 @@
|
||||
resetPasswordRef.value.openModal(record);
|
||||
}
|
||||
|
||||
async function openIPWhitelist(record: State) {
|
||||
operationUser.value = record;
|
||||
const result = await IPWhitelist({ tenantId: record.tenantId, userId: Number(record.userId) });
|
||||
ipWhitelistText.value = (result?.ipList || []).join('\n');
|
||||
ipVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitIPWhitelist() {
|
||||
if (!operationUser.value) return;
|
||||
const ipList = ipWhitelistText.value
|
||||
.split(/[\n,,]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
submitting.value = true;
|
||||
try {
|
||||
await IPWhitelistSave({
|
||||
tenantId: operationUser.value.tenantId,
|
||||
userId: Number(operationUser.value.userId),
|
||||
ipList,
|
||||
});
|
||||
message.success('IP白名单保存成功');
|
||||
ipVisible.value = false;
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openAdjustGroup(record: State) {
|
||||
operationUser.value = record;
|
||||
adjustGroupId.value = record.policyGroupId || null;
|
||||
groupVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitAdjustGroup() {
|
||||
if (!operationUser.value || !adjustGroupId.value) {
|
||||
message.warning('请选择用户分组');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await AdjustGroup({
|
||||
tenantId: operationUser.value.tenantId,
|
||||
userId: Number(operationUser.value.userId),
|
||||
groupId: adjustGroupId.value,
|
||||
});
|
||||
message.success('用户分组调整成功');
|
||||
groupVisible.value = false;
|
||||
reloadTable();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBanToggle(record: State) {
|
||||
const banning = record.state === 0;
|
||||
dialog.warning({
|
||||
title: banning ? '封禁用户' : '解封用户',
|
||||
content: `确定${banning ? '封禁' : '解封'}用户“${record.username}”吗?`,
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const action = banning ? Ban : Unban;
|
||||
await action({
|
||||
tenantId: record.tenantId,
|
||||
userId: Number(record.userId),
|
||||
reason: banning ? '后台封禁' : '后台解封',
|
||||
});
|
||||
message.success(`${banning ? '封禁' : '解封'}命令已提交`);
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleForceCancel(record: State) {
|
||||
dialog.error({
|
||||
title: '强制注销',
|
||||
content: `确定强制注销用户“${record.username}”吗?注销后账号不可继续登录。`,
|
||||
positiveText: '确认注销',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await AccountCancel({
|
||||
tenantId: record.tenantId,
|
||||
userId: Number(record.userId),
|
||||
reason: '后台强制注销',
|
||||
});
|
||||
message.success('强制注销命令已提交');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function currentTenantId() {
|
||||
return Number(searchFormRef.value?.formModel?.tenantId || 0);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(createForm, {
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
groupId: null,
|
||||
});
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
await createFormRef.value?.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
const result = await Create({ ...createForm, tenantId: currentTenantId() });
|
||||
message.success(`用户 ${result.username} 创建成功`);
|
||||
createVisible.value = false;
|
||||
reloadTable();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openBatchCreate() {
|
||||
Object.assign(batchForm, { count: 10, nicknamePrefix: '用户', password: '', groupId: null });
|
||||
batchVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitBatchCreate() {
|
||||
await batchFormRef.value?.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
const result = await BatchCreate({ ...batchForm, tenantId: currentTenantId() });
|
||||
message.success(`创建完成:成功 ${result.successCount},失败 ${result.failedCount}`);
|
||||
batchVisible.value = false;
|
||||
reloadTable();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openImport() {
|
||||
importGroupId.value = null;
|
||||
importVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleImport(options: UploadCustomRequestOptions) {
|
||||
try {
|
||||
const result = await ImportUsers({
|
||||
file: options.file.file as File,
|
||||
tenantId: currentTenantId(),
|
||||
groupId: importGroupId.value || 0,
|
||||
});
|
||||
options.onFinish();
|
||||
message.success(`导入完成:成功 ${result.successCount},失败 ${result.failedCount}`);
|
||||
importVisible.value = false;
|
||||
reloadTable();
|
||||
} catch {
|
||||
options.onError();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => searchFormRef.value?.formModel?.tenantId,
|
||||
(tenantId) => loadPolicyGroupOptions(Number(tenantId || 0))
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
loadOptions();
|
||||
if (isPlatformAdmin.value) {
|
||||
@@ -154,8 +575,13 @@
|
||||
message.warning('暂无可用租户');
|
||||
}
|
||||
}
|
||||
await loadPolicyGroupOptions(currentTenantId());
|
||||
reloadTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
:deep(.user-dialog) {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { NSwitch } from 'naive-ui';
|
||||
import { NAvatar, NImage, NTag } from 'naive-ui';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { defRangeShortcuts } from '@/utils/dateUtil';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { List as TenantList } from '@/api/imTenant';
|
||||
import { Status } from '@/api/imUser';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { List as PolicyGroupList } from '@/api/imUserPolicyGroup';
|
||||
|
||||
const dict = useDictStore();
|
||||
const { hasPermission } = usePermission();
|
||||
const $message = window['$message'];
|
||||
|
||||
export const tenantOptions = ref<any[]>([]);
|
||||
export const policyGroupOptions = ref<any[]>([]);
|
||||
|
||||
export class State {
|
||||
public tenantId = 0; // 租户ID
|
||||
@@ -25,6 +22,8 @@ export class State {
|
||||
public gender = ''; // 性别
|
||||
public region = ''; // 地区
|
||||
public state = 0; // 账号状态
|
||||
public policyGroupId = 0; // 用户策略分组ID
|
||||
public policyGroupName = ''; // 用户策略分组名称
|
||||
public authVersion = 0; // 认证版本
|
||||
public passwordUpdatedAt = ''; // 密码更新时间
|
||||
public authNames = ''; // 登录标识
|
||||
@@ -35,6 +34,8 @@ export class State {
|
||||
public updatedAt = ''; // 修改时间
|
||||
public signature = ''; // 个性签名
|
||||
public avatarFileId = ''; // 头像文件ID
|
||||
public avatar = ''; // 头像地址
|
||||
public registerType = ''; // 注册方式
|
||||
public birthDate = ''; // 出生日期
|
||||
public profileJson = ''; // Tinode Public资料
|
||||
public trustedJson = ''; // Tinode Trusted资料
|
||||
@@ -88,25 +89,19 @@ export const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'userId',
|
||||
component: 'NInput',
|
||||
label: '用户ID',
|
||||
label: '编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户ID',
|
||||
placeholder: '用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
component: 'NInput',
|
||||
label: '昵称',
|
||||
field: 'groupId',
|
||||
component: 'NSelect',
|
||||
label: '用户分组',
|
||||
componentProps: {
|
||||
placeholder: '请输入昵称',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'authName',
|
||||
component: 'NInput',
|
||||
label: '登录标识',
|
||||
componentProps: {
|
||||
placeholder: '请输入登录名/手机号/邮箱',
|
||||
clearable: true,
|
||||
options: computed(() => policyGroupOptions.value),
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -116,27 +111,16 @@ export const schemas = ref<FormSchema[]>([
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: dict.getOption('ImUserStateOptions'),
|
||||
placeholder: '请选择状态',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
component: 'NDatePicker',
|
||||
label: '创建时间',
|
||||
field: 'keyword',
|
||||
component: 'NInput',
|
||||
label: '关键词',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
clearable: true,
|
||||
shortcuts: defRangeShortcuts(),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'lastSeen',
|
||||
component: 'NDatePicker',
|
||||
label: '最后在线',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
clearable: true,
|
||||
shortcuts: defRangeShortcuts(),
|
||||
placeholder: '用户名/昵称/手机号',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -144,85 +128,90 @@ export const schemas = ref<FormSchema[]>([
|
||||
// 表格列
|
||||
export const columns = [
|
||||
{
|
||||
title: '用户ID',
|
||||
title: '编号',
|
||||
key: 'userId',
|
||||
align: 'left',
|
||||
width: 120,
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '头像',
|
||||
key: 'avatar',
|
||||
align: 'left',
|
||||
width: 80,
|
||||
render(row: State) {
|
||||
if (row.avatar) {
|
||||
return h(NImage, { width: 40, height: 40, src: row.avatar, objectFit: 'cover' });
|
||||
}
|
||||
return h(
|
||||
NAvatar,
|
||||
{ size: 40 },
|
||||
{ default: () => (row.nickname || row.username || '?').slice(0, 1) }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
key: 'username',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
key: 'nickname',
|
||||
align: 'left',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '登录账号',
|
||||
key: 'username',
|
||||
align: 'left',
|
||||
width: 180,
|
||||
width: 150,
|
||||
render(row: State) {
|
||||
return row.nickname || '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
key: 'phone',
|
||||
align: 'left',
|
||||
width: 180,
|
||||
width: 140,
|
||||
render(row: State) {
|
||||
return row.phone || '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '公开账号',
|
||||
key: 'publicAccount',
|
||||
title: '注册方式',
|
||||
key: 'registerType',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
width: 110,
|
||||
render(row: State) {
|
||||
return dict.getLabel('ImUserRegisterTypeOptions', row.registerType) || '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'state',
|
||||
align: 'left',
|
||||
width: 110,
|
||||
width: 90,
|
||||
render(row: State) {
|
||||
if (row.state === 20) {
|
||||
return dict.getLabel('ImUserStateOptions', row.state);
|
||||
}
|
||||
return h(NSwitch, {
|
||||
value: row.state === 0,
|
||||
checked: '正常',
|
||||
unchecked: '停用',
|
||||
disabled: !hasPermission(['/imUser/status']),
|
||||
onUpdateValue: async (value) => {
|
||||
const oldState = row.state;
|
||||
row.state = value ? 0 : 10;
|
||||
try {
|
||||
await Status({ tenantId: row.tenantId, userId: row.userId, state: row.state });
|
||||
$message.success('操作成功');
|
||||
} catch {
|
||||
row.state = oldState;
|
||||
}
|
||||
},
|
||||
});
|
||||
return h(
|
||||
NTag,
|
||||
{ type: row.state === 0 ? 'success' : row.state === 10 ? 'error' : 'default' },
|
||||
{ default: () => dict.getLabel('ImUserStateOptions', row.state) || '--' }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录标识',
|
||||
key: 'authNames',
|
||||
title: '用户分组',
|
||||
key: 'policyGroupName',
|
||||
align: 'left',
|
||||
width: 240,
|
||||
width: 130,
|
||||
render(row: State) {
|
||||
return row.policyGroupName || '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '凭证',
|
||||
key: 'credentials',
|
||||
align: 'left',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: '最后在线',
|
||||
title: '最近登录',
|
||||
key: 'lastSeen',
|
||||
align: 'left',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '客户端',
|
||||
key: 'userAgent',
|
||||
align: 'left',
|
||||
width: 240,
|
||||
render(row: State) {
|
||||
return row.lastSeen || '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
@@ -230,12 +219,6 @@ export const columns = [
|
||||
align: 'left',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
key: 'updatedAt',
|
||||
align: 'left',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
|
||||
export const deviceColumns = [
|
||||
@@ -260,7 +243,7 @@ export const deviceColumns = [
|
||||
|
||||
// 加载字典数据选项
|
||||
export function loadOptions() {
|
||||
dict.loadOptions(['ImUserStateOptions']);
|
||||
dict.loadOptions(['ImUserStateOptions', 'ImUserRegisterTypeOptions']);
|
||||
}
|
||||
|
||||
export async function loadTenantOptions() {
|
||||
@@ -271,3 +254,12 @@ export async function loadTenantOptions() {
|
||||
}));
|
||||
return tenantOptions.value;
|
||||
}
|
||||
|
||||
export async function loadPolicyGroupOptions(tenantId: number) {
|
||||
const res = await PolicyGroupList({ tenantId, page: 1, pageSize: 200 });
|
||||
policyGroupOptions.value = (res?.list || []).map((item) => ({
|
||||
label: item.groupName,
|
||||
value: item.id,
|
||||
}));
|
||||
return policyGroupOptions.value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user