docs: 更新 development.md 到 v0.1.0-rc1,覆盖全部 Phase 及审计修复
Some checks failed
CI / Lint + Build (push) Has been cancelled

This commit is contained in:
2026-06-22 03:12:29 +08:00
parent c3c7390498
commit 3d69dae799

View File

@ -1,562 +1,194 @@
# 开发记录 — 阶段一:基础设施 + 用户认证(含"记住我"与令牌刷新)
# 开发记录 — MetaLab RC
> 最后更新2026-05-24
> 最后更新2026-06-22 | 版本v0.1.0-rc1
## 一、已完成事项
### 1. 数据库配置与连接
**问题**PostgreSQL 使用 `peer` 认证Go 进程以系统用户 `victor_jay` 运行,无法连接数据库用户 `metazone`
**解决**
-`metazone` 数据库用户设置密码(`MetalabDev2026!`
- 连接方式从 Unix socket`/var/run/postgresql`)改为 TCP`127.0.0.1:5432`),触发 `scram-sha-256` 密码认证
- 数据库名称:`metalab_dev`
**涉及文件**`config.yaml``database.host` 改为 `127.0.0.1`)、`.env``DATABASE_PASSWORD`
---
### 2. 配置加载架构修复
**问题**Viper 的 `SetConfigFile(".env")` 会**覆盖**之前设置的 `config.yaml` 路径,导致所有 YAML 配置被清空,数据库名称为空。
**根本原因**Viper 不支持同时加载多个不同类型的配置文件。
**解决方案**
`internal/config/config.go` 中的加载流程:
```
1. loadEnvFile(".env") → 逐行读取,通过 os.Setenv() 注入环境变量
2. viper 只读取 config.yaml
3. bindEnvOverride() → 将 DATABASE_PASSWORD、JWT_SECRET 显式绑定到对应配置键
4. viper.AutomaticEnv() → 自动读取其余环境变量
```
环境变量优先级:**系统环境变量 > .env 文件**`.env` 仅在对应环境变量未设置时生效)
**涉及文件**`internal/config/config.go`(重写 `Load()` 函数)
---
### 3. 数据模型设计
#### 3.1 公共基础模型 (`common.go`)
| 字段 | 类型 | GORM 标签 | 说明 |
|---|---|---|---|
| `ID` | `uint` | `primarykey;column:uid` | 主键,数据库列名为 `uid`,自增 bigint非 UUID |
| `CreatedAt` | `time.Time` | — | 自动管理 |
| `UpdatedAt` | `time.Time` | — | 自动管理 |
| `DeletedAt` | `gorm.DeletedAt` | `index;json:"-"` | 软删除JSON 不暴露 |
#### 3.2 用户模型 (`user.go`)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `Email` | `varchar(255)` | `uniqueIndex; not null` | 注册和登录的唯一凭证 |
| `PasswordHash` | `varchar(255)` | `not null; json:"-"` | bcrypt(12)JSON 永不暴露 |
| `Username` | `varchar(16)` | `uniqueIndex; not null` | 全局唯一10 位随机 `[a-z0-9]` 自动生成 |
| `Avatar` | `varchar(500)` | `default:''` | 预留头像字段 |
| `Bio` | `text` | — | 预留个人简介 |
| `Role` | `varchar(20)` | `default:user; index` | `user` / `moderator` / `admin` |
| `Status` | `varchar(20)` | `default:active; index` | `active` / `banned` |
**关键设计决策:**
| 决策项 | 选择 | 原因 |
|---|---|---|
| 主键方案 | 自增 bigint`uid` 列)而非 UUID | 社区应用索引性能更好URL 简洁 `/users/42``JOIN` 体积小 |
| 角色控制 | `role string` 而非 `is_admin bool` | 可扩展(已有 user/moderator/admin 三级),符合最佳实践 |
| 用户名生成 | `crypto/rand` 生成 10 位 `[a-z0-9]` | 3,656 万亿种组合,千万用户碰撞概率 < 0.01% |
| 用户名长度 | `varchar(16)` | 作为最大长度限制暂未支持中文字符名 |
| 密码哈希 | bcrypt(12) | 行业标准成本 12 平衡安全与性能 |
| 软删除 | GORM `DeletedAt` | 保留数据误删可恢复 |
---
### 4. 用户注册
**流程**
```
POST /api/auth/register
→ 验证 Email/Password/ConfirmPasswordbinding 标签)
→ Service.Register()
→ validatePassword()≥8位 + 含字母 + 含数字
→ userRepo.FindByEmail():邮箱查重
→ bcrypt.GenerateFromPassword(,12):密码哈希
→ generateUsername():生成 10 位随机用户名,重试 20 次去重
→ userRepo.Create():写入数据库
→ buildToken():生成 access JWTHS25615 分钟)
→ 如果 req.RememberMe → buildRefreshToken():生成 refresh JWTHS25630 天)
→ SetAuthCookies(accessToken, refreshToken) → 设置 Cookie
→ 返回 JSON { success, message, data: user }
```
**关键点**注册即登录Cookie 自动设置前端无需额外操作勾选"记住我"则下发 refresh token + 标记 Cookie未勾选则不下发
**涉及文件**
- `internal/controller/auth_controller.go` `Register()` POST handler
- `internal/service/auth_service.go` `Register()``validatePassword()``generateUsername()``buildToken()``buildRefreshToken()`
- `internal/repository/user_repo.go` `Create()``FindByEmail()`
- `internal/model/dto.go` `RegisterRequest`binding 校验标签 + `remember_me`
---
### 5. 用户登录
**流程**
```
POST /api/auth/login
→ 验证 Email/Password
→ Service.Login()
→ userRepo.FindByEmail():查找用户
→ 检查 Status != banned封禁用户拒绝登录
→ bcrypt.CompareHashAndPassword():密码校验
→ buildToken():生成 access JWT
→ 如果 req.RememberMe → buildRefreshToken():生成 refresh JWT
→ SetAuthCookies(accessToken, refreshToken)
→ 返回 JSON { success, message, data: user }
```
**错误处理**根据错误类型返回不同 HTTP 状态码和消息
- 401邮箱不存在 / 密码错误
- 403用户已封禁
- 500服务器内部错误
**涉及文件**`auth_controller.go``Login()`)、`auth_service.go``Login()``ErrUserBanned``buildRefreshToken()`
---
### 6. JWT 认证中间件
| 中间件 | 用途 | 注入上下文的值 |
|---|---|---|
| `AuthRequired` | 严格认证失败返回 401 | `uid``email``username``role` |
| `AuthOptional` | 可选认证 token 就注入没有也放行 | 同上 token 时不注入 |
**Cookie 配置**
| Cookie | 名称 | HttpOnly | Secure | SameSite | 过期 |
|---|---|---|---|---|---|
| Access token | `mlb_token` | | ✅(debug 模式 HTTP | Lax | 15 min |
| Refresh token | `mlb_refresh` | | | Lax | 30 day记住我时 |
| 记住我标记 | `mlb_rm` | | | | 30 day记住我时 |
**中间件公开函数**
| 函数 | 用途 |
|---|---|
| `SetAuthCookies(c, accessToken, refreshToken, cfg)` | 设置 access + refresh+ mlb_rm 标记 |
| `SetAccessCookie(c, accessToken, cfg)` | 仅刷新 access cookie续期调用 |
| `ClearAuthCookies(c, cfg)` | 清除全部三个 Cookie登出/过期 |
**涉及文件**`internal/middleware/auth.go`
---
### 7. 登录状态注入SSR 模板渲染)
`internal/common/helper.go` 中的 `BuildPageData()`
```go
func BuildPageData(c *gin.Context, extra gin.H) gin.H
```
- `extra` 键值对复制到 `gin.H`
- Gin 上下文中存在 `username` `AuthOptional` 中间件注入自动添加
- `IsLoggedIn: true`
- `Username: "..."`
- 所有页面 Controller 通过此函数构建模板数据
---
### 8. 页面路由与模板
| 路由 | 方法 | 中间件 | 模板 | 说明 |
|---|---|---|---|---|
| `/` | GET | `AuthOptional` | `home/index.html` | 已登录显示首页未登录重定向 `/auth/login` |
| `/auth/register` | GET | `AuthOptional` | `auth/register.html` | 已登录重定向 `/` |
| `/auth/login` | GET | `AuthOptional` | `auth/login.html` | 已登录重定向 `/` |
---
### 9. NAV 导航栏逻辑
```html
{{if .IsLoggedIn}}
→ 显示用户名链接(指向 /settings+ "退出"按钮
{{else}}
→ 显示"登录"按钮(指向 /auth/login
{{end}}
```
**设计决策**NAV 只显示"登录"按钮不显示独立"注册"按钮注册入口在登录页面内部提供作为开发者社区用户应能理解)。已登录用户可通过"退出"按钮清除所有 Cookie 并返回首页
---
### 10. 模板加载器重构
**问题**原加载器使用 `info.Name()`纯文件名导致 `home/index.html` 注册为 `index.html`与路由引用的 `home_index.html` 不匹配模板渲染失败
**解决**改为使用相对路径作为模板名
## 一、架构总览
```
layout/header.html
layout/nav.html
layout/footer.html
auth/register.html
auth/login.html
home/index.html
Web 适配器 (Gin Controller) → ISP Ports (interfaces.go)
Service 核心逻辑
Store Interfaces (repository.go) ← DB 适配器 (GORM Repo)
```
`{{template}}` 引用全部更新为这些相对路径名
| 层次 | 职责 | 单文件上限 |
|------|------|-----------|
| Router | 路由映射 + 依赖注入 | ≤ 60 行 |
| Controller | 参数绑定 + 调用服务 + 返回 | ≤ 120 行 |
| Middleware | 认证/授权/安全头/限流 | ≤ 60 行 |
| Service | 业务逻辑 | ≤ 300 行 |
| Repository | 纯数据库操作 | ≤ 200 行 |
| Model | 纯结构体 | ≤ 80 行 |
**涉及文件**`internal/theme/loader.go`所有 `.html` 模板文件中的 `{{template}}` 引用
## 二、已完成阶段
---
### Phase 1基础设施 + 用户认证
### 11. "记住我" + 刷新令牌Refresh Token
- PostgreSQL + AutoMigrate + 部分唯一索引(软删除用户邮箱复用
- Gin + 模板渲染 + 静态资源哈希缓存破坏
- 用户注册/登录/登出 + bcrypt(12) 密码哈希
- 服务端 Session滑动窗口续期Redis + 内存 FallbackStore 自动降级
- 全局 `SecurityHeaders` 中间件CSP nonce、HSTS、X-Frame-Options、X-Content-Type-Options
- `InjectSiteInfo` 中间件注入站点品牌信息到所有 SSR 页面
- 角色系统user/moderator/admin/ownerOCP 可扩展)
- `BuildPageData()` / `BuildAdminPageData()` 统一模板数据注入
- CSRF Double Submit Cookie 保护
- 敏感操作限流(`SensitiveRateLimit`:改密/注销/签到)+ 登录限流(账户+IP 双维度滑动窗口)
- 维护模式(仅 owner 可访问,其余用户重定向登录页)
#### 11.1 设计目标
### Phase 2公告系统
| 场景 | 行为 |
|---|---|
| 用户勾选"记住我" | Cookie`mlb_token`(15min) + `mlb_refresh`(30d) |
| 用户未勾选 | Cookie`mlb_token`(15min)过期需重新登录 |
| 页面持续开着 | 前端每 10 分钟静默调用 `/api/auth/refresh` 续期 access token |
| 关闭浏览器 15+ min 后回来 | access token 过期 JS 自动刷新 页面 reload 恢复登录态 |
| 30 天内任意时间回来 | refresh token 仍有效自动静默恢复登录 |
- 公告 CRUD + 管理后台管理页面
- 前台首页公告区域
#### 11.2 Cookie 架构
### Phase 3文章属性
| Cookie | 作用 | HttpOnly | JS 可读 | 过期时间 | 说明 |
|---|---|---|---|---|---|
| `mlb_token` | access token | | | 15 min | JWT `uid/email/username/role/exp/iat` |
| `mlb_refresh` | refresh token | | | 30 day勾选记住我时 | JWT `uid/email/username/role/exp/iat/purpose:"refresh"` |
| `mlb_rm` | 记住我标记 | | | 30 day勾选记住我时 | 值为 `"1"`仅标记位非凭据 |
- Post 模型Visibilitypublic/private、PostTypeoriginal/reprint、ReprintSource
- 创作声明7 种)、禁止转载标记
- 文章置顶category/global+ 管理后台 Pin/Unpin API
- 原创/转载:写文章页条件联动
- 模板函数 `declarationLabel()`
**设计决策 — `mlb_rm` 标记 Cookie**
- 本质是**非安全标记位**not a credential用于 JS 判断"是否该尝试刷新"
- 如果不存在未勾选记住我`common.js` 跳过所有刷新逻辑 **不会产生 401 控制台报错**
- 值仅为 `"1"`即使被 XSS 读取也无法用于任何认证操作
### Phase 4分类系统 + 标签系统
#### 11.3 配置 (`config.yaml` + `config.go`)
- Category 两级树形结构 + 管理后台页面
- Tag 系统 + 落地页 `/tags/{slug}`
- 写文章页分类选择 + 标签输入
```yaml
jwt:
access_expire: 15 # 分钟
refresh_expire: 168 # 小时 (7 天) — 备用字段,当前未启用
remember_expire: 720 # 小时 (30 天) — refresh token 有效期
### Phase 5定时发布
- Post 模型 `ScheduledAt` + 审核逻辑
- Scheduler goroutine 每分钟扫描发布到期文章
- 前端日期时间选择器min=now+30min
### Phase 6收尾与审计修复2026-06-22
**安全加固:**
| 项目 | 变更 |
|------|------|
| CSP | `'unsafe-inline'` 移除nonce 替代(`'unsafe-eval'` 保留供 Vditor |
| HSTS | 始终启用 `max-age=31536000; includeSubDomains; preload` |
| 密码强度 | 四级low/medium/high/very_high+ 连续字符检查 |
| 注册限流 | IP 维度 1min×5 次 |
| CSRF Token 日志 | 仅记录长度,不记明文 |
| DB 健康降级 | 5s ping + 全局 `dbHealthy` 标志 + 中间件拦截返回固定错误页 |
| 文件上传 | `image.Decode` 强制解码验证(替代 magic bytes |
**代码质量:**
| 项目 | 变更 |
|------|------|
| golangci-lint | 57 告警 → 0CI strict 模式零告警 |
| 包注释 | 16 个包全部添加 |
| 错误消息 | 全部小写开头 |
| errorlint | `==` 比较 → `errors.Is` |
| unused 代码 | `hasUnicode`/`energyStore`/`current`/`energyAdminUseCase` 移除 |
| gofumpt | 全量格式化 |
**架构重构:**
| 项目 | 变更 |
|------|------|
| `router/api.go` | 198 行拆为 7 个域文件auth/settings/posts/comments/reactions/social/studio |
| 管理后台站点设置 | 单页 310 行拆为 4 个子页brand/security/registration/content+ 子菜单,旧路由 301 重定向 |
| 前台用户设置 | 1201 行拆为 6 个独立模板 + 6 个独立 JS 文件 |
| Controller Exp 读取 | `common.GetGinExp()` 封装,不再直接读 context |
| UID 解析 | `common.ParseUIDParam()` 统一follow/admin controller 改用 |
| BaseModel 统一 | Post/Comment/Announcement/Category/Folder/FolderItem 嵌入 BaseModel`primaryKey``primarykey` |
| 时区 | `config.yaml server.timezone``time.Local` 初始化 |
**Footer 合规信息:**
- `site_settings.go` 扩展 8 个字段operator/ICP/公安/文网文/算法备案/隐私/条款)
- `footer.html` 渲染合规行(`safeHTML` 模板函数,空值跳过)
- 管理后台合规信息输入区
**注册准则控制:**
- 三层控制:`show_guidelines` / `force_guidelines` / `guidelines_timer`
- 管理后台注册设置区
## 三、文件清单
```text
mce/
├── cmd/server/main.go # 入口配置加载、DB/Redis、模板、路由、调度器
├── config.yaml # 默认配置
├── .golangci.yml # golangci-lint v2 配置
├── .gitea/workflows/ci.yml # CI 流水线strict 模式)
├── scripts/ci.sh # CI 脚本(支持 --strict
├── Makefile # fmt/lint/ci/test 命令
├── internal/
│ ├── common/ # 通用工具响应格式、Context 提取、模板数据构建、错误哨兵
│ ├── config/ # 配置加载 + SiteSettings DB 持久化内存缓存
│ ├── model/ # 所有数据模型25 个)
│ ├── repository/ # GORM 数据访问17 个 repo
│ ├── service/ # 业务逻辑18 个 service含 repository.go 接口定义)
│ ├── controller/ # Gin handler38 个 controller 文件)
│ │ └── admin/ # 管理后台 controller7 个)
│ ├── middleware/ # 中间件:认证/CSRF/安全头/限流/维护/DB健康
│ ├── router/ # 路由注册 + 依赖注入(按域拆分)
│ ├── session/ # 服务端 SessionRedis + Memory + Fallback
│ ├── scheduler/ # 定时发布调度器
│ ├── cache/ # 首页缓存
│ └── theme/ # 模板加载器 + safeHTML 函数
├── templates/
│ ├── MetaLab-2026/ # 前端主题
│ │ ├── html/ # Go 模板(按功能分目录)
│ │ └── static/ # CSS/JS/图片 + Vditor 编辑器
│ ├── admin/ # 管理后台模板
│ └── shared/ # 共享静态资源
└── docs/ # 设计文档
```
`JWTConfig` 新增字段
```go
RememberExpire int `mapstructure:"remember_expire"` // 小时
```
**关键**`mapstructure` 标签**不可省略**否则 Viper 无法将 YAML 键映射到结构体字段导致过期时间全为 0
#### 11.4 请求 DTO (`dto.go`)
`LoginRequest` `RegisterRequest` 都新增了 `RememberMe` 字段
```go
RememberMe bool `json:"remember_me"`
```
#### 11.5 后端业务逻辑 (`auth_service.go`)
**变更的方法:**
| 方法 | 旧返回值 | 新返回值 |
|---|---|---|
| `Register()` | `(token, user, error)` | `(accessToken, refreshToken, user, error)` |
| `Login()` | `(token, user, error)` | `(accessToken, refreshToken, user, error)` |
`req.RememberMe == false` `refreshToken` 返回空字符串 `""`
**新增方法:**
| 方法 | 签名 | 说明 |
|---|---|---|
| `buildRefreshToken(user)` | `(string, error)` | 构建 30 天过期 JWT`purpose` claim `"refresh"` |
| `RefreshAccessToken(refreshTokenStr)` | `(string, error)` | 校验 refresh token 返回新 access token |
**`RefreshAccessToken` 校验流程**
```
1. 解析 refresh token JWT
→ 失败ErrTokenExpired登录已过期
2. 校验 claims["purpose"] == "refresh"
→ 不匹配ErrTokenInvalid防止 access token 被用于刷新)
3. 从 claims 提取 uid/email/username/role → 构建新 access token
```
**安全点**`purpose: "refresh"` 声明确保**access token 无法充当 refresh token**防止短生命周期令牌被滥用为长期凭据
#### 11.6 Session 恢复实现(前端 JS
`common.js` 中的自动刷新逻辑
```
页面加载(非 /auth/* 路径)
→ 检查 document.cookie 是否存在 mlb_rm
→ 不存在 → 跳过所有刷新(无 401 报错)
→ 存在 → fetch POST /api/auth/refresh
→ 成功:
1. 启动 10 分钟定时器
2. 页面未显示登录态(无 logoutBtn
→ sessionStorage 防重 → window.location.reload()
重载后 access token 存在 → SSR 正确渲染登录状态
→ 失败 → 停止定时器(用户需手动重新登录)
```
**关键实现细节**
- `fetch()` 而非 `XMLHttpRequest``fetch` HTTP 2xx 不会触发浏览器控制台静默日志
- `sessionStorage` 防无限循环`mlb_refreshed` 标志确保 reload 只执行一次
- 不在认证页面执行`/auth/login``/auth/register` 路径直接跳过
#### 11.7 界面
登录页和注册页各增加一个复选框
```html
<div class="form-group remember-group">
<label class="remember-label">
<input type="checkbox" id="rememberMe">
保持登录 30 天
</label>
<span class="remember-hint">非共享设备推荐</span>
</div>
```
**默认不勾选**——公共环境教室网吧共享电脑不会意外留下长期凭据
---
### 12. 登出功能
#### 12.1 后端 API
**路由**`POST /api/auth/logout`
**处理流程**`auth_controller.go` `Logout()`
```
1. middleware.ClearAuthCookies(c, cfg)
→ 清除 mlb_token、mlb_refresh、mlb_rm 三个 CookieMaxAge=-1
2. 返回 JSON { success: true, message: "已退出登录" }
```
#### 12.2 前端
**NAV 导航栏**`nav.html`
```html
{{if .IsLoggedIn}}
<li><a href="/settings" class="nav-user">{{.Username}}</a></li>
<li><a href="javascript:void(0)" class="nav-logout" id="logoutBtn">退出</a></li>
{{end}}
```
**JS 处理**`common.js`
```
点击"退出" → POST /api/auth/logout → Cookie 清除完毕 → 跳转首页
```
---
### 13. 刷新令牌 API
**路由**`POST /api/auth/refresh`
**处理流程**`auth_controller.go` `RefreshToken()`
```
1. 从 Cookie 读取 mlb_refresh
→ 为空401 "请重新登录"
2. 调用 authService.RefreshAccessToken(refreshToken)
→ 失败ClearAuthCookies() + 401 "登录已过期"
3. 成功SetAccessCookie() → 下发新 access token
→ 返回 200 { success: true }
```
**注意**此端点**只刷新 access token**不刷新 refresh tokenrefresh token 30 天固定有效期到期后用户需手动登录这避免了无限续期带来的安全隐患长期不活跃的 refresh token 最终会自然过期)。
---
### 14. 个人设置页与头像上传
**涉及文件**参考 [`docs/settings.md`](settings.md) 获取完整实现文档
**功能概要**
| 功能 | 实现文件 |
|------|----------|
| 用户名编辑 | `auth_service.go` `UpdateProfile()`正则 `[\p{Han}a-zA-Z0-9_-]` 白名单 |
| 个性签名编辑 | 同上0-128 字符 |
| 头像处理管线 | `avatar_service.go`新文件)— 解码裁切CatmullRom缩放WebP二分编码原子写入 |
| 前端裁切弹窗 | `settings/index.html` 固定框 + 图片平移/缩放 + 坐标映射 |
| CSS样式 | `settings.css` 浅色主题裁切弹窗Toast通知输入框样式 |
| CSP放宽 | `security.go` `img-src 'self' data:`裁切预览用data URI |
| 静态路由 | `main.go` `/uploads/avatars` `./storage/uploads/avatars` |
**设计决策**
## 四、关键设计决策
| 决策 | 选择 | 原因 |
|------|------|------|
| 用户名校验 | 白名单 regex | 比黑名单更安全直接排除 `<>&"'` XSS 向量 |
| 字符计数 | `utf8.RuneCountInString` | 确保与 PostgreSQL `varchar(16)` 语义一致 |
| 头像编码 | WebP + 二分查找质量 | 目标 100KB兼顾质量与体积 |
| 原子写入 | `.tmp` `os.Rename` | 防止写入中断产生损坏文件 |
| 裁切方式 | 固定框 + 图片平移/缩放 | 框大小不变zoom使框覆盖更小区域真正支持精确定位 |
| GIF支持 | 不支持 | 裁切后无法保持动画无实际用途 |
| 最小分辨率 | 128×128 | 防止马赛克图被拉升到 512×512 |
| CSP data: URI | 显式允许 img-src | 裁切预览使用 FileReader data URI |
| Web 框架 | Gin | 性能好,生态成熟 |
| ORM | GORM v2 | AutoMigrate + 软删除 + 回调 |
| 数据库 | PostgreSQL | 社区级应用首选 |
| 会话 | 服务端 Session | 支持踢出设备、登录管理 |
| 存储 | Redis + Memory FallbackStore | Redis 不可达自动降级,恢复自动切回 |
| 模板 | Go html/template | 无前端框架依赖SSR 直出 |
| 编辑器 | Vditor v3.11.2(本地部署) | Markdown 所见即所得 |
| 图表 | Chart.js v4.4.0(本地化) | 趋势可视化 |
| 密码 | bcrypt(12) | 行业标准 |
| 架构 | 分层 + ISP 接口隔离 | 依赖倒置Controller 不依赖完整 Service |
| 重构 | 不向后兼容 | 全新项目,废弃旧代码不做兼容层 |
| Lint | golangci-lint v2.12.217 个 linter | CI strict 模式零告警 |
---
## 五、数据模型一览
## 二、解决方案建议
| 模型 | BaseModel | 说明 |
|------|-----------|------|
| User | ✅ | 用户(邮箱/用户名/密码/角色/状态/经验) |
| Post | ✅ | 帖子/文章(标题/正文/状态/分类/标签/属性/计数器) |
| Comment | ✅ | 评论(含 is_deleted 自定义软删除) |
| AuditSubmission | ✅ | 审核记录 |
| Notification | ✅ | 通知 |
| Announcement | ✅ | 公告 |
| Category | ✅ | 文章分类(两级树形) |
| Folder | ✅ | 收藏夹 |
| FolderItem | ✅ | 收藏记录 |
| Tag | ❌ | 标签 |
| PostTag | ❌ | 文章-标签关联(联合主键) |
| PostLike | ❌ | 点赞(联合主键) |
| PostDislike | ❌ | 踩(联合主键) |
| UserFollow | ❌ | 关注关系(联合主键) |
| CommentMention | ❌ | @提及映射 |
| SiteSetting | ❌ | 站点设置Key-Value |
| EnergyLog / FundLog / PostEnergizeLog | ❌ | 域能/资金流日志 |
| UserCheckIn / UserTask | ✅ | 签到/任务 |
| DailyExpSummary / DailyLikeSummary | ❌ | 每日统计聚合 |
| PostReadLog / PostGuestReadLog | ❌ | 阅读日志 |
| CommunityFund | ❌ | 公户余额(单行表) |
| Level | ❌ | 等级配置(内存) |
### 2.1 PostgreSQL 密码认证(问题一记录)
## 六、待做事项
**系统信息**
- 分布Linux Mint
- 数据库用户`metazone`
- 连接方式TCP `127.0.0.1:5432` Unix socket
- 认证方式`scram-sha-256`
- 密码`MetalabDev2026!`存储于 `.env` `DATABASE_PASSWORD`
- 数据库`metalab_dev`
**验证命令**
```bash
# 测试连接
PGPASSWORD='MetalabDev2026!' psql -h 127.0.0.1 -U metazone -d metalab_dev -c "SELECT current_user, version();"
```
### 2.2 .env 文件结构
```bash
# 必填
DATABASE_PASSWORD=MetalabDev2026!
JWT_SECRET=p2BVUsELbny4W4sYNvoyFesyglD2TlPRT+bzpDk8Dq/E2s9tZFzT6jC8N3AvQpzE
```
`.env` 已加入 `.gitignore`提供 `.env.example` 模板文件
---
## 三、代码规范新增/修改
### 3.1 新增规范
| 编号 | 规范 | 说明 |
|---|---|---|
| C01 | **`BuildPageData()` 统一模板数据注入** | 所有页面 Controller 必须通过此函数构建 `gin.H`禁止手动拼装 `IsLoggedIn`/`Username` |
| C02 | **Cookie 认证优先** | 使用 HttpOnly Cookie`mlb_token`传递 JWT而非 `Authorization` Header兼顾安全与 SSR |
| C03 | **主键列名:`uid`** | Go 侧字段名 `ID`GORM 标签 `column:uid`JSON 序列化 `"uid"` |
| C04 | **用户名10 位随机 `[a-z0-9]`** | 使用 `crypto/rand` `math/rand`20 次重试去重 |
| C05 | **配置 env 注入模式** | `.env` 文件通过 `os.Setenv()` 注入环境变量Viper 通过 `BindEnv` + `AutomaticEnv` 读取不再使用 viper `.env` 文件加载 |
| C06 | **模板名:相对路径** | `layout/header.html``auth/register.html` 禁止用纯文件名避免冲突 |
### 3.2 修改的已有规范
| 编号 | 变更 | | |
|---|---|---|---|
| M01 | 用户标识字段 | `nickname`非唯一 | `username`全局唯一varchar(16) |
| M02 | JSON 序列化字段名 | `"id"` | `"uid"` |
| M03 | `BaseModel.ID` GORM 列名 | `id`默认 | `uid``column:uid` |
---
## 四、当前文件清单
### 后端Go
```
internal/
├── config/
│ └── config.go # Config 结构体 + Load() + loadEnvFile() + DSN()
│ # JWTConfig 含 AccessExpire/RefreshExpire/RememberExpire
├── model/
│ ├── common.go # BaseModeluid 主键 + 时间戳 + 软删除)
│ ├── user.go # User 结构体 + Role/Status 常量
│ └── dto.go # RegisterRequest / LoginRequest 含 RememberMe
├── repository/
│ └── user_repo.go # UserRepoCreate/FindByEmail/FindByID/FindByUsername/ExistsByEmail/ExistsByUsername
├── service/
│ └── auth_service.go # AuthServiceLogin/Register/RefreshAccessToken + buildToken/buildRefreshToken + 8 个错误哨兵)
├── controller/
│ └── auth_controller.go # AuthControllerRegisterPage/LoginPage/Register/Login/Logout/RefreshToken/CheckEmail
├── middleware/
│ └── auth.go # AuthRequired/AuthOptional/SetAuthCookies/SetAccessCookie/ClearAuthCookies/parseToken
│ # 三个 Cookiemlb_token / mlb_refresh / mlb_rm
├── router/
│ └── router.go # Setup() — 路由注册 + 依赖注入链(含 /auth/logout、/auth/refresh
├── common/
│ └── helper.go # BuildPageData() — SSR 登录状态注入
└── theme/
└── loader.go # LoadTemplates() — 相对路径模板加载 + LoadContent()
```
### 前端(模板 + 静态资源)
```
templates/MetaLab-2026/
├── html/
│ ├── layout/
│ │ ├── header.html # HTML headtitle/meta/CSS 引入)
│ │ ├── nav.html # 导航栏(已登录显示用户名+退出按钮 / 未登录显示登录按钮)
│ │ └── footer.html # 页脚
│ ├── auth/
│ │ ├── register.html # 注册页(含准则模态框 + 60s 倒计时 + 记住我复选框)
│ │ └── login.html # 登录页(简洁表单 + 记住我复选框)
│ └── home/
│ └── index.html # 首页Hero 区域 + 欢迎信息)
├── static/
│ ├── css/
│ │ ├── common.css # 全局样式 + NAV 样式(含 .nav-logout
│ │ └── auth.css # 认证页样式表单、模态框、Toast、.remember-group/.remember-label/.remember-hint
│ └── js/
│ ├── common.js # 通用工具Toast + 登出 + 自动令牌刷新 + mlb_rm 检测)
│ ├── register.js # 注册页逻辑(表单验证/AJAX 提交/准则模态框/倒计时/记住我)
│ └── login.js # 登录页逻辑(表单验证/AJAX 提交/Toast/记住我)
```
### 配置文件
```
config.yaml # 主配置(服务/数据库/JWT 含 remember_expire/BCrypt
.env # 敏感信息(密码/JWT密钥→ .gitignore
.env.example # 敏感信息模板(无真实值)
```
---
## 五、待做事项
按优先级排序
| 优先级 | 功能 | 状态 | 说明 |
|---|---|---|---|
| **P0** | 邮箱验证码 | 待做 | 注册时发送验证码`auth/verify-email`预留了路由和接口 |
| ~~**P0**~~ | ~~登出功能~~ | 已完成 | `POST /api/auth/logout` 清除三个 Cookie + 跳转首页 |
| **P1** | 首页完善 | 待做 | 当前仅有 Hero 区域需添加帖子列表话题导航等 |
| **P1** | 帖子系统 | 待做 | CRUD + 话题分类 + Markdown 编辑 |
| **P1** | 用户设置页 | 已完成 | `/settings/profile``/settings/account` 用户名修改个性签名头像上传与裁切 |
| **P2** | 管理后台 | 待做 | `/admin` 用户管理内容审核主题切换 |
| ~~**P2**~~ | ~~刷新令牌~~ | 已完成 | 记住我 + access/refresh Token + 自动续期 + 登出 |
| **P3** | 多主题支持 | 待做 | `MetaLab-2026` `MetaLab-2027` 切换 |
---
## 六、约定速查
| 类别 | 约定 |
|---|---|
| 数据库主键 | Go: `ID uint`DB: `uid`bigint 自增JSON: `"uid"` |
| Access Token Cookie | `mlb_token`JWT(HS256)15 minHttpOnly + Secure + SameSite=Lax |
| Refresh Token Cookie | `mlb_refresh`JWT(HS256)30 day记住我时HttpOnly + Secure + SameSite=Lax `purpose:"refresh"` 声明 |
| 记住我标记 Cookie | `mlb_rm` `"1"`30 day记住我时 HttpOnlyJS 可读非凭据 |
| 密码哈希 | bcrypt(12)Go 字段 `PasswordHash`JSON 不暴露 |
| 用户名 | 10 `[a-z0-9]``crypto/rand` 生成全局唯一 |
| 角色 | `user` / `moderator` / `admin`string bool |
| 配置 | `.env` `os.Setenv` viper `BindEnv` + `AutomaticEnv` |
| 模板名 | 相对路径`layout/header.html``auth/register.html` |
| API 响应 | `common.Ok(ctx, data)` / `common.Error(ctx, code, msg)` |
| 页面数据 | `common.BuildPageData(c, extra)` 自动注入 `IsLoggedIn` / `Username` |
| 包依赖方向 | Router Controller Service Repository Model不可反向 |
| 令牌刷新 | `POST /api/auth/refresh`前端每 10 min 静默调用 |
| 优先级 | 功能 | 状态 |
|--------|------|------|
| P2 | B8: BaseModel 主键列名不一致 | ✅ 已完成 |
| P3 | 集成测试 | ⬜ 暂缓 |
| P3 | `git tag v0.1.0-rc1` | ⬜ 暂缓 |
| — | 黑名单系统 | ⬜ 未来迭代 |
| — | 首页推荐算法 | ⬜ 未来迭代 |
| — | 模板审计修复 | ⬜ 未来迭代 |