This commit is contained in:
Austin
2026-09-21 09:28:09 +08:00
28 changed files with 1607 additions and 142 deletions
@@ -0,0 +1,137 @@
-- 消息加密密钥管理。
-- 可重复执行。根 KEK 只在首次执行时生成,重复执行不会覆盖已有值。
SET NAMES utf8mb4;
SET @now := NOW();
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
AND COLUMN_NAME = 'message_encryption_enabled') = 0,
'ALTER TABLE `im_tenant_config` ADD COLUMN `message_encryption_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用消息加密'' AFTER `local_moderation_enabled`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
CREATE TABLE IF NOT EXISTS `im_message_encryption_keys` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL COMMENT '商户ID',
`key_version` int unsigned NOT NULL COMMENT '密钥版本',
`algorithm` varchar(32) NOT NULL DEFAULT 'AES-256-GCM' COMMENT '数据加密算法',
`encrypted_dek` blob NOT NULL COMMENT '由根KEK包裹后的商户DEK',
`dek_nonce` varbinary(12) NOT NULL COMMENT '包裹DEK使用的随机nonce',
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/active/retired/disabled',
`activated_at` datetime DEFAULT NULL,
`retired_at` datetime DEFAULT NULL,
`remark` varchar(255) NOT NULL DEFAULT '',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_by` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_message_key_tenant_version` (`tenant_id`, `key_version`),
KEY `idx_im_message_key_tenant_status` (`tenant_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商户消息加密密钥版本';
INSERT INTO `hg_sys_dict_type`
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 0, 'IM内部安全材料', 'im_internal_secret', 9999,
'仅供服务端读取,通用字典接口禁止查询和维护', 1, @now, @now
WHERE NOT EXISTS (
SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_internal_secret'
);
INSERT INTO `hg_sys_dict_data`
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 'core_material_01', TO_BASE64(RANDOM_BYTES(32)), 'string', 'im_internal_secret',
'', 2, 1, '消息DEK包裹根密钥KEK,请勿通过后台展示或修改', 1, @now, @now
WHERE NOT EXISTS (
SELECT 1 FROM `hg_sys_dict_data`
WHERE `type` = 'im_internal_secret' AND `label` = 'core_material_01'
);
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings' LIMIT 1);
INSERT INTO `hg_admin_menu`
(`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 @settingsId, '消息密钥管理', 'imMessageEncryptionIndex', 'message-encryption/index',
'KeyOutlined', 2, '', '/imMessageEncryption/list', '', '/imMessageEncryption/index',
1, 'imManager', 0, 0, '', 0, 0, 0, 3,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), 80,
'商户消息加密开关及密钥版本管理', 1, @now, @now
WHERE @settingsId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex');
SET @keyMenuId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex' LIMIT 1);
UPDATE `hg_admin_menu`
SET `pid` = @settingsId,
`title` = '消息密钥管理',
`path` = 'message-encryption/index',
`icon` = 'KeyOutlined',
`permissions` = '/imMessageEncryption/list',
`component` = '/imMessageEncryption/index',
`active_menu` = 'imManager',
`level` = 3,
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '),
`sort` = 80,
`hidden` = 0,
`status` = 1,
`updated_at` = @now
WHERE `id` = @keyMenuId;
INSERT INTO `hg_admin_menu`
(`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 @keyMenuId, '初始化密钥', 'imMessageEncryptionInitialize', '', '', 3, '',
'/imMessageEncryption/initialize', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 10, '', 1, @now, @now
WHERE @keyMenuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionInitialize');
INSERT INTO `hg_admin_menu`
(`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 @keyMenuId, '轮换密钥', 'imMessageEncryptionRotate', '', '', 3, '',
'/imMessageEncryption/rotate', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 20, '', 1, @now, @now
WHERE @keyMenuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionRotate');
INSERT INTO `hg_admin_menu`
(`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 @keyMenuId, '切换消息加密', 'imMessageEncryptionSwitch', '', '', 3, '',
'/imMessageEncryption/switch', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 30, '', 1, @now, @now
WHERE @keyMenuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionSwitch');
UPDATE `hg_admin_menu`
SET `level` = 4,
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '),
`updated_at` = @now
WHERE `pid` = @keyMenuId AND `type` = 3;
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `hg_admin_menu`
WHERE `name` IN (
'imManager',
'imSystemSettings',
'imMessageEncryptionIndex',
'imMessageEncryptionInitialize',
'imMessageEncryptionRotate',
'imMessageEncryptionSwitch'
);
+49 -8
View File
@@ -1,6 +1,10 @@
package imconfiginternal
import "github.com/gogf/gf/v2/frame/g"
import (
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type TenantReq struct {
g.Meta `path:"/internal/im-config/tenants/by-code/{code}" method:"get" tags:"IM内部配置" summary:"按编码获取IM租户"`
@@ -99,14 +103,24 @@ type SecurityPolicyReq struct {
}
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"`
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"`
LoginRateLimit *SecurityLoginRateLimitRes `json:"login_rate_limit"`
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
}
type RealnamePolicyReq struct {
g.Meta `path:"/internal/im-config/tenants/by-id/{id}/realname-policy" method:"get" tags:"IM内部配置" summary:"获取租户实名认证策略"`
Id int64 `json:"id" in:"path" dc:"租户ID"`
}
type RealnamePolicyRes struct {
TenantID int64 `json:"tenant_id"`
RealnameReviewPolicy int `json:"realname_review_policy"`
}
type SecurityLoginRateLimitRes struct {
@@ -145,6 +159,33 @@ type SensitiveWordsRes struct {
Items []*SensitiveWordModel `json:"items"`
}
type MessageKeyActiveReq struct {
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/active" method:"get" tags:"IM内部配置" summary:"获取商户当前消息密钥"`
TenantCode string `json:"tenantCode" in:"path"`
}
type MessageKeyActiveRes struct {
*sysin.ImMessageKeyModel
}
type MessageKeyVersionReq struct {
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/{version}" method:"get" tags:"IM内部配置" summary:"获取商户指定版本消息密钥"`
TenantCode string `json:"tenantCode" in:"path"`
Version uint `json:"version" in:"path"`
}
type MessageKeyVersionRes struct {
*sysin.ImMessageKeyModel
}
type MessageKeyAckReq struct {
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/ack" method:"post" tags:"IM内部配置" summary:"确认商户消息密钥可用"`
TenantCode string `json:"tenantCode" in:"path"`
Version uint `json:"version" v:"required|min:1#密钥版本不能为空|密钥版本不正确"`
}
type MessageKeyAckRes struct{}
type SensitiveWordModel struct {
ID int64 `json:"id"`
Word string `json:"word"`
@@ -0,0 +1,36 @@
package immessageencryption
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imMessageEncryption/list" method:"get" tags:"消息加密密钥" summary:"获取商户消息加密状态"`
sysin.ImMessageEncryptionListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImMessageEncryptionListModel `json:"list" dc:"数据列表"`
}
type InitializeReq struct {
g.Meta `path:"/imMessageEncryption/initialize" method:"post" tags:"消息加密密钥" summary:"初始化商户消息密钥"`
sysin.ImMessageEncryptionActionInp
}
type InitializeRes struct{}
type RotateReq struct {
g.Meta `path:"/imMessageEncryption/rotate" method:"post" tags:"消息加密密钥" summary:"轮换商户消息密钥"`
sysin.ImMessageEncryptionActionInp
}
type RotateRes struct{}
type SwitchReq struct {
g.Meta `path:"/imMessageEncryption/switch" method:"post" tags:"消息加密密钥" summary:"切换商户消息加密"`
sysin.ImMessageEncryptionSwitchInp
}
type SwitchRes struct{}
+1
View File
@@ -13,6 +13,7 @@ const (
RegionSpilt = " / " // 地区分隔符
Unknown = "Unknown" // Unknown
SuperRoleKey = "super" // 超管角色唯一标识符,通过角色验证超管
ImPlatformRoleKey = "supreme_manager" // IM平台超管角色唯一标识符
MaxServeLogContentLen = 2048 // 最大保留服务日志内容大小
SysDefaultLanguage = "zh_CN" // 系统默认语言,当配置文件没有国际化配置时生效
)
@@ -0,0 +1,12 @@
package consts
const (
ImInternalSecretDictType = "im_internal_secret"
ImMessageRootKEKLabel = "core_material_01"
ImMessageKeyAlgorithm = "AES-256-GCM"
ImMessageKeyStatusPending = "pending"
ImMessageKeyStatusActive = "active"
ImMessageKeyStatusRetired = "retired"
ImMessageKeyStatusDisabled = "disabled"
)
@@ -0,0 +1,41 @@
package sys
import (
"context"
"hotgo/api/admin/immessageencryption"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImMessageEncryption = cImMessageEncryption{}
type cImMessageEncryption struct{}
func (c *cImMessageEncryption) List(ctx context.Context, req *immessageencryption.ListReq) (res *immessageencryption.ListRes, err error) {
list, total, err := service.SysImMessageEncryption().List(ctx, &req.ImMessageEncryptionListInp)
if err != nil {
return nil, err
}
if list == nil {
list = []*sysin.ImMessageEncryptionListModel{}
}
res = new(immessageencryption.ListRes)
res.List = list
res.PageRes.Pack(req, total)
return res, nil
}
func (c *cImMessageEncryption) Initialize(ctx context.Context, req *immessageencryption.InitializeReq) (res *immessageencryption.InitializeRes, err error) {
err = service.SysImMessageEncryption().Initialize(ctx, &req.ImMessageEncryptionActionInp)
return
}
func (c *cImMessageEncryption) Rotate(ctx context.Context, req *immessageencryption.RotateReq) (res *immessageencryption.RotateRes, err error) {
err = service.SysImMessageEncryption().Rotate(ctx, &req.ImMessageEncryptionActionInp)
return
}
func (c *cImMessageEncryption) Switch(ctx context.Context, req *immessageencryption.SwitchReq) (res *immessageencryption.SwitchRes, err error) {
err = service.SysImMessageEncryption().Switch(ctx, &req.ImMessageEncryptionSwitchInp)
return
}
@@ -9,6 +9,7 @@ import (
"hotgo/internal/library/immoderation"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strings"
@@ -302,6 +303,29 @@ func (c *cConfig) SecurityPolicy(ctx context.Context, req *api.SecurityPolicyReq
return res, nil
}
func (c *cConfig) RealnamePolicy(ctx context.Context, req *api.RealnamePolicyReq) (res *api.RealnamePolicyRes, err error) {
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Id, req.Id)
if err != nil {
return nil, err
}
res = defaultRealnamePolicy(tenant.Id)
var row *struct {
RealnameReviewPolicy int `json:"realname_review_policy"`
}
if err = imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
Fields("realname_review_policy").
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Scan(&row); err != nil {
return nil, gerror.Wrap(err, "获取租户实名认证策略失败")
}
if row == nil {
return res, nil
}
if row.RealnameReviewPolicy == 1 {
res.RealnameReviewPolicy = 1
}
return res, nil
}
func parseStringArray(raw string) []string {
values := []string{}
if strings.TrimSpace(raw) == "" {
@@ -359,6 +383,27 @@ func (c *cConfig) SensitiveWords(ctx context.Context, req *api.SensitiveWordsReq
return res, nil
}
func (c *cConfig) MessageKeyActive(ctx context.Context, req *api.MessageKeyActiveReq) (res *api.MessageKeyActiveRes, err error) {
key, err := service.SysImMessageEncryption().GetKey(ctx, &sysin.ImMessageKeyGetInp{TenantCode: req.TenantCode})
if err != nil {
return nil, err
}
return &api.MessageKeyActiveRes{ImMessageKeyModel: key}, nil
}
func (c *cConfig) MessageKeyVersion(ctx context.Context, req *api.MessageKeyVersionReq) (res *api.MessageKeyVersionRes, err error) {
key, err := service.SysImMessageEncryption().GetKey(ctx, &sysin.ImMessageKeyGetInp{TenantCode: req.TenantCode, Version: req.Version})
if err != nil {
return nil, err
}
return &api.MessageKeyVersionRes{ImMessageKeyModel: key}, nil
}
func (c *cConfig) MessageKeyAck(ctx context.Context, req *api.MessageKeyAckReq) (res *api.MessageKeyAckRes, err error) {
err = service.SysImMessageEncryption().Ack(ctx, &sysin.ImMessageKeyAckInp{TenantCode: req.TenantCode, Version: req.Version})
return
}
func defaultModerationPolicy(tenant *entity.ImTenant) *api.ModerationPolicyRes {
return &api.ModerationPolicyRes{TenantID: tenant.Id, TenantCode: tenant.Code, Enabled: false,
AliyunEnabled: false, LocalWordsEnabled: false, Scenes: cloneBoolMap(defaultModerationScenes),
@@ -366,6 +411,13 @@ func defaultModerationPolicy(tenant *entity.ImTenant) *api.ModerationPolicyRes {
BlockMessage: "内容不符合平台规范,请修改后重试"}
}
func defaultRealnamePolicy(tenantID int64) *api.RealnamePolicyRes {
return &api.RealnamePolicyRes{
TenantID: tenantID,
RealnameReviewPolicy: 0,
}
}
func cloneBoolMap(src map[string]bool) map[string]bool {
dst := make(map[string]bool, len(src))
for k, v := range src {
@@ -0,0 +1,76 @@
package immessagecrypto
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/utility/encrypt"
"github.com/gogf/gf/v2/errors/gerror"
)
func GenerateDEK() ([]byte, error) {
key := make([]byte, encrypt.AESGCMKeySize)
if _, err := rand.Read(key); err != nil {
return nil, gerror.Wrap(err, "生成商户消息密钥失败")
}
return key, nil
}
func WrapDEK(ctx context.Context, tenantID int64, version uint, dek []byte) (ciphertext, nonce []byte, err error) {
kek, err := loadRootKEK(ctx)
if err != nil {
return nil, nil, err
}
return encrypt.AESGCMEncrypt(dek, kek, keyAAD(tenantID, version))
}
func UnwrapDEK(ctx context.Context, tenantID int64, version uint, ciphertext, nonce []byte) ([]byte, error) {
kek, err := loadRootKEK(ctx)
if err != nil {
return nil, err
}
dek, err := encrypt.AESGCMDecrypt(ciphertext, nonce, kek, keyAAD(tenantID, version))
if err != nil {
return nil, gerror.Wrap(err, "解包商户消息密钥失败")
}
if len(dek) != encrypt.AESGCMKeySize {
return nil, gerror.New("商户消息密钥长度不正确")
}
return dek, nil
}
func EncodeDEK(dek []byte) string {
return base64.StdEncoding.EncodeToString(dek)
}
func loadRootKEK(ctx context.Context) ([]byte, error) {
columns := dao.SysDictData.Columns()
value, err := dao.SysDictData.Ctx(ctx).
Fields(columns.Value).
Where(columns.Type, consts.ImInternalSecretDictType).
Where(columns.Label, consts.ImMessageRootKEKLabel).
Where(columns.Status, consts.StatusEnabled).
Value()
if err != nil {
return nil, gerror.Wrap(err, "读取消息根密钥失败")
}
if value.IsEmpty() {
return nil, gerror.New("消息根密钥未初始化")
}
kek, err := base64.StdEncoding.DecodeString(value.String())
if err != nil || len(kek) != encrypt.AESGCMKeySize {
return nil, gerror.New("消息根密钥格式不正确")
}
return kek, nil
}
func keyAAD(tenantID int64, version uint) []byte {
aad := make([]byte, 16)
binary.BigEndian.PutUint64(aad[:8], uint64(tenantID))
binary.BigEndian.PutUint64(aad[8:], uint64(version))
return aad
}
+52 -4
View File
@@ -468,7 +468,7 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
return
}
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
if service.AdminMember().VerifySuperId(ctx, opMemberId) || contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
if in.TenantId > 0 {
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
Where(dao.ImTenant.Columns().Id, in.TenantId).
@@ -508,6 +508,9 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
if err = service.AdminRole().VerifyRoleId(ctx, in.RoleId); err != nil {
return
}
if err = s.normalizeTenantByRole(ctx, in); err != nil {
return
}
// 验证部门ID
if err = service.AdminDept().VerifyDeptId(ctx, in.DeptId); err != nil {
@@ -606,6 +609,50 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
})
}
func (s *sAdminMember) normalizeTenantByRole(ctx context.Context, in *adminin.MemberEditInp) error {
var role *entity.AdminRole
if err := dao.AdminRole.Ctx(ctx).
Fields(dao.AdminRole.Columns().Id, dao.AdminRole.Columns().Key).
WherePri(in.RoleId).
Scan(&role); err != nil {
return gerror.Wrap(err, "获取角色信息失败,请稍后重试!")
}
if role == nil || role.Id <= 0 {
return gerror.New("角色不存在")
}
switch role.Key {
case consts.ImPlatformRoleKey:
deptId, err := s.getCompanyDeptId(ctx)
if err != nil {
return err
}
in.TenantId = 0
in.DeptId = deptId
case "imManage":
if in.TenantId <= 0 {
return gerror.New("IM管理员必须选择所属商户")
}
}
return nil
}
func (s *sAdminMember) getCompanyDeptId(ctx context.Context) (int64, error) {
value, err := dao.AdminDept.Ctx(ctx).
Fields(dao.AdminDept.Columns().Id).
Where(dao.AdminDept.Columns().Type, consts.DeptTypeCompany).
Where(dao.AdminDept.Columns().Status, consts.StatusEnabled).
OrderAsc(dao.AdminDept.Columns().Id).
Limit(1).
Value()
if err != nil {
return 0, gerror.Wrap(err, "获取公司部门失败,请稍后重试!")
}
if value.IsEmpty() {
return 0, gerror.New("请先配置公司部门")
}
return value.Int64(), nil
}
// View 获取用户信息
func (s *sAdminMember) View(ctx context.Context, in *adminin.MemberViewInp) (res *adminin.MemberViewModel, err error) {
if err = s.FilterAuthModel(ctx, contexts.GetUserId(ctx)).Hook(hook.MemberInfo).WherePri(in.Id).Scan(&res); err != nil {
@@ -912,9 +959,6 @@ func (s *sAdminMember) ClusterSyncSuperAdmin(ctx context.Context, message *gredi
// 非超管用户只能操作自己的下级角色用户,并且需要满足自身角色的数据权限设置
func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb.Model {
m := dao.AdminMember.Ctx(ctx)
if s.VerifySuperId(ctx, memberId) {
return m
}
var roleId int64
if contexts.GetUserId(ctx) == memberId {
@@ -929,6 +973,10 @@ func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb
roleId = ro.Int64()
}
if isAdminScopeSuper(ctx, memberId, roleId) {
return m
}
roleIds, err := service.AdminRole().GetSubRoleIds(ctx, roleId, false)
if err != nil {
g.Log().Panicf(ctx, "get the subordinate role permission exception, err:%+v", err)
+39 -4
View File
@@ -37,6 +37,35 @@ func init() {
service.RegisterAdminRole(NewAdminRole())
}
func isAdminScopeSuper(ctx context.Context, memberId, roleId int64) bool {
if service.AdminMember().VerifySuperId(ctx, memberId) {
return true
}
if memberId == contexts.GetUserId(ctx) && contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
return true
}
if roleId <= 0 {
return false
}
value, err := dao.AdminRole.Ctx(ctx).Fields(dao.AdminRole.Columns().Key).WherePri(roleId).Value()
if err != nil {
g.Log().Warningf(ctx, "query admin role key failed, roleId:%d err:%+v", roleId, err)
return false
}
return value.String() == consts.ImPlatformRoleKey
}
func getSuperRoleId(ctx context.Context) (int64, error) {
value, err := dao.AdminRole.Ctx(ctx).Fields(dao.AdminRole.Columns().Id).Where(dao.AdminRole.Columns().Key, consts.SuperRoleKey).Value()
if err != nil {
return 0, err
}
return value.Int64(), nil
}
// Verify 验证权限
func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
var (
@@ -64,13 +93,19 @@ func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
// List 获取列表
func (s *sAdminRole) List(ctx context.Context, in *adminin.RoleListInp) (res *adminin.RoleListModel, totalCount int, err error) {
var (
mod = dao.AdminRole.Ctx(ctx)
mod = dao.AdminRole.Ctx(ctx).WhereNot(dao.AdminRole.Columns().Key, consts.SuperRoleKey)
models []*entity.AdminRole
pid int64 = 0
)
// 非超管只获取下级角色
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
// 超管隐藏内置根角色,但从内置根角色的子级开始展示角色树。
if isAdminScopeSuper(ctx, contexts.GetUserId(ctx), contexts.GetRoleId(ctx)) {
pid, err = getSuperRoleId(ctx)
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
} else {
pid = contexts.GetRoleId(ctx)
mod = mod.WhereLike(dao.AdminRole.Columns().Tree, "%"+tree.GetIdLabel(pid)+"%")
}
@@ -326,7 +361,7 @@ func (s *sAdminRole) VerifyRoleId(ctx context.Context, id int64) (err error) {
return
}
ids, err := s.GetSubRoleIds(ctx, mb.RoleId, service.AdminMember().VerifySuperId(ctx, mb.Id))
ids, err := s.GetSubRoleIds(ctx, mb.RoleId, isAdminScopeSuper(ctx, mb.Id, mb.RoleId))
if err != nil {
err = gerror.New("验证角色信息失败!")
return
+17 -1
View File
@@ -30,6 +30,11 @@ func init() {
// Delete 删除
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
if count, err := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); err != nil {
return gerror.Wrap(err, consts.ErrorORM)
} else if count > 0 {
return gerror.New("内部密钥字典禁止通过通用接口操作")
}
_, err := dao.SysDictData.Ctx(ctx).Where("id", in.Id).Delete()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
@@ -42,6 +47,11 @@ func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp)
func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err error) {
// 修改
if in.Id > 0 {
if count, queryErr := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); queryErr != nil {
return gerror.Wrap(queryErr, consts.ErrorORM)
} else if count > 0 {
return gerror.New("内部密钥字典禁止通过通用接口操作")
}
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
@@ -60,6 +70,9 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
if in.Type == "" {
return gerror.Wrap(err, "类型选择无效,请检查")
}
if in.Type == consts.ImInternalSecretDictType {
return gerror.New("内部密钥字典禁止通过通用接口操作")
}
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataInsertFields{}).Data(in).OmitEmptyData().Insert()
if err != nil {
@@ -71,7 +84,7 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
// List 获取列表
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
mod := dao.SysDictData.Ctx(ctx)
mod := dao.SysDictData.Ctx(ctx).WhereNot(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType)
// 类型ID
if in.TypeID > 0 {
types, err := s.GetTypes(ctx, in.TypeID)
@@ -151,6 +164,9 @@ func (s *sSysDictData) GetTypes(ctx context.Context, id int64) (types []string,
// Select 获取列表
func (s *sSysDictData) Select(ctx context.Context, in *sysin.DataSelectInp) (list sysin.DataSelectModel, err error) {
if in.Type == consts.ImInternalSecretDictType {
return nil, gerror.New("内部密钥字典禁止通过通用接口读取")
}
// 优先从内置字典中获取
options, err := dict.GetOptions(ctx, in.Type)
if err == nil {
+17 -2
View File
@@ -31,7 +31,7 @@ func init() {
// Tree 树
func (s *sSysDictType) Tree(ctx context.Context) (list []*sysin.DictTypeTree, err error) {
var (
mod = dao.SysDictType.Ctx(ctx)
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
models []*entity.SysDictType
)
@@ -55,6 +55,9 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
err = gerror.New("数据不存在或已删除!")
return
}
if models.Type == consts.ImInternalSecretDictType {
return gerror.New("内部密钥字典禁止通过通用接口操作")
}
exist, err := dao.SysDictData.Ctx(ctx).Where("type", models.Type).One()
if err != nil {
@@ -83,6 +86,18 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
// Edit 修改/新增
func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err error) {
if in.Type == consts.ImInternalSecretDictType {
return gerror.New("内部密钥字典禁止通过通用接口操作")
}
if in.Id > 0 {
count, queryErr := dao.SysDictType.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType).Count()
if queryErr != nil {
return gerror.Wrap(queryErr, consts.ErrorORM)
}
if count > 0 {
return gerror.New("内部密钥字典禁止通过通用接口操作")
}
}
if err = hgorm.IsUnique(ctx, &dao.SysDictType, g.Map{dao.SysDictType.Columns().Name: in.Name}, "名称已存在", in.Id); err != nil {
return
}
@@ -105,7 +120,7 @@ func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err
// TreeSelect 获取类型关系树选项
func (s *sSysDictType) TreeSelect(ctx context.Context, in *sysin.DictTreeSelectInp) (list []*sysin.DictTypeTree, err error) {
var (
mod = dao.SysDictType.Ctx(ctx)
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
models []*entity.SysDictType
)
@@ -71,6 +71,9 @@ func isImPlatformAdmin(ctx context.Context, operatorId int64) bool {
if service.AdminMember().VerifySuperId(ctx, operatorId) {
return true
}
if contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
return true
}
return contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) <= 0
}
@@ -0,0 +1,298 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/immessagecrypto"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strings"
"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/gtime"
)
const (
imMessageEncryptionKeyTable = "im_message_encryption_keys"
imTenantMessageEncryptionColumn = "message_encryption_enabled"
)
type sSysImMessageEncryption struct{}
func NewSysImMessageEncryption() *sSysImMessageEncryption { return &sSysImMessageEncryption{} }
func init() {
service.RegisterSysImMessageEncryption(NewSysImMessageEncryption())
}
func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
if err = s.requireSuperAdmin(ctx); err != nil {
return nil, 0, err
}
db := imtenant.RegistryDB()
mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx).
LeftJoin(dao.ImTenantConfig.Table()+" c", "c.tenant_id=t.id").
Fields(
"t.id tenant_id", "t.code tenant_code", "t.name tenant_name",
"COALESCE(c."+imTenantMessageEncryptionColumn+",0) message_encryption_enabled",
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='active'),0) active_version",
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='pending'),0) pending_version",
"(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count",
"(SELECT MAX(COALESCE(k.activated_at,k.created_at)) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) last_rotated_at",
)
if in.TenantId > 0 {
mod = mod.Where("t.id", in.TenantId)
}
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
if in.Enabled != nil {
mod = mod.Where("COALESCE(c."+imTenantMessageEncryptionColumn+",0)=?", *in.Enabled)
}
if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败")
}
return
}
func (s *sSysImMessageEncryption) Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
if err := in.Filter(ctx); err != nil {
return err
}
if err := s.requireSuperAdmin(ctx); err != nil {
return err
}
tenant, err := s.tenantByID(ctx, in.TenantId)
if err != nil {
return err
}
dek, err := immessagecrypto.GenerateDEK()
if err != nil {
return err
}
ciphertext, nonce, err := immessagecrypto.WrapDEK(ctx, tenant.Id, 1, dek)
if err != nil {
return err
}
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
count, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id).Count()
if queryErr != nil {
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
}
if count > 0 {
return gerror.New("该商户已经初始化消息密钥")
}
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
"tenant_id": tenant.Id, "key_version": 1, "algorithm": consts.ImMessageKeyAlgorithm,
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusActive,
"created_at": gtime.Now(), "activated_at": gtime.Now(), "created_by": contexts.GetUserId(ctx),
"remark": strings.TrimSpace(in.Remark),
}).Insert()
if insertErr != nil {
return gerror.Wrap(insertErr, "初始化商户消息密钥失败")
}
return nil
})
if err != nil {
return err
}
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_initialize", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": 1})
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", 1)
}
func (s *sSysImMessageEncryption) Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
if err := in.Filter(ctx); err != nil {
return err
}
if err := s.requireSuperAdmin(ctx); err != nil {
return err
}
tenant, err := s.tenantByID(ctx, in.TenantId)
if err != nil {
return err
}
var version uint
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
pending, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusPending).Count()
if queryErr != nil {
return gerror.Wrap(queryErr, "检查待激活消息密钥失败")
}
if pending > 0 {
return gerror.New("该商户已有待激活密钥,请等待IM Server确认")
}
value, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Fields("COALESCE(MAX(key_version),0)").Where("tenant_id", tenant.Id).Value()
if queryErr != nil {
return gerror.Wrap(queryErr, "获取消息密钥版本失败")
}
if value.Uint() == 0 {
return gerror.New("请先初始化商户消息密钥")
}
version = value.Uint() + 1
dek, generateErr := immessagecrypto.GenerateDEK()
if generateErr != nil {
return generateErr
}
ciphertext, nonce, wrapErr := immessagecrypto.WrapDEK(ctx, tenant.Id, version, dek)
if wrapErr != nil {
return wrapErr
}
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
"tenant_id": tenant.Id, "key_version": version, "algorithm": consts.ImMessageKeyAlgorithm,
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusPending,
"created_at": gtime.Now(), "created_by": contexts.GetUserId(ctx), "remark": strings.TrimSpace(in.Remark),
}).Insert()
if insertErr != nil {
return gerror.Wrap(insertErr, "创建待激活消息密钥失败")
}
return nil
})
if err != nil {
return err
}
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_rotate", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": version, "status": consts.ImMessageKeyStatusPending})
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", version)
}
func (s *sSysImMessageEncryption) Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error {
if err := in.Filter(ctx); err != nil {
return err
}
if err := s.requireSuperAdmin(ctx); err != nil {
return err
}
tenant, err := s.tenantByID(ctx, in.TenantId)
if err != nil {
return err
}
if in.Enabled == 1 {
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).Count()
if queryErr != nil {
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
}
if count == 0 {
return gerror.New("启用消息加密前必须先初始化有效密钥")
}
}
result, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).
Data(g.Map{imTenantMessageEncryptionColumn: in.Enabled, dao.ImTenantConfig.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "更新商户消息加密开关失败")
}
if affected, _ := result.RowsAffected(); affected == 0 {
return gerror.New("商户配置不存在")
}
_ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", "im_tenant_config", tenant.Id, "success", g.Map{"enabled": in.Enabled})
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0)
}
func (s *sSysImMessageEncryption) GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error) {
tenant, err := s.tenantByCode(ctx, in.TenantCode)
if err != nil {
return nil, err
}
enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
Fields(imTenantMessageEncryptionColumn).Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Value()
if err != nil {
return nil, gerror.Wrap(err, "获取商户消息加密开关失败")
}
mod := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id)
if in.Version > 0 {
mod = mod.Where("key_version", in.Version).WhereIn("status", []string{consts.ImMessageKeyStatusPending, consts.ImMessageKeyStatusActive, consts.ImMessageKeyStatusRetired})
} else {
mod = mod.Where("status", consts.ImMessageKeyStatusActive).OrderDesc("key_version")
}
row, err := mod.One()
if err != nil {
return nil, gerror.Wrap(err, "获取商户消息密钥失败")
}
if row.IsEmpty() {
return nil, gerror.New("商户消息密钥不存在")
}
version := row["key_version"].Uint()
dek, err := immessagecrypto.UnwrapDEK(ctx, tenant.Id, version, row["encrypted_dek"].Bytes(), row["dek_nonce"].Bytes())
if err != nil {
return nil, err
}
return &sysin.ImMessageKeyModel{
TenantId: tenant.Id, TenantCode: tenant.Code, Enabled: enabledValue.Int() == 1,
Version: version, Algorithm: row["algorithm"].String(), Status: row["status"].String(), Key: immessagecrypto.EncodeDEK(dek),
}, nil
}
func (s *sSysImMessageEncryption) Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error {
tenant, err := s.tenantByCode(ctx, in.TenantCode)
if err != nil {
return err
}
if in.Version == 0 {
return gerror.New("消息密钥版本不能为空")
}
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
row, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("key_version", in.Version).One()
if queryErr != nil {
return gerror.Wrap(queryErr, "获取待激活消息密钥失败")
}
if row.IsEmpty() || row["status"].String() != consts.ImMessageKeyStatusPending {
return gerror.New("待激活消息密钥不存在")
}
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).
Data(g.Map{"status": consts.ImMessageKeyStatusRetired, "retired_at": gtime.Now()}).Update(); updateErr != nil {
return gerror.Wrap(updateErr, "退役旧消息密钥失败")
}
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Where("tenant_id", tenant.Id).Where("key_version", in.Version).
Data(g.Map{"status": consts.ImMessageKeyStatusActive, "activated_at": gtime.Now()}).Update(); updateErr != nil {
return gerror.Wrap(updateErr, "激活新消息密钥失败")
}
return nil
})
if err != nil {
return err
}
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.activated", "message_encryption", in.Version)
}
func (s *sSysImMessageEncryption) requireSuperAdmin(ctx context.Context) error {
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
return gerror.New("只有最高权限平台管理员可以管理消息密钥")
}
return nil
}
func (s *sSysImMessageEncryption) tenantByID(ctx context.Context, tenantID int64) (*entity.ImTenant, error) {
var tenant *entity.ImTenant
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantID).Scan(&tenant); err != nil {
return nil, gerror.Wrap(err, "获取商户失败")
}
if tenant == nil || tenant.Id == 0 {
return nil, gerror.New("商户不存在")
}
return tenant, nil
}
func (s *sSysImMessageEncryption) tenantByCode(ctx context.Context, code string) (*entity.ImTenant, error) {
var tenant *entity.ImTenant
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
Where(dao.ImTenant.Columns().Code, strings.TrimSpace(code)).Scan(&tenant); err != nil {
return nil, gerror.Wrap(err, "获取商户失败")
}
if tenant == nil || tenant.Id == 0 {
return nil, gerror.New("商户不存在")
}
return tenant, nil
}
+46 -19
View File
@@ -26,18 +26,22 @@ import (
type sSysImTenant struct{}
const (
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
tenantConfigMessageEncryptionEnabled = "message_encryption_enabled"
tenantConfigRealnameReviewPolicy = "realname_review_policy"
)
type imTenantConfigModeration struct {
Id int64 `json:"id"`
TenantId int64 `json:"tenant_id"`
MaxUsers uint64 `json:"max_users"`
MaxGroups uint64 `json:"max_groups"`
MaxGroupMembers uint `json:"max_group_members"`
AliyunModerationEnabled int `json:"aliyun_moderation_enabled"`
LocalModerationEnabled int `json:"local_moderation_enabled"`
Id int64 `json:"id" orm:"id"`
TenantId int64 `json:"tenant_id" orm:"tenant_id"`
MaxUsers uint64 `json:"max_users" orm:"max_users"`
MaxGroups uint64 `json:"max_groups" orm:"max_groups"`
MaxGroupMembers uint `json:"max_group_members" orm:"max_group_members"`
AliyunModerationEnabled int `json:"aliyun_moderation_enabled" orm:"aliyun_moderation_enabled"`
LocalModerationEnabled int `json:"local_moderation_enabled" orm:"local_moderation_enabled"`
MessageEncryptionEnabled int `json:"message_encryption_enabled" orm:"message_encryption_enabled"`
RealnameReviewPolicy int `json:"realname_review_policy" orm:"realname_review_policy"`
}
func NewSysImTenant() *sSysImTenant {
@@ -157,6 +161,8 @@ func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenan
item.MaxGroupMembers = conf.MaxGroupMembers
item.AliyunModerationEnabled = conf.AliyunModerationEnabled
item.LocalModerationEnabled = conf.LocalModerationEnabled
item.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
item.RealnameReviewPolicy = conf.RealnameReviewPolicy
}
item.DbDsnConfigured = dsnMap[item.Id] != ""
}
@@ -210,7 +216,7 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
WherePri(in.Id).Data(in).Update(); err != nil {
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
}
return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, in.AliyunModerationEnabled, in.LocalModerationEnabled, userId)
return s.saveConfig(ctx, in, userId)
}
in.CreatedBy = userId
@@ -222,7 +228,8 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
}
tenantID = id
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, in.AliyunModerationEnabled, in.LocalModerationEnabled, userId)
in.Id = id
return s.saveConfig(ctx, in, userId)
})
if err != nil {
return err
@@ -246,20 +253,34 @@ func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code strin
return
}
func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, aliyunModerationEnabled, localModerationEnabled int, userId int64) (err error) {
func (s *sSysImTenant) saveConfig(ctx context.Context, in *sysin.ImTenantEditInp, userId int64) (err error) {
if in.MessageEncryptionEnabled == 1 {
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
Where("tenant_id", in.Id).
Where("status", consts.ImMessageKeyStatusActive).
Count()
if queryErr != nil {
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
}
if count == 0 {
return gerror.New("启用消息加密前必须先在消息密钥管理中初始化有效密钥")
}
}
var conf *entity.ImTenantConfig
if err = s.configModel(ctx).
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
Scan(&conf); err != nil {
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
}
data := g.Map{
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
tenantConfigAliyunModerationEnabled: aliyunModerationEnabled,
tenantConfigLocalModerationEnabled: localModerationEnabled,
dao.ImTenantConfig.Columns().MaxUsers: in.MaxUsers,
dao.ImTenantConfig.Columns().MaxGroups: in.MaxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers: in.MaxGroupMembers,
tenantConfigAliyunModerationEnabled: in.AliyunModerationEnabled,
tenantConfigLocalModerationEnabled: in.LocalModerationEnabled,
tenantConfigMessageEncryptionEnabled: in.MessageEncryptionEnabled,
tenantConfigRealnameReviewPolicy: in.RealnameReviewPolicy,
}
if conf != nil && conf.Id > 0 {
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
@@ -270,6 +291,8 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
dao.ImTenantConfig.Columns().MaxGroupMembers,
tenantConfigAliyunModerationEnabled,
tenantConfigLocalModerationEnabled,
tenantConfigMessageEncryptionEnabled,
tenantConfigRealnameReviewPolicy,
dao.ImTenantConfig.Columns().UpdatedBy,
).
WherePri(conf.Id).Data(data).Update(); err != nil {
@@ -278,7 +301,7 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
return
}
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
data[dao.ImTenantConfig.Columns().TenantId] = in.Id
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
if _, err = s.configModel(ctx).
@@ -289,6 +312,8 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
dao.ImTenantConfig.Columns().MaxGroupMembers,
tenantConfigAliyunModerationEnabled,
tenantConfigLocalModerationEnabled,
tenantConfigMessageEncryptionEnabled,
tenantConfigRealnameReviewPolicy,
dao.ImTenantConfig.Columns().CreatedBy,
dao.ImTenantConfig.Columns().UpdatedBy,
).
@@ -324,6 +349,8 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
res.MaxGroupMembers = conf.MaxGroupMembers
res.AliyunModerationEnabled = conf.AliyunModerationEnabled
res.LocalModerationEnabled = conf.LocalModerationEnabled
res.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
res.RealnameReviewPolicy = conf.RealnameReviewPolicy
}
res.DbDsnConfigured = res.DbDsn != ""
return
@@ -0,0 +1,78 @@
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImMessageEncryptionListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"商户ID"`
Keyword string `json:"keyword" dc:"商户编码或名称"`
Enabled *int `json:"enabled" dc:"消息加密开关"`
}
func (in *ImMessageEncryptionListInp) Filter(ctx context.Context) error {
return nil
}
type ImMessageEncryptionListModel struct {
TenantId int64 `json:"tenantId" dc:"商户ID"`
TenantCode string `json:"tenantCode" dc:"商户编码"`
TenantName string `json:"tenantName" dc:"商户名称"`
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
ActiveVersion uint `json:"activeVersion" dc:"当前密钥版本"`
PendingVersion uint `json:"pendingVersion" dc:"待激活密钥版本"`
KeyCount int `json:"keyCount" dc:"密钥版本数"`
LastRotatedAt *gtime.Time `json:"lastRotatedAt" dc:"最后轮换时间"`
}
type ImMessageEncryptionActionInp struct {
TenantId int64 `json:"tenantId" v:"required|min:1#请选择商户|商户不正确" dc:"商户ID"`
Remark string `json:"remark" dc:"操作备注"`
}
func (in *ImMessageEncryptionActionInp) Filter(ctx context.Context) error {
if in.TenantId <= 0 {
return gerror.New("请选择商户")
}
return nil
}
type ImMessageEncryptionSwitchInp struct {
TenantId int64 `json:"tenantId" v:"required|min:1#请选择商户|商户不正确" dc:"商户ID"`
Enabled int `json:"enabled" dc:"是否启用"`
}
func (in *ImMessageEncryptionSwitchInp) Filter(ctx context.Context) error {
if in.TenantId <= 0 {
return gerror.New("请选择商户")
}
if in.Enabled != 0 && in.Enabled != 1 {
return gerror.New("消息加密开关值不正确")
}
return nil
}
type ImMessageKeyGetInp struct {
TenantCode string `json:"tenantCode" dc:"商户编码"`
Version uint `json:"version" dc:"密钥版本,0表示当前版本"`
}
type ImMessageKeyModel struct {
TenantId int64 `json:"tenantId" dc:"商户ID"`
TenantCode string `json:"tenantCode" dc:"商户编码"`
Enabled bool `json:"enabled" dc:"消息加密是否启用"`
Version uint `json:"version" dc:"密钥版本"`
Algorithm string `json:"algorithm" dc:"算法"`
Status string `json:"status" dc:"密钥状态"`
Key string `json:"key" dc:"Base64编码DEK,仅内部接口返回"`
}
type ImMessageKeyAckInp struct {
TenantCode string `json:"tenantCode" dc:"商户编码"`
Version uint `json:"version" dc:"密钥版本"`
}
+62 -48
View File
@@ -43,33 +43,39 @@ type ImTenantInsertFields struct {
// ImTenantConfigUpdateFields 修改IM租户配置字段过滤
type ImTenantConfigUpdateFields struct {
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
}
// ImTenantConfigInsertFields 新增IM租户配置字段过滤
type ImTenantConfigInsertFields struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
TenantId int64 `json:"tenantId" dc:"租户ID"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
}
// ImTenantEditInp 修改/新增IM租户
type ImTenantEditInp struct {
entity.ImTenant
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
}
func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) {
@@ -86,8 +92,12 @@ func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) {
return gerror.New("租户状态不正确")
}
if (in.AliyunModerationEnabled != 0 && in.AliyunModerationEnabled != 1) ||
(in.LocalModerationEnabled != 0 && in.LocalModerationEnabled != 1) {
return gerror.New("内容审核开关值不正确")
(in.LocalModerationEnabled != 0 && in.LocalModerationEnabled != 1) ||
(in.MessageEncryptionEnabled != 0 && in.MessageEncryptionEnabled != 1) {
return gerror.New("商户功能开关值不正确")
}
if in.RealnameReviewPolicy != 0 && in.RealnameReviewPolicy != 1 {
return gerror.New("实名审核策略不正确")
}
return
}
@@ -105,16 +115,18 @@ func (in *ImTenantViewInp) Filter(ctx context.Context) (err error) {
type ImTenantViewModel struct {
entity.ImTenant
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
}
// ImTenantListInp 获取IM租户列表
@@ -133,24 +145,26 @@ func (in *ImTenantListInp) Filter(ctx context.Context) (err error) {
}
type ImTenantListModel struct {
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
DbName string `json:"dbName" dc:"业务库名"`
State int `json:"state" dc:"状态"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
DbName string `json:"dbName" dc:"业务库名"`
State int `json:"state" dc:"状态"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
// ImTenantStatusInp 更新IM租户状态
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImMessageEncryption)
}
+74 -54
View File
@@ -211,6 +211,14 @@ type (
// Restore 恢复IM租户
Restore(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error)
}
ISysImMessageEncryption interface {
List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error)
Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error
Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error
Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error
GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error)
Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error
}
ISysImRegistration interface {
// GetConfig 获取IM注册配置
GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error)
@@ -781,60 +789,61 @@ type (
)
var (
localSysAddons ISysAddons
localSysAddonsConfig ISysAddonsConfig
localSysAttachment ISysAttachment
localSysBlacklist ISysBlacklist
localSysConfig ISysConfig
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysCultivateFlower ISysCultivateFlower
localSysCultivateContent ISysCultivateContent
localSysImTenant ISysImTenant
localSysImRegistration ISysImRegistration
localSysImInvite ISysImInvite
localSysImUser ISysImUser
localSysImGroup ISysImGroup
localSysImRealname ISysImRealname
localSysImWorkOrder ISysImWorkOrder
localSysImOutbox ISysImOutbox
localSysImOrg ISysImOrg
localSysImFile ISysImFile
localSysImFilePolicy ISysImFilePolicy
localSysImUserPolicyGroup ISysImUserPolicyGroup
localSysImSensitiveWord ISysImSensitiveWord
localSysImSecurityPolicy ISysImSecurityPolicy
localSysImDashboard ISysImDashboard
localSysImPrivateMessage ISysImPrivateMessage
localSysImGroupMessage ISysImGroupMessage
localSysImUserLoginHistory ISysImUserLoginHistory
localSysImLoginFailureLog ISysImLoginFailureLog
localSysImExternalConfig ISysImExternalConfig
localSysImNotification ISysImNotification
localSysImAppRelease ISysImAppRelease
localSysImAbuseReason ISysImAbuseReason
localSysImMailboxConfig ISysImMailboxConfig
localSysImFeedbackConfig ISysImFeedbackConfig
localSysImWorkbench ISysImWorkbench
localSysImTaskDashboard ISysImTaskDashboard
localSysImAuditLog ISysImAuditLog
localSysImKafkaAudit ISysImKafkaAudit
localSysImKafkaCommand ISysImKafkaCommand
localSysImDeadLetter ISysImDeadLetter
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
localSysGenCodes ISysGenCodes
localSysLog ISysLog
localSysLoginLog ISysLoginLog
localSysNormalTreeDemo ISysNormalTreeDemo
localSysOptionTreeDemo ISysOptionTreeDemo
localSysProvinces ISysProvinces
localSysServeLicense ISysServeLicense
localSysServeLog ISysServeLog
localSysSmsLog ISysSmsLog
localSysTestCategory ISysTestCategory
localSysAddons ISysAddons
localSysAddonsConfig ISysAddonsConfig
localSysAttachment ISysAttachment
localSysBlacklist ISysBlacklist
localSysConfig ISysConfig
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysCultivateFlower ISysCultivateFlower
localSysCultivateContent ISysCultivateContent
localSysImTenant ISysImTenant
localSysImMessageEncryption ISysImMessageEncryption
localSysImRegistration ISysImRegistration
localSysImInvite ISysImInvite
localSysImUser ISysImUser
localSysImGroup ISysImGroup
localSysImRealname ISysImRealname
localSysImWorkOrder ISysImWorkOrder
localSysImOutbox ISysImOutbox
localSysImOrg ISysImOrg
localSysImFile ISysImFile
localSysImFilePolicy ISysImFilePolicy
localSysImUserPolicyGroup ISysImUserPolicyGroup
localSysImSensitiveWord ISysImSensitiveWord
localSysImSecurityPolicy ISysImSecurityPolicy
localSysImDashboard ISysImDashboard
localSysImPrivateMessage ISysImPrivateMessage
localSysImGroupMessage ISysImGroupMessage
localSysImUserLoginHistory ISysImUserLoginHistory
localSysImLoginFailureLog ISysImLoginFailureLog
localSysImExternalConfig ISysImExternalConfig
localSysImNotification ISysImNotification
localSysImAppRelease ISysImAppRelease
localSysImAbuseReason ISysImAbuseReason
localSysImMailboxConfig ISysImMailboxConfig
localSysImFeedbackConfig ISysImFeedbackConfig
localSysImWorkbench ISysImWorkbench
localSysImTaskDashboard ISysImTaskDashboard
localSysImAuditLog ISysImAuditLog
localSysImKafkaAudit ISysImKafkaAudit
localSysImKafkaCommand ISysImKafkaCommand
localSysImDeadLetter ISysImDeadLetter
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
localSysGenCodes ISysGenCodes
localSysLog ISysLog
localSysLoginLog ISysLoginLog
localSysNormalTreeDemo ISysNormalTreeDemo
localSysOptionTreeDemo ISysOptionTreeDemo
localSysProvinces ISysProvinces
localSysServeLicense ISysServeLicense
localSysServeLog ISysServeLog
localSysSmsLog ISysSmsLog
localSysTestCategory ISysTestCategory
)
func SysAddons() ISysAddons {
@@ -956,6 +965,17 @@ func RegisterSysImTenant(i ISysImTenant) {
localSysImTenant = i
}
func SysImMessageEncryption() ISysImMessageEncryption {
if localSysImMessageEncryption == nil {
panic("implement not found for interface ISysImMessageEncryption, forgot register?")
}
return localSysImMessageEncryption
}
func RegisterSysImMessageEncryption(i ISysImMessageEncryption) {
localSysImMessageEncryption = i
}
func SysImRegistration() ISysImRegistration {
if localSysImRegistration == nil {
panic("implement not found for interface ISysImRegistration, forgot register?")
@@ -18,7 +18,7 @@ SELECT @imRootId, '日志审计', 'imLogAudit', 'audit', 'AuditOutlined', 1, '/i
WHERE @imRootId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imLogAudit');
INSERT INTO `hg_admin_menu` (`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 @imRootId, '系统设置', 'imSystemSettings', 'settings', 'SettingOutlined', 1, '/im/settings/tenant/index', '', '', 'ParentLayout', 1, '', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @imRootId, ' '), 40, '', 1, @now, @now
SELECT @imRootId, '平台设置', 'imSystemSettings', 'settings', 'SettingOutlined', 1, '/im/settings/tenant/index', '', '', 'ParentLayout', 1, '', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @imRootId, ' '), 40, '', 1, @now, @now
WHERE @imRootId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings');
SET @peopleId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imUserAndGroup' LIMIT 1);
@@ -29,7 +29,7 @@ SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSet
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='用户与群组', `path`='people', `icon`='TeamOutlined', `type`=1, `redirect`='/im/people/user/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=10, `updated_at`=@now WHERE `id`=@peopleId;
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='内容审核', `path`='content', `icon`='FileSearchOutlined', `type`=1, `redirect`='/im/content/private-message/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=20, `updated_at`=@now WHERE `id`=@contentId;
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='日志审计', `path`='audit', `icon`='AuditOutlined', `type`=1, `redirect`='/im/audit/operation/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=30, `updated_at`=@now WHERE `id`=@auditId;
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='系统设置', `path`='settings', `icon`='SettingOutlined', `type`=1, `redirect`='/im/settings/tenant/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=40, `updated_at`=@now WHERE `id`=@settingsId;
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `title`='平台设置', `path`='settings', `icon`='SettingOutlined', `type`=1, `redirect`='/im/settings/tenant/index', `component`='ParentLayout', `always_show`=1, `hidden`=0, `status`=1, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=40, `updated_at`=@now WHERE `id`=@settingsId;
UPDATE `hg_admin_menu` SET `pid`=@imRootId, `icon`='DashboardOutlined', `type`=2, `level`=2, `tree`=CONCAT('tr_', @imRootId, ' '), `sort`=5, `updated_at`=@now WHERE `name`='imDashboardIndex';
UPDATE `hg_admin_menu` SET `redirect`='/im/dashboard/index', `updated_at`=@now WHERE `id`=@imRootId;
@@ -0,0 +1,137 @@
-- 消息加密密钥管理。
-- 可重复执行。根 KEK 只在首次执行时生成,重复执行不会覆盖已有值。
SET NAMES utf8mb4;
SET @now := NOW();
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
AND COLUMN_NAME = 'message_encryption_enabled') = 0,
'ALTER TABLE `im_tenant_config` ADD COLUMN `message_encryption_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用消息加密'' AFTER `local_moderation_enabled`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
CREATE TABLE IF NOT EXISTS `im_message_encryption_keys` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL COMMENT '商户ID',
`key_version` int unsigned NOT NULL COMMENT '密钥版本',
`algorithm` varchar(32) NOT NULL DEFAULT 'AES-256-GCM' COMMENT '数据加密算法',
`encrypted_dek` blob NOT NULL COMMENT '由根KEK包裹后的商户DEK',
`dek_nonce` varbinary(12) NOT NULL COMMENT '包裹DEK使用的随机nonce',
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/active/retired/disabled',
`activated_at` datetime DEFAULT NULL,
`retired_at` datetime DEFAULT NULL,
`remark` varchar(255) NOT NULL DEFAULT '',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_by` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_message_key_tenant_version` (`tenant_id`, `key_version`),
KEY `idx_im_message_key_tenant_status` (`tenant_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商户消息加密密钥版本';
INSERT INTO `hg_sys_dict_type`
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 0, 'IM内部安全材料', 'im_internal_secret', 9999,
'仅供服务端读取,通用字典接口禁止查询和维护', 1, @now, @now
WHERE NOT EXISTS (
SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_internal_secret'
);
INSERT INTO `hg_sys_dict_data`
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 'core_material_01', TO_BASE64(RANDOM_BYTES(32)), 'string', 'im_internal_secret',
'', 2, 1, '消息DEK包裹根密钥KEK,请勿通过后台展示或修改', 1, @now, @now
WHERE NOT EXISTS (
SELECT 1 FROM `hg_sys_dict_data`
WHERE `type` = 'im_internal_secret' AND `label` = 'core_material_01'
);
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings' LIMIT 1);
INSERT INTO `hg_admin_menu`
(`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 @settingsId, '消息密钥管理', 'imMessageEncryptionIndex', 'message-encryption/index',
'KeyOutlined', 2, '', '/imMessageEncryption/list', '', '/imMessageEncryption/index',
1, 'imManager', 0, 0, '', 0, 0, 0, 3,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), 80,
'商户消息加密开关及密钥版本管理', 1, @now, @now
WHERE @settingsId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex');
SET @keyMenuId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex' LIMIT 1);
UPDATE `hg_admin_menu`
SET `pid` = @settingsId,
`title` = '消息密钥管理',
`path` = 'message-encryption/index',
`icon` = 'KeyOutlined',
`permissions` = '/imMessageEncryption/list',
`component` = '/imMessageEncryption/index',
`active_menu` = 'imManager',
`level` = 3,
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '),
`sort` = 80,
`hidden` = 0,
`status` = 1,
`updated_at` = @now
WHERE `id` = @keyMenuId;
INSERT INTO `hg_admin_menu`
(`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 @keyMenuId, '初始化密钥', 'imMessageEncryptionInitialize', '', '', 3, '',
'/imMessageEncryption/initialize', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 10, '', 1, @now, @now
WHERE @keyMenuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionInitialize');
INSERT INTO `hg_admin_menu`
(`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 @keyMenuId, '轮换密钥', 'imMessageEncryptionRotate', '', '', 3, '',
'/imMessageEncryption/rotate', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 20, '', 1, @now, @now
WHERE @keyMenuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionRotate');
INSERT INTO `hg_admin_menu`
(`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 @keyMenuId, '切换消息加密', 'imMessageEncryptionSwitch', '', '', 3, '',
'/imMessageEncryption/switch', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 30, '', 1, @now, @now
WHERE @keyMenuId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionSwitch');
UPDATE `hg_admin_menu`
SET `level` = 4,
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '),
`updated_at` = @now
WHERE `pid` = @keyMenuId AND `type` = 3;
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `hg_admin_menu`
WHERE `name` IN (
'imManager',
'imSystemSettings',
'imMessageEncryptionIndex',
'imMessageEncryptionInitialize',
'imMessageEncryptionRotate',
'imMessageEncryptionSwitch'
);
+46
View File
@@ -6,10 +6,56 @@
package encrypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/forgoer/openssl"
"io"
)
const AESGCMKeySize = 32
// AESGCMEncrypt encrypts plaintext with AES-256-GCM and a fresh random nonce.
func AESGCMEncrypt(plaintext, key, additionalData []byte) (ciphertext, nonce []byte, err error) {
if len(key) != AESGCMKeySize {
return nil, nil, fmt.Errorf("AES-256-GCM key must be %d bytes", AESGCMKeySize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
nonce = make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, nil, err
}
return gcm.Seal(nil, nonce, plaintext, additionalData), nonce, nil
}
// AESGCMDecrypt authenticates and decrypts ciphertext encrypted by AESGCMEncrypt.
func AESGCMDecrypt(ciphertext, nonce, key, additionalData []byte) ([]byte, error) {
if len(key) != AESGCMKeySize {
return nil, fmt.Errorf("AES-256-GCM key must be %d bytes", AESGCMKeySize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(nonce) != gcm.NonceSize() {
return nil, fmt.Errorf("AES-GCM nonce must be %d bytes", gcm.NonceSize())
}
return gcm.Open(nil, nonce, ciphertext, additionalData)
}
// AesECBEncrypt 加密
func AesECBEncrypt(src, key []byte) (dst []byte, err error) {
return openssl.AesECBEncrypt(src, key, openssl.PKCS7_PADDING)
+34
View File
@@ -0,0 +1,34 @@
package encrypt
import (
"bytes"
"testing"
)
func TestAESGCMRoundTripAndRandomNonce(t *testing.T) {
key := bytes.Repeat([]byte{7}, AESGCMKeySize)
plain := []byte("message encryption key")
aad := []byte("tenant:1:version:1")
first, firstNonce, err := AESGCMEncrypt(plain, key, aad)
if err != nil {
t.Fatal(err)
}
second, secondNonce, err := AESGCMEncrypt(plain, key, aad)
if err != nil {
t.Fatal(err)
}
if bytes.Equal(firstNonce, secondNonce) || bytes.Equal(first, second) {
t.Fatal("repeated encryption must use a different nonce and ciphertext")
}
decoded, err := AESGCMDecrypt(first, firstNonce, key, aad)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(decoded, plain) {
t.Fatalf("unexpected plaintext: %q", decoded)
}
first[0] ^= 1
if _, err = AESGCMDecrypt(first, firstNonce, key, aad); err == nil {
t.Fatal("tampered ciphertext must fail authentication")
}
}
+17
View File
@@ -0,0 +1,17 @@
import { http } from '@/utils/http/axios';
export function List(params) {
return http.request({ url: '/imMessageEncryption/list', method: 'GET', params });
}
export function Initialize(params) {
return http.request({ url: '/imMessageEncryption/initialize', method: 'POST', params });
}
export function Rotate(params) {
return http.request({ url: '/imMessageEncryption/rotate', method: 'POST', params });
}
export function Switch(params) {
return http.request({ url: '/imMessageEncryption/switch', method: 'POST', params });
}
+137
View File
@@ -0,0 +1,137 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="消息密钥管理" />
</div>
<n-alert type="warning" :bordered="false" class="mb-3">
密钥明文不会在管理后台展示。启用消息加密前必须先初始化密钥;轮换密钥将在 IM Server
确认加载后自动激活。
</n-alert>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
/>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.tenantId"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicForm, useForm } from '@/components/Form/index';
import { BasicTable, TableAction } from '@/components/Table';
import { List, Initialize, Rotate, Switch } from '@/api/imMessageEncryption';
import { columns, schemas, State } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const searchFormRef = ref<any>({});
const actionColumn = reactive({
width: 240,
title: '操作',
key: 'action',
fixed: 'right',
render(record: State) {
const actions: Record<string, any>[] = [];
if (record.keyCount === 0) {
actions.push({
label: '初始化',
type: 'primary',
auth: ['/imMessageEncryption/initialize'],
onClick: () => handleInitialize(record),
});
} else {
actions.push({
label: record.messageEncryptionEnabled ? '停用加密' : '启用加密',
type: record.messageEncryptionEnabled ? 'warning' : 'primary',
auth: ['/imMessageEncryption/switch'],
onClick: () => handleSwitch(record),
});
if (record.pendingVersion === 0) {
actions.push({
label: '轮换密钥',
auth: ['/imMessageEncryption/rotate'],
onClick: () => handleRotate(record),
});
}
}
return h(TableAction as any, { style: 'button', actions });
},
});
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
const [register] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 90,
schemas,
});
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
function reloadTable() {
actionRef.value?.reload();
}
function handleInitialize(record: State) {
dialog.warning({
title: '初始化消息密钥',
content: `将为商户“${record.tenantName}”生成首个 AES-256-GCM 数据密钥。密钥不会在页面展示,是否继续?`,
positiveText: '确认初始化',
negativeText: '取消',
onPositiveClick: async () => {
await Initialize({ tenantId: record.tenantId });
message.success('消息密钥初始化成功');
reloadTable();
},
});
}
function handleSwitch(record: State) {
const enabled = record.messageEncryptionEnabled ? 0 : 1;
dialog.warning({
title: enabled ? '启用消息加密' : '停用消息加密',
content: enabled
? `启用后,商户“${record.tenantName}”的新消息将由 IM Server 加密存储。是否继续?`
: `停用后仅影响新消息,历史密文仍按原密钥解密。是否继续?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await Switch({ tenantId: record.tenantId, enabled });
message.success(enabled ? '消息加密已启用' : '消息加密已停用');
reloadTable();
},
});
}
function handleRotate(record: State) {
dialog.warning({
title: '轮换消息密钥',
content: `将为商户“${record.tenantName}”生成新版本密钥。IM Server 确认加载前,当前密钥仍保持有效。是否继续?`,
positiveText: '确认轮换',
negativeText: '取消',
onPositiveClick: async () => {
await Rotate({ tenantId: record.tenantId });
message.success('待激活密钥已生成');
reloadTable();
},
});
}
</script>
@@ -0,0 +1,87 @@
import { h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { FormSchema } from '@/components/Form';
export class State {
public tenantId = 0;
public tenantCode = '';
public tenantName = '';
public messageEncryptionEnabled = 0;
public activeVersion = 0;
public pendingVersion = 0;
public keyCount = 0;
public lastRotatedAt = '';
}
export const schemas = ref<FormSchema[]>([
{
field: 'keyword',
component: 'NInput',
label: '商户',
componentProps: {
clearable: true,
placeholder: '商户编码/名称',
},
},
{
field: 'enabled',
component: 'NSelect',
label: '加密状态',
componentProps: {
clearable: true,
options: [
{ label: '已启用', value: 1 },
{ label: '未启用', value: 0 },
],
placeholder: '全部',
},
},
]);
export const columns = [
{ title: '商户编码', key: 'tenantCode', align: 'left', width: 150 },
{ title: '商户名称', key: 'tenantName', align: 'left', width: 180 },
{
title: '消息加密',
key: 'messageEncryptionEnabled',
align: 'left',
width: 110,
render(row: State) {
return h(
NTag,
{ type: row.messageEncryptionEnabled ? 'success' : 'default', bordered: false },
{ default: () => (row.messageEncryptionEnabled ? '已启用' : '未启用') }
);
},
},
{
title: '当前版本',
key: 'activeVersion',
align: 'left',
width: 110,
render(row: State) {
return row.activeVersion > 0 ? `v${row.activeVersion}` : '--';
},
},
{
title: '待激活版本',
key: 'pendingVersion',
align: 'left',
width: 120,
render(row: State) {
return row.pendingVersion > 0
? h(NTag, { type: 'warning', bordered: false }, { default: () => `v${row.pendingVersion}` })
: '--';
},
},
{ title: '版本数', key: 'keyCount', align: 'left', width: 90 },
{
title: '最后轮换时间',
key: 'lastRotatedAt',
align: 'left',
width: 180,
render(row: State) {
return row.lastRotatedAt || '--';
},
},
];
+22
View File
@@ -100,6 +100,24 @@
/>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="消息加密存储" path="messageEncryptionEnabled">
<n-switch
v-model:value="formValue.messageEncryptionEnabled"
:checked-value="1"
:unchecked-value="0"
/>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="实名审核方式" path="realnameReviewPolicy">
<n-select
v-model:value="formValue.realnameReviewPolicy"
:options="realnameReviewPolicyOptions"
placeholder="请选择实名审核方式"
/>
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="商户描述" path="tenantDesc">
<n-input
@@ -145,6 +163,10 @@
return adaModalWidth(840);
});
const stateOptions = dict.getOption('ImTenantStateOptions');
const realnameReviewPolicyOptions = [
{ label: '不自动审核', value: 0 },
{ label: '自动审核,失败后转人工', value: 1 },
];
// 提交表单
function confirmForm(e) {
+28
View File
@@ -21,6 +21,8 @@ export class State {
public maxGroupMembers = 0; // 最大群成员数
public aliyunModerationEnabled = 0; // 是否启用阿里云内容审核
public localModerationEnabled = 0; // 是否启用本地内容审核
public messageEncryptionEnabled = 0; // 是否启用消息加密
public realnameReviewPolicy = 0; // 实名审核策略
public dbDsnConfigured = false; // 是否配置独立连接串
public createdBy = 0; // 创建者
public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
@@ -236,6 +238,32 @@ export const columns = [
);
},
},
{
title: '消息加密',
key: 'messageEncryptionEnabled',
align: 'left',
width: 120,
render(row: State) {
return h(
NTag,
{ type: row.messageEncryptionEnabled === 1 ? 'success' : 'default', bordered: false },
{ default: () => (row.messageEncryptionEnabled === 1 ? '已启用' : '未启用') }
);
},
},
{
title: '实名审核',
key: 'realnameReviewPolicy',
align: 'left',
width: 150,
render(row: State) {
return h(
NTag,
{ type: row.realnameReviewPolicy === 1 ? 'success' : 'default', bordered: false },
{ default: () => (row.realnameReviewPolicy === 1 ? '自动审核' : '人工审核') }
);
},
},
{
title: '创建者',
key: 'createdBy',