From 9ed8aaf1428c47c15098071a8b64855d9d60af45 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Thu, 17 Sep 2026 00:25:04 +0800 Subject: [PATCH] refactor: reuse global aliyun moderation config --- .../sql/阿里云文本审核复用全局配置.sql | 33 ++++++++++ .../服务端消息加密与内容审核改造文档.md | 17 +++-- .../internalapi/imconfig/im_config.go | 30 +++++---- .../internal/library/immoderation/config.go | 63 +++++++++++++++++++ .../internal/logic/sys/im_external_config.go | 31 ++++----- web/src/views/imExternalConfig/edit.vue | 28 +-------- 6 files changed, 147 insertions(+), 55 deletions(-) create mode 100644 docs/admin-adfahi-research/sql/阿里云文本审核复用全局配置.sql create mode 100644 server/internal/library/immoderation/config.go diff --git a/docs/admin-adfahi-research/sql/阿里云文本审核复用全局配置.sql b/docs/admin-adfahi-research/sql/阿里云文本审核复用全局配置.sql new file mode 100644 index 0000000..b58da1f --- /dev/null +++ b/docs/admin-adfahi-research/sql/阿里云文本审核复用全局配置.sql @@ -0,0 +1,33 @@ +-- 阿里云文本审核统一复用 hg_sys_config.im_third_party 中的公共凭证、地域和端点。 +-- 可重复执行;清理 im_external_config 中历史重复保存的密钥和连接参数。 + +INSERT INTO `im_external_config` + (`tenant_id`, `service`, `provider`, `endpoint`, `enabled`, `status`, `config_json`, `secret_json`, `created_by`, `updated_by`) +SELECT + t.`id`, + 'moderation', + 'aliyun_green', + '', + 1, + 'unconfigured', + JSON_OBJECT( + 'nickname_service', 'nickname_detection_pro', + 'chat_service', 'chat_detection_pro', + 'timeout_ms', 1500, + 'failure_mode', 'allow' + ), + JSON_OBJECT(), + 0, + 0 +FROM `im_tenant` t +WHERE t.`code` = 'default' +ON DUPLICATE KEY UPDATE + `provider` = VALUES(`provider`), + `endpoint` = VALUES(`endpoint`), + `enabled` = VALUES(`enabled`), + `status` = VALUES(`status`), + `config_json` = VALUES(`config_json`), + `secret_json` = VALUES(`secret_json`), + `checked_at` = NULL, + `check_result` = '', + `updated_at` = CURRENT_TIMESTAMP; diff --git a/docs/admin-adfahi-research/服务端消息加密与内容审核改造文档.md b/docs/admin-adfahi-research/服务端消息加密与内容审核改造文档.md index 6c261d6..f723c1c 100644 --- a/docs/admin-adfahi-research/服务端消息加密与内容审核改造文档.md +++ b/docs/admin-adfahi-research/服务端消息加密与内容审核改造文档.md @@ -112,20 +112,27 @@ IM Server #### 6.1 阿里云服务配置 -复用 `im_external_config` 的 `service=moderation`,只允许平台管理员维护: +复用 `im_external_config` 的 `service=moderation`,只允许平台管理员维护审核策略参数。阿里云账号与连接参数统一复用 `hg_sys_config` 的 `im_third_party` 分组,不在商户审核配置中重复保存: | 配置 | 说明 | | --- | --- | | `provider` | 固定 `aliyun_green` | -| `endpoint` | 默认 `green-cip.cn-shanghai.aliyuncs.com` | | `enabled` | 平台总开关 | -| `config_json.region_id` | 默认 `cn-shanghai` | | `config_json.nickname_service` | 默认 `nickname_detection_pro` | | `config_json.chat_service` | 默认 `chat_detection_pro` | | `config_json.timeout_ms` | 单次请求超时,建议 1000~2000 ms | | `config_json.failure_mode` | `allow` 或 `block` | -| `secret_json.access_key_id` | 只写不回显 | -| `secret_json.access_key_secret` | 只写不回显 | + +公共配置来源如下: + +| `hg_sys_config` 键 | 说明 | +| --- | --- | +| `im_third_party.aliyun_access_key_id` | 阿里云 AccessKey ID | +| `im_third_party.aliyun_access_key_secret` | 阿里云 AccessKey Secret | +| `im_third_party.aliyun_green_region_id` | 内容安全地域,默认 `cn-shanghai` | +| `im_third_party.aliyun_green_endpoint` | 内容安全端点,默认 `green-cip.cn-shanghai.aliyuncs.com` | + +`im_external_config.secret_json` 不再保存内容审核密钥;升级时清空历史重复密钥。Admin Server 的内部接口负责把平台总开关、场景服务参数和公共阿里云配置合并后下发给 IM Server。 “检查配置”必须真实调用阿里云测试文本,返回成功与否、耗时、RequestID 和脱敏错误,不能只检查字段非空。 diff --git a/server/internal/controller/internalapi/imconfig/im_config.go b/server/internal/controller/internalapi/imconfig/im_config.go index 8fed9b0..ab3bb72 100644 --- a/server/internal/controller/internalapi/imconfig/im_config.go +++ b/server/internal/controller/internalapi/imconfig/im_config.go @@ -6,6 +6,7 @@ import ( api "hotgo/api/admin/imconfiginternal" "hotgo/internal/consts" "hotgo/internal/dao" + "hotgo/internal/library/immoderation" "hotgo/internal/library/imtenant" "hotgo/internal/model/entity" "hotgo/internal/service" @@ -112,29 +113,36 @@ func (c *cConfig) ModerationProvider(ctx context.Context, req *api.ModerationPro } var row struct { Provider string `json:"provider"` - Endpoint string `json:"endpoint"` Enabled int `json:"enabled"` ConfigJSON string `json:"config_json"` - SecretJSON string `json:"secret_json"` } - if err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx). - Fields("provider", "endpoint", "enabled", "CAST(config_json AS CHAR) config_json", "CAST(secret_json AS CHAR) secret_json"). - Where("tenant_id", tenant.Id).Where("service", consts.ImExternalServiceModeration).Scan(&row); err != nil { + record, err := imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx). + Fields("provider", "enabled", "CAST(config_json AS CHAR) config_json"). + Where("tenant_id", tenant.Id).Where("service", consts.ImExternalServiceModeration).One() + if err != nil { return nil, gerror.Wrap(err, "获取内容审核服务配置失败") } + if !record.IsEmpty() { + if err = record.Struct(&row); err != nil { + return nil, gerror.Wrap(err, "解析内容审核服务配置失败") + } + } + globalConfig, err := immoderation.LoadAliyunGlobalConfig(ctx) + if err != nil { + return nil, err + } config := map[string]any{} - secret := map[string]any{} _ = json.Unmarshal([]byte(row.ConfigJSON), &config) - _ = json.Unmarshal([]byte(row.SecretJSON), &secret) res = &api.ModerationProviderRes{ - Provider: row.Provider, Endpoint: row.Endpoint, Enabled: row.Enabled == 1, - RegionID: stringValue(config, "region_id", "cn-shanghai"), + Provider: stringValue(map[string]any{"provider": row.Provider}, "provider", "aliyun_green"), + Endpoint: globalConfig.Endpoint, Enabled: row.Enabled == 1, + RegionID: globalConfig.RegionID, NicknameService: stringValue(config, "nickname_service", "nickname_detection_pro"), ChatService: stringValue(config, "chat_service", "chat_detection_pro"), TimeoutMS: intValue(config, "timeout_ms", 1500), FailureMode: moderationFailureMode(stringValue(config, "failure_mode", "allow")), - AccessKeyID: stringValue(secret, "access_key_id", ""), - AccessKeySecret: stringValue(secret, "access_key_secret", ""), + AccessKeyID: globalConfig.AccessKeyID, + AccessKeySecret: globalConfig.AccessKeySecret, } return res, nil } diff --git a/server/internal/library/immoderation/config.go b/server/internal/library/immoderation/config.go new file mode 100644 index 0000000..e36e058 --- /dev/null +++ b/server/internal/library/immoderation/config.go @@ -0,0 +1,63 @@ +package immoderation + +import ( + "context" + "hotgo/internal/dao" + "hotgo/internal/model/entity" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" +) + +const ( + AliyunConfigGroup = "im_third_party" + AliyunAccessKeyIDKey = "aliyun_access_key_id" + AliyunAccessKeySecretKey = "aliyun_access_key_secret" + AliyunGreenRegionIDKey = "aliyun_green_region_id" + AliyunGreenEndpointKey = "aliyun_green_endpoint" + DefaultAliyunGreenRegionID = "cn-shanghai" + DefaultAliyunGreenEndpoint = "green-cip.cn-shanghai.aliyuncs.com" +) + +type AliyunGlobalConfig struct { + AccessKeyID string + AccessKeySecret string + RegionID string + Endpoint string +} + +func LoadAliyunGlobalConfig(ctx context.Context) (config AliyunGlobalConfig, err error) { + columns := dao.SysConfig.Columns() + keys := []string{ + AliyunAccessKeyIDKey, + AliyunAccessKeySecretKey, + AliyunGreenRegionIDKey, + AliyunGreenEndpointKey, + } + var rows []*entity.SysConfig + if err = dao.SysConfig.Ctx(ctx). + Fields(columns.Key, columns.Value). + Where(columns.Group, AliyunConfigGroup). + Where(columns.Status, 1). + WhereIn(columns.Key, keys). + Scan(&rows); err != nil { + return config, gerror.Wrap(err, "获取阿里云内容审核公共配置失败") + } + values := make(map[string]string, len(rows)) + for _, row := range rows { + values[row.Key] = strings.TrimSpace(row.Value) + } + return AliyunGlobalConfig{ + AccessKeyID: values[AliyunAccessKeyIDKey], + AccessKeySecret: values[AliyunAccessKeySecretKey], + RegionID: firstValue(values[AliyunGreenRegionIDKey], DefaultAliyunGreenRegionID), + Endpoint: firstValue(values[AliyunGreenEndpointKey], DefaultAliyunGreenEndpoint), + }, nil +} + +func firstValue(value, fallback string) string { + if strings.TrimSpace(value) == "" { + return fallback + } + return strings.TrimSpace(value) +} diff --git a/server/internal/logic/sys/im_external_config.go b/server/internal/logic/sys/im_external_config.go index 4984233..18bfdb5 100644 --- a/server/internal/logic/sys/im_external_config.go +++ b/server/internal/logic/sys/im_external_config.go @@ -116,11 +116,9 @@ func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalC if in.Provider != "aliyun_green" { return gerror.New("内容审核服务商必须为aliyun_green") } - if strings.TrimSpace(in.Endpoint) == "" { - in.Endpoint = "green-cip.cn-shanghai.aliyuncs.com" - } + in.Endpoint = "" if in.ConfigJson == nil { - in.ConfigJson = gjson.New(map[string]any{"region_id": "cn-shanghai", "nickname_service": "nickname_detection_pro", "chat_service": "chat_detection_pro", "timeout_ms": 1500, "failure_mode": "allow"}) + in.ConfigJson = gjson.New(map[string]any{"nickname_service": "nickname_detection_pro", "chat_service": "chat_detection_pro", "timeout_ms": 1500, "failure_mode": "allow"}) } config := in.ConfigJson.Map() if timeout := gconv.Int(config["timeout_ms"]); timeout < 200 || timeout > 10000 { @@ -129,12 +127,12 @@ func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalC if mode := gconv.String(config["failure_mode"]); mode != "allow" && mode != "block" { return gerror.New("内容审核异常策略不合法") } - if in.SecretJson != nil && in.SecretJson.String() != "" && in.SecretJson.String() != "null" && in.SecretJson.String() != "{}" { - secret := in.SecretJson.Map() - if strings.TrimSpace(gconv.String(secret["access_key_id"])) == "" || strings.TrimSpace(gconv.String(secret["access_key_secret"])) == "" { - return gerror.New("AccessKey ID和AccessKey Secret必须同时填写") - } - } + in.ConfigJson = gjson.New(map[string]any{ + "nickname_service": firstExternalValue(gconv.String(config["nickname_service"]), "nickname_detection_pro"), + "chat_service": firstExternalValue(gconv.String(config["chat_service"]), "chat_detection_pro"), + "timeout_ms": gconv.Int(config["timeout_ms"]), + "failure_mode": gconv.String(config["failure_mode"]), + }) } configJson := "{}" if in.ConfigJson != nil && in.ConfigJson.String() != "" && in.ConfigJson.String() != "null" { @@ -151,6 +149,9 @@ func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalC if in.SecretJson != nil && in.SecretJson.String() != "" && in.SecretJson.String() != "null" && in.SecretJson.String() != "{}" { data["secret_json"] = in.SecretJson.String() } + if in.Service == consts.ImExternalServiceModeration { + data["secret_json"] = "{}" + } if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx). Where("tenant_id", tenant.Id).Where("service", in.Service).Data(data).Update(); err != nil { return gerror.Wrap(err, "更新IM外部服务配置失败,请稍后重试!") @@ -211,16 +212,18 @@ func (s *sSysImExternalConfig) checkModeration(ctx context.Context, row gdb.Reco return consts.ImExternalConfigStatusUnconfigured, "内容审核服务商必须为aliyun_green" } config := map[string]any{} - secret := map[string]any{} _ = json.Unmarshal([]byte(row["config_json"].String()), &config) - _ = json.Unmarshal([]byte(row["secret_json"].String()), &secret) + globalConfig, err := immoderation.LoadAliyunGlobalConfig(ctx) + if err != nil { + return consts.ImExternalConfigStatusUnconfigured, sanitizeExternalError(err) + } timeoutMS := gconv.Int(config["timeout_ms"]) if timeoutMS <= 0 { timeoutMS = 1500 } check, err := immoderation.CheckAliyunText(ctx, immoderation.AliyunConfig{ - AccessKeyID: gconv.String(secret["access_key_id"]), AccessKeySecret: gconv.String(secret["access_key_secret"]), - RegionID: firstExternalValue(gconv.String(config["region_id"]), "cn-shanghai"), Endpoint: row["endpoint"].String(), + AccessKeyID: globalConfig.AccessKeyID, AccessKeySecret: globalConfig.AccessKeySecret, + RegionID: globalConfig.RegionID, Endpoint: globalConfig.Endpoint, Service: firstExternalValue(gconv.String(config["nickname_service"]), "nickname_detection_pro"), Timeout: time.Duration(timeoutMS) * time.Millisecond, }) if err != nil { diff --git a/web/src/views/imExternalConfig/edit.vue b/web/src/views/imExternalConfig/edit.vue index 3aeb4ad..be925e6 100644 --- a/web/src/views/imExternalConfig/edit.vue +++ b/web/src/views/imExternalConfig/edit.vue @@ -14,16 +14,13 @@ placeholder="请输入服务商" /> - + formValue.value.service === 'moderation'); const moderationConfig = ref({ - region_id: 'cn-shanghai', nickname_service: 'nickname_detection_pro', chat_service: 'chat_detection_pro', timeout_ms: 1500, failure_mode: 'allow', }); - const moderationSecret = ref({ access_key_id: '', access_key_secret: '' }); function closeForm() { showModal.value = false; @@ -138,9 +119,7 @@ ? moderationConfig.value : parseJson(formValue.value.configJson || '{}', '普通配置') || {}; const secretJson = isModeration.value - ? moderationSecret.value.access_key_id || moderationSecret.value.access_key_secret - ? moderationSecret.value - : null + ? null : parseJson(formValue.value.secretJson, '敏感配置'); await Update({ tenantId: formValue.value.tenantId, @@ -167,12 +146,11 @@ if (!formValue.value.configJson) formValue.value.configJson = '{}'; if (formValue.value.service === 'moderation') { formValue.value.provider = 'aliyun_green'; - formValue.value.endpoint ||= 'green-cip.cn-shanghai.aliyuncs.com'; + formValue.value.endpoint = ''; moderationConfig.value = { ...moderationConfig.value, ...(parseJson(formValue.value.configJson, '普通配置') || {}), }; - moderationSecret.value = { access_key_id: '', access_key_secret: '' }; } showModal.value = true; }