324 lines
12 KiB
Go
324 lines
12 KiB
Go
package sys
|
||
|
||
import (
|
||
"context"
|
||
"hotgo/internal/consts"
|
||
"hotgo/internal/dao"
|
||
"hotgo/internal/library/contexts"
|
||
"hotgo/internal/library/immoderation"
|
||
"hotgo/internal/library/imtenant"
|
||
"hotgo/internal/model/entity"
|
||
"hotgo/internal/model/input/sysin"
|
||
"hotgo/internal/service"
|
||
|
||
"encoding/json"
|
||
"fmt"
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/encoding/gjson"
|
||
"github.com/gogf/gf/v2/errors/gerror"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
"github.com/gogf/gf/v2/util/gconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type sSysImExternalConfig struct{}
|
||
|
||
func NewSysImExternalConfig() *sSysImExternalConfig { return &sSysImExternalConfig{} }
|
||
|
||
func init() {
|
||
service.RegisterSysImExternalConfig(NewSysImExternalConfig())
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) List(ctx context.Context, in *sysin.ImExternalConfigListInp) (list []*sysin.ImExternalConfigListModel, totalCount int, err error) {
|
||
if err = in.Filter(ctx); err != nil {
|
||
return
|
||
}
|
||
tenant, err := s.resolveTenant(ctx, in.TenantId)
|
||
if err != nil {
|
||
return
|
||
}
|
||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||
return
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||
return nil, 0, gerror.New("第一阶段仅默认商户可配置内容审核服务")
|
||
}
|
||
mod := s.model(ctx).Where("tenant_id", tenant.Id)
|
||
if tenant.Code != "default" {
|
||
mod = mod.WhereNot("service", consts.ImExternalServiceModeration)
|
||
}
|
||
if in.Service != "" {
|
||
mod = mod.Where("service", in.Service)
|
||
}
|
||
if in.Provider != "" {
|
||
mod = mod.WhereLike("provider", "%"+in.Provider+"%")
|
||
}
|
||
if in.Enabled != nil {
|
||
mod = mod.Where("enabled", *in.Enabled)
|
||
}
|
||
mod = mod.Page(in.Page, in.PerPage).OrderAsc("service")
|
||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||
err = gerror.Wrap(err, "获取IM外部服务配置失败,请稍后重试!")
|
||
return
|
||
}
|
||
for _, item := range list {
|
||
item.TenantName = tenant.Name
|
||
}
|
||
return
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) View(ctx context.Context, in *sysin.ImExternalConfigViewInp) (res *sysin.ImExternalConfigViewModel, err error) {
|
||
if err = in.Filter(ctx); err != nil {
|
||
return
|
||
}
|
||
tenant, err := s.resolveTenant(ctx, in.TenantId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||
return nil, err
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||
return nil, gerror.New("第一阶段仅默认商户可查看内容审核服务")
|
||
}
|
||
if err = s.model(ctx).Where("tenant_id", tenant.Id).Where("service", in.Service).Scan(&res); err != nil {
|
||
return nil, gerror.Wrap(err, "获取IM外部服务配置详情失败,请稍后重试!")
|
||
}
|
||
if res != nil {
|
||
res.TenantName = tenant.Name
|
||
}
|
||
return
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalConfigUpdateInp) (err error) {
|
||
if err = in.Filter(ctx); err != nil {
|
||
return
|
||
}
|
||
if !s.validService(in.Service) {
|
||
return gerror.New("服务类型不正确")
|
||
}
|
||
tenant, err := s.resolveTenant(ctx, in.TenantId)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||
return err
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||
return gerror.New("第一阶段仅默认商户可配置内容审核服务")
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration && !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||
return gerror.New("只有平台管理员可以维护内容审核服务")
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration {
|
||
if in.Provider != "aliyun_green" {
|
||
return gerror.New("内容审核服务商必须为aliyun_green")
|
||
}
|
||
in.Endpoint = ""
|
||
if in.ConfigJson == nil {
|
||
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 {
|
||
return gerror.New("内容审核超时必须为200至10000毫秒")
|
||
}
|
||
if mode := gconv.String(config["failure_mode"]); mode != "allow" && mode != "block" {
|
||
return gerror.New("内容审核异常策略不合法")
|
||
}
|
||
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" {
|
||
configJson = in.ConfigJson.String()
|
||
}
|
||
data := g.Map{
|
||
"provider": in.Provider,
|
||
"endpoint": in.Endpoint,
|
||
"enabled": in.Enabled,
|
||
"config_json": configJson,
|
||
"updated_at": gtime.Now(),
|
||
"updated_by": contexts.GetUserId(ctx),
|
||
}
|
||
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外部服务配置失败,请稍后重试!")
|
||
}
|
||
_ = writeImAdminAudit(ctx, tenant.Id, "im_external_config_update", "im_external_config", 0, "success", g.Map{
|
||
"service": in.Service, "provider": in.Provider, "enabled": in.Enabled,
|
||
})
|
||
if in.Service == consts.ImExternalServiceModeration {
|
||
_ = publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "third_party.updated", "moderation", 0)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) Check(ctx context.Context, in *sysin.ImExternalConfigCheckInp) (err error) {
|
||
if err = in.Filter(ctx); err != nil {
|
||
return
|
||
}
|
||
tenant, err := s.resolveTenant(ctx, in.TenantId)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||
return err
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||
return gerror.New("第一阶段仅默认商户可检查内容审核服务")
|
||
}
|
||
if in.Service == consts.ImExternalServiceModeration && !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||
return gerror.New("只有平台管理员可以检查内容审核服务")
|
||
}
|
||
var row gdb.Record
|
||
if row, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||
Where("tenant_id", tenant.Id).Where("service", in.Service).One(); err != nil {
|
||
return gerror.Wrap(err, "检查IM外部服务配置失败,请稍后重试!")
|
||
}
|
||
status := consts.ImExternalConfigStatusConfigured
|
||
result := "配置完整"
|
||
if row == nil || row["enabled"].Int() != 1 || row["provider"].String() == "" {
|
||
status = consts.ImExternalConfigStatusUnconfigured
|
||
result = "未启用或服务商为空"
|
||
} else if in.Service == consts.ImExternalServiceModeration {
|
||
status, result = s.checkModeration(ctx, row)
|
||
}
|
||
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||
Where("tenant_id", tenant.Id).Where("service", in.Service).
|
||
Data(g.Map{"status": status, "checked_at": gtime.Now(), "check_result": result, "updated_at": gtime.Now()}).
|
||
Update(); err != nil {
|
||
return gerror.Wrap(err, "更新IM外部服务检查结果失败,请稍后重试!")
|
||
}
|
||
_ = writeImAdminAudit(ctx, tenant.Id, "im_external_config_check", "im_external_config", 0, "success", g.Map{
|
||
"service": in.Service, "status": status,
|
||
})
|
||
return nil
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) checkModeration(ctx context.Context, row gdb.Record) (string, string) {
|
||
if row["provider"].String() != "aliyun_green" {
|
||
return consts.ImExternalConfigStatusUnconfigured, "内容审核服务商必须为aliyun_green"
|
||
}
|
||
config := map[string]any{}
|
||
_ = json.Unmarshal([]byte(row["config_json"].String()), &config)
|
||
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: 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 {
|
||
return consts.ImExternalConfigStatusUnconfigured, "连通性检查失败:" + sanitizeExternalError(err)
|
||
}
|
||
return consts.ImExternalConfigStatusConfigured, fmt.Sprintf("检查成功,耗时%dms,RequestID=%s", check.Latency.Milliseconds(), check.RequestID)
|
||
}
|
||
|
||
func firstExternalValue(value, fallback string) string {
|
||
if strings.TrimSpace(value) == "" {
|
||
return fallback
|
||
}
|
||
return strings.TrimSpace(value)
|
||
}
|
||
func sanitizeExternalError(err error) string {
|
||
value := strings.ReplaceAll(err.Error(), "\n", " ")
|
||
if len([]rune(value)) > 120 {
|
||
value = string([]rune(value)[:120])
|
||
}
|
||
return value
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) model(ctx context.Context) *gdb.Model {
|
||
return imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).Fields(
|
||
"tenant_id",
|
||
"id",
|
||
"service",
|
||
"provider",
|
||
"endpoint",
|
||
"enabled",
|
||
"status",
|
||
"CAST(config_json AS CHAR) config_json",
|
||
"CASE WHEN secret_json IS NULL OR CAST(secret_json AS CHAR)='{}' THEN 0 ELSE 1 END has_secret",
|
||
"checked_at",
|
||
"check_result",
|
||
"created_at",
|
||
"updated_at",
|
||
)
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) resolveTenant(ctx context.Context, selectedTenantId int64) (tenant *entity.ImTenant, err error) {
|
||
scope, err := resolveImAdminScope(ctx, selectedTenantId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(scope.TenantId).Scan(&tenant); err != nil {
|
||
return nil, gerror.Wrap(err, "获取IM租户失败")
|
||
}
|
||
if tenant == nil {
|
||
return nil, gerror.New("IM租户不存在")
|
||
}
|
||
return tenant, nil
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) ensureRows(ctx context.Context, tenantId int64) error {
|
||
services := []string{
|
||
consts.ImExternalServiceSms,
|
||
consts.ImExternalServicePush,
|
||
consts.ImExternalServiceTurn,
|
||
consts.ImExternalServiceStorage,
|
||
consts.ImExternalServiceModeration,
|
||
}
|
||
for _, serviceName := range services {
|
||
count, err := imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||
Where("tenant_id", tenantId).Where("service", serviceName).Count()
|
||
if err != nil {
|
||
return gerror.Wrap(err, "初始化IM外部服务配置失败")
|
||
}
|
||
if count > 0 {
|
||
continue
|
||
}
|
||
_, err = imtenant.RegistryDB().Exec(ctx, `INSERT INTO im_external_config
|
||
(tenant_id,service,provider,endpoint,enabled,status,config_json,secret_json,created_by,updated_by)
|
||
VALUES(?,?,?,?,?,?,CAST(? AS JSON),CAST(? AS JSON),?,?)`,
|
||
tenantId, serviceName, "", "", 0, consts.ImExternalConfigStatusUnconfigured, "{}", "{}", contexts.GetUserId(ctx), contexts.GetUserId(ctx))
|
||
if err != nil {
|
||
return gerror.Wrap(err, "初始化IM外部服务配置失败")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *sSysImExternalConfig) validService(serviceName string) bool {
|
||
switch serviceName {
|
||
case consts.ImExternalServiceSms, consts.ImExternalServicePush, consts.ImExternalServiceTurn, consts.ImExternalServiceStorage, consts.ImExternalServiceModeration:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func externalConfigJson(value string) *gjson.Json {
|
||
if value == "" {
|
||
return gjson.New("{}")
|
||
}
|
||
return gjson.New(value)
|
||
}
|