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
+51 -50
View File
@@ -39,46 +39,47 @@
}
</style>
<div class="uk-container uk-container-small uk-margin-top">
<h2 class="uk-text-center">
<div class="uk-container uk-container-small uk-margin-top" data-page-node-id="VJALGxXgA3LjvngL2idiPd">
{{template "base/alert" .}}
<h2 class="uk-text-center" data-page-node-id="4ZhL4EHH1LeTPtFA5wpgbh">
{{ if .IsOwner }}{{t .LangCode "chat.withAsker"}}{{ else }}{{t .LangCode "chat.withUser" .PageUser.Name}}{{ end }}
</h2>
<p class="uk-text-center uk-text-meta">
<p class="uk-text-center uk-text-meta" data-page-node-id="NNKGVnXP49GNhyCvho0PAw">
{{ if .Conversation.IsClosed }}
<span class="uk-label uk-label-danger">{{t .LangCode "chat.closed"}}</span>
<span class="uk-label uk-label-danger" data-page-node-id="QuKE1FE9CzlNmbQOXq68zA">{{t .LangCode "chat.closed"}}</span>
{{ else }}
{{t .LangCode "chat.remaining" (sub .Conversation.MaxMessages .Conversation.MessageCount) .Conversation.MaxMessages}}
{{ end }}
</p>
<div class="uk-card uk-card-default uk-card-body uk-margin uk-padding-small">
<ul class="uk-list uk-list-divider">
<div class="uk-card uk-card-default uk-card-body uk-margin uk-padding-small" data-page-node-id="Ngi6SbUffTDHaNpDFcCzjT">
<ul class="uk-list uk-list-divider" data-page-node-id="VrxQ4Bp6BfmklKAkEqykZf">
{{ range .Messages }}
{{ $isMyMessage := eq .SenderID $.LoggedUser.ID }}
<li class="uk-margin-small-bottom">
<div class="uk-flex {{ if $isMyMessage }}uk-flex-right{{ else }}uk-flex-left{{ end }}">
<li class="uk-margin-small-bottom" data-page-node-id="LSFm4EINd1XxnwZSFbAlji">
<div class="uk-flex {{ if $isMyMessage }}uk-flex-right{{ else }}uk-flex-left{{ end }}" data-page-node-id="afhl9aZDsknmAlut7imPnp">
<div class="uk-card uk-card-hover uk-card-body uk-padding-small uk-margin-small-left uk-margin-small-right"
style="max-width: 75%; border-radius: 12px; background-color: {{ if $isMyMessage }}#e6ffe6{{ else }}#f0f8ff{{ end }};">
<div class="uk-flex uk-flex-middle uk-margin-small-bottom">
<span class="uk-icon" uk-icon="user"></span>
<strong class="uk-margin-small-left">
style="max-width: 75%; border-radius: 12px; background-color: {{ if $isMyMessage }}#e6ffe6{{ else }}#f0f8ff{{ end }};" data-page-node-id="zPuwctcKF6xPKnwQKaYADD">
<div class="uk-flex uk-flex-middle uk-margin-small-bottom" data-page-node-id="NuHyY57VYOUYQWdpRPqeS6">
<span class="uk-icon" uk-icon="user" data-page-node-id="ctN3HeenrU4wiv39zSfOgY"></span>
<strong class="uk-margin-small-left" data-page-node-id="V7t5pBo43sNBv1a6rvp0jv">
{{ if $isMyMessage }}{{t $.LangCode "chat.me"}}{{ else }}{{t $.LangCode "chat.other"}}{{ end }}
</strong>
<span class="uk-text-meta uk-margin-small-left" style="font-size: 0.75rem;">
<span class="uk-text-meta uk-margin-small-left" style="font-size: 0.75rem;" data-page-node-id="JidfstvpABpsDBPRL3WY6I">
{{ .CreatedAt.Format "01-02 15:04" }}
</span>
</div>
{{ if and $.IsOwner $.IsBlocked (eq .SenderID $.Conversation.AskerID) }}
<p class="uk-margin-remove"><span class="uk-text-muted uk-text-italic">{{t $.LangCode "chat.userBlocked"}}</span></p>
<p class="uk-margin-remove" data-page-node-id="LSDlwCDxP0hWTAw2tjVSvA"><span class="uk-text-muted uk-text-italic" data-page-node-id="nTFMPXU5SSa8Affx4vPAm8">{{t $.LangCode "chat.userBlocked"}}</span></p>
{{ else }}
{{ if .Content }}
<p class="uk-margin-remove">{{ .Content }}</p>
<p class="uk-margin-remove" data-page-node-id="KMOMzUxidmbA4tMskOa1z2">{{ .Content }}</p>
{{ end }}
{{ range (index $.MessageImages .ID) }}
<div class="uk-margin-small-top">
<div class="uk-margin-small-top" data-page-node-id="sB1P5iHpDXEjFBdUymfkxz">
<img src="{{ .URL }}" alt="{{ .FileName }}"
style="max-width: 200px; max-height: 200px; border-radius: 8px; cursor: pointer;"
onclick="openLightbox('{{ .URL }}', '{{ .FileName }}')" />
onclick="openLightbox('{{ .URL }}', '{{ .FileName }}')" data-page-node-id="siJUfJdLhG2zPrXzGC1KZ3" />
</div>
{{ end }}
{{ end }}
@@ -90,58 +91,58 @@
</div>
{{ if .CanSend }}
<div class="uk-card uk-card-default uk-card-body uk-margin-top">
<form method="post" action="/_/{{.PageUser.Domain}}/chat/{{.Conversation.Token}}/send" enctype="multipart/form-data">
<div class="uk-card uk-card-default uk-card-body uk-margin-top" data-page-node-id="ZAXQKcFkCuSMdF0fOQ5dYH">
<form method="post" action="/_/{{.PageUser.Domain}}/chat/{{.Conversation.Token}}/send" enctype="multipart/form-data" data-page-node-id="9xSD9GJlKfqAkKPgVBuOAn">
{{ .CSRFTokenHTML }}
<textarea name="content" class="uk-textarea" rows="3" placeholder="{{t .LangCode "chat.input"}}" maxlength="{{ .ChatMessageLimit }}"></textarea>
<textarea name="content" class="uk-textarea" rows="3" placeholder="{{t .LangCode "chat.input"}}" maxlength="{{ .ChatMessageLimit }}">{{ .Content }}</textarea>
<div class="uk-margin-small-top" style="display: flex; align-items: center; gap: 10px;">
<div uk-form-custom>
<a href="#" class="uk-icon-link" uk-icon="image"></a>
<span style="font-size: 13px; color: #666;">{{t .LangCode "chat.addImage"}}</span>
<input type="file" name="image" accept="image/*">
<div class="uk-margin-small-top" style="display: flex; align-items: center; gap: 10px;" data-page-node-id="jtjlLfDzoOkYDKdiOzZ6vl">
<div uk-form-custom data-page-node-id="elqLK3luiM7Q4FamlGb3pV">
<a href="#" class="uk-icon-link" uk-icon="image" data-page-node-id="uZN2LSBiXgLUmffpnhXWH6"></a>
<span style="font-size: 13px; color: #666;" data-page-node-id="TKZ0z7rbdw4u4YcNs6VmAv">{{t .LangCode "chat.addImage"}}</span>
<input type="file" name="image" accept="image/*" data-page-node-id="37rmGJRHggLssaZUDxV4FV">
</div>
<span class="uk-text-meta" id="file-name"></span>
<span class="uk-text-meta" id="file-name" data-page-node-id="EBlY0CCL4DtDxgbNBrlLaw"></span>
</div>
<div class="uk-margin-small-top uk-text-right">
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "chat.send"}}</button>
<div class="uk-margin-small-top uk-text-right" data-page-node-id="Tr2UTO7GlEX9LFxDVo1RlK">
<button type="submit" class="uk-button uk-button-primary" data-page-node-id="IH6ljh8Db7Wo4xfsEz9P5w">{{t .LangCode "chat.send"}}</button>
</div>
</form>
</div>
{{ else }}
<div class="uk-text-center uk-margin-top">
<p class="uk-text-warning">{{t .LangCode "chat.closedNoSend"}}</p>
<a href="/_/{{.PageUser.Domain}}" class="uk-button uk-button-default">{{t .LangCode "common.backHome"}}</a>
<div class="uk-text-center uk-margin-top" data-page-node-id="ZfG2zLwjExCXoA9YaDrqCn">
<p class="uk-text-warning" data-page-node-id="YKwFufPMOEUVlUvX4yBHAB">{{t .LangCode "chat.closedNoSend"}}</p>
<a href="/_/{{.PageUser.Domain}}" class="uk-button uk-button-default" data-page-node-id="iVHgoa52j3R1yuhJWbLPXF">{{t .LangCode "common.backHome"}}</a>
</div>
{{ end }}
{{ if .IsOwner }}
<div class="uk-margin-top uk-text-center">
<a class="uk-button uk-button-{{ if .IsBlocked }}default{{ else }}warning{{ end }} uk-button-small" href="#">
<div class="uk-margin-top uk-text-center" data-page-node-id="YuzOGI8cGxQAGXx3EsCnKq">
<a class="uk-button uk-button-{{ if .IsBlocked }}default{{ else }}warning{{ end }} uk-button-small" href="#" data-page-node-id="sG8R1GMpGC52pNeUFJEkTD">
{{ if .IsBlocked }}{{t .LangCode "chat.unblock"}}{{ else }}{{t .LangCode "chat.block"}}{{ end }}
</a>
<div class="uk-dropbar uk-dropbar-top" uk-drop="stretch: x; mode: click">
<div class="uk-card uk-card-default uk-card-body">
<h3 class="uk-card-title">{{ if .IsBlocked }}{{t .LangCode "chat.unblock"}}{{ else }}{{t .LangCode "chat.block"}}{{ end }}?</h3>
<p>{{ if .IsBlocked }}{{t .LangCode "chat.confirmUnblock"}}{{ else }}{{t .LangCode "chat.confirmBlock"}}{{ end }}</p>
<form method="post" action="/_/{{.PageUser.Domain}}/chat/{{.Conversation.Token}}/block">
<div class="uk-dropbar uk-dropbar-top" uk-drop="stretch: x; mode: click" data-page-node-id="iW9QyGsZ1XWa4Bq7Ch0805">
<div class="uk-card uk-card-default uk-card-body" data-page-node-id="UebLy6cpqCXeiPoX1JLRcE">
<h3 class="uk-card-title" data-page-node-id="qVRELxzwy4UR2ANNo6PNCi">{{ if .IsBlocked }}{{t .LangCode "chat.unblock"}}{{ else }}{{t .LangCode "chat.block"}}{{ end }}?</h3>
<p data-page-node-id="DrquhcYxj4Dkx7yGjoed4o">{{ if .IsBlocked }}{{t .LangCode "chat.confirmUnblock"}}{{ else }}{{t .LangCode "chat.confirmBlock"}}{{ end }}</p>
<form method="post" action="/_/{{.PageUser.Domain}}/chat/{{.Conversation.Token}}/block" data-page-node-id="gN7rHfCpdQ5b3HR05NNiaI">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-{{ if .IsBlocked }}primary{{ else }}danger{{ end }}">
<button type="submit" class="uk-button uk-button-{{ if .IsBlocked }}primary{{ else }}danger{{ end }}" data-page-node-id="Bv6TCmw3NFsNXmUfGQ0s9K">
{{t .LangCode "chat.confirm"}}{{ if .IsBlocked }}{{t .LangCode "chat.unblock"}}{{ else }}{{t .LangCode "chat.block"}}{{ end }}
</button>
</form>
</div>
</div>
<a class="uk-button uk-button-danger uk-button-small" href="#">{{t .LangCode "chat.delete"}}</a>
<div class="uk-dropbar uk-dropbar-top" uk-drop="stretch: x; mode: click">
<div class="uk-card uk-card-default uk-card-body">
<h3 class="uk-card-title">{{t .LangCode "qitem.danger"}}</h3>
<p>{{t .LangCode "chat.deleteConfirm"}}</p>
<form method="post" action="/_/{{.PageUser.Domain}}/chat/{{.Conversation.Token}}/delete">
<a class="uk-button uk-button-danger uk-button-small" href="#" data-page-node-id="LD7jP6dOcVE2UuLTnEuNCZ">{{t .LangCode "chat.delete"}}</a>
<div class="uk-dropbar uk-dropbar-top" uk-drop="stretch: x; mode: click" data-page-node-id="hmBJTLVxTGAh3eaZJFbd4u">
<div class="uk-card uk-card-default uk-card-body" data-page-node-id="vdlAgEiEKDXhfsLG3U6qt3">
<h3 class="uk-card-title" data-page-node-id="uELuLnRDZnyl09lrlJGgpG">{{t .LangCode "qitem.danger"}}</h3>
<p data-page-node-id="vCyeXBe3K710pdnGTnCpBl">{{t .LangCode "chat.deleteConfirm"}}</p>
<form method="post" action="/_/{{.PageUser.Domain}}/chat/{{.Conversation.Token}}/delete" data-page-node-id="83mnoGpvXiYyvWETOUlsna">
{{ .CSRFTokenHTML }}
<button type="submit" class="uk-button uk-button-danger">{{t .LangCode "qitem.deleteConfirmBtn"}}</button>
<button type="submit" class="uk-button uk-button-danger" data-page-node-id="XjUAxGnN5V8BrLG9n4zVcX">{{t .LangCode "qitem.deleteConfirmBtn"}}</button>
</form>
</div>
</div>
@@ -151,10 +152,10 @@
</div>
<!-- 自定义灯箱结构 -->
<div id="customLightbox" class="custom-lightbox" onclick="closeLightbox()">
<span class="close-btn" onclick="closeLightbox()">&times;</span>
<img id="lightboxImg" src="" alt="" />
<div id="lightboxCaption" class="caption"></div>
<div id="customLightbox" class="custom-lightbox" onclick="closeLightbox()" data-page-node-id="xmqzFCKLG0BSnAeRPCQBEP">
<span class="close-btn" onclick="closeLightbox()" data-page-node-id="UVNFSFTxluTqzyZ4rPyYLe">&times;</span>
<img id="lightboxImg" src="" alt="" data-page-node-id="wX0DtrO8n9OxRAAdFaDtAx" />
<div id="lightboxCaption" class="caption" data-page-node-id="bCQzLy8AgSWOKL7ZcMU3ZR"></div>
</div>
<script>
+3 -3
View File
@@ -1,10 +1,10 @@
{{template "base/header" .}}
<div class="uk-card uk-card-default uk-card-body">
<h1 class="uk-heading-small">{{t .LangCode "site.title"}}</h1>
<p style="color: red;">重大消息!本站已切换到<a href="https://box.shiroko.one/">box.shiroko.one</a>,原域名将在2026年12月24日到期,期间将通过301转发指向新域名,到期后旧域名将彻底不在使用。<br>问:为什么?答:.guru域名连续涨价,现在一年三百出头了遭不住。<br>问:我该做什么?答:记住新域名,如果之前有通过二维码分享出去的,记得替换成新的二维码。</p><hr>
<p style="color: red;">重大消息!本站已切换到<a href="https://box.shiroko.one/">box.shiroko.one</a>,原域名将在2026年12月24日到期,期间将通过301转发指向新域名,到期后旧域名将彻底不在使用。<br>
现已切换内容审核服务到阿里云,审核速度提升且误伤变少了。另外允许用户调整审核严格度,在不碰底线的前提下允许用户设置不接受辱骂、广告等信息(即不听坏话模式)按理来说之前的一些逆天的言论现在都能挡住了。 </p><hr>
<p><br>⭐一个无需注册即可提问的匿名提问箱。你不需要下载App,不需要绑定社交账号,甚至连邮箱都不需要是真的。<br>⭐本站在原版的基础上新增了问题页面的二维码、私聊、投票、追加提问等功能以及更多自定义内容。各位用户可自行修改二维码背景图、颜色等个性化配置。<br>⭐可以到站长唯一提问提问箱链接<a href="//box.shiroko.one/_/tamakyi">小狼的提问箱</a>尝试相关功能或给我提意见。<br>⭐可以加群:<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=rRy9g5XY34LIzOITGGmQHjX6BIICFpam&authKey=EjyNjp60KyG8BtKZGN8BXJaf1jl8okkCcUhVF2zAnGPRjhvuSdopnVqOSfSymnGg&noverify=0&group_code=777163254">小狼的群</a>一起水群。<br></p>
<hr><p style="color: red;">近期将国内段访问交由EdgeOne接管,境外访问照旧由CloudFlare提供,经测试国内访问速度有所提升。但因EdgeOne Free套餐不提供CIDR列表,IP放行采取自动抓取手动维护模式,如果出现521错误请刷新一下页面,过几天应该就好了。</p>
<hr><p style="color: red; font-weight: bold;">近期更新:<br>⭐2026.9 增加了提问、投票、私聊的总开关,可以设置定时收集、定量收集或关闭功能,顺便调整了前后端显示,看起来更易用了一些。<br>⭐2026.8 新增了<a href="https://box.shiroko.one/vote/sANyLkiBNUGN">投票功能</a>、追加提问、多语言、<a href="https://box.shiroko.one/_/tamakyi/2894">Markdown显示</a>、置顶显示等多个功能,详情见<a href="https://blog.shiroko.one/record/tamabox_changelog_20260813">这里</a>。<br>⭐2026.8 放弃使用recaptcha等对国内用户非常不友好的验证方式,更新了密码加密方式,修复了图片上传等一系列问题。<br>⭐2026.5 增加了私聊功能,可进行连续性对话,详见<a href="https://blog.shiroko.one/record/tamabox-update-20260513">介绍</a>;另有增加提问箱的卡片分享,更直观和美观。<br>⭐2026.2 新增了用户展示和标签功能,如果你希望陌生人给你提问,可以在设置页面给自己贴上5个标签并选择是否展示在探索页面:<a href="https://box.shiroko.one/explore">点我访问</a>,详情见<a href="https://blog.shiroko.one/record/tamabox-tags">狼的公告板</a><br></p>
<hr><p style="color: red; font-weight: bold;">近期更新:<br>⭐2026.9 新增审核严格度选项,增加了提问、投票、私聊的总开关,可以设置定时收集、定量收集或关闭功能,顺便调整了前后端显示,看起来更易用了一些。<br>⭐2026.8 新增了<a href="https://box.shiroko.one/vote/sANyLkiBNUGN">投票功能</a>、追加提问、多语言、<a href="https://box.shiroko.one/_/tamakyi/2894">Markdown显示</a>、置顶显示等多个功能,详情见<a href="https://blog.shiroko.one/record/tamabox_changelog_20260813">这里</a>。<br>⭐2026.8 放弃使用recaptcha等对国内用户非常不友好的验证方式,更新了密码加密方式,修复了图片上传等一系列问题。<br>⭐2026.5 增加了私聊功能,可进行连续性对话,详见<a href="https://blog.shiroko.one/record/tamabox-update-20260513">介绍</a>;另有增加提问箱的卡片分享,更直观和美观。<br>⭐2026.2 新增了用户展示和标签功能,如果你希望陌生人给你提问,可以在设置页面给自己贴上5个标签并选择是否展示在探索页面:<a href="https://box.shiroko.one/explore">点我访问</a>,详情见<a href="https://blog.shiroko.one/record/tamabox-tags">狼的公告板</a><br></p>
<hr><p>一点提醒:<br>⭐本站唯一指定认证站长:<a href="//box.shiroko.one/_/tamakyi">小狼</a>,注意不要被骗。
<br>⭐本站不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问的出现,如有涉证或不良言论将直接删除。</p>
</div>
+94 -1
View File
@@ -4,6 +4,8 @@ import (
"bytes"
"fmt"
gotemplate "html/template"
"regexp"
"sort"
"strings"
"testing"
"time"
@@ -12,9 +14,37 @@ import (
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/db"
"github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/security/moderation"
templatepkg "github.com/tamakyi/TamaBox/internal/template"
)
// 审核严格度下拉框的解析:不依赖属性的书写顺序或是否存在额外属性
// (模板可能被页面编辑器注入 data-* 属性),因此用正则而不是精确子串匹配。
var (
strictnessOptionRe = regexp.MustCompile(`<option value="(lax|standard|strict)"([^>]*)>`)
selectedAttrRe = regexp.MustCompile(`\bselected\b`)
)
// selectedStrictnessOptions 返回渲染结果中被标记 selected 的档位值(去重后升序)。
// 三个面板各渲染一份下拉框,正常情况下应得到恰好一个值;返回多个说明面板之间不一致。
func selectedStrictnessOptions(out string) []string {
seen := map[string]struct{}{}
var values []string
for _, m := range strictnessOptionRe.FindAllStringSubmatch(out, -1) {
if !selectedAttrRe.MatchString(m[2]) {
continue
}
if _, ok := seen[m[1]]; ok {
continue
}
seen[m[1]] = struct{}{}
values = append(values, m[1])
}
sort.Strings(values)
return values
}
// renderWithData 用给定数据渲染模板(复刻生产环境 Templater + FuncMap)。
func renderWithData(t *testing.T, name string, data map[string]interface{}) error {
t.Helper()
@@ -73,6 +103,8 @@ type mockLoggedUser struct {
Language string
HarassmentSetting string
BlockWords string
// 审核严格度档位(lax / standard / strict),设置页三个功能面板共用一份。
ModerationStrictness string
Tags string
ShowInHome bool
Acfunlink string
@@ -97,7 +129,8 @@ func profileBaseData() map[string]interface{} {
Qrcodepdpcolor: "#000", Qrcodebackcolor: "#fff", Dotscale: "0.35",
Backgroundimagealpha: "0.8", Notify: "email", Language: "zh-CN",
HarassmentSetting: "none", BlockWords: "广告,spam", Tags: "杂谈",
ChatMaxMessages: 20, DailyMessageLimit: 0,
ModerationStrictness: "standard",
ChatMaxMessages: 20, DailyMessageLimit: 0,
},
"CSRFTokenHTML": gotemplate.HTML(`<input type="hidden" name="_csrf" value="x">`),
"IsUserPage": false,
@@ -207,6 +240,26 @@ func TestRenderUserProfileTabs(t *testing.T) {
t.Fatalf("account: 屏蔽词输入缺 data-tb-max 注入,want %q", wantBlock)
}
}
// 审核严格度:提问 / 私聊 / 投票三个面板都必须渲染同一份设置,且把每个档位的
// 拦截范围平铺展示出来 —— 要求是用户按下保存前就知道哪些内容会被拦、哪些会放行。
if tab == "question" || tab == "chat" || tab == "vote" {
if got := selectedStrictnessOptions(out); len(got) != 1 || got[0] != "standard" {
t.Fatalf("%s: 审核严格度未按当前档位渲染 selected 状态,got %v", tab, got)
}
for _, key := range []string{
"admin.moderationStrictnessScope.lax",
"admin.moderationStrictnessScope.standard",
"admin.moderationStrictnessScope.strict",
} {
if want := i18n.T("zh-CN", key); !strings.Contains(out, want) {
t.Fatalf("%s: 档位拦截范围缺失 %s,want %q", tab, key, want)
}
}
// 底线拦截是强制项、任何档位不可关闭,界面上必须明确告知。
if want := i18n.T("zh-CN", "admin.moderationStrictnessBaseline"); !strings.Contains(out, want) {
t.Fatalf("%s: 缺少底线拦截说明,want %q", tab, want)
}
}
if tab == "chat" {
// 私聊面板的 number 输入框 min/max 与 conf.Limits 同源
wantChat := fmt.Sprintf(`name="chat_max_messages" class="uk-input uk-width-small" type="number" min="1" max="%d"`, conf.Limits.ChatMaxMessages)
@@ -232,6 +285,46 @@ func TestRenderUserProfileTabs(t *testing.T) {
}
}
// TestRenderProfileStrictnessReflectsEffectiveTier 覆盖「档位回显」这个易错点。
//
// 存量账号的 moderation_strictness 为空值(语义是"跟随站点默认")。若模板直接拿该字段值
// 去和三个 <option> 逐个比较,会全部不匹配 —— 浏览器随后默认选中第一项「宽松」,
// 界面显示的档位就与真实生效的档位(站点默认,通常为严格)相反,把箱主误导到最松的一档。
// 因此回显必须走「最终生效档位」。
func TestRenderProfileStrictnessReflectsEffectiveTier(t *testing.T) {
for _, tc := range []struct {
name string
stored string
want []string
}{
{name: "未设置:回显生效档位,而非浏览器默认选中的宽松", stored: "", want: []string{string(moderation.StrictnessFor(""))}},
{name: "宽松", stored: "lax", want: []string{"lax"}},
{name: "标准", stored: "standard", want: []string{"standard"}},
{name: "严格", stored: "strict", want: []string{"strict"}},
{name: "非法值:回退严格", stored: "bogus", want: []string{string(moderation.StrictnessFor("bogus"))}},
} {
t.Run(tc.name, func(t *testing.T) {
base := profileBaseData()
base["ActiveProfileTab"] = "question"
base["LoggedUser"].(*mockLoggedUser).ModerationStrictness = tc.stored
out, err := renderToString(t, "user/profile", base)
if err != nil {
t.Fatalf("render user/profile failed: %v", err)
}
got := selectedStrictnessOptions(out)
if fmt.Sprint(got) != fmt.Sprint(tc.want) {
t.Fatalf("档位回显不符:stored=%q got=%v want=%v", tc.stored, got, tc.want)
}
// 核心回归点:未设置时绝不能显示为「宽松」,那是浏览器默认选中首项的陷阱。
if tc.stored == "" && len(got) == 1 && got[0] == "lax" {
t.Fatal("未设置档位时回显为「宽松」—— 与真实生效档位相反")
}
})
}
}
// TestRenderUserQuestionList 模拟 /user/questions 的数据渲染,复现 500 错误。
func TestRenderUserQuestionList(t *testing.T) {
type mockItem struct {
+66
View File
@@ -0,0 +1,66 @@
<!--
审核严格度表单部分模板:提问 / 私聊 / 投票 三个功能面板共用同一份设置
(与 user/global-blockwords 相同的渲染约定:dict{ Root: 根数据, Action: 提交地址 })。
这里刻意把「底线拦截」与每个档位的拦截范围全部平铺展示,而不是藏在折叠提示里:
要求是让用户在按下保存之前就清楚知道哪些内容会被拦、哪些会被放行。
档位的判定规则以 internal/security/censor/policy.go 为唯一事实来源,
本模板只负责展示,不重复实现判定。
-->
{{ $root := .Root }}
<div class="uk-margin">
<form method="post" action="{{ .Action }}">
{{ $root.CSRFTokenHTML }}
<legend class="uk-legend">{{t $root.LangCode "admin.moderationStrictness"}}</legend>
<p class="uk-text-meta uk-text-small">{{t $root.LangCode "admin.moderationStrictnessHint"}}</p>
<p class="uk-text-meta uk-text-small">{{t $root.LangCode "admin.moderationStrictnessBaseline"}}</p>
<table class="uk-table uk-table-small uk-table-divider">
<thead>
<tr>
<th>{{t $root.LangCode "admin.moderationStrictnessColumnLevel"}}</th>
<th>{{t $root.LangCode "admin.moderationStrictnessColumnScope"}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{t $root.LangCode "admin.moderationStrictnessOption.lax"}}</td>
<td>{{t $root.LangCode "admin.moderationStrictnessScope.lax"}}</td>
</tr>
<tr>
<td>{{t $root.LangCode "admin.moderationStrictnessOption.standard"}}</td>
<td>{{t $root.LangCode "admin.moderationStrictnessScope.standard"}}</td>
</tr>
<tr>
<td>{{t $root.LangCode "admin.moderationStrictnessOption.strict"}}</td>
<td>{{t $root.LangCode "admin.moderationStrictnessScope.strict"}}</td>
</tr>
</tbody>
</table>
<div class="uk-margin">
<label class="uk-form-label">{{t $root.LangCode "admin.moderationStrictness"}}</label>
{{/* 回显「最终生效」档位而非原始字段值:字段为空(跟随站点默认)时若直接比较,
三个 option 都不匹配,浏览器会默认选中第一项(宽松),与实际生效的严格档不符。
解析走 internal/template 的 moderationStrictness 函数(同一份事实来源)。 */}}
{{ $current := moderationStrictness $root.LoggedUser.ModerationStrictness }}
<select name="strictness" class="uk-select uk-width-medium">
<option value="lax"{{ if eq $current "lax" }} selected{{ end }}>
{{t $root.LangCode "admin.moderationStrictnessOption.lax"}}
</option>
<option value="standard"{{ if eq $current "standard" }} selected{{ end }}>
{{t $root.LangCode "admin.moderationStrictnessOption.standard"}}
</option>
<option value="strict"{{ if eq $current "strict" }} selected{{ end }}>
{{t $root.LangCode "admin.moderationStrictnessOption.strict"}}
</option>
</select>
<span class="uk-text-muted uk-text-small">{{t $root.LangCode "admin.moderationStrictnessUnsetHint"}}</span>
</div>
<div class="tb-card-save">
<button type="submit" class="uk-button uk-button-primary">{{t $root.LangCode "admin.updateModerationStrictness"}}</button>
</div>
</form>
</div>
+8
View File
@@ -834,6 +834,9 @@
<!-- 全局屏蔽词:三个功能面板共用同一份(见 user/global-blockwords 部分模板) -->
{{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=question") }}
<!-- 审核严格度:三个功能面板共用同一份(见 user/moderation-strictness 部分模板) -->
{{ template "user/moderation-strictness" (dict "Root" $ "Action" "/user/moderation-strictness/update?tab=question") }}
<hr>
<div class="uk-margin">
<form method="post" action="/user/profile/update-ask-password">
@@ -882,6 +885,8 @@
<hr>
<!-- 全局屏蔽词:提问/私聊/投票三面板共用同一份(见 user/global-blockwords 部分模板) -->
{{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=chat") }}
<!-- 审核严格度:同样三面板共用一份(见 user/moderation-strictness 部分模板) -->
{{ template "user/moderation-strictness" (dict "Root" $ "Action" "/user/moderation-strictness/update?tab=chat") }}
</div><!-- /私聊功能管理面板 -->
<!-- ===================== 投票功能管理面板 ===================== -->
@@ -891,6 +896,9 @@
<hr>
<!-- 全局屏蔽词:三个功能面板共用同一份(见 user/global-blockwords 部分模板) -->
{{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=vote") }}
<!-- 审核严格度:三个功能面板共用同一份(见 user/moderation-strictness 部分模板) -->
{{ template "user/moderation-strictness" (dict "Root" $ "Action" "/user/moderation-strictness/update?tab=vote") }}
</div><!-- /投票功能管理面板 -->
<script>