From 4ffcaecba231ef0edeb063eb32e34be4870d7ca9 Mon Sep 17 00:00:00 2001 From: freefire Date: Sun, 30 Aug 2026 04:02:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Admin=20=E8=AE=BE=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AD=89=E7=BA=A7=E5=B9=B6=E5=B1=95=E7=A4=BA=E5=BE=BD=E7=AB=A0?= =?UTF-8?q?=E4=BA=8E=E7=94=A8=E6=88=B7=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 SetUserLevel 表单;用户主页/资料页展示徽章并触发 EvaluateAuto;勾选 §H Exp 与防刷。 Co-authored-by: Cursor --- docs/rebuild-spec/02-features.md | 8 +- docs/rebuild-spec/06-pages-ux.md | 4 +- docs/rebuild-spec/09-ssr-progress.md | 99 +++++-------------- .../plans/level-badges-display.md | 17 ++++ public/assets/site.css | 27 +++++ routers/web/admin_users.go | 48 +++++++-- routers/web/home.go | 1 + routers/web/profile.go | 9 ++ routers/web/user.go | 16 +++ templates/admin/users.tmpl | 10 ++ templates/profile/view.tmpl | 7 ++ templates/user/view.tmpl | 7 ++ web_src/css/site.css | 27 +++++ 13 files changed, 194 insertions(+), 86 deletions(-) create mode 100644 docs/rebuild-spec/plans/level-badges-display.md diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index 54e8142..b02aa0a 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -129,11 +129,11 @@ - [x] 每日签到:基础 5,连签每日 +1,封顶 15 — `POST /profile/checkin` - [x] 每日抽奖:奖池加权(0/2/5/10/20),成本 0 — `POST /profile/lottery` - [x] 积分解锁分成:读者付全额,作者约 70% — 门控 unlock 服务 -- [ ] 短龄同 IP 互刷拒绝分成 -- [ ] Exp → 等级 Lv1–10 -- [ ] 自动徽章(注册天数 / 获赞 / 创作分成)— 种子定义 + `EvaluateAuto` 已有;Admin 可改定义,独立调度 UI 未做 +- [x] 短龄同 IP 互刷拒绝分成 — `UnlockPointsBlock` / `suspiciousUnlockPair`(双方注册未满 7 天且 LastLoginIP 相同) +- [x] Exp → 等级 Lv1–10 — 门槛推导 + 发帖/评论/获赞加 Exp;Admin 设等级 SSR `/admin/users` +- [x] 自动徽章(注册天数 / 获赞 / 创作分成)— 种子 + `EvaluateAuto`(用户页/资料页访问时评估);无独立定时调度 UI - [x] 限定徽章:管理员定义与授予/撤销 — SSR `/admin/badges` -- [x] 管理员调整积分、设定认证(免审)— SSR `/admin/users`(等级设定 UI 未做,仅只读展示 Lv) +- [x] 管理员调整积分、设定认证(免审)、设定等级 — SSR `/admin/users` --- diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index a70fb1b..1099ec7 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -172,7 +172,7 @@ - 公开页:签名、等级/积分只读、统计、最近帖 — SSR `/user/:id`(无邮箱) - 收藏列表 — SSR `/favorites` - 积分钱包面板(余额、近 N 条流水、签到/抽奖表单 PRG)— SSR `/profile`;导航展示当前积分 -- 徽章展示:**未迁** +- 徽章展示:用户主页 / 个人中心 — SSR(访问时 `EvaluateAuto`) --- @@ -196,7 +196,7 @@ | Posts | 审核通过/拒绝 — SSR `/admin/moderation`;置顶/版顶/精华/锁编/锁评/软删 — 帖详情 Admin 条;回收站恢复/清除 — SSR `/admin/trash` | | Comments | 审核 — SSR `/admin/moderation`;修订查看未迁 | | Reports | 处理动作:dismiss / resolve / reject_post / reject_comment — SSR `/admin/reports` | -| Users | 搜索;禁言;认证;调积分 — SSR `/admin/users`(设等级 / 授徽章挂在徽章页) | +| Users | 搜索;禁言;认证;调积分;设等级 — SSR `/admin/users`(授徽章在 `/admin/badges`) | | Badges | 定义自动/限定徽章;颁发/收回限定 — SSR `/admin/badges` | | Media | 分类浏览;单删/批量删;同步索引 — SSR `/admin/media` | | Media | 分类浏览;批量删 | diff --git a/docs/rebuild-spec/09-ssr-progress.md b/docs/rebuild-spec/09-ssr-progress.md index fc4b5cf..336c72d 100644 --- a/docs/rebuild-spec/09-ssr-progress.md +++ b/docs/rebuild-spec/09-ssr-progress.md @@ -12,15 +12,15 @@ | 项 | 值 | |----|-----| -| **上一刀** | Admin `/admin/media`(列表 / 删除 / 同步索引) | -| **下一刀** | **等级/自动徽章 UI** 或 **防刷分成**(`02` §H P2;开刀前确认偏好) | +| **上一刀** | P2:Admin 设等级 + 用户页徽章;勾选 Exp / 防刷 / 自动徽章 | +| **下一刀** | **P3 体验**(主题 / 伪静态 / 编辑器)或 **后置**(OIDC / 存储热切换 / Gitea);需点名 | | **工作区** | 应干净;有未提交改动时先处理再开新刀 | --- ## 一句话状态 -论坛核心闭环 + Admin 用户/徽章/媒体 **已迁完**。余量主要是成长体系与防刷、OIDC/存储产品化与体验打磨。 +核心论坛 + Admin P1 + §H 成长/防刷(服务层 + Admin 设等级)**已齐**。余量:P3 体验打磨与后置产品化(OIDC / S3 热切换 / Gitea)。 ```mermaid flowchart LR @@ -28,43 +28,28 @@ flowchart LR admin[Admin用户徽章媒体] growth[等级徽章防刷] polish[主题伪静态编辑器] - deferred[Gitea码桶后置] - core --> admin --> growth - admin --> polish - deferred -.-> admin + deferred[OIDC存储Gitea] + core --> admin --> growth --> polish + deferred -.-> polish ``` --- ## 已完成里程碑 -### 公开站 +### 公开站 / Admin / §H | 域 | 状态 | |----|------| -| `/install`、登录/注册/忘记密码、opaque session | 已迁 | -| Feed / 板块 / 搜索 / 三栏 / 右栏 widgets | 已迁 | -| 帖详情:赞藏举报、门控、审核横幅、修订历史 | 已迁 | -| 帖类型:`normal` / `question` / `poll` / `bounty` / `lottery` | 已迁 | -| 评论:发评/回复/赞/私密/编辑删除/@/回复通知 | 已迁(扁平楼层;嵌套树未做) | -| 私信/系统通知、收藏、个人中心、用户页 | 已迁 | -| 友链前台 + Admin(含复检) | 已迁 | -| 站点单页 | 已迁 | -| `/health`、`robots.txt`、`sitemap.xml`、`/uploads` | 已落地(`02` §O 已勾) | - -### Admin - -| 路径 | 状态 | -|------|------| -| dashboard / boards / moderation / reports / trash | 已迁 | -| settings:品牌、限流、敏感词、SMTP、侧栏、SQLite 备份 | 已迁 | -| friend-links / pages | 已迁 | -| **users / badges / media** | **已迁** | -| settings:OIDC / Gitea / 存储 / 伪静态 Tab | **未迁** | +| 五种帖类型、评论、私信、友链、单页、§O | 已迁 | +| Admin:users / badges / media / settings 基础 | 已迁 | +| Exp→Lv、Admin 设等级、用户页徽章展示 | 已迁 | +| 短龄同 IP 互刷拒绝分成 | 已落地(unlock 服务) | +| 自动徽章 EvaluateAuto | 已落地(访问用户页触发) | ### 近期提交(摘) -Admin users → badges → **media** +media → **等级设定 + 徽章展示 / §H 勾选** --- @@ -72,31 +57,25 @@ Admin users → badges → **media** ### P0 — 小清理(可夹带) -- 发评路径挂 `RateLimiter` `comment` 动作(服务层已有键,web 未挂) -- Logo/Favicon/OG 上传、头像裁剪等声明为未做的边角 +- 发评路径挂 `RateLimiter` `comment` 动作 +- Logo/Favicon/OG 上传、头像裁剪 -### P1 — Admin 核心 +### P2 — 成长与防刷 -1. ~~Admin 用户~~ ✓ -2. ~~Admin 徽章~~ ✓ -3. ~~Admin 媒体~~ ✓ +~~已完成主路径~~;无独立「自动徽章定时任务」Admin UI(可接受)。 -### P2 — 成长与防刷 ← 当前指针候选 - -- Exp → Lv1–10 设定 UI、自动徽章调度说明、短龄同 IP 互刷拒绝分成(`02` §H) - -### P3 — 体验 / 编辑器(可砍或长期) +### P3 — 体验 / 编辑器 ← 当前候选 - 主题、侧栏折叠、虚拟滚动、`feed_list_style`、伪静态 Admin -- TipTap / Markdown 双模(现行为 textarea + 门控) -- 游客评论、评论嵌套树(建议:全局 `#floor` + 缩进) +- TipTap / Markdown 双模 +- 游客评论、评论嵌套树 - 修订 diff、完整 Limits 字数 -### 后置(不阻塞验收) +### 后置 - K:Gitea `/projects` - L:OIDC 产品化 Admin CRUD -- M:S3 热切换、WebP thumb 全链路产品化 +- M:S3 热切换、WebP 全链路产品化 --- @@ -104,39 +83,15 @@ Admin users → badges → **media** | 序 | 刀 | 产出 | |----|----|------| -| ✓ | 五种帖类型 + 备份/复检 + §O | 核心闭环 | -| ✓ | Admin users / badges / media | Admin P1 | -| → | 等级设定 **或** 防刷分成 | 需偏好 | -| … | P3 / 后置 | 需明确点名再开 | +| ✓ | 核心闭环 + Admin P1 | … | +| ✓ | §H 等级/防刷/徽章展示 | … | +| → | 需点名 P3 或后置 | … | --- -## 下一刀实现提纲:P2 候选 +## 已完成计划 -> 「继续」前请点名:**等级/Exp UI**、**防刷分成**,或其它(OIDC/伪静态等)。 - -### A. 等级 - -- Admin 设用户 Exp/Lv(`SetUserLevel` 已有) -- 公开页等级展示对齐 - -### B. 防刷分成 - -- 短龄同 IP 互刷拒绝创作分成(`02` §H) - ---- - -## 已完成:Admin 媒体 - -见 [plans/admin-media.md](plans/admin-media.md)。 - -## 已完成:Admin 徽章 - -见 [plans/admin-badges.md](plans/admin-badges.md)。 - -## 已完成:Admin 用户管理 - -见 [plans/admin-users.md](plans/admin-users.md)。 +- [admin-users](plans/admin-users.md) · [admin-badges](plans/admin-badges.md) · [admin-media](plans/admin-media.md) --- diff --git a/docs/rebuild-spec/plans/level-badges-display.md b/docs/rebuild-spec/plans/level-badges-display.md new file mode 100644 index 0000000..f1ac869 --- /dev/null +++ b/docs/rebuild-spec/plans/level-badges-display.md @@ -0,0 +1,17 @@ +# P2 等级设定与徽章展示 — 实现计划 + +> 状态:已完成 +> 分支:`rebuild/gitea-ssr` + +## 范围 + +| 能力 | 实现 | +|------|------| +| Admin 设等级 | `POST /admin/users/:id/level` → `SetUserLevel`(Exp 调至门槛) | +| 公开等级 | 既有 `/user/:id`、`/profile` Lv/Exp | +| 徽章展示 | 用户页 / 资料页列表;访问时 `EvaluateAuto` | +| 防刷分成 | 确认 `suspiciousUnlockPair` 已落地并勾选 `02` | + +## 验证 + +`build.bat`;冒烟:设用户 Lv5 → Exp=200;用户页可见等级。 diff --git a/public/assets/site.css b/public/assets/site.css index 119f225..9047b2a 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -1040,6 +1040,33 @@ points-only[data-locked="false"] { align-items: center; margin: 0; } +.j13-admin-level { + display: inline-flex; + flex-wrap: wrap; + gap: 0.25rem; + align-items: center; + margin: 0; +} +.j13-admin-level select { + min-width: 4.5rem; +} +.j13-user-badges { + list-style: none; + margin: 0.5rem 0 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.j13-user-badge { + display: inline-block; + padding: 0.15rem 0.45rem; + font-size: 0.8rem; + border: 1px solid var(--j13-border); + border-radius: 4px; + color: var(--j13-muted); +} +.j13-user-badge[title] { cursor: help; } .j13-admin-badge-award-btns { display: flex; flex-wrap: wrap; diff --git a/routers/web/admin_users.go b/routers/web/admin_users.go index 92fb975..ce0fa56 100644 --- a/routers/web/admin_users.go +++ b/routers/web/admin_users.go @@ -30,15 +30,16 @@ type adminUserRow struct { type adminUsersData struct { AdminChrome - Users []adminUserRow - Keyword string - Filter string - Page int - HasPrev bool - HasMore bool - PrevPage int - NextPage int + Users []adminUserRow + Keyword string + Filter string + Page int + HasPrev bool + HasMore bool + PrevPage int + NextPage int QuerySuffix string + LevelOptions []int } // AdminUsersGet 用户列表 @@ -71,6 +72,7 @@ func (d Deps) renderAdminUsers(ctx *webctx.Context, errMsg string) { PrevPage: page - 1, NextPage: page + 1, HasPrev: page > 1, + LevelOptions: adminLevelOptions(), } q := url.Values{} if keyword != "" { @@ -120,6 +122,15 @@ func adminUserRoleLabel(role models.Role) string { } } +func adminLevelOptions() []int { + n := models.MaxLevel() + out := make([]int, n) + for i := 0; i < n; i++ { + out[i] = i + 1 + } + return out +} + // AdminUserBanPost 禁言 / 解禁 func (d Deps) AdminUserBanPost(c *gin.Context) { ctx := d.ctx(c) @@ -192,3 +203,24 @@ func (d Deps) AdminUserPointsPost(c *gin.Context) { ctx.SetFlash(fmt.Sprintf("积分已调整,当前余额 %d", bal)) ctx.Redirect("/admin/users") } + +// AdminUserLevelPost 设定等级(同步 Exp 到门槛) +func (d Deps) AdminUserLevelPost(c *gin.Context) { + ctx := d.ctx(c) + if !ctx.CheckCSRF() { + d.renderAdminUsers(ctx, "无效请求,请重试") + return + } + id64, _ := strconv.ParseUint(c.Param("id"), 10, 64) + level, err := strconv.Atoi(strings.TrimSpace(c.PostForm("level"))) + if err != nil { + d.renderAdminUsers(ctx, "请选择有效等级") + return + } + if err := services.SetUserLevel(uint(id64), level); err != nil { + d.renderAdminUsers(ctx, err.Error()) + return + } + ctx.SetFlash(fmt.Sprintf("已设为 Lv%d(经验门槛 %d)", level, models.ExpForLevel(level))) + ctx.Redirect("/admin/users") +} diff --git a/routers/web/home.go b/routers/web/home.go index ef39562..f29d00c 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -101,6 +101,7 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) { admin.POST("/users/:id/ban", deps.AdminUserBanPost) admin.POST("/users/:id/verify", deps.AdminUserVerifyPost) admin.POST("/users/:id/points", deps.AdminUserPointsPost) + admin.POST("/users/:id/level", deps.AdminUserLevelPost) admin.GET("/badges", deps.AdminBadgesGet) admin.POST("/badges", deps.AdminBadgeCreate) admin.POST("/badges/award", deps.AdminBadgeAwardPost) diff --git a/routers/web/profile.go b/routers/web/profile.go index c575915..24ad8bf 100644 --- a/routers/web/profile.go +++ b/routers/web/profile.go @@ -33,6 +33,7 @@ type profileData struct { Level int Exp int Points int + Badges []userPublicBadge PostCount int64 CommentCount int64 FavoriteCount int64 @@ -112,6 +113,14 @@ func (d Deps) renderProfile(ctx *webctx.Context, errMsg string) { AvatarMaxMB: d.Settings.AvatarMaxMB(), SignatureMax: d.Settings.SignatureMax(), } + if d.Badge != nil { + _ = d.Badge.EvaluateAuto(uid) + if rows, err := d.Badge.ListUserBadges(uid); err == nil { + for _, v := range services.BadgeViews(rows, 0) { + data.Badges = append(data.Badges, userPublicBadge{Name: v.Name, Description: v.Description}) + } + } + } if d.Points != nil { data.CheckIn, _ = d.Points.GetCheckInStatus(uid) data.Lottery, _ = d.Points.GetLotteryStatus(uid) diff --git a/routers/web/user.go b/routers/web/user.go index 469ef19..c3a1f5e 100644 --- a/routers/web/user.go +++ b/routers/web/user.go @@ -10,6 +10,11 @@ import ( "github.com/gin-gonic/gin" ) +type userPublicBadge struct { + Name string + Description string +} + type userPublicData struct { PageChrome UserID uint @@ -20,6 +25,7 @@ type userPublicData struct { Level int Exp int Points int + Badges []userPublicBadge PostCount int64 CommentCount int64 FavoriteCount int64 @@ -75,6 +81,15 @@ func (d Deps) UserPublic(c *gin.Context) { nick := displayName(user) chrome := d.chrome(ctx, nick+" · "+d.Settings.SiteBranding().Name, strings.TrimSpace(user.Signature), "") hasMore := int64(page*size) < total + badges := []userPublicBadge{} + if d.Badge != nil { + _ = d.Badge.EvaluateAuto(user.ID) + if rows, err := d.Badge.ListUserBadges(user.ID); err == nil { + for _, v := range services.BadgeViews(rows, 0) { + badges = append(badges, userPublicBadge{Name: v.Name, Description: v.Description}) + } + } + } ctx.HTML(http.StatusOK, "user/view", userPublicData{ PageChrome: chrome, UserID: user.ID, @@ -85,6 +100,7 @@ func (d Deps) UserPublic(c *gin.Context) { Level: models.LevelFromExp(user.Exp), Exp: user.Exp, Points: user.Points, + Badges: badges, PostCount: st.PostCount, CommentCount: st.CommentCount, FavoriteCount: st.FavoriteCount, LikeReceived: st.LikeReceived, IsSelf: ctx.UserID() == user.ID, diff --git a/templates/admin/users.tmpl b/templates/admin/users.tmpl index fb154e9..270f58b 100644 --- a/templates/admin/users.tmpl +++ b/templates/admin/users.tmpl @@ -86,6 +86,16 @@ +
+ + {{$cur := .Level}} + + +
{{end}} diff --git a/templates/profile/view.tmpl b/templates/profile/view.tmpl index a4bab16..ac216be 100644 --- a/templates/profile/view.tmpl +++ b/templates/profile/view.tmpl @@ -75,6 +75,13 @@

用户名 {{.Username}}

邮箱 {{.Email}}

等级 Lv.{{.Level}} · 经验 {{.Exp}}

+ {{if .Badges}} + + {{end}} {{if .Avatar}}

头像

{{end}} diff --git a/templates/user/view.tmpl b/templates/user/view.tmpl index 0a9ccaa..3f5f94e 100644 --- a/templates/user/view.tmpl +++ b/templates/user/view.tmpl @@ -11,6 +11,13 @@

{{if .Nickname}}{{.Nickname}}{{else}}{{.Username}}{{end}}

@{{.Username}} · Lv.{{.Level}} · 积分 {{.Points}}

+ {{if .Badges}} +
    + {{range .Badges}} +
  • {{.Name}}
  • + {{end}} +
+ {{end}} {{if .Signature}}

{{.Signature}}

{{end}} {{if .IsSelf}}

编辑资料

{{else if .LoggedIn}}

发私信

{{end}} diff --git a/web_src/css/site.css b/web_src/css/site.css index 119f225..9047b2a 100644 --- a/web_src/css/site.css +++ b/web_src/css/site.css @@ -1040,6 +1040,33 @@ points-only[data-locked="false"] { align-items: center; margin: 0; } +.j13-admin-level { + display: inline-flex; + flex-wrap: wrap; + gap: 0.25rem; + align-items: center; + margin: 0; +} +.j13-admin-level select { + min-width: 4.5rem; +} +.j13-user-badges { + list-style: none; + margin: 0.5rem 0 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.j13-user-badge { + display: inline-block; + padding: 0.15rem 0.45rem; + font-size: 0.8rem; + border: 1px solid var(--j13-border); + border-radius: 4px; + color: var(--j13-muted); +} +.j13-user-badge[title] { cursor: help; } .j13-admin-badge-award-btns { display: flex; flex-wrap: wrap;