对访客公开的措辞按身份分支:访客不该看到「我的结论」
用户发现未登录也显示「推荐 & 踩雷(我的结论)」,访客没法解读「谁的我的」。
顺着把访客可见模板与 static/js 全过了一遍,同病共四处——都是**写模板时站在站长视角下笔、
而那一块又对访客公开**:
1. stats.html:标题 → {{if .IsAdmin}}我的结论{{else}}站长结论{{end}};
副标题「只统计你自己…」→ 站长「你自己」/ 访客「站长」;两列「我推荐/我踩雷的地点」同理。
2. stats.html 访客块:「与我的结论互不影响:你标你的,访客投访客的」——对访客说不通,
改成对两边都成立的「与上面的站长结论互不影响:各存各的表,谁也不会覆盖谁」。
3. home.html:「我的标签」→「足迹标签」(同级卡片全是中性的:足迹地图/城市足迹/天气足迹…)。
4. report.html:「这一年里,你第一次踏足的这些城市」→ 去掉人称(年报是站长的回顾)。
已经写对的照它学:place.html 的投票区早就是 {{if .IsAdmin}} 分支(站长「我的结论」、
访客「大家怎么说」),rond-map.js 的气泡同理;「含家/工作」chip 与一串「只有你自己能看到」
本来就在站长分支里。
门禁 TestVisitorPagesUseThirdPersonCopy:以**访客 ctx(无 user)**渲染 /、/stats、/report,
逐个禁用词断言,同时断言站长那份**仍旧说「我的」**(别把站长自己的视角也改掉);
前置需打开 marks_public(访客才看得到那一块),用完还原。
⚠️ 变异验证时踩了老坑:直接 `go test -run …` 因缺 ROND_TEST_CONF/ROND_SRC 会**静默 skip 并报 ok**
(0.17s 就 ok,一眼假绿)。改用 ./scripts/verify.sh 后变异被正确抓住
(visitor_copy_test.go:85 报「我的结论」),还原后转绿。
全量验收 PASS=339 FAIL=0 SKIP=0。
This commit is contained in:
@@ -50,3 +50,23 @@
|
||||
上一轮跑剩的票会被算进来。新增 `isolateVoteState` 按 place_key 清两张表(前置 + cleanup)。
|
||||
|
||||
顺带订正 README:那里还写着站长结论「按 src_pk 存」,其实早就是坐标键了。
|
||||
|
||||
## 措辞分身份:访客不该看到「我的结论」(02:05)
|
||||
|
||||
用户:「无论是否登陆,都显示『推荐 & 踩雷(我的结论)』,这对访客是不是会产生困扰,再检查类似的表述」。
|
||||
顺着 `grep "[我你咱]"` 把访客可见模板 + `static/js` 过了一遍,共 4 处同病(都是**站在站长视角下笔、
|
||||
而那一块对访客公开**):
|
||||
|
||||
1. `stats.html`:标题「我的结论」→ 按身份分支(站长「我的结论」/ 访客「站长结论」);
|
||||
副标题「只统计你自己…」→ 站长「你自己」/ 访客「站长」;两列「我推荐/我踩雷的地点」同理。
|
||||
2. `stats.html` 访客块副标题:「你标你的,访客投访客的」→ 对两边都成立的「各存各的表,谁也不会覆盖谁」。
|
||||
3. `home.html`:「我的标签」→「足迹标签」(同级卡片全是中性的:足迹地图/城市足迹/天气足迹…)。
|
||||
4. `report.html`:「这一年里,**你**第一次踏足…」→ 去掉人称(年报是站长的回顾)。
|
||||
|
||||
已写对的(照它学):`place.html` 投票区早就是 `{{if .IsAdmin}}` 分支,`rond-map.js` 气泡同理。
|
||||
|
||||
门禁 `TestVisitorPagesUseThirdPersonCopy`:以访客 ctx(无 user)渲染 `/`、`/stats`、`/report`,
|
||||
逐个禁用词断言,同时确认站长那份仍说「我的」(别改过头)。
|
||||
|
||||
⚠️ 变异验证又踩老坑:**直接 `go test -run` 缺环境变量会静默 skip 并报 ok**(0.17s 就 ok,一眼假绿),
|
||||
必须走 `verify.sh`。改用 verify.sh 后变异被正确抓住(`visitor_copy_test.go:85` 报「我的结论」)。
|
||||
|
||||
@@ -753,3 +753,32 @@ Core Data 元数据、索引、持久化历史、`ZRAWVISIT` 采样点、`ZTRIP*
|
||||
比数总数更能证明两块真的没串台。
|
||||
|
||||
顺带订正 README:那里还写着站长结论「按 `src_pk` 存」,其实早就是**坐标键**了。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三十七、措辞要分「站长视角」与「访客视角」(2026-10-01)
|
||||
|
||||
用户发现:**未登录也显示「推荐 & 踩雷(我的结论)」**,访客没法解读「谁的我的」。
|
||||
顺着做了一次系统排查(`grep "[我你咱]"` 扫访客可见模板 + `static/js`),
|
||||
发现同一个毛病还有三处,都是**写模板时站在站长视角下笔、而那一块对访客公开**:
|
||||
|
||||
| 位置 | 原文 | 改成 |
|
||||
|---|---|---|
|
||||
| `stats.html` 标题/副标题/两列小标题 | 我的结论 / 只统计**你自己**… / 我推荐的地点 / 我踩雷的地点 | `{{if .IsAdmin}}我的…{{else}}站长…{{end}}`(**按身份分支**) |
|
||||
| `stats.html` 访客块副标题 | 与「我的结论」互不影响:**你标你的**,访客投访客的 | 「与上面的站长结论互不影响:各存各的表,谁也不会覆盖谁」 |
|
||||
| `home.html` 标签卡标题 | 我的标签(同级卡片都是中性的:足迹地图/城市足迹/天气足迹…) | 足迹标签 |
|
||||
| `report.html` | 这一年里,**你**第一次踏足的这些城市(年报是站长的回顾) | 这一年里第一次踏足的这些城市 |
|
||||
|
||||
**已经写对的(照它学)**:`place.html` 的投票区早就是 `{{if .IsAdmin}}` 分支
|
||||
(站长「我的结论」/ 访客「大家怎么说」),`rond-map.js` 的气泡同理;
|
||||
`layout.html`/`map.html` 的「含家/工作」chip 与一串「只有你自己能看到」都在 `{{if .IsAdmin}}` 里,没问题。
|
||||
|
||||
**门禁用例** `TestVisitorPagesUseThirdPersonCopy`:以**访客身份**(`context.Background()`,ctx 里没有 user)
|
||||
取回 `/stats`、`/`、`/report`,逐个禁用词断言(`visitorCopyForbidden`),
|
||||
同时断言站长那份**仍旧说「我的」**(别把站长自己的视角也改成第三人称)。
|
||||
前置要把 `settings.site.marks_public` 打开(访客才看得到那一块),用完还原。
|
||||
|
||||
⚠️ 变异验证踩的坑(老坑,载入史册):**直接 `go test -run …` 会因为缺 `ROND_TEST_CONF`/`ROND_SRC`
|
||||
静默 skip 并报 ok** —— 我第一次做变异就是这么「假绿」的(0.17s 就 ok,明显不对)。
|
||||
变异验证必须走 `./scripts/verify.sh --pkg … --run …`(它会拦住 SKIP)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# rond-with-you 硬规矩(索引)
|
||||
|
||||
> 全文:`.workbuddy/memory/MEMORY-details.md`(37 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
|
||||
> 全文:`.workbuddy/memory/MEMORY-details.md`(38 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。
|
||||
>
|
||||
> ⚠️ 本文件**按字符数**注入,硬上限 `MAX_MEMORY_CHARS = 1e4`(WorkBuddy `app.asar` 里 `getWorkingMemoryContent`;中文 1 字算 1,不是按字节)。超了会被 `substring(0,1e4)` 截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 details **不参与注入**,可以随便写。现值见 `wc -m`,留 20% 余量。
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
- `tileDigits` 补前导 0;`tileName` 尾缀 `len(s)-2` 单数字编号时为 -1 ⇒ panic;解码守卫 `len(name)<8`。
|
||||
- `fogIndexV` 必须 `atomic.Pointer`(普通指针=真 data race)。`fog_race_test.go`(-race)
|
||||
|
||||
## 前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D)
|
||||
## 前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七)
|
||||
- ⚠️ **对访客公开的块,措辞必须按身份分支**(`{{if .IsAdmin}}我的…{{else}}站长…{{end}}`):
|
||||
写模板时人站在站长视角下笔,「我的结论 / 你自己 / 我推荐的地点」露给访客就没人读得懂。
|
||||
照 `place.html` 投票区与 `rond-map.js` 气泡的写法。门禁 `TestVisitorPagesUseThirdPersonCopy`
|
||||
(以访客 ctx 渲染 `/`、`/stats`、`/report` 逐个断言禁用词,并确认站长那份仍说「我的」)。
|
||||
- 改 Go 必须重启;模板/CSS/JS 只在 `debug=true` 热加载(`AssetV` 启动哈希)。
|
||||
- 拼查询串走返回 `template.URL` 的方法(`PlaceLink`/`EditPlaceLink`/`MapLink`/`EditLink`),勿手工转义。
|
||||
- 窄屏覆盖写 `app.css` **末尾那块**;宽表堆叠类名**成对** `class="tb tb-stack"`;触控 44px。`tb_stack_test.go`
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
{{if and (.Settings.Has "tags") .Tags}}
|
||||
<div class="card">
|
||||
<h2>我的标签</h2>
|
||||
<h2>足迹标签</h2>
|
||||
<div class="card-sub">记录时打上的自定义标签</div>
|
||||
<div class="chips">
|
||||
{{range .Tags}}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
{{if .Cities}}
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h2>🏅 首次到达的城市</h2>
|
||||
<div class="card-sub">这一年里,你第一次踏足的这些城市(按到达日期排序)</div>
|
||||
<div class="card-sub">这一年里第一次踏足的这些城市(按到达日期排序)</div>
|
||||
<div class="chips">
|
||||
{{range .Cities}}
|
||||
<span class="chip on" title="{{.Province}} · 首次到达 {{.First}}">
|
||||
|
||||
@@ -21,18 +21,20 @@
|
||||
</div>
|
||||
|
||||
{{if or .IsAdmin .Settings.MarksPublic}}
|
||||
{{/* 第一块:站长自己的结论。只有站长能标,所以这里只说「我的」。 */}}
|
||||
{{/* 第一块:站长的结论。
|
||||
措辞必须**按身份分支**:访客看到「我的结论」只会一头雾水(谁的「我」),
|
||||
地点页的投票区早就是这么分的(站长看「我的结论」、访客看「大家怎么说」),这里跟上。 */}}
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="row" style="justify-content:space-between;align-items:center;flex-wrap:wrap">
|
||||
<h2 style="margin:0">推荐 & 踩雷(我的结论)</h2>
|
||||
<h2 style="margin:0">推荐 & 踩雷({{if .IsAdmin}}我的结论{{else}}站长结论{{end}})</h2>
|
||||
<span class="muted" style="font-size:12.5px">推荐 {{num .VoteRec}} · 踩雷 {{num .VoteAvoid}}</span>
|
||||
</div>
|
||||
<div class="card-sub">
|
||||
只统计<strong>你自己</strong>在地点页或地图气泡里标的 👍/👎——按坐标键存,重新上传备份也不会丢。
|
||||
只统计<strong>{{if .IsAdmin}}你自己{{else}}站长{{end}}</strong>在地点页或地图气泡里标的 👍/👎——按坐标键存,重新上传备份也不会丢。
|
||||
</div>
|
||||
<div class="grid g2">
|
||||
<div>
|
||||
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👍 我推荐的地点</div>
|
||||
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👍 {{if .IsAdmin}}我{{else}}站长{{end}}推荐的地点</div>
|
||||
{{if .Recommended}}
|
||||
<div class="chips">
|
||||
{{range .Recommended}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#34c759"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}{{if .City}}<span class="muted"> · {{.City}}</span>{{end}}</a>{{end}}
|
||||
@@ -40,7 +42,7 @@
|
||||
{{else}}<div class="empty" style="padding:10px 0">还没有标记</div>{{end}}
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👎 我踩雷的地点</div>
|
||||
<div class="muted" style="font-size:12.5px;margin-bottom:6px">👎 {{if .IsAdmin}}我{{else}}站长{{end}}踩雷的地点</div>
|
||||
{{if .Avoided}}
|
||||
<div class="chips">
|
||||
{{range .Avoided}}<a class="chip" href="{{$.PlaceLink .ID}}"><span class="dot" style="background:#ff3b30"></span>{{if .Name}}{{.Name}}{{else}}未命名{{end}}{{if .City}}<span class="muted"> · {{.City}}</span>{{end}}</a>{{end}}
|
||||
@@ -57,7 +59,7 @@
|
||||
<span class="muted" style="font-size:12.5px">👍 {{num .VoteUp}} 票 · 👎 {{num .VoteDown}} 票</span>
|
||||
</div>
|
||||
<div class="card-sub">
|
||||
访客在地点页点的 👍/👎,一人一票、可改可撤。与「我的结论」互不影响:你标你的,访客投访客的。
|
||||
访客在地点页点的 👍/👎,一人一票、可改可撤。与上面的站长结论<strong>互不影响</strong>:各存各的表,谁也不会覆盖谁。
|
||||
</div>
|
||||
<div class="grid g2">
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ---------- 措辞要分清「站长视角」与「访客视角」 ----------
|
||||
//
|
||||
// 用户实测反馈:未登录时统计页也显示「推荐 & 踩雷(我的结论)」,访客会困惑「谁的我的」。
|
||||
// 这类毛病都出在同一处:**写模板时站在站长视角下笔**(我/你自己),而那一块又对访客公开。
|
||||
// 地点页的投票区早就是按身份分支的(站长看「我的结论」、访客看「大家怎么说」),
|
||||
// 这里把它做成回归用例:以**访客身份**取回页面,逐个钉死那些只有站长读得懂的说法。
|
||||
|
||||
// renderAsVisitor 以访客身份取回页面(ctx 里没有登录态)。
|
||||
func renderAsVisitor(t *testing.T, f *movementFixture, h http.HandlerFunc, path string) string {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
// 访客就是「没有 user 的 ctx」:IsAdmin() 看的是 p.User 是否为 nil
|
||||
req := httptest.NewRequest("GET", path, nil).WithContext(context.Background())
|
||||
h(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("访客取 %s 状态码 %d:%s", path, rec.Code, rec.Body.String())
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.HasSuffix(strings.TrimSpace(body), "</html>") {
|
||||
t.Fatalf("访客取 %s 没渲染到底:…%s", path, body[len(body)-300:])
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// renderAsAdmin 以站长身份取回页面。
|
||||
func renderAsAdmin(t *testing.T, f *movementFixture, h http.HandlerFunc, path string) string {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", path, nil).WithContext(f.ctx)
|
||||
h(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("站长取 %s 状态码 %d:%s", path, rec.Code, rec.Body.String())
|
||||
}
|
||||
return rec.Body.String()
|
||||
}
|
||||
|
||||
// forceMarksPublic 把「推荐 / 踩雷对访客公开」打开——访客版页面里那一块才会渲染出来。
|
||||
// 用例结束后还原设置(站点设置是全局一行,别把后面的用例带偏)。
|
||||
func forceMarksPublic(t *testing.T, f *movementFixture) {
|
||||
t.Helper()
|
||||
var old sql.NullString
|
||||
if err := f.pg.QueryRowContext(f.ctx, `SELECT value::text FROM settings WHERE key='site'`).Scan(&old); err != nil && err != sql.ErrNoRows {
|
||||
t.Fatalf("读站点设置失败: %v", err)
|
||||
}
|
||||
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO settings (key, value)
|
||||
VALUES ('site', '{"marks_public":true}'::jsonb)
|
||||
ON CONFLICT (key) DO UPDATE SET value = jsonb_set(settings.value, '{marks_public}', 'true'::jsonb, true)`); err != nil {
|
||||
t.Fatalf("打开「对访客公开」失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if old.Valid {
|
||||
f.pg.ExecContext(f.ctx, `UPDATE settings SET value=$1::jsonb WHERE key='site'`, old.String)
|
||||
return
|
||||
}
|
||||
f.pg.ExecContext(f.ctx, `DELETE FROM settings WHERE key='site'`)
|
||||
})
|
||||
}
|
||||
|
||||
// visitorCopyForbidden 是**只有站长读得懂**的说法:访客看到「我 / 你自己」会一头雾水。
|
||||
var visitorCopyForbidden = []string{"我的结论", "你自己", "我推荐的地点", "我踩雷的地点", "你标你的", "我的标签"}
|
||||
|
||||
func TestVisitorPagesUseThirdPersonCopy(t *testing.T) {
|
||||
f := newMovementFixture(t)
|
||||
forceMarksPublic(t, f)
|
||||
|
||||
// ① 统计页:站长仍旧说「我的」,访客一律第三人称
|
||||
adminStats := renderAsAdmin(t, f, f.srv.statsPage, "/stats")
|
||||
if !strings.Contains(adminStats, "我的结论") {
|
||||
t.Error("站长自己看时应当说「我的结论」——按身份分支,不该把站长那份也改成第三人称")
|
||||
}
|
||||
visitorStats := renderAsVisitor(t, f, f.srv.statsPage, "/stats")
|
||||
for _, bad := range visitorCopyForbidden {
|
||||
if strings.Contains(visitorStats, bad) {
|
||||
t.Errorf("访客版统计页出现了只有站长读得懂的说法:%q", bad)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(visitorStats, "站长结论") {
|
||||
t.Error("访客版统计页应当用「站长结论」这类第三人称说法")
|
||||
}
|
||||
// 访客那一块自己得说清楚「谁投的」
|
||||
if !strings.Contains(visitorStats, "访客点赞") {
|
||||
t.Error("访客版统计页应当有独立的「访客点赞」一块")
|
||||
}
|
||||
|
||||
// ② 首页:标签卡原先写「我的标签」,访客看不出是谁的
|
||||
visitorHome := renderAsVisitor(t, f, f.srv.home, "/")
|
||||
for _, bad := range visitorCopyForbidden {
|
||||
if strings.Contains(visitorHome, bad) {
|
||||
t.Errorf("访客版首页出现了只有站长读得懂的说法:%q", bad)
|
||||
}
|
||||
}
|
||||
|
||||
// ③ 年报:那是站长的年度回顾,不该对访客说「你这一年…」
|
||||
visitorReport := renderAsVisitor(t, f, f.srv.reportPage, "/report")
|
||||
if strings.Contains(visitorReport, "你第一次踏足") {
|
||||
t.Error("访客版年报里把站长的一年说成了「你」的")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user