restrict v7
This commit is contained in:
@@ -274,3 +274,51 @@ admin6 当前处于 L2(到期次日),必要时 `unrestrict` 解除。
|
|||||||
(项目自带模板语法检查工具,改模板后必跑)。
|
(项目自带模板语法检查工具,改模板后必跑)。
|
||||||
|
|
||||||
未做:箱主列表的「只看被举报」筛选(目前只有 badge);举报驳回/成立的处置动作(目前 status 只有 pending)。
|
未做:箱主列表的「只看被举报」筛选(目前只有 badge);举报驳回/成立的处置动作(目前 status 只有 pending)。
|
||||||
|
|
||||||
|
### 举报链线上验证:4/4 通过
|
||||||
|
|
||||||
|
- 入口:单题页 `/73` 出现「举报这条内容」+ form action `/_/admin6/73/report`;
|
||||||
|
- 首次举报:POST 302 → flash「已收到举报,感谢反馈」;
|
||||||
|
- 同 IP 重复:同样 302 + 同样文案,无失败提示(幂等);
|
||||||
|
- 箱主 `/user/questions`:出现「被举报 **1** 次」(多次重复提交后计数仍为 1,反证幂等生效)。
|
||||||
|
|
||||||
|
**过程教训**:中途 python urllib 报「读超时 25s」,一度怀疑服务端挂起。实测 curl 同一接口
|
||||||
|
1.6~1.7s 正常返回 302,`location=/_/admin6/73`,手动 GET 也 200。真因是 python 走本地代理
|
||||||
|
(`https_proxy=127.0.0.1:54881`)偶发挂起 + `/tmp` 路径在 Windows python 与 MSYS 之间不互通。
|
||||||
|
已写进 MEMORY.md:**验证线上行为优先用 curl**。
|
||||||
|
|
||||||
|
### 告警触发验证(进行中)
|
||||||
|
|
||||||
|
直接 SQL INSERT 不会触发告警(`notifyAbuseIfNeeded` 只在 HTTP 举报 `created=true` 时调用)。
|
||||||
|
正确姿势:SQL 补到 7 条 → 删掉自己那条旧记录 → 再用 HTTP 提交一次,使 count 恰好到 8。
|
||||||
|
已按此执行(curl 提交返回 302 + 「已收到举报」),等待确认日志
|
||||||
|
`Abuse alert mail sent` / `Failed to send abuse alert mail` / 什么都没有(= `security_alert_email` 为空)。
|
||||||
|
|
||||||
|
### 邮件告警验证通过
|
||||||
|
|
||||||
|
配置 `security_alert_email` + 重启后,补到 7 条再 HTTP 提交第 8 条,邮件成功收到;测试数据已清理。
|
||||||
|
举报链至此全部闭环。
|
||||||
|
|
||||||
|
### 静默丢失专项清理(用户要求的那项)
|
||||||
|
|
||||||
|
静态扫描 `route/**/*.go`:flash 调用后第一个 `Success(`/`Redirect(` 判定,共 **9 处**缺陷
|
||||||
|
(`follow_up.go` 2、`page.go` 1、`question.go` 1、`profile.go` 5),全部改为
|
||||||
|
`SetError(err[, f])` / `SetInternalError()`。`Set*Flash + Redirect` 112 处正常,未动;
|
||||||
|
`vote.go:702/704` 是扫描误报(其后 switch 之后才是 Redirect)。
|
||||||
|
`go build ./...` EXIT=0;`go test ./internal/db/ ./internal/i18n/` 全绿(36s)。
|
||||||
|
⚠️ 这 9 处只做了编译与静态验证,未逐条构造触发条件(需传超大图/失效追问 id),
|
||||||
|
部署后实际遇到对应错误时观察提示是否可见。
|
||||||
|
|
||||||
|
### 举报加验证码 + 箱主列表「只看被举报」筛选
|
||||||
|
|
||||||
|
- 举报验证码:`form.ReportQuestion` 加 `Recaptcha`(**刻意不加 valid:required**,空值交给
|
||||||
|
`captcha.Verify` 判定,否则 Bind 失败会被统一提示成「请选择一个举报理由」);
|
||||||
|
`Report` handler 签名加 `recaptcha recaptcha.RecaptchaV3`,调 `captcha.Verify`(与提问/私聊同源)。
|
||||||
|
模板按 `CaptchaType == "math"` 双分支:math 用隐藏域 + `.math-captcha-trigger`(header 的全局弹窗
|
||||||
|
用 `btn.closest('form')` 定位表单、`form.querySelector('[name="g-recaptcha-response"]')` 回填,
|
||||||
|
**不依赖 id**,所以举报表单可直接复用);非 math 用 `.g-recaptcha` + 自定义 callback `tbReportCaptchaOk`。
|
||||||
|
- 筛选:`/user/questions?reported=reported`,复用已查出的 `reportCounts` 在内存过滤,
|
||||||
|
新增 `SelectedReported` + 模板第 5 个下拉(reportFilter / reportedOnly)+ 4 语言文案。
|
||||||
|
|
||||||
|
验证:`go build ./...` EXIT=0;`go run ./dev/checktpl` → all templates syntax OK;
|
||||||
|
`go test ./internal/i18n/` 通过。**未部署**,且加了验证码之后原来的 curl 测试方式要改成先算题再提交。
|
||||||
|
|||||||
@@ -39,6 +39,10 @@
|
|||||||
- ⚠️ **探测端口 200 可能是别的进程**:8899 被本机无关进程占用,服务 panic 后 curl 仍返回 200,
|
- ⚠️ **探测端口 200 可能是别的进程**:8899 被本机无关进程占用,服务 panic 后 curl 仍返回 200,
|
||||||
差点误判成「改动没生效」。起服务后必须看任务日志确认 `Starting web server` 且无 panic,
|
差点误判成「改动没生效」。起服务后必须看任务日志确认 `Starting web server` 且无 panic,
|
||||||
再信任 curl 结果;端口探测本身不能证明是自己的服务。
|
再信任 curl 结果;端口探测本身不能证明是自己的服务。
|
||||||
|
- ⚠️ **本机 python urllib 不稳定**:`https_proxy=127.0.0.1:54881` 下偶发「响应读超时」
|
||||||
|
(同一时刻 curl 完全正常)。曾据此误判「举报接口卡死 25 秒」,实际是客户端问题。
|
||||||
|
**验证线上行为优先用 curl**;必须用 python 时加短超时 + 重试,且不要用 `/tmp` 路径
|
||||||
|
(Windows python 把 `/tmp/x` 解析成 `C:\tmp\x`,而 MSYS curl 写的是 MSYS 的 `/tmp`,两者不通)。
|
||||||
|
|
||||||
## 图片上传与编码(`internal/storage/s3.go`)
|
## 图片上传与编码(`internal/storage/s3.go`)
|
||||||
|
|
||||||
@@ -98,8 +102,11 @@
|
|||||||
2026-10-18 实测:L3 拒绝发布时箱主完全看不到提示,改 `SetError` 后正常显示。
|
2026-10-18 实测:L3 拒绝发布时箱主完全看不到提示,改 `SetError` 后正常显示。
|
||||||
- 项目里仍有若干 `SetErrorFlash + Success("question/item")` 的既有组合(`follow_up.go` 的 errNotFound、
|
- 项目里仍有若干 `SetErrorFlash + Success("question/item")` 的既有组合(`follow_up.go` 的 errNotFound、
|
||||||
`question.go` 的上传图片过大等),同属该缺陷,遇到时顺手改成 `SetError`。
|
`question.go` 的上传图片过大等),同属该缺陷,遇到时顺手改成 `SetError`。
|
||||||
- **TODO(用户 2026-10-18 明确要求后续处理)**:全量排查 `SetErrorFlash + Success(...)` 组合并统一改为
|
- ✅ **2026-10-18 已完成全量清理**:扫描 `route/**/*.go` 中 `Set*Flash` / `SetInternalErrorFlash`
|
||||||
`SetError`,做一轮专项清理。
|
后 8 行内跟 `ctx.Success(...)`(而非 `Redirect`)的位置,共 **9 处**:`follow_up.go` 2 处、
|
||||||
|
`page.go` 1 处、`question.go` 1 处、`profile.go` 5 处,全部改为 `SetError(err[, f])` / `SetInternalError()`。
|
||||||
|
`Set*Flash + Redirect` 112 处属正常模式,未动。`vote.go:702/704` 是扫描器误报(其后是 Redirect)。
|
||||||
|
扫描脚本思路可复用:逐行找 flash 调用 → 向下找第一个 `Success(`/`Redirect(` → 前者即缺陷。
|
||||||
|
|
||||||
## 表单校验
|
## 表单校验
|
||||||
|
|
||||||
|
|||||||
@@ -46,4 +46,8 @@ type AnswerFollowUpQuestion struct {
|
|||||||
// 不做自由文本 —— 否则举报框本身就成了新的辱骂输入口。
|
// 不做自由文本 —— 否则举报框本身就成了新的辱骂输入口。
|
||||||
type ReportQuestion struct {
|
type ReportQuestion struct {
|
||||||
Reason string `form:"reason" valid:"required;maxlen:32" label:"举报理由" msg:"report.errInvalidReason"`
|
Reason string `form:"reason" valid:"required;maxlen:32" label:"举报理由" msg:"report.errInvalidReason"`
|
||||||
|
// Recaptcha 由算数验证码弹窗回填(格式 id:答案)。
|
||||||
|
// 刻意不加 valid:required:空值交由 captcha.Verify 判定,
|
||||||
|
// 否则 Bind 失败会被上层统一提示成「请选择一个举报理由」,误导用户。
|
||||||
|
Recaptcha string `form:"g-recaptcha-response"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ var enDict = map[string]string{
|
|||||||
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
||||||
"qlist.reported": "%d reports",
|
"qlist.reported": "%d reports",
|
||||||
"qlist.reportedShort": "!%d",
|
"qlist.reportedShort": "!%d",
|
||||||
|
"qlist.reportFilter": "Reports",
|
||||||
|
"qlist.reportedOnly": "Reported only",
|
||||||
|
|
||||||
// Anonymous voting
|
// Anonymous voting
|
||||||
"nav.votes": "Polls",
|
"nav.votes": "Polls",
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ var jaDict = map[string]string{
|
|||||||
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
||||||
"qlist.reported": "%d 件の報告",
|
"qlist.reported": "%d 件の報告",
|
||||||
"qlist.reportedShort": "報%d",
|
"qlist.reportedShort": "報%d",
|
||||||
|
"qlist.reportFilter": "報告",
|
||||||
|
"qlist.reportedOnly": "報告ありのみ",
|
||||||
|
|
||||||
// 匿名投票
|
// 匿名投票
|
||||||
"nav.votes": "投票",
|
"nav.votes": "投票",
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ var zhCNDict = map[string]string{
|
|||||||
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
||||||
"qlist.reported": "被举报 %d 次",
|
"qlist.reported": "被举报 %d 次",
|
||||||
"qlist.reportedShort": "报%d",
|
"qlist.reportedShort": "报%d",
|
||||||
|
"qlist.reportFilter": "举报",
|
||||||
|
"qlist.reportedOnly": "仅被举报",
|
||||||
|
|
||||||
// 匿名投票
|
// 匿名投票
|
||||||
"nav.votes": "投票",
|
"nav.votes": "投票",
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ var zhTWDict = map[string]string{
|
|||||||
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
// 箱主侧举报标记(只给数量,不给举报人 IP)
|
||||||
"qlist.reported": "被檢舉 %d 次",
|
"qlist.reported": "被檢舉 %d 次",
|
||||||
"qlist.reportedShort": "檢%d",
|
"qlist.reportedShort": "檢%d",
|
||||||
|
"qlist.reportFilter": "檢舉",
|
||||||
|
"qlist.reportedOnly": "僅被檢舉",
|
||||||
|
|
||||||
// 匿名投票
|
// 匿名投票
|
||||||
"nav.votes": "投票",
|
"nav.votes": "投票",
|
||||||
|
|||||||
@@ -185,12 +185,13 @@ func AnswerFollowUp(ctx context.Context, pageUser *db.User, question *db.Questio
|
|||||||
|
|
||||||
if err := db.FollowUps.UpdateAnswerByID(ctx.Request().Context(), question.ID, followUpID, f.Answer); err != nil {
|
if err := db.FollowUps.UpdateAnswerByID(ctx.Request().Context(), question.ID, followUpID, f.Answer); err != nil {
|
||||||
if errors.Is(err, db.ErrFollowUpNotFound) {
|
if errors.Is(err, db.ErrFollowUpNotFound) {
|
||||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "followup.errNotFound"))
|
// 同请求渲染需用 SetError(SetErrorFlash 要等下一次请求才显示)。
|
||||||
|
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "followup.errNotFound")), f)
|
||||||
ctx.Success("question/item")
|
ctx.Success("question/item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to answer follow up")
|
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to answer follow up")
|
||||||
ctx.SetInternalErrorFlash()
|
ctx.SetInternalError()
|
||||||
ctx.Success("question/item")
|
ctx.Success("question/item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -623,7 +623,7 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
|
|||||||
UploaderUserID: askerUserID,
|
UploaderUserID: askerUserID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if errors.Is(err, ErrUploadImageSizeTooLarge) || errors.Is(err, imagecheck.ErrImageTooLarge) {
|
if errors.Is(err, ErrUploadImageSizeTooLarge) || errors.Is(err, imagecheck.ErrImageTooLarge) {
|
||||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "question.errImageTooLarge"))
|
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "question.errImageTooLarge")), f)
|
||||||
ctx.Success("question/list")
|
ctx.Success("question/list")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ func PublishAnswer(ctx context.Context, pageUser *db.User, question *db.Question
|
|||||||
IsDeletingPrevious: true,
|
IsDeletingPrevious: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if errors.Is(err, ErrUploadImageSizeTooLarge) || errors.Is(err, imagecheck.ErrImageTooLarge) {
|
if errors.Is(err, ErrUploadImageSizeTooLarge) || errors.Is(err, imagecheck.ErrImageTooLarge) {
|
||||||
ctx.SetErrorFlash(err.Error())
|
ctx.SetError(err, f)
|
||||||
ctx.Success("question/item")
|
ctx.Success("question/item")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ package question
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/flamego/recaptcha"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/tamakyi/TamaBox/internal/captcha"
|
||||||
"github.com/tamakyi/TamaBox/internal/context"
|
"github.com/tamakyi/TamaBox/internal/context"
|
||||||
"github.com/tamakyi/TamaBox/internal/db"
|
"github.com/tamakyi/TamaBox/internal/db"
|
||||||
"github.com/tamakyi/TamaBox/internal/form"
|
"github.com/tamakyi/TamaBox/internal/form"
|
||||||
@@ -23,9 +25,11 @@ import (
|
|||||||
// - 理由只接受固定枚举,不做自由文本 —— 否则举报框本身成了新的辱骂输入口;
|
// - 理由只接受固定枚举,不做自由文本 —— 否则举报框本身成了新的辱骂输入口;
|
||||||
// - 同 IP 同内容重复举报**幂等成功**,且提示与首次完全一致,
|
// - 同 IP 同内容重复举报**幂等成功**,且提示与首次完全一致,
|
||||||
// 否则「已举报过」会变成「这条内容是否存在」的探测接口;
|
// 否则「已举报过」会变成「这条内容是否存在」的探测接口;
|
||||||
|
// - **要验证码**:举报是匿名入口,不设成本等于给批量刷量开门(同 IP 唯一索引只挡单来源,
|
||||||
|
// 换 IP 即可绕过);这里与提问/私聊共用同一套验证码通道;
|
||||||
// - 举报只是信号:处置仍由箱主(后台删除/设私密)或运维(TamaBox restrict)完成,
|
// - 举报只是信号:处置仍由箱主(后台删除/设私密)或运维(TamaBox restrict)完成,
|
||||||
// 本 handler 不改变任何内容可见性。
|
// 本 handler 不改变任何内容可见性。
|
||||||
func Report(ctx context.Context, pageUser *db.User, question *db.Question, f form.ReportQuestion) {
|
func Report(ctx context.Context, pageUser *db.User, question *db.Question, f form.ReportQuestion, recaptcha recaptcha.RecaptchaV3) {
|
||||||
back := fmt.Sprintf("/_/%s/%d", pageUser.Domain, question.ID)
|
back := fmt.Sprintf("/_/%s/%d", pageUser.Domain, question.ID)
|
||||||
|
|
||||||
if ctx.HasError() || !db.IsValidReportReason(f.Reason) {
|
if ctx.HasError() || !db.IsValidReportReason(f.Reason) {
|
||||||
@@ -34,6 +38,20 @@ func Report(ctx context.Context, pageUser *db.User, question *db.Question, f for
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证码:与提问 / 私聊同源(recaptcha / turnstile / math 三种由配置决定)。
|
||||||
|
ok, err := captcha.Verify(f.Recaptcha, shared.ResolveClientIP(ctx.Request().Request), ctx.Session, recaptcha, ctx.Lang())
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to check captcha for report")
|
||||||
|
ctx.SetErrorFlash(err.Error())
|
||||||
|
ctx.Redirect(back)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "captcha.error"))
|
||||||
|
ctx.Redirect(back)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 兜底:只有公开且已回答的内容可被举报(Questioner 中间件已挡住私密/未回答)。
|
// 兜底:只有公开且已回答的内容可被举报(Questioner 中间件已挡住私密/未回答)。
|
||||||
if question.Answer == "" || question.IsPrivate {
|
if question.Answer == "" || question.IsPrivate {
|
||||||
ctx.Redirect(back)
|
ctx.Redirect(back)
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ func UpdateProfile(ctx context.Context, f form.UpdateProfile) {
|
|||||||
uploadedURL, err := storage.UploadAvatarToS3(avatarFile, avatarFileHeader)
|
uploadedURL, err := storage.UploadAvatarToS3(avatarFile, avatarFileHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, imagecheck.ErrImageTooLarge) {
|
if errors.Is(err, imagecheck.ErrImageTooLarge) {
|
||||||
ctx.SetErrorFlash(err.Error())
|
ctx.SetError(err, f)
|
||||||
ctx.Success("user/profile")
|
ctx.Success("user/profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, imagecheck.ErrBadAspectRatio) {
|
if errors.Is(err, imagecheck.ErrBadAspectRatio) {
|
||||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "profile.err.avatarRatio"))
|
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "profile.err.avatarRatio")), f)
|
||||||
ctx.Success("user/profile")
|
ctx.Success("user/profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -122,12 +122,12 @@ func UpdateProfile(ctx context.Context, f form.UpdateProfile) {
|
|||||||
uploadedURL, err := storage.UploadBackgroundToS3(backgroundFile, backgroundFileHeader)
|
uploadedURL, err := storage.UploadBackgroundToS3(backgroundFile, backgroundFileHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, imagecheck.ErrImageTooLarge) {
|
if errors.Is(err, imagecheck.ErrImageTooLarge) {
|
||||||
ctx.SetErrorFlash(err.Error())
|
ctx.SetError(err, f)
|
||||||
ctx.Success("user/profile")
|
ctx.Success("user/profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, imagecheck.ErrBadAspectRatio) {
|
if errors.Is(err, imagecheck.ErrBadAspectRatio) {
|
||||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "profile.err.backgroundRatio"))
|
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "profile.err.backgroundRatio")), f)
|
||||||
ctx.Success("user/profile")
|
ctx.Success("user/profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func UpdateProfile(ctx context.Context, f form.UpdateProfile) {
|
|||||||
uploadedURL, err := storage.UploadPictureToS3(backgroundimageFile, backgroundimageFileHeader)
|
uploadedURL, err := storage.UploadPictureToS3(backgroundimageFile, backgroundimageFileHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, imagecheck.ErrImageTooLarge) {
|
if errors.Is(err, imagecheck.ErrImageTooLarge) {
|
||||||
ctx.SetErrorFlash(err.Error())
|
ctx.SetError(err, f)
|
||||||
ctx.Success("user/profile")
|
ctx.Success("user/profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ func QuestionList(ctx context.Context) {
|
|||||||
if statusParam != "all" && statusParam != "answered" && statusParam != "unanswered" {
|
if statusParam != "all" && statusParam != "answered" && statusParam != "unanswered" {
|
||||||
statusParam = "all"
|
statusParam = "all"
|
||||||
}
|
}
|
||||||
|
// reported 筛选:只看当前有「待处理举报」的提问。
|
||||||
|
// 依赖下方 reportCounts(同一份查询复用),不额外打库。
|
||||||
|
reportedParam := ctx.Query("reported")
|
||||||
|
if reportedParam != "reported" {
|
||||||
|
reportedParam = "all"
|
||||||
|
}
|
||||||
typeParam := ctx.Query("type")
|
typeParam := ctx.Query("type")
|
||||||
// 私聊已独立成"私聊"标签页(见 ReceivedChats),不再参与提问列表的类型筛选;
|
// 私聊已独立成"私聊"标签页(见 ReceivedChats),不再参与提问列表的类型筛选;
|
||||||
// 旧的 type=chat 链接统一归一为"全部",避免空列表。
|
// 旧的 type=chat 链接统一归一为"全部",避免空列表。
|
||||||
@@ -196,6 +202,9 @@ func QuestionList(ctx context.Context) {
|
|||||||
if readStatus == 2 && isRead {
|
if readStatus == 2 && isRead {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if reportedParam == "reported" && reportCounts[q.ID] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
itemType := "question_public"
|
itemType := "question_public"
|
||||||
if q.IsPrivate {
|
if q.IsPrivate {
|
||||||
itemType = "question_private"
|
itemType = "question_private"
|
||||||
@@ -352,6 +361,7 @@ func QuestionList(ctx context.Context) {
|
|||||||
ctx.Data["SelectedStatus"] = statusParam
|
ctx.Data["SelectedStatus"] = statusParam
|
||||||
ctx.Data["SelectedRead"] = readParam
|
ctx.Data["SelectedRead"] = readParam
|
||||||
ctx.Data["SelectedType"] = typeParam
|
ctx.Data["SelectedType"] = typeParam
|
||||||
|
ctx.Data["SelectedReported"] = reportedParam
|
||||||
|
|
||||||
// 我发起的私聊 — 同样使用批量查询与分页(复用 chat_cursor 游标)
|
// 我发起的私聊 — 同样使用批量查询与分页(复用 chat_cursor 游标)
|
||||||
askerConvs, err := db.Conversations.ListByAsker(ctx.Request().Context(), ctx.User.ID, chatPageSize, int(chatOffset))
|
askerConvs, err := db.Conversations.ListByAsker(ctx.Request().Context(), ctx.User.ID, chatPageSize, int(chatOffset))
|
||||||
|
|||||||
@@ -99,7 +99,19 @@
|
|||||||
<option value="divisive">{{t .LangCode "report.reason.divisive"}}</option>
|
<option value="divisive">{{t .LangCode "report.reason.divisive"}}</option>
|
||||||
<option value="other">{{t .LangCode "report.reason.other"}}</option>
|
<option value="other">{{t .LangCode "report.reason.other"}}</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="uk-button uk-button-default uk-button-small">{{t .LangCode "report.submit"}}</button>
|
{{ if eq .CaptchaType "math" }}
|
||||||
|
<!-- 算数验证码:点击后由 header 的全局弹窗接管,答案回填到本表单的隐藏域 -->
|
||||||
|
<input type="hidden" name="g-recaptcha-response" value="">
|
||||||
|
<button type="button" class="uk-button uk-button-default uk-button-small math-captcha-trigger">{{t .LangCode "report.submit"}}</button>
|
||||||
|
{{ else }}
|
||||||
|
<button type="submit" class="uk-button uk-button-default uk-button-small g-recaptcha" data-sitekey="{{.RecaptchaSiteKey}}" data-callback="tbReportCaptchaOk">{{t .LangCode "report.submit"}}</button>
|
||||||
|
<script>
|
||||||
|
function tbReportCaptchaOk() {
|
||||||
|
var f = document.querySelector('form[action$="/report"]');
|
||||||
|
if (f) { f.submit(); }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
<option value="private_question" {{if eq .SelectedType "private_question"}}selected{{end}}>{{t .LangCode "qlist.privateQuestion"}}</option>
|
<option value="private_question" {{if eq .SelectedType "private_question"}}selected{{end}}>{{t .LangCode "qlist.privateQuestion"}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="uk-margin-small-right" style="width: 120px;">
|
||||||
|
<label class="uk-form-label uk-text-small">{{t .LangCode "qlist.reportFilter"}}</label>
|
||||||
|
<select name="reported" class="uk-select uk-form-small" onchange="this.form.submit()" style="width: 100%;">
|
||||||
|
<option value="all" {{if eq .SelectedReported "all"}}selected{{end}}>{{t .LangCode "qlist.all"}}</option>
|
||||||
|
<option value="reported" {{if eq .SelectedReported "reported"}}selected{{end}}>{{t .LangCode "qlist.reportedOnly"}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div style="width: 120px;">
|
<div style="width: 120px;">
|
||||||
<label class="uk-form-label uk-text-small">{{t .LangCode "qlist.readStatus"}}</label>
|
<label class="uk-form-label uk-text-small">{{t .LangCode "qlist.readStatus"}}</label>
|
||||||
<select name="read" class="uk-select uk-form-small" onchange="this.form.submit()" style="width: 100%;">
|
<select name="read" class="uk-select uk-form-small" onchange="this.form.submit()" style="width: 100%;">
|
||||||
|
|||||||
Reference in New Issue
Block a user