fix templates v3
This commit is contained in:
@@ -283,3 +283,34 @@
|
|||||||
- 测试:conf 侧 7 组(语言回退/归一化/东八区/时区覆盖/无效时区)、
|
- 测试:conf 侧 7 组(语言回退/归一化/东八区/时区覆盖/无效时区)、
|
||||||
templates 侧 2 条渲染断言(zh-CN 与 en 只出现各自文案、窗口外不渲染 `tb-announce`)。
|
templates 侧 2 条渲染断言(zh-CN 与 en 只出现各自文案、窗口外不渲染 `tb-announce`)。
|
||||||
- 未改:`NextUnread` 仍不看后台筛选(产品取舍);画板页公告问题按用户确认忽略(画板已下线)。
|
- 未改:`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}` 拿到四套不同文案,
|
||||||
|
链接渲染成 `<a target="_blank" rel="noopener noreferrer nofollow">`。
|
||||||
|
- 移动端(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<ts>@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 全量匹配。
|
||||||
|
|||||||
@@ -127,13 +127,21 @@ func Questioner(ctx context.Context, pageUser *db.User) {
|
|||||||
// 从后台返回时,把进入时携带的筛选参数(sort/status/type/read/q)原样回传,
|
// 从后台返回时,把进入时携带的筛选参数(sort/status/type/read/q)原样回传,
|
||||||
// 使"返回"跳回带筛选条件的后台列表,避免丢失已选的筛选/搜索条件。
|
// 使"返回"跳回带筛选条件的后台列表,避免丢失已选的筛选/搜索条件。
|
||||||
// 「下一条未读」同样带上这份参数,否则答完一条后"返回"就掉回箱主页了。
|
// 「下一条未读」同样带上这份参数,否则答完一条后"返回"就掉回箱主页了。
|
||||||
|
// 表单 action 必须在这里拼成「完整 URL」再交给模板:写成 action="...?{{ .NextQuery }}"
|
||||||
|
// 时 html/template 会把这段当成 URL 的查询值编码,= 与 & 变成 %3d / %26,
|
||||||
|
// 服务端再也解析不出来源(实测过)。完整 URL 只会把 & 转成 &,浏览器会还原。
|
||||||
if backQuery := adminBackQuery(ctx); backFrom == "admin" {
|
if backQuery := adminBackQuery(ctx); backFrom == "admin" {
|
||||||
backTo := "/user/questions"
|
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 != "" {
|
if backQuery != "" {
|
||||||
backTo += "?" + backQuery
|
backTo += "?" + backQuery
|
||||||
|
answerAction += "?" + backQuery
|
||||||
|
nextUnreadAction += "?" + backQuery
|
||||||
}
|
}
|
||||||
ctx.Data["BackTo"] = backTo
|
ctx.Data["BackTo"] = backTo
|
||||||
ctx.Data["NextQuery"] = backQuery
|
ctx.Data["AnswerAction"] = answerAction
|
||||||
|
ctx.Data["NextUnreadAction"] = nextUnreadAction
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Map(question)
|
ctx.Map(question)
|
||||||
|
|||||||
@@ -276,7 +276,7 @@
|
|||||||
<div class="uk-card-body uk-padding-remove-top">
|
<div class="uk-card-body uk-padding-remove-top">
|
||||||
<hr>
|
<hr>
|
||||||
<h5 class="uk-text-center">{{t .LangCode "qitem.answerTitle"}}</h5>
|
<h5 class="uk-text-center">{{t .LangCode "qitem.answerTitle"}}</h5>
|
||||||
<form id="tb-answer-form" method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/answer{{ if .NextQuery }}?{{ .NextQuery }}{{ end }}" enctype="multipart/form-data">
|
<form id="tb-answer-form" method="post" action="{{ if .AnswerAction }}{{ .AnswerAction }}{{ else }}/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/answer{{ end }}" enctype="multipart/form-data">
|
||||||
{{ .CSRFTokenHTML }}
|
{{ .CSRFTokenHTML }}
|
||||||
<div class="uk-margin uk-text-center">
|
<div class="uk-margin uk-text-center">
|
||||||
<textarea name="answer" class="uk-textarea" rows="5" maxlength="{{ .Limits.AnswerContentLen }}"
|
<textarea name="answer" class="uk-textarea" rows="5" maxlength="{{ .Limits.AnswerContentLen }}"
|
||||||
@@ -436,7 +436,7 @@
|
|||||||
{{ if and .CanDelete .NextUnreadID }}
|
{{ if and .CanDelete .NextUnreadID }}
|
||||||
<!-- 下一条未读:标记本条已读并跳转,会改状态,故用 POST 表单而非链接 -->
|
<!-- 下一条未读:标记本条已读并跳转,会改状态,故用 POST 表单而非链接 -->
|
||||||
<span class="tb-foot-sep" aria-hidden="true">·</span>
|
<span class="tb-foot-sep" aria-hidden="true">·</span>
|
||||||
<form class="uk-display-inline" method="post" action="/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/next-unread{{ if .NextQuery }}?{{ .NextQuery }}{{ end }}">
|
<form class="uk-display-inline" method="post" action="{{ if .NextUnreadAction }}{{ .NextUnreadAction }}{{ else }}/_/{{ .PageUser.Domain }}/{{ .Question.ID }}/next-unread{{ end }}">
|
||||||
{{ .CSRFTokenHTML }}
|
{{ .CSRFTokenHTML }}
|
||||||
<button type="submit" class="uk-button uk-button-link" title="{{t .LangCode "qitem.nextUnreadTitle"}}">{{t .LangCode "qitem.nextUnread"}} ›</button>
|
<button type="submit" class="uk-button uk-button-link" title="{{t .LangCode "qitem.nextUnreadTitle"}}">{{t .LangCode "qitem.nextUnread"}} ›</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 从后台进入单题页时,表单 action 必须原样带上筛选参数。
|
||||||
|
// 回归点:曾经写成 action="...?{{ .NextQuery }}",html/template 会把查询片段整体编码,
|
||||||
|
// = 和 & 变成 %3d / %26,服务端解析不出 from,跳转后「返回」掉回箱主页(线上实测过)。
|
||||||
|
func TestOwnerFormsKeepAdminQuery(t *testing.T) {
|
||||||
|
data := publicBaseData()
|
||||||
|
data["CanDelete"] = true
|
||||||
|
data["IsOwnPage"] = true
|
||||||
|
data["NextUnreadID"] = uint(2)
|
||||||
|
data["BackFrom"] = "admin"
|
||||||
|
data["BackTo"] = "/user/questions?from=admin&sort=new"
|
||||||
|
data["AnswerAction"] = "/_/tester/1/answer?from=admin&sort=new"
|
||||||
|
data["NextUnreadAction"] = "/_/tester/1/next-unread?from=admin&sort=new"
|
||||||
|
data["Question"] = map[string]interface{}{
|
||||||
|
"ID": 1, "Content": "测试提问", "Answer": "",
|
||||||
|
"CreatedAt": time.Now(), "UpdatedAt": time.Now(),
|
||||||
|
"IsPrivate": false, "IsPinned": false, "IsRead": false,
|
||||||
|
"Token": "abc123", "ReceiveReplyEmail": "",
|
||||||
|
}
|
||||||
|
data["QuestionToken"] = "abc123"
|
||||||
|
data["AskUploadImages"] = []interface{}{}
|
||||||
|
data["AnswerUploadImages"] = []interface{}{}
|
||||||
|
data["FollowUps"] = []interface{}{}
|
||||||
|
data["CanFollowUp"] = false
|
||||||
|
data["IsQuestioner"] = false
|
||||||
|
|
||||||
|
out, err := renderToString(t, "question/item", data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render question/item: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`action="/_/tester/1/answer?from=admin&sort=new"`,
|
||||||
|
`action="/_/tester/1/next-unread?from=admin&sort=new"`,
|
||||||
|
`href="/user/questions?from=admin&sort=new"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Fatalf("渲染结果缺少 %s", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(out), "%3d") || strings.Contains(out, "%26") {
|
||||||
|
t.Fatal("action 里的 = / & 被 percent-encode 了,服务端会解析不出 from=admin")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user