aliyun sensor server

This commit is contained in:
tamakyi
2026-09-10 18:04:19 +08:00
parent e3e59e7c69
commit 8e2325eda5
38 changed files with 3147 additions and 344 deletions
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2022 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"github.com/sirupsen/logrus"
"github.com/tamakyi/TamaBox/internal/context"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/form"
"github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/security/censor"
)
// UpdateModerationStrictness 保存当前账号的「内容审核严格度」档位。
//
// 该档位同时作用于提问、投票、私聊三条链路(由 internal/security/moderation.Check
// 统一裁决),因此只需在账号侧存一份。
func UpdateModerationStrictness(ctx context.Context, f form.UpdateModerationStrictness) {
// 反序列化失败(如请求体超限)时零值 Strictness="" 会被当成「跟随站点默认」写入,
// 把账号已选择的档位静默改回默认档 —— 先中止,不触碰现有设置。
if abortOnFormError(ctx, returnTab(ctx)) {
return
}
// 非法取值一律拒绝保存:宁可保留原档位,也不要写入一个解释不了的中间状态。
if !censor.IsValidStrictness(f.Strictness) {
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "admin.flash.moderationStrictnessInvalid"))
ctx.Redirect(returnTab(ctx))
return
}
if err := db.Users.UpdateModerationStrictness(ctx.Request().Context(), ctx.User.ID, f.Strictness); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update moderation strictness")
ctx.SetInternalErrorFlash()
ctx.Redirect(returnTab(ctx))
return
}
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.moderationStrictnessUpdated"))
ctx.Redirect(returnTab(ctx))
}
+39 -25
View File
@@ -26,6 +26,7 @@ import (
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/security/censor"
"github.com/tamakyi/TamaBox/internal/security/moderation"
votepkg "github.com/tamakyi/TamaBox/route/vote"
)
@@ -58,17 +59,13 @@ type voteForm struct {
}
// censorCheck 对单段文本做内容审核。
// fail-closed:审核服务异常时拒绝发布(与提问/回答流程一致);
// 未开启 EnableTextCensor 时 censor.Text 直接放行,不影响开发环境。
//
// 与提问 / 私聊共用同一个守卫(internal/security/moderation.Check):是否拦截由
// 投票发起人本人的「审核严格度」档位决定。
// fail-closed:审核服务异常时拒绝发布;未开启 EnableTextCensor 时底层直接放行。
func censorCheck(ctx context.Context, text string) error {
resp, err := censor.Text(ctx.Request().Context(), text)
if err != nil {
return errors.New(i18n.T(ctx.Lang(), "censor.unavailable"))
}
if !resp.Pass {
return errors.New(resp.ErrorMessage())
}
return nil
_, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(ctx.User.ModerationStrictness), ctx.Lang(), text)
return err
}
// parseTimeInput 解析 "YYYY-MM-DDTHH:MM"(HTML datetime-local 格式),空字符串返回 nil。
@@ -159,12 +156,9 @@ func parseVoteMeta(ctx context.Context, draft bool) (*voteForm, error) {
// 内容审核(fail-closed):非草稿执行;草稿跳过(见上说明)
var censorMeta datatypes.JSON
if !draft {
titleCensor, err := censor.Text(ctx.Request().Context(), title)
titleCensor, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(ctx.User.ModerationStrictness), ctx.Lang(), title)
if err != nil {
return nil, errors.New("内容审核服务暂时不可用,请稍后再试")
}
if !titleCensor.Pass {
return nil, errors.New(titleCensor.ErrorMessage())
return nil, err
}
censorMeta = datatypes.JSON(titleCensor.ToJSON())
if description != "" {
@@ -996,32 +990,52 @@ func validateForPublish(ctx context.Context, vote *db.Vote) error {
return nil
}
// voteOwnerStrictness 读取投票发起人本人的「审核严格度」档位。
//
// 这里不用 ctx.User 的原因:发布动作未必由发起人本人触发(后台管理、补发布等场景),
// ctx.User 有可能是操作者而非发起人,直接沿用会导致「按别人的档位审核别人箱子的内容」。
// 取不到发起人时回退最严档位 —— 宁可沿用严格判定,也不要因为查不到就把判定放宽。
func voteOwnerStrictness(ctx context.Context, ownerID uint) censor.Strictness {
owner, err := db.Users.GetByID(ctx.Request().Context(), ownerID)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).WithField("owner_id", ownerID).
Error("Failed to get vote owner for moderation strictness")
return censor.StrictnessStrict
}
return moderation.StrictnessFor(owner.ModerationStrictness)
}
// censorVoteForPublish 发布前对投票全部可见文本做内容审核(fail-closed)。
// 草稿保存阶段跳过了审核,发布(对外公开)时必须补齐,确保公开内容受管控。
func censorVoteForPublish(ctx context.Context, vote *db.Vote) error {
// 同一次发布的全部可见文本必须用「同一个」档位裁决:这里只解析一次发起人的档位并复用。
// 否则标题走发起人档位、描述/选项走 ctx.User 档位(censorCheck 内部读 ctx.User),
// 一旦发布动作不是发起人本人触发,同一条投票的两段文本就会落到两个账号的松紧档上。
strictness := voteOwnerStrictness(ctx, vote.UserID)
check := func(text string) error {
_, err := moderation.Check(ctx.Request().Context(), strictness, ctx.Lang(), text)
return err
}
if vote.Title != "" {
resp, err := censor.Text(ctx.Request().Context(), vote.Title)
if err != nil {
return errors.New(i18n.T(ctx.Lang(), "censor.unavailable"))
}
if !resp.Pass {
return errors.New(resp.ErrorMessage())
if err := check(vote.Title); err != nil {
return err
}
}
if vote.Description != "" {
if err := censorCheck(ctx, vote.Description); err != nil {
if err := check(vote.Description); err != nil {
return err
}
}
options, _ := db.VoteOptions.ListByVoteID(ctx.Request().Context(), vote.ID)
for _, o := range options {
if err := censorCheck(ctx, o.Content); err != nil {
if err := check(o.Content); err != nil {
return err
}
}
fields, _ := db.VoteFormFields.ListByVoteID(ctx.Request().Context(), vote.ID)
for _, f := range fields {
if err := censorCheck(ctx, f.Label); err != nil {
if err := check(f.Label); err != nil {
return err
}
// 预置选项(select/radio/checkbox/rank)也纳入审核
@@ -1029,7 +1043,7 @@ func censorVoteForPublish(ctx context.Context, vote *db.Vote) error {
if fo == "" {
continue
}
if err := censorCheck(ctx, fo); err != nil {
if err := check(fo); err != nil {
return err
}
}