diff --git a/server/api/admin/imconfiginternal/im_config.go b/server/api/admin/imconfiginternal/im_config.go index be24174..424c429 100644 --- a/server/api/admin/imconfiginternal/im_config.go +++ b/server/api/admin/imconfiginternal/im_config.go @@ -93,6 +93,38 @@ type ModerationPolicyRes struct { BlockMessage string `json:"block_message"` } +type SecurityPolicyReq struct { + g.Meta `path:"/internal/im-config/tenants/{code}/security-policy" method:"get" tags:"IM内部配置" summary:"获取商户安全策略"` + Code string `json:"code" in:"path" dc:"商户编码"` +} + +type SecurityPolicyRes struct { + TenantID int64 `json:"tenant_id"` + TenantCode string `json:"tenant_code"` + PolicyVersion uint `json:"policy_version"` + GlobalFriendEnabled bool `json:"global_friend_enabled"` + FriendDisabledPolicy map[string]interface{} `json:"friend_disabled_policy"` + MessageCurfewEnabled bool `json:"message_curfew_enabled"` + CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"` +} + +type SecurityCurfewRuleRes struct { + RuleCode string `json:"rule_code"` + RuleName string `json:"rule_name"` + DateType string `json:"date_type"` + CustomDates []string `json:"custom_dates"` + Weekdays []int `json:"weekdays"` + Timezone string `json:"timezone"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Scope string `json:"scope"` + WhitelistUserIds []string `json:"whitelist_user_ids"` + WhitelistGroupIds []string `json:"whitelist_group_ids"` + Message string `json:"message"` + Status int `json:"status"` + Sort int `json:"sort"` +} + type SensitiveWordsReq struct { g.Meta `path:"/internal/im-config/tenants/{code}/sensitive-words" method:"get" tags:"IM内部配置" summary:"获取商户有效敏感词"` Code string `json:"code" in:"path" dc:"商户编码"` diff --git a/server/api/admin/imuser/im_user.go b/server/api/admin/imuser/im_user.go index ec9f5a2..732a4ea 100644 --- a/server/api/admin/imuser/im_user.go +++ b/server/api/admin/imuser/im_user.go @@ -99,6 +99,13 @@ type AdjustGroupReq struct { type AdjustGroupRes struct{} +type FriendAddReq struct { + g.Meta `path:"/imUser/friendAdd" method:"post" tags:"IM用户" summary:"设置IM用户加好友开关"` + sysin.ImUserFriendAddInp +} + +type FriendAddRes struct{} + type IPWhitelistReq struct { g.Meta `path:"/imUser/ipWhitelist" method:"get" tags:"IM用户" summary:"获取IM用户IP白名单"` sysin.ImUserIPWhitelistInp diff --git a/server/internal/controller/admin/sys/im_user.go b/server/internal/controller/admin/sys/im_user.go index d66289e..74725bb 100644 --- a/server/internal/controller/admin/sys/im_user.go +++ b/server/internal/controller/admin/sys/im_user.go @@ -108,6 +108,11 @@ func (c *cImUser) AdjustGroup(ctx context.Context, req *imuser.AdjustGroupReq) ( return } +func (c *cImUser) FriendAdd(ctx context.Context, req *imuser.FriendAddReq) (res *imuser.FriendAddRes, err error) { + err = service.SysImUser().FriendAdd(ctx, &req.ImUserFriendAddInp) + 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 { diff --git a/server/internal/controller/internalapi/imconfig/im_config.go b/server/internal/controller/internalapi/imconfig/im_config.go index ab3bb72..206bcfa 100644 --- a/server/internal/controller/internalapi/imconfig/im_config.go +++ b/server/internal/controller/internalapi/imconfig/im_config.go @@ -211,6 +211,99 @@ func (c *cConfig) ModerationPolicy(ctx context.Context, req *api.ModerationPolic return res, nil } +func (c *cConfig) SecurityPolicy(ctx context.Context, req *api.SecurityPolicyReq) (res *api.SecurityPolicyRes, err error) { + tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code)) + if err != nil { + return nil, err + } + res = &api.SecurityPolicyRes{ + TenantID: tenant.Id, + TenantCode: tenant.Code, + GlobalFriendEnabled: true, + FriendDisabledPolicy: map[string]interface{}{}, + MessageCurfewEnabled: false, + CurfewRules: []*api.SecurityCurfewRuleRes{}, + } + columns := dao.ImSecurityPolicy.Columns() + record, err := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx). + Fields(columns.Id, columns.PolicyVersion, columns.GlobalFriendEnabled, "CAST("+columns.FriendDisabledPolicy+" AS CHAR) friend_disabled_policy", columns.MessageCurfewEnabled). + Where(columns.TenantId, tenant.Id).Where(columns.PublishStatus, consts.ImSecurityPublishPublished). + WhereNull(columns.DeletedAt).OrderDesc(columns.PolicyVersion).One() + if err != nil { + return nil, gerror.Wrap(err, "获取商户安全策略失败") + } + if record.IsEmpty() { + return res, nil + } + policyID := record[columns.Id].Int64() + res.PolicyVersion = record[columns.PolicyVersion].Uint() + res.GlobalFriendEnabled = record[columns.GlobalFriendEnabled].Int() == 1 + res.MessageCurfewEnabled = record[columns.MessageCurfewEnabled].Int() == 1 + if raw := strings.TrimSpace(record["friend_disabled_policy"].String()); raw != "" && raw != "null" { + _ = json.Unmarshal([]byte(raw), &res.FriendDisabledPolicy) + } + ruleColumns := dao.ImMessageCurfewRules.Columns() + var rows []struct { + RuleCode string `json:"rule_code"` + RuleName string `json:"rule_name"` + DateType string `json:"date_type"` + CustomDates string `json:"custom_dates"` + Weekdays string `json:"weekdays"` + Timezone string `json:"timezone"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Scope string `json:"scope"` + WhitelistUserIds string `json:"whitelist_user_ids"` + WhitelistGroupIds string `json:"whitelist_group_ids"` + Message string `json:"message"` + Status int `json:"status"` + Sort int `json:"sort"` + } + if err = imtenant.RegistryDB().Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx). + Fields(ruleColumns.RuleCode, ruleColumns.RuleName, ruleColumns.DateType, "CAST("+ruleColumns.CustomDates+" AS CHAR) custom_dates", "CAST("+ruleColumns.Weekdays+" AS CHAR) weekdays", ruleColumns.Timezone, "TIME_FORMAT("+ruleColumns.StartTime+",'%H:%i') start_time", "TIME_FORMAT("+ruleColumns.EndTime+",'%H:%i') end_time", ruleColumns.Scope, "CAST("+ruleColumns.WhitelistUserIds+" AS CHAR) whitelist_user_ids", "CAST("+ruleColumns.WhitelistGroupIds+" AS CHAR) whitelist_group_ids", ruleColumns.Message, ruleColumns.Status, ruleColumns.Sort). + Where(ruleColumns.PolicyId, policyID).Where(ruleColumns.Status, consts.StatusEnabled). + WhereNull(ruleColumns.DeletedAt).OrderAsc(ruleColumns.Sort).OrderAsc(ruleColumns.Id).Scan(&rows); err != nil { + return nil, gerror.Wrap(err, "获取商户宵禁规则失败") + } + for _, row := range rows { + res.CurfewRules = append(res.CurfewRules, &api.SecurityCurfewRuleRes{ + RuleCode: row.RuleCode, + RuleName: row.RuleName, + DateType: row.DateType, + CustomDates: parseStringArray(row.CustomDates), + Weekdays: parseIntArray(row.Weekdays), + Timezone: row.Timezone, + StartTime: row.StartTime, + EndTime: row.EndTime, + Scope: row.Scope, + WhitelistUserIds: parseStringArray(row.WhitelistUserIds), + WhitelistGroupIds: parseStringArray(row.WhitelistGroupIds), + Message: row.Message, + Status: row.Status, + Sort: row.Sort, + }) + } + return res, nil +} + +func parseStringArray(raw string) []string { + values := []string{} + if strings.TrimSpace(raw) == "" { + return values + } + _ = json.Unmarshal([]byte(raw), &values) + return values +} + +func parseIntArray(raw string) []int { + values := []int{} + if strings.TrimSpace(raw) == "" { + return values + } + _ = json.Unmarshal([]byte(raw), &values) + return values +} + func (c *cConfig) SensitiveWords(ctx context.Context, req *api.SensitiveWordsReq) (res *api.SensitiveWordsRes, err error) { tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code)) if err != nil { diff --git a/server/internal/logic/sys/im_user.go b/server/internal/logic/sys/im_user.go index 084c038..e43b70c 100644 --- a/server/internal/logic/sys/im_user.go +++ b/server/internal/logic/sys/im_user.go @@ -360,6 +360,7 @@ func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model { "up.public_account", "up.gender", "up.region", + "COALESCE(up.friend_add_enabled, 1) friend_add_enabled", "us.auth_version", "us.password_updated_at", ) @@ -589,6 +590,54 @@ func (s *sSysImUser) AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGrou return nil } +func (s *sSysImUser) FriendAdd(ctx context.Context, in *sysin.ImUserFriendAddInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return err + } + db, _, err := imtenant.DB(ctx, scope.TenantId) + if err != nil { + return err + } + now := gtime.Now() + result, err := db.Model("im_user_profiles").Safe().Ctx(ctx). + Where("user_id", in.UserId). + Data(g.Map{ + "friend_add_enabled": in.Enabled, + "friend_add_disabled_reason": in.Reason, + "friend_add_policy_version": gdb.Raw("friend_add_policy_version+1"), + "updated_at": now, + }).Update() + if err != nil { + return gerror.Wrap(err, "设置用户加好友开关失败") + } + affected, _ := result.RowsAffected() + if affected == 0 { + profileID, idErr := imtenant.NextSnowflakeID(ctx) + if idErr != nil { + return gerror.Wrap(idErr, "生成用户资料ID失败") + } + _, err = db.Model("im_user_profiles").Safe().Ctx(ctx).Data(g.Map{ + "id": profileID, + "user_id": in.UserId, + "public_account": fmt.Sprintf("usr%d", in.UserId), + "friend_add_enabled": in.Enabled, + "friend_add_disabled_reason": in.Reason, + "friend_add_policy_version": 1, + "created_at": now, + "updated_at": now, + }).Insert() + if err != nil { + return gerror.Wrap(err, "补全用户资料并设置加好友开关失败") + } + } + _ = writeImAdminAudit(ctx, scope.TenantId, "im_user_friend_add_update", "user", in.UserId, "success", g.Map{"enabled": in.Enabled, "reason": in.Reason}) + 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 diff --git a/server/internal/model/input/sysin/im_user.go b/server/internal/model/input/sysin/im_user.go index 00319f8..2759726 100644 --- a/server/internal/model/input/sysin/im_user.go +++ b/server/internal/model/input/sysin/im_user.go @@ -63,6 +63,7 @@ type ImUserListModel struct { PolicyGroupId int64 `json:"policyGroupId" dc:"用户策略分组ID"` PolicyGroupName string `json:"policyGroupName" dc:"用户策略分组名称"` RegisterType string `json:"registerType" dc:"注册方式"` + FriendAddEnabled int `json:"friendAddEnabled" dc:"是否允许发起加好友"` } type ImUserCreateInp struct { @@ -263,6 +264,27 @@ func (in *ImUserAdjustGroupInp) Filter(ctx context.Context) error { return nil } +type ImUserFriendAddInp struct { + TenantId int64 `json:"tenantId" dc:"租户ID"` + UserId int64 `json:"userId" v:"required|min:1#用户ID不能为空|用户ID不合法" dc:"用户ID"` + Enabled int `json:"enabled" dc:"是否允许发起加好友:1允许 0禁止"` + Reason string `json:"reason" dc:"禁用原因"` +} + +func (in *ImUserFriendAddInp) Filter(ctx context.Context) error { + in.Reason = strings.TrimSpace(in.Reason) + if in.UserId <= 0 { + return gerror.New("用户ID不能为空") + } + if in.Enabled != 0 && in.Enabled != 1 { + return gerror.New("加好友开关不合法") + } + if in.Enabled == 0 && in.Reason == "" { + in.Reason = "后台禁止该用户发起加好友" + } + return nil +} + type ImUserIPWhitelistInp struct { TenantId int64 `json:"tenantId" dc:"租户ID"` UserId int64 `json:"userId" v:"required|min:1#用户ID不能为空|用户ID不合法" dc:"用户ID"` diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go index fb91818..303f564 100644 --- a/server/internal/service/sys.go +++ b/server/internal/service/sys.go @@ -233,6 +233,7 @@ type ( // Status 更新IM用户状态 Status(ctx context.Context, in *sysin.ImUserStatusInp) (err error) AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGroupInp) (err error) + FriendAdd(ctx context.Context, in *sysin.ImUserFriendAddInp) (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用户设备列表 diff --git a/server/storage/data/generate/im_user_menu.sql b/server/storage/data/generate/im_user_menu.sql index e2e74b6..a18a6a1 100644 --- a/server/storage/data/generate/im_user_menu.sql +++ b/server/storage/data/generate/im_user_menu.sql @@ -68,6 +68,10 @@ INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type 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, '设置加好友开关', 'imUserFriendAdd', '', '', '3', '', '/imUser/friendAdd', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '15', '', '1', @now, @now +WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserFriendAdd'); + 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'); diff --git a/server/storage/data/im_registration_invite_hotgo.sql b/server/storage/data/im_registration_invite_hotgo.sql index 4dc9847..9be310a 100644 --- a/server/storage/data/im_registration_invite_hotgo.sql +++ b/server/storage/data/im_registration_invite_hotgo.sql @@ -225,3 +225,10 @@ SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '更新用户状态', 'i '/imUser/status', '', 1, 50, 1, NOW(), NOW() WHERE @imUserPage IS NOT NULL AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserStatus'); + +INSERT INTO hg_admin_menu + (pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at) +SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '设置加好友开关', 'imUserFriendAdd', '', '', 3, + '/imUser/friendAdd', '', 1, 55, 1, NOW(), NOW() +WHERE @imUserPage IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserFriendAdd'); diff --git a/web/src/api/imUser/index.ts b/web/src/api/imUser/index.ts index dc5b350..ac2797f 100644 --- a/web/src/api/imUser/index.ts +++ b/web/src/api/imUser/index.ts @@ -93,6 +93,10 @@ export function AdjustGroup(params) { return http.request({ url: '/imUser/adjustGroup', method: 'POST', params }); } +export function FriendAdd(params) { + return http.request({ url: '/imUser/friendAdd', method: 'POST', params }); +} + export function IPWhitelist(params) { return http.request({ url: '/imUser/ipWhitelist', method: 'GET', params }); } diff --git a/web/src/views/imUser/index.vue b/web/src/views/imUser/index.vue index 0c2e12c..5a3af15 100644 --- a/web/src/views/imUser/index.vue +++ b/web/src/views/imUser/index.vue @@ -218,6 +218,7 @@ BatchCreate, Create, DownloadTemplate, + FriendAdd, ImportUsers, IPWhitelist, IPWhitelistSave, @@ -287,7 +288,7 @@ } const actionColumn = reactive({ - width: 600, + width: 700, title: '操作', key: 'action', fixed: 'right', @@ -322,6 +323,13 @@ auth: ['/imUser/adjustGroup'], ifShow: record.state !== 20, }, + { + label: record.friendAddEnabled === 0 ? '允许加好友' : '禁止加好友', + type: record.friendAddEnabled === 0 ? 'default' : 'error', + onClick: handleFriendAddToggle.bind(null, record), + auth: ['/imUser/friendAdd'], + ifShow: record.state !== 20, + }, { label: record.state === 0 ? '封禁' : '解封', type: record.state === 0 ? 'error' : 'default', @@ -399,6 +407,29 @@ resetPasswordRef.value.openModal(record); } + function handleFriendAddToggle(record: State) { + const nextEnabled = record.friendAddEnabled === 0 ? 1 : 0; + dialog.warning({ + title: nextEnabled === 1 ? '允许加好友' : '禁止加好友', + content: + nextEnabled === 1 + ? `确定允许用户“${record.username}”发起加好友吗?` + : `确定禁止用户“${record.username}”发起加好友吗?`, + positiveText: '确认', + negativeText: '取消', + onPositiveClick: async () => { + await FriendAdd({ + tenantId: record.tenantId, + userId: Number(record.userId), + enabled: nextEnabled, + reason: nextEnabled === 1 ? '' : '后台禁止该用户发起加好友', + }); + message.success(nextEnabled === 1 ? '已允许加好友' : '已禁止加好友'); + reloadTable(); + }, + }); + } + async function openIPWhitelist(record: State) { operationUser.value = record; const result = await IPWhitelist({ tenantId: record.tenantId, userId: Number(record.userId) }); diff --git a/web/src/views/imUser/model.ts b/web/src/views/imUser/model.ts index 5f6c050..c5e739b 100644 --- a/web/src/views/imUser/model.ts +++ b/web/src/views/imUser/model.ts @@ -26,6 +26,7 @@ export class State { public state = 0; // 账号状态 public policyGroupId = 0; // 用户策略分组ID public policyGroupName = ''; // 用户策略分组名称 + public friendAddEnabled = 1; // 是否允许发起加好友 public authVersion = 0; // 认证版本 public passwordUpdatedAt = ''; // 密码更新时间 public authNames = ''; // 登录标识 @@ -206,6 +207,19 @@ export const columns = [ return row.policyGroupName || '--'; }, }, + { + title: '加好友', + key: 'friendAddEnabled', + align: 'left', + width: 100, + render(row: State) { + return h( + NTag, + { type: row.friendAddEnabled === 0 ? 'error' : 'success' }, + { default: () => (row.friendAddEnabled === 0 ? '禁止' : '允许') } + ); + }, + }, { title: '最近登录', key: 'lastSeen',