fix deactivate

This commit is contained in:
tamakyi
2026-09-15 12:10:42 +08:00
parent adcc42cc8c
commit 0e0c16f786
9 changed files with 95 additions and 9 deletions
+33
View File
@@ -250,3 +250,36 @@ GOSUMDB=off GOFLAGS=-mod=mod` 就秒级完成。**
### ⚠️ 新踩到的 gofmt 陷阱
在**文档注释**里写 `''` 会被 gofmt 重排成 `”`(Go 1.19+ 的 doc comment 规范化:`` `` ``→`“`、`''`→`”`),
导致 `gofmt -l` 反复报未格式化。**注释里避免使用 `''`**(改写为「空字符串」等文字)。
---
## 账号停用体验修复:独立成功页(/deactivated)
### 问题
停用账号后 `DeactivateProfileAction` 把用户重定向到 `/login`,只在登录页顶部弹一条 flash。
用户会觉得「被丢回登录页」,下意识以为操作没生效。
诊断结论:flash **确实渲染**(Flush 清 session 数据但保留 session,flash 能带到下一页),
真正的体验错配是「停用后落点仍是登录页」——而账号已删/停,用户根本不可能再登录。
### 方案(用户选「独立成功页(推荐)」)
1. `route/user/profile.go`
- 新增公开 handler `DeactivatedProfile`:`ctx.SetTitle(i18n.T(ctx.Lang(), "deactivated.title"))` + `ctx.Success("user/deactivated")`。
- `DeactivateProfileAction` 末尾 `ctx.Redirect("/login")` → `ctx.Redirect("/deactivated")`,**并移除原来的 `SetSuccessFlash`**(页面自身即提示)。
- 顺手修正 `UpdateProfile` 里一处陈旧注释(原写「顺序与 DeactivateProfileAction 保持一致…随后跳登录页」,停用路径已不再如此)。
2. `internal/route/route.go`:在公开区(`/explore` 旁)加 `f.Get("/deactivated", user.DeactivatedProfile)`。
**必须是公开路由**:停用后会话已 Flush、`reqUserSignIn` 会把人弹回登录页,正是要避免的现象。
3. 新建 `templates/user/deactivated.html`:复用 `question/success.html` 的范式(大 ✓ 图标 + 卡片 + 单按钮返回首页)。
`templates/embed.go` 是整目录 `//go:embed ... user ...`,新模板**自动被嵌入**,无需改 embed 指令。
4. i18n:四份字典都加 `deactivated.title` / `deactivated.desc` / `deactivated.backHome`
(`TestDictionaryComplete` 要求 zh_cn/zh_tw/en/ja **键集合完全一致**,漏一份必挂)。
同时**删除已无引用的 `admin.flash.deactivated`**(4 份字典同步删;删它不影响对齐组,因为该组最长键是 `admin.flash.blockWordsTooMany`)。
### 验证
- `go build ./...` / `go vet ./...` EXIT=0;`go test ./...`(含本地 PG)**全绿**,
其中 `internal/i18n`(键一致性)、`internal/route`、`templates`、`internal/template`(模板解析)均 ok → 新模板可被解析、键齐备。
- gofmt 无 diff;行尾未变(4 份 dict + route.go 保持 CRLF,`route/user/profile.go` 实为 **LF**)。
### 备注
- `route/user/profile.go` 的行尾是 **LF**(与「route/** 皆 CRLF」的旧印象不符,按文件单独确认即可)。
+14 -2
View File
@@ -44,8 +44,9 @@
⚠️ `go env -w` 在本沙箱**会失败**(`cannot find go env config: %AppData% is not defined`,Git Bash 无该变量),
无法持久化,只能每条命令 export。GROUP1(security/censor、conf、form、template、shared、captcha)已验证 EXIT=0(~10s)。
- **行尾:仓库是「混合行尾」,不是统一 CRLF**(`core.autocrlf=true`,提交 blob 为 LF,Git 自动归一,**不污染 diff**)。
实测:`internal/db/*`、`internal/mail/mail.go` 是 **LF**;`route/**`、`internal/route/route.go`、
`internal/db/users.go` 是 **CRLF**。→ 编辑时**保持各文件原行尾**,不要整体转换。
实测:`internal/db/*`、`internal/mail/mail.go`、`route/user/profile.go` 是 **LF**;
而 `internal/route/route.go`、`internal/db/users.go`、`internal/i18n/dict_*.go` 是 **CRLF**。
→ **不要按「route/** 皆 CRLF」这类规律推断**,编辑前逐个文件用 Python 数 `b'\r\n'` 确认。
`gofmt -l` 会把 CRLF 文件**误报**为未格式化(false positive)。
✅ 改注释/字段后统一用 `dev/gofmt_keep_crlf.py <files>`(原 Temp 版被清,已重建到 `dev/`):跑 gofmt 后按原文件行尾还原,
既应用格式化又保留工作区 CRLF、避免 gofmt -l 噪声。(`grep -c $'\r'` 在 Git Bash 不可靠,查行尾用 Python 数 `b'\r\n'`。)
@@ -122,6 +123,17 @@
- ⚠️ `gofmt` 是 **Windows 原生程序,看不到 MSYS 的 `/tmp` 路径**(报 `GetFileAttributesEx ... cannot find`)。
临时比对文件要放到 Windows 可见路径(如 `C:/Users/<user>/AppData/Local/Temp/...`)。
## 会话失效后的落点(闪信 vs 独立页)
- **终止性操作(账号停用/注销)用「独立公开成功页」,不要落登录页**。
既有实现 `/deactivated`(`user.DeactivatedProfile` → `templates/user/deactivated.html`):
停用后 `Session.Flush()` 会清空会话,若落 `/login`,用户第一直觉是「没生效」。
→ 该路由必须注册在**公开区**(`reqUserSignIn` 之外),否则会被弹回登录页,正好复现要避免的现象。
- **继续可登录的操作(如改密码后强制重登)才用闪信 + `/login`**:顺序 **先 `Session.Flush()` 再 `SetSuccessFlash()`**——
Flush 只清 session 数据、保留同一个 session,故 flash 仍能带到下一跳渲染。
- i18n 键名区分:`deactivate.*` = 停用**表单页**;`deactivated.*` = 停用**成功页**(仅 title/desc/backHome)。
- `templates/embed.go` 用整目录 `//go:embed ... user ...`,**新增模板文件自动嵌入**,无需改 embed 指令。
## 仓库状态
- **git 仓库已损坏**(`.git/refs` 缺失、index 截断、对象库近乎空),用户明确要求「不要动 git」。
备份/回滚一律用 tar.gz(排除 `.git`、`.workbuddy`),放在项目同级目录:
+5 -1
View File
@@ -419,6 +419,11 @@ var enDict = map[string]string{
"deactivate.confirm": "I confirm I want to deactivate my account",
"deactivate.submit": "Deactivate account",
// Deactivated page
"deactivated.title": "Account deactivated",
"deactivated.desc": "Your question box and all data have been deleted. Thank you for using Wolf's Question Box. We hope to see you again.",
"deactivated.backHome": "Back to home",
// Anonymous voting
"nav.votes": "Polls",
@@ -722,7 +727,6 @@ var enDict = map[string]string{
"admin.flash.blockWordTooLong": "A blocked word cannot exceed 10 characters: %s",
"admin.flash.harassmentUpdated": "Anti-harassment settings updated successfully",
"admin.flash.blockWordsUpdated": "Blocked words updated",
"admin.flash.deactivated": "Your account has been deactivated. Thank you for using Wolf's Question Box. We hope to see you again 👋🏻",
"admin.export.errUser": "Export failed: could not get user information",
"admin.export.errQuestion": "Export failed: could not get question information",
+5 -1
View File
@@ -419,6 +419,11 @@ var jaDict = map[string]string{
"deactivate.confirm": "アカウントを停止することに同意します",
"deactivate.submit": "アカウントを停止",
// アカウント停止完了ページ
"deactivated.title": "アカウントを停止しました",
"deactivated.desc": "質問箱とすべてのデータを削除しました。ご利用いただきありがとうございました。またのお越しをお待ちしています。",
"deactivated.backHome": "ホームに戻る",
// 匿名投票
"nav.votes": "投票",
@@ -722,7 +727,6 @@ var jaDict = map[string]string{
"admin.flash.blockWordTooLong": "ブロックワードの長さは 10 文字以内である必要があります:%s",
"admin.flash.harassmentUpdated": "迷惑行為対策を更新しました",
"admin.flash.blockWordsUpdated": "ブロックワードを更新しました",
"admin.flash.deactivated": "アカウントを停止しました。狼の質問箱をご利用いただきありがとうございます。またのお越しをお待ちしています 👋🏻",
"admin.export.errUser": "エクスポート失敗:ユーザー情報の取得に失敗しました",
"admin.export.errQuestion": "エクスポート失敗:質問情報の取得に失敗しました",
+5 -1
View File
@@ -419,6 +419,11 @@ var zhCNDict = map[string]string{
"deactivate.confirm": "我确认要停用账号",
"deactivate.submit": "停用账号",
// 停用成功页
"deactivated.title": "账号已停用",
"deactivated.desc": "你的提问箱及所有数据已删除,感谢你的使用,期待未来再见。",
"deactivated.backHome": "返回首页",
// 匿名投票
"nav.votes": "投票",
@@ -722,7 +727,6 @@ var zhCNDict = map[string]string{
"admin.flash.blockWordTooLong": "屏蔽词长度不能超过 10 个字符:%s",
"admin.flash.harassmentUpdated": "更新防骚扰设置成功",
"admin.flash.blockWordsUpdated": "屏蔽词已更新",
"admin.flash.deactivated": "您的账号已停用,感谢您使用 狼的提问箱。期待未来还能再见 👋🏻",
"admin.export.errUser": "导出失败:获取用户信息失败",
"admin.export.errQuestion": "导出失败:获取问题信息失败",
+5 -1
View File
@@ -419,6 +419,11 @@ var zhTWDict = map[string]string{
"deactivate.confirm": "我確認要停用帳號",
"deactivate.submit": "停用帳號",
// 停用成功頁
"deactivated.title": "帳號已停用",
"deactivated.desc": "你的提問箱及所有資料已刪除,感謝你的使用,期待未來再見。",
"deactivated.backHome": "返回首頁",
// 匿名投票
"nav.votes": "投票",
@@ -722,7 +727,6 @@ var zhTWDict = map[string]string{
"admin.flash.blockWordTooLong": "屏蔽詞長度不能超過 10 個字元:%s",
"admin.flash.harassmentUpdated": "更新防騷擾設定成功",
"admin.flash.blockWordsUpdated": "屏蔽詞已更新",
"admin.flash.deactivated": "您的帳號已停用,感謝您使用 狼的提問箱。期待未來還能再見 👋🏻",
"admin.export.errUser": "匯出失敗:獲取使用者資訊失敗",
"admin.export.errQuestion": "匯出失敗:獲取問題資訊失敗",
+4
View File
@@ -284,6 +284,10 @@ func New() *flamego.Flame {
f.Get("/explore", user.Explore)
// 账号停用成功页(公开):停用后会话已清空,不能再要求登录,
// 故为独立公开页,让用户明确看到「已停用」而非被丢回登录页。
f.Get("/deactivated", user.DeactivatedProfile)
// 匿名投票参与端(公开):token 链接入口
f.Group("/vote/{token}", func() {
f.Get("", vote.Page)
+10 -3
View File
@@ -225,7 +225,7 @@ func UpdateProfile(ctx context.Context, f form.UpdateProfile) {
ctx.SetInternalErrorFlash()
} else if passwordChanged {
// 密码修改成功且其余资料也保存成功:强制登出重登。
// 顺序与 DeactivateProfileAction 保持一致:先 Flush 会话再写入 flash,随后跳登录页。
// 先 Flush 清会话再写入 flash,保证提示能带到登录页显示。
ctx.Session.Flush()
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "profile.flash.passwordChanged"))
ctx.Redirect("/login")
@@ -431,6 +431,13 @@ func DeactivateProfile(ctx context.Context) {
ctx.Success("user/deactivate")
}
// DeactivatedProfile 账号停用成功页。停用后会话已被清空、无法再登录,
// 故为公开页,用于明确告知用户「已停用」而不是把其丢回登录页。
func DeactivatedProfile(ctx context.Context) {
ctx.SetTitle(i18n.T(ctx.Lang(), "deactivated.title"))
ctx.Success("user/deactivated")
}
// ExportProfileJSON 以 JSON 格式全量导出账号数据(用户信息 + 提问/回答/追问 + 私聊消息),
// 符合可携带权要求,供用户备份/迁移。
func ExportProfileJSON(ctx context.Context) {
@@ -594,6 +601,6 @@ func DeactivateProfileAction(ctx context.Context) {
return
}
ctx.Session.Flush()
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "admin.flash.deactivated"))
ctx.Redirect("/login")
// 跳独立成功页而非登录页:账号已停用、无法再登录,落在登录页易让用户误以为操作未生效。
ctx.Redirect("/deactivated")
}
+14
View File
@@ -0,0 +1,14 @@
{{template "base/header" .}}
<div class="uk-card uk-card-default uk-card-body uk-text-center">
<div class="uk-margin-bottom">
<span uk-icon="icon: check; ratio: 3" class="uk-text-success"></span>
<h3 class="uk-margin-small-top uk-margin-remove-bottom">{{t .LangCode "deactivated.title"}}</h3>
</div>
<p class="uk-text-muted">{{t .LangCode "deactivated.desc"}}</p>
<div class="uk-margin-top">
<a class="uk-button uk-button-primary" href="/">{{t .LangCode "deactivated.backHome"}}</a>
</div>
</div>
{{template "base/footer" .}}