chore: Remove unrelated files and ignored local artifacts
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
.run/
|
||||
backend/.run/
|
||||
data/
|
||||
.sisyphus/
|
||||
docs/
|
||||
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
@@ -31,4 +34,3 @@ go.work.sum
|
||||
# Editor/IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
LFLXGWYNTKLWKGBHYKCI
|
||||
@@ -1,23 +0,0 @@
|
||||
[classic_mirrored_queue_version,
|
||||
classic_queue_type_delivery_support,
|
||||
detailed_queues_endpoint,
|
||||
direct_exchange_routing_v2,
|
||||
drop_unroutable_metric,
|
||||
empty_basic_get_metric,
|
||||
feature_flags_v2,
|
||||
implicit_default_bindings,
|
||||
listener_records_in_ets,
|
||||
maintenance_mode_status,
|
||||
message_containers,
|
||||
message_containers_deaths_v2,
|
||||
quorum_queue,
|
||||
quorum_queue_non_voters,
|
||||
restart_streams,
|
||||
stream_filtering,
|
||||
stream_queue,
|
||||
stream_sac_coordinator_unblock_group,
|
||||
stream_single_active_consumer,
|
||||
stream_update_config_command,
|
||||
tracking_records_in_ets,
|
||||
user_limits,
|
||||
virtual_host_metadata].
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{[rabbit@1ae715dbc287],[rabbit@1ae715dbc287]}.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,11 +0,0 @@
|
||||
#{id => {rabbitmq_metadata,rabbit@1ae715dbc287},
|
||||
machine =>
|
||||
{module,khepri_machine,
|
||||
#{member => {rabbitmq_metadata,rabbit@1ae715dbc287},
|
||||
store_id => rabbitmq_metadata}},
|
||||
friendly_name => "RabbitMQ metadata store",
|
||||
cluster_name => rabbitmq_metadata,uid => <<"RABBITDRASO8J6QGNT">>,
|
||||
initial_members => [],
|
||||
log_init_args => #{uid => <<"RABBITDRASO8J6QGNT">>},
|
||||
tick_timeout => 1000,broadcast_time => 100,
|
||||
install_snap_rpc_timeout => 120000,await_condition_timeout => 30000}.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
%% This file is auto-generated! Edit at your own risk!
|
||||
{segment_entry_count, 2048}.
|
||||
@@ -1 +0,0 @@
|
||||
/
|
||||
@@ -1,2 +0,0 @@
|
||||
{client_refs,[<<64,206,177,233,7,189,195,164,71,45,63,18,117,108,68,115>>]}.
|
||||
{index_module,rabbit_msg_store_ets_index}.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
{client_refs,[]}.
|
||||
{index_module,rabbit_msg_store_ets_index}.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
VHOST: /
|
||||
QUEUE: task_queue
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
disc.
|
||||
@@ -1 +0,0 @@
|
||||
[rabbit@1ae715dbc287].
|
||||
@@ -1 +0,0 @@
|
||||
RAWA
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
cXM
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
3.
|
||||
@@ -1 +0,0 @@
|
||||
cXM
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,90 +0,0 @@
|
||||
# 用户体系 + 社交深化 设计文档
|
||||
|
||||
> **日期**: 2025-04-25
|
||||
> **状态**: 待实施
|
||||
> **方案**: 依赖驱动分批(3 阶段)
|
||||
|
||||
## 概述
|
||||
|
||||
在现有短视频 Feed 系统基础上,扩展用户 profile 体系(头像、简介、Refresh Token、主页统计)和社交互动能力(通知、私信、话题、@提及)。
|
||||
|
||||
---
|
||||
|
||||
## P1 — 用户基石(4 项)
|
||||
|
||||
### 1. 头像上传 + 个人简介
|
||||
|
||||
**Account 模型扩展**:
|
||||
```go
|
||||
AvatarURL string `gorm:"type:varchar(512)" json:"avatar_url,omitempty"`
|
||||
Bio string `gorm:"type:varchar(255)" json:"bio,omitempty"`
|
||||
```
|
||||
|
||||
**新增接口**:
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/account/uploadAvatar` | multipart 上传头像,校验类型/大小,存 `.run/uploads/avatars/{id}/` |
|
||||
| POST | `/account/updateProfile` | JSON `{ avatar_url?, bio? }` 更新当前用户 |
|
||||
|
||||
**前端**: UserAvatar 组件支持 `src`,AccountView 加头像上传 + bio 编辑,Feed 卡片显示头像。
|
||||
|
||||
### 2. 登录态优化(Refresh Token)
|
||||
|
||||
**双 Token**:
|
||||
- Access Token: 15min 过期
|
||||
- Refresh Token: 7天过期,落库 + Redis 缓存
|
||||
|
||||
**新增接口**: `POST /account/refresh` — 接收 refresh token 返回新 access token
|
||||
|
||||
**前端**: auth store 存双 token,client.ts 401 自动刷新,登录页"记住我"。
|
||||
|
||||
---
|
||||
|
||||
## P2 — Feed 可见 + 通知(3 项)
|
||||
|
||||
### 3. 粉丝数 / 关注数展示
|
||||
|
||||
**后端**: 社交接口返回中加 `follower_count` / `vlogger_count`,通过聚合查询计数。
|
||||
|
||||
**前端**: UserProfileView 和 Feed 卡片显示粉丝数。
|
||||
|
||||
### 4. 用户主页增强
|
||||
|
||||
**后端**: `POST /account/getProfile` — 返回用户信息 + 视频列表 + 获赞总数。
|
||||
|
||||
**前端**: UserProfileView 加视频列表网格 + 统计卡片。
|
||||
|
||||
### 5. 实时消息通知
|
||||
|
||||
**架构**: 复用 MQ 事件(like.events / comment.events / social.events)→ NotificationWorker 消费 → 写 `Notification` 表 + WebSocket 推送。
|
||||
|
||||
**新增表**: `Notification` — id, recipient_id, sender_id, type, target_id, is_read, created_at。
|
||||
|
||||
**新增接口**:
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/notification/list` | 返回当前用户未读通知列表 |
|
||||
| POST | `/notification/markRead` | 标记单条/全部已读 |
|
||||
| GET | `/ws/notifications` | WebSocket 升级,实时推送 |
|
||||
|
||||
**前端**: AppShell 右上角通知铃铛 + 未读红点。
|
||||
|
||||
---
|
||||
|
||||
## P3 — 互动深化(3 项)
|
||||
|
||||
### 6. 私信 / 即时通讯
|
||||
|
||||
**新增表**: `Message` — id, from_id, to_id, content, is_read, created_at
|
||||
|
||||
**后端**: WebSocket 双向通道,`POST /message/send` + `POST /message/list`
|
||||
|
||||
### 7. #话题标签
|
||||
|
||||
**新增表**: `Tag` — id, name (unique);`VideoTag` — video_id, tag_id
|
||||
|
||||
**改动**: 视频发布时从 title/description 中提取 `#xxx`,写入 `VideoTag` 关联表;`POST /feed/listByTag` 按话题浏览。
|
||||
|
||||
### 8. @提及
|
||||
|
||||
**改动**: 评论发布时解析 `@username`,创建 Notification 并推送。
|
||||
@@ -1,149 +0,0 @@
|
||||
# feedsystem_video_go 全项目优化设计文档
|
||||
|
||||
> **日期**: 2025-04-25
|
||||
> **状态**: 待实施
|
||||
> **方案**: 风险驱动分批(方案 A)
|
||||
|
||||
## 概述
|
||||
|
||||
基于全项目代码审查,识别出 17 项优化点,覆盖安全、数据库、MQ 可靠性、代码质量、架构、前端六大维度。按风险优先级分为三批实施。
|
||||
|
||||
---
|
||||
|
||||
## P1 止血(4项)— 消除生产风险
|
||||
|
||||
### 1. Router 变量赋值 Bug
|
||||
|
||||
- **文件**: `backend/internal/http/router.go:147`
|
||||
- **问题**: `timelineMQ` 初始化失败时错误地将 `socialMQ` 设为 nil
|
||||
- **修复**: `socialMQ = nil` → `timelineMQ = nil`
|
||||
- **影响**: 1 行
|
||||
|
||||
### 2. 数据库复合索引
|
||||
|
||||
- **文件**: `backend/internal/video/video_entity.go`
|
||||
- **问题**: Feed 流排序查询缺少索引,可能全表扫描
|
||||
- **修复**: 在 Video 模型 GORM tag 中添加 3 个复合索引
|
||||
- `idx_videos_create_time` — `ListLatest`
|
||||
- `idx_videos_likes_count_id` — `ListLikesCountWithCursor`
|
||||
- `idx_videos_popularity_time_id` — `ListByPopularity`
|
||||
- **实施**: 修改 model tag + AutoMigrate 自动创建
|
||||
|
||||
### 3. ListByAuthorID 加 LIMIT
|
||||
|
||||
- **文件**: `backend/internal/video/video_repo.go:39-48`
|
||||
- **问题**: 查询无上限,单作者海量视频可导致内存溢出
|
||||
- **修复**: 加 `Limit(200)` 硬上限
|
||||
|
||||
### 4. MQ Worker 死信队列 + 退避重试
|
||||
|
||||
- **文件**: `middleware/rabbitmq/` + 4 个 Worker 文件
|
||||
- **问题**: 所有 Worker 使用 `Nack(false, true)` 无限重试
|
||||
- **修复**:
|
||||
- 声明死信交换 + 死信队列
|
||||
- 利用 `x-death` header 判断重试次数,≥3 次 Ack 并告警
|
||||
|
||||
---
|
||||
|
||||
## P2 加固(6项)— 安全隐患 + 规范化
|
||||
|
||||
### 5. rand.Read 错误处理
|
||||
|
||||
- **文件**: `backend/internal/video/video_handler.go:164-168`
|
||||
- **问题**: 忽略 `rand.Read` 错误,失败时文件名全零可能覆盖
|
||||
- **修复**: `randHex()` 返回 error,调用方处理
|
||||
|
||||
### 6. Handler 错误码精确化
|
||||
|
||||
- **文件**: 所有 handler 文件
|
||||
- **问题**: DB/内部错误统一返回 400
|
||||
- **修复**: 新增 `classifyHTTPStatus()` 辅助函数,Service 层返回哨兵错误区分 400/401/404/500
|
||||
|
||||
### 7. JWT Secret 弱默认值
|
||||
|
||||
- **文件**: `backend/internal/auth/jwt.go`
|
||||
- **问题**: 默认值 `"change-me-in-env"` 过于明显
|
||||
- **修复**: 未设环境变量时生成随机密钥并警告
|
||||
|
||||
### 8. 配置密码集中管理
|
||||
|
||||
- **文件**: `docker-compose.yml` + 3 个 config YAML
|
||||
- **问题**: 多处重复硬编码密码
|
||||
- **修复**: docker-compose 引用 `.env`,创建 `.env.example`,config YAML 保持现状
|
||||
|
||||
### 9. 前端路由鉴权守卫
|
||||
|
||||
- **文件**: `frontend/src/router/index.ts`
|
||||
- **问题**: Settings/Video 页面无登录拦截
|
||||
- **修复**: 添加 `router.beforeEach` 守卫
|
||||
|
||||
### 10. pprof 生产保护
|
||||
|
||||
- **现状**: 已监听 `127.0.0.1`,`config.docker.yaml` 已禁用
|
||||
- **动作**: 确认安全,仅需注释说明
|
||||
|
||||
---
|
||||
|
||||
## P3 优化(7项)— 架构 + 可维护性
|
||||
|
||||
### 11. HomeView.vue 拆分
|
||||
|
||||
- **文件**: `frontend/src/views/HomeView.vue` (918 行)
|
||||
- **拆分目标**:
|
||||
- `composables/useVideoFeed.ts`
|
||||
- `composables/useVideoPlayer.ts`
|
||||
- `composables/useLikeFollow.ts`
|
||||
- `components/CommentDrawer.vue`
|
||||
- `views/HomeView.vue`(精简至 ~350 行)
|
||||
|
||||
### 12. Feed Service 策略拆分
|
||||
|
||||
- **文件**: `backend/internal/feed/service.go` (547 行)
|
||||
- **拆分目标**: 按查询策略拆为 4 个文件
|
||||
- `strategy_latest.go` — 热冷分离 + ZSET
|
||||
- `strategy_follow.go` — 缓存穿透防护
|
||||
- `strategy_hot.go` — 快照合并 + 降级
|
||||
- `build_feed.go` — 公共方法
|
||||
|
||||
### 13. 视频列表虚拟滚动
|
||||
|
||||
- **问题**: 所有视频渲染 DOM,内存压力大
|
||||
- **修复**: 仅保留当前 ±1 条 DOM,离屏 `display:none` + `pause()`
|
||||
|
||||
### 14. 缓存键版本化
|
||||
|
||||
- **文件**: `middleware/redis/redis.go` + 所有 service
|
||||
- **修复**: `Client` 增加 `keyPrefix`,所有键通过 `c.Key()` 生成
|
||||
|
||||
### 15. Docker 健康检查
|
||||
|
||||
- **文件**: `docker-compose.yml`
|
||||
- **修复**: 为 backend/worker/frontend 增加 healthcheck
|
||||
|
||||
### 16. 前端错误监控
|
||||
|
||||
- **文件**: `frontend/src/api/client.ts` + 新增 `utils/error-reporter.ts`
|
||||
- **修复**: 增加全局错误上报钩子
|
||||
|
||||
### 17. Worker 优雅重启
|
||||
|
||||
- **文件**: `backend/cmd/worker/main.go`
|
||||
- **修复**: 替换 `log.Fatal` 为指数退避重试
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
```
|
||||
P1 (第1周) P2 (第2周) P3 (第3-4周)
|
||||
────────────────────────────────────────────
|
||||
#1 Router Bug #5 rand.Read #11 HomeView 拆分
|
||||
#2 DB 索引 #6 错误码 #12 Feed 拆分
|
||||
#3 LIMIT #7 JWT #13 虚拟滚动
|
||||
#4 MQ 死信 #8 密码管理 #14 缓存版本化
|
||||
#9 路由守卫 #15 健康检查
|
||||
#10 pprof #16 错误监控
|
||||
#17 Worker 重启
|
||||
```
|
||||
|
||||
每批独立验证:`go test ./...` + `npm run build` + 冒烟测试。
|
||||
@@ -1,411 +0,0 @@
|
||||
# P1 用户基石 实施计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 扩展 Account 模型(头像+简介)、实现双 Token 登录态优化(Access + Refresh Token)
|
||||
|
||||
**Architecture:** Account 模型加 avatar_url/bio/refresh_token 字段;复用 UploadCover 的文件上传逻辑做头像;JWT 双 Token 机制 — Access 15min / Refresh 7天
|
||||
|
||||
**Tech Stack:** Go + Gin + GORM + JWT + Vue 3 + Pinia
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Account 模型扩展
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/account/entity.go:3-8`
|
||||
|
||||
**Step 1: 修改 Account struct**
|
||||
|
||||
```go
|
||||
type Account struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
Password string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
RefreshToken string `json:"-"`
|
||||
AvatarURL string `gorm:"type:varchar(512)" json:"avatar_url,omitempty"`
|
||||
Bio string `gorm:"type:varchar(255)" json:"bio,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 编译验证**
|
||||
|
||||
Run: `go build ./...`
|
||||
Expected: 编译通过(AutoMigrate 自动加列)
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/account/entity.go
|
||||
git commit -m "feat: Account 模型加 avatar_url/bio/refresh_token 字段"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 头像上传 Handler
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/account/handler.go` — 新增 UploadAvatar 方法
|
||||
- Modify: `backend/internal/http/router.go` — 注册路由
|
||||
|
||||
**Step 1: 添加 UploadAvatar handler**
|
||||
|
||||
参考 `video/video_handler.go` 的 `UploadCover`,在 `account/handler.go` 中新增:
|
||||
|
||||
```go
|
||||
func (ah *AccountHandler) UploadAvatar(c *gin.Context) {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
const maxSize = 10 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".webp":
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp allowed"})
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(".run", "uploads", "avatars", strconv.FormatUint(uint64(accountID), 10))
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename, err := randHex(16)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename = filename + ext
|
||||
absPath := filepath.Join(dir, filename)
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
urlPath := path.Join("/static", "avatars", strconv.FormatUint(uint64(accountID), 10), filename)
|
||||
avatarURL := buildAbsoluteURL(c, urlPath)
|
||||
|
||||
// 更新数据库
|
||||
if err := ah.accountService.UpdateAvatar(c.Request.Context(), accountID, avatarURL); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"avatar_url": avatarURL})
|
||||
}
|
||||
```
|
||||
|
||||
需要新增 import: `"os"`, `"path"`, `"path/filepath"`, `"crypto/rand"`, `"encoding/hex"`, `"strconv"`, `"strings"`, `"net/http"` — 但 account/handler.go 已有部分,按需补。
|
||||
|
||||
同时需要从 `video_handler.go` 复制 `randHex` 和 `buildAbsoluteURL` 函数(或提取到公共 util)。
|
||||
|
||||
**Step 2: 在 router.go 注册路由**
|
||||
|
||||
```go
|
||||
protectedAccountGroup.POST("/uploadAvatar", accountHandler.UploadAvatar)
|
||||
```
|
||||
|
||||
**Step 3: 添加 AccountService.UpdateAvatar 方法**
|
||||
|
||||
```go
|
||||
func (as *AccountService) UpdateAvatar(ctx context.Context, accountID uint, avatarURL string) error {
|
||||
return as.accountRepo.UpdateAvatar(ctx, accountID, avatarURL)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 添加 AccountRepository.UpdateAvatar 方法**
|
||||
|
||||
```go
|
||||
func (ar *AccountRepository) UpdateAvatar(ctx context.Context, accountID uint, avatarURL string) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", accountID).Update("avatar_url", avatarURL).Error
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: 编译验证**
|
||||
|
||||
Run: `go build ./...`
|
||||
Expected: 通过
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/account/handler.go backend/internal/account/service.go backend/internal/account/repo.go backend/internal/http/router.go
|
||||
git commit -m "feat: 头像上传接口 /account/uploadAvatar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 更新个人简介接口
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/account/handler.go` — 新增 UpdateProfile
|
||||
- Modify: `backend/internal/http/router.go` — 注册路由
|
||||
|
||||
**Step 1: 新增 request struct + handler**
|
||||
|
||||
在 `entity.go` 加:
|
||||
```go
|
||||
type UpdateProfileRequest struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
}
|
||||
```
|
||||
|
||||
Handler:
|
||||
```go
|
||||
func (ah *AccountHandler) UpdateProfile(c *gin.Context) {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var req UpdateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := ah.accountService.UpdateProfile(c.Request.Context(), accountID, &req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "profile updated"})
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Service + Repo 层**
|
||||
|
||||
```go
|
||||
func (as *AccountService) UpdateProfile(ctx context.Context, accountID uint, req *UpdateProfileRequest) error {
|
||||
updates := map[string]interface{}{}
|
||||
if req.Bio != "" {
|
||||
updates["bio"] = strings.TrimSpace(req.Bio)
|
||||
}
|
||||
if req.AvatarURL != "" {
|
||||
updates["avatar_url"] = strings.TrimSpace(req.AvatarURL)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return errors.New("nothing to update")
|
||||
}
|
||||
return as.accountRepo.UpdateFields(ctx, accountID, updates)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: 注册路由**
|
||||
|
||||
```go
|
||||
protectedAccountGroup.POST("/updateProfile", accountHandler.UpdateProfile)
|
||||
```
|
||||
|
||||
**Step 4: 编译 + 提交**
|
||||
|
||||
Run: `go build ./...`
|
||||
Expected: 通过
|
||||
|
||||
```bash
|
||||
git add backend/internal/account/ && git add backend/internal/http/router.go
|
||||
git commit -m "feat: 个人简介更新接口 /account/updateProfile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Refresh Token 机制
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/auth/jwt.go` — 新增 GenerateRefreshToken + ValidateRefreshToken
|
||||
- Modify: `backend/internal/account/handler.go` — 新增 Refresh handler
|
||||
- Modify: `backend/internal/account/service.go` — Login 返回双 token
|
||||
- Modify: `backend/internal/http/router.go` — 注册 refresh 路由
|
||||
|
||||
**Step 1: auth/jwt.go 增加 Refresh Token**
|
||||
|
||||
```go
|
||||
const (
|
||||
AccessTokenTTL = 15 * time.Minute
|
||||
RefreshTokenTTL = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
func GenerateAccessToken(accountID uint, username string) (string, error) {
|
||||
// 原 GenerateToken 逻辑,TTL 改为 15min
|
||||
}
|
||||
|
||||
func GenerateRefreshToken(accountID uint) (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Login 返回双 token**
|
||||
|
||||
修改 `account/service.go` 的 `Login` 方法,返回值从 `(string, error)` 改为 `(accessToken, refreshToken string, err error)`,并更新 `entity.go` 中的 `LoginResponse`。
|
||||
|
||||
```go
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"` // access token
|
||||
RefreshToken string `json:"refresh_token"` // refresh token
|
||||
AccountID uint `json:"account_id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
```
|
||||
|
||||
Login 时生成两个 token,access token 落库 `account.token`,refresh token 落库 `account.refresh_token`,两者都缓存到 Redis。
|
||||
|
||||
**Step 3: Refresh handler**
|
||||
|
||||
新增 `POST /account/refresh`:
|
||||
|
||||
```go
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func (ah *AccountHandler) Refresh(c *gin.Context) {
|
||||
var req RefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
newAccessToken, err := ah.accountService.RefreshAccessToken(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"token": newAccessToken})
|
||||
}
|
||||
```
|
||||
|
||||
`AccountService.RefreshAccessToken`:查 Redis `account:{id}:refresh` → 匹配 → 生成新 access token → 更新 token 字段。
|
||||
|
||||
**Step 4: 登出/改密时同时清空 refresh_token**
|
||||
|
||||
在 `Logout` 和 `ChangePassword` 中增加 `Del("account:{id}:refresh")`。
|
||||
|
||||
**Step 5: 编译 + 提交**
|
||||
|
||||
```bash
|
||||
git add backend/internal/auth/jwt.go backend/internal/account/
|
||||
git commit -m "feat: Refresh Token 机制 — Access 15min + Refresh 7天"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 前端 auth store + client.ts 适配双 Token
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/stores/auth.ts`
|
||||
- Modify: `frontend/src/api/client.ts`
|
||||
- Modify: `frontend/src/api/account.ts`
|
||||
|
||||
**Step 1: auth store 存储双 token**
|
||||
|
||||
```typescript
|
||||
const ACCESS_KEY = 'access_token'
|
||||
const REFRESH_KEY = 'refresh_token'
|
||||
|
||||
// 新增字段
|
||||
const refreshToken = ref<string | null>(readToken(REFRESH_KEY))
|
||||
|
||||
function setTokens(access: string, refresh: string) {
|
||||
token.value = access; refreshToken.value = refresh
|
||||
writeToken(ACCESS_KEY, access); writeToken(REFRESH_KEY, refresh)
|
||||
}
|
||||
|
||||
function clearTokens() {
|
||||
token.value = null; refreshToken.value = null
|
||||
removeToken(ACCESS_KEY); removeToken(REFRESH_KEY)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: client.ts 401 自动刷新**
|
||||
|
||||
```typescript
|
||||
async function tryRefresh(): Promise<string | null> {
|
||||
const auth = useAuthStore()
|
||||
if (!auth.refreshToken) return null
|
||||
try {
|
||||
const res = await postJson<{ token: string }>('/account/refresh', { refresh_token: auth.refreshToken })
|
||||
auth.setToken(res.token)
|
||||
return res.token
|
||||
} catch {
|
||||
auth.clearTokens()
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 `postJson` 和 `postForm` 的 `!res.ok` 分支中,401 时先尝试刷新,成功则重试原请求。
|
||||
|
||||
**Step 3: 编译验证**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 通过
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/stores/auth.ts frontend/src/api/client.ts frontend/src/api/account.ts
|
||||
git commit -m "feat: 前端双 Token 适配 — 401 自动刷新 + Refresh Token 存储"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 前端用户 Profile UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/AccountView.vue`
|
||||
- Modify: `frontend/src/components/UserAvatar.vue`
|
||||
- Modify: `frontend/src/views/HomeView.vue` — Feed 卡片中 UserAvatar 传递头像 URL
|
||||
- Modify: `frontend/src/api/account.ts` — 新增 API 调用
|
||||
|
||||
**Step 1: UserAvatar 支持 src**
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
defineProps<{ username: string; id: number; size?: number; src?: string }>()
|
||||
</script>
|
||||
<template>
|
||||
<img v-if="src" :src="src" :width="size" :height="size" class="avatar-img" />
|
||||
<svg v-else ...> <!-- 默认 SVG -->
|
||||
</template>
|
||||
```
|
||||
|
||||
**Step 2: AccountView 加头像上传 + bio 编辑**
|
||||
|
||||
在登录后的 AccountView 中增加:头像上传按钮(调用 `/account/uploadAvatar`)、bio 编辑输入框(调用 `/account/updateProfile`)。
|
||||
|
||||
**Step 3: 编译验证**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: 通过
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/UserAvatar.vue frontend/src/views/AccountView.vue frontend/src/views/HomeView.vue frontend/src/api/account.ts
|
||||
git commit -m "feat: 前端用户 Profile UI — 头像上传 + bio 编辑 + 登录记住我"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
完成所有 Task 后:
|
||||
|
||||
```bash
|
||||
cd backend && go build ./... && go vet ./... && go test ./...
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: 全部通过
|
||||
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ShortVideo</title>
|
||||
</head>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
41
frontend/src/components/HelloWorld.vue
vendored
41
frontend/src/components/HelloWorld.vue
vendored
@@ -1,41 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user