From 0181fc5c6633f626aae0ef2e88311372f264c783 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Thu, 22 Oct 2026 18:26:30 +0800 Subject: [PATCH] fix templates v3 --- .workbuddy/memory/2026-10-22.md | 31 +++++++++++++++++++ route/question/question.go | 10 +++++- templates/question/item.html | 4 +-- templates/question_action_test.go | 51 +++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 templates/question_action_test.go diff --git a/.workbuddy/memory/2026-10-22.md b/.workbuddy/memory/2026-10-22.md index 788d0d5..655a822 100644 --- a/.workbuddy/memory/2026-10-22.md +++ b/.workbuddy/memory/2026-10-22.md @@ -283,3 +283,34 @@ - 测试:conf 侧 7 组(语言回退/归一化/东八区/时区覆盖/无效时区)、 templates 侧 2 条渲染断言(zh-CN 与 en 只出现各自文案、窗口外不渲染 `tb-announce`)。 - 未改:`NextUnread` 仍不看后台筛选(产品取舍);画板页公告问题按用户确认忽略(画板已下线)。 + +## 测试站部署验证(box-dev-api) + +- 结论:部署的确实是最新代码。判据:`base.css?v=20261022c`、CSS 含 `.tb-announce-wrap`、 + `--tb-primary:#0e6fd0`、`themes/new-sakura.css` 含 `backdrop-filter`。 +- **公告分语言在线上生效**:同一页面切 `/i18n/{zh-CN,zh-TW,en,ja}` 拿到四套不同文案, + 链接渲染成 ``。 +- 移动端(CDP `Emulation.setDeviceMetricsOverride` 390×844, mobile:true): + `scrollWidth == innerWidth == 390`,无横向溢出,公告块 right=390 贴容器边 —— 正常。 + ⚠️ 纯 `chrome --headless --screenshot --window-size=390` 的截图会让人误判「被切」, + 真要量布局用 CDP + mobile:true,别只看图。 +- 外部验证不到的三项:时区解析(看启动日志 `zone=Asia/Shanghai` + `+08:00`)、 + 复合索引(`SELECT indexname FROM pg_indexes WHERE tablename='questions'`)、 + 「下一条未读」(需箱主登录,且不能靠 POST 探测 —— 路由缺失会落到蜜罐返回 200 假登录页)。 +- 偶发一次 CF 520(同 URL 复测 3 次均 200),未复现,非部署问题。 +- 本机 curl 走 `http_proxy=127.0.0.1:58589`,node 直连本地调试端口要 `NO_PROXY=127.0.0.1`。 + +## 登录态实测(自建测试账号)→ 又抓到一个 P1 残留 + +- 账号:`tbunread@example.com` / TbUnread!2026(脚本 `Temp/tb_unread_check.py`,注册→发提问→登录→走流程→删题)。 +- 🔴 **模板 URL 上下文会把 `?{{ .NextQuery }}` 整段 percent-encode**: + 实测渲染出 `action=".../answer?from%3dadmin%26read%3dunread%26sort%3dnew"`, + `ctx.Query("from")` 永远为空 ⇒ 浏览器提交时 P1 修复失效。之前只测了 handler 层,没测渲染结果。 + 本地 html/template 实验确认:`action="{{ .A }}"`(值是完整 URL)→ 只把 `&` 转 `&`; + `action="...?{{ .B }}"`(值是查询片段)→ `%3d`/`%26`。**以后 query 一律在 handler 拼完整 URL。** +- 修法:Questioner 里生成 `AnswerAction` / `NextUnreadAction`(完整 URL),模板直接引用; + 守门 `templates/question_action_test.go`(断言无 `%3d`/`%26` 且有 `&sort=new`)。 +- 顺带确认(线上):单题页「回答并进入下一条未读」跳转、页脚「下一条未读」跳转、 + 全部读完的「没有未读提问了」提示 —— 三条行为都正确,`Location` 带 `from=admin&read=unread&sort=new`。 +- 断言坑:页面上有 `/user/questions`(导航)和 `/user/questions?from=admin&…`(页脚返回)两个链接, + 用 `re.search` 取第一个会误判「返回丢参数」。要按 href 全量匹配。 diff --git a/route/question/question.go b/route/question/question.go index f313d45..b25e013 100644 --- a/route/question/question.go +++ b/route/question/question.go @@ -127,13 +127,21 @@ func Questioner(ctx context.Context, pageUser *db.User) { // 从后台返回时,把进入时携带的筛选参数(sort/status/type/read/q)原样回传, // 使"返回"跳回带筛选条件的后台列表,避免丢失已选的筛选/搜索条件。 // 「下一条未读」同样带上这份参数,否则答完一条后"返回"就掉回箱主页了。 + // 表单 action 必须在这里拼成「完整 URL」再交给模板:写成 action="...?{{ .NextQuery }}" + // 时 html/template 会把这段当成 URL 的查询值编码,= 与 & 变成 %3d / %26, + // 服务端再也解析不出来源(实测过)。完整 URL 只会把 & 转成 &,浏览器会还原。 if backQuery := adminBackQuery(ctx); backFrom == "admin" { backTo := "/user/questions" + answerAction := fmt.Sprintf("/_/%s/%d/answer", pageUser.Domain, question.ID) + nextUnreadAction := fmt.Sprintf("/_/%s/%d/next-unread", pageUser.Domain, question.ID) if backQuery != "" { backTo += "?" + backQuery + answerAction += "?" + backQuery + nextUnreadAction += "?" + backQuery } ctx.Data["BackTo"] = backTo - ctx.Data["NextQuery"] = backQuery + ctx.Data["AnswerAction"] = answerAction + ctx.Data["NextUnreadAction"] = nextUnreadAction } ctx.Map(question) diff --git a/templates/question/item.html b/templates/question/item.html index 10b7606..fce9330 100644 --- a/templates/question/item.html +++ b/templates/question/item.html @@ -276,7 +276,7 @@

{{t .LangCode "qitem.answerTitle"}}
-
+ {{ .CSRFTokenHTML }}