V2: 多人使用 —— 「给团队也用上」
V1的HTML记账工具你自己用了两周,老板觉得不错,说让10人的团队都用上。问题来了:localStorage的数据只在你电脑的浏览器里,同事打开同一个HTML文件,看到的是他自己的空数据。更麻烦的是,老板说他要能审批报销,还要能看所有人的记录。
当前状态: V1是纯前端+localStorage,数据只在本地浏览器,无法共享。 目标: 加共享后端,多人能同时用,每个人只看自己的费用,管理员能看全部并审批。 约束: 10个人用,Go+PostgreSQL后端,JWT认证,不用OAuth(内部系统)。
问题分析(5层框架)
Section titled “问题分析(5层框架)”| 层级 | 问题 | 思考 |
|---|---|---|
| Why | 为什么要改? | 多人使用必须隔离数据,报销需要审批流程 |
| What | 改什么? | 加用户体系 + 权限控制 + 审批状态 |
| How (架构) | 怎么加? | JWT中间件拦截请求,从token取user_id做数据隔离 |
| How (实现) | 具体改哪些? | 新增User模型、注册登录接口、中间件、Expense加外键和状态 |
| How (迁移) | 老数据怎么办? | V1数据在localStorage,可以导出JSON手动导入;也可以直接重新录入(数量不多) |
graph LR B["浏览器<br/>(React)"] -->|"HTTP/JSON"| G["Go/Gin 后端<br/>+ JWT 认证"] G -->|"SQL"| DB["PostgreSQL<br/>数据库"] G -->|"JSON 响应"| B数据模型变更
Section titled “数据模型变更”// 新增 User 表type User struct { ID uint `json:"id" gorm:"primaryKey"` Email string `json:"email" gorm:"uniqueIndex;not null"` PasswordHash string `json:"-"` // json:"-" 永远不返回给前端 Name string `json:"name"` Role string `json:"role" gorm:"default:user"` // user / admin CreatedAt time.Time `json:"created_at"`}
// Expense 表增加字段type Expense struct { // ... 原有字段不变 UserID uint `json:"user_id" gorm:"index;not null"` // 外键 Status string `json:"status" gorm:"default:pending"` // pending/approved/rejected User User `json:"user,omitempty" gorm:"foreignKey:UserID"`}新增/修改API
Section titled “新增/修改API”| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| POST | /api/auth/register | 公开 | 注册 |
| POST | /api/auth/login | 公开 | 登录,返回JWT |
| GET | /api/auth/me | 登录 | 获取当前用户信息 |
| GET | /api/expenses | 登录 | 普通用户只看自己的 |
| GET | /api/admin/expenses | admin | 管理员看全部 |
| PUT | /api/admin/expenses/:id/approve | admin | 审批通过 |
| PUT | /api/admin/expenses/:id/reject | admin | 审批驳回 |
| 决策点 | 选项A | 选项B | 选择 | 理由 |
|---|---|---|---|---|
| 认证方式 | Session+Cookie | JWT | JWT | 前后端分离,无状态更好扩展 |
| 密码加密 | SHA256 | bcrypt | bcrypt | 自带盐值,抗彩虹表,业界标准 |
| 权限控制 | RBAC完整实现 | 简单role字段 | 简单role | 只有user/admin两种,不需要过度设计 |
| Token过期 | 永不过期 | 24小时+Refresh | 24小时 | V2先简单,后续再加refresh token |
| 数据隔离 | 应用层WHERE | 数据库RLS | 应用层 | GORM操作更直观,团队小不需要RLS |
给AI的Prompt
Section titled “给AI的Prompt”我有一个V1版本的记账工具(纯HTML+localStorage,单用户),现在要把它升级成多人共用的系统。请帮我用 Go + Gin + GORM + PostgreSQL + React + Tailwind CSS 重新实现,并支持以下功能:
## 新增需求1. 用户注册/登录(JWT认证)2. 角色:user(普通用户)和 admin(管理员)3. 数据隔离:普通用户只能看到自己的费用4. 审批流程:费用有 pending/approved/rejected 三种状态5. 管理员可以查看所有费用、批准或驳回
## 数据模型变更新增 User 表:- id: 主键- email: string, 唯一索引- password_hash: string(用bcrypt加密,json标签设为"-"不返回前端)- name: string- role: string, 默认"user", 可选"admin"- created_at
Expense 表新增:- user_id: uint, 外键关联User,加索引- status: string, 默认"pending"
## 认证方案- 注册:POST /api/auth/register,接收email+password+name- 登录:POST /api/auth/login,返回 { token: "xxx" }- JWT存在Authorization: Bearer xxx头里- 写一个Gin中间件解析JWT,把user_id和role塞到gin.Context- 再写一个admin中间件检查role
## 密码安全要求- 用golang.org/x/crypto/bcrypt- cost factor用默认值(10)- 注册时检查email是否已存在- 登录失败统一返回"邮箱或密码错误",不要区分是邮箱不存在还是密码错
## API权限- /api/auth/* 公开- /api/expenses/* 需要登录(中间件自动注入user_id,查询自动加WHERE user_id=?)- /api/admin/* 需要admin角色
## 前端改造- 加登录/注册页面- 登录后把token存localStorage- 每次请求在header带上token- 401时跳转登录页- 管理员多一个"审批管理"页面
请保持V1的所有功能不变,在其基础上新增。- 注册接口能创建用户,密码在数据库里是bcrypt哈希
- 登录返回有效的JWT token
- 不带token访问/api/expenses返回401
- 用户A看不到用户B的费用
- 新建的费用status默认是pending
- 管理员能看到所有人的pending费用
- 审批通过后status变为approved
- 普通用户访问/api/admin/*返回403
- 登录失败不泄露是邮箱错还是密码错
- password_hash不出现在任何API响应中
你学到了什么
Section titled “你学到了什么”| 知识点 | 对应模块 |
|---|---|
| JWT生成、解析、中间件拦截 | → Module 12(认证授权) |
| bcrypt密码加密、cost factor、盐值 | → Module 14(密码安全) |
| 基于角色的访问控制(RBAC简化版) | → Module 12(认证授权) |
| 登录错误信息不应区分原因 | → Module 14(密码安全) |
| 数据隔离:WHERE user_id = ? | → Module 12(认证授权) |
1. 密码明文存储
Section titled “1. 密码明文存储”// 严重错误:直接存密码user.Password = input.Password
// 正确:bcrypt加密hash, _ := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)user.PasswordHash = string(hash)2. JWT密钥硬编码在代码里
Section titled “2. JWT密钥硬编码在代码里”// 错误:密钥写死token.SignedString([]byte("secret123"))
// 正确:从环境变量读取token.SignedString([]byte(os.Getenv("JWT_SECRET")))// 并且JWT_SECRET至少32个字符的随机字符串3. 忘记在查询里加user_id过滤
Section titled “3. 忘记在查询里加user_id过滤”// 错误:任何人都能看到所有数据db.Find(&expenses)
// 正确:从context取user_id做过滤userID := c.GetUint("user_id")db.Where("user_id = ?", userID).Find(&expenses)4. 登录错误信息泄露
Section titled “4. 登录错误信息泄露”// 错误:告诉攻击者哪个环节出了问题if user not found { return "用户不存在" }if wrong password { return "密码错误" }
// 正确:统一错误信息return "邮箱或密码错误"5. 前端token存储和清理
Section titled “5. 前端token存储和清理”// 登录成功后存tokenlocalStorage.setItem('token', data.token)
// 每次请求带上headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
// 401时清理并跳转(别忘了这步!不然用户会卡在白屏)if (res.status === 401) { localStorage.removeItem('token') window.location.href = '/login'}6. GORM AutoMigrate不会删列
Section titled “6. GORM AutoMigrate不会删列”// AutoMigrate只会加字段,不会删或改字段// 如果你改了字段名(比如password→password_hash),老字段还在// 开发阶段可以手动DROP TABLE重来// 生产环境必须写migration脚本