Browse Source

docs: 添加热重载+登录改造实施计划

master
dark 1 month ago
parent
commit
fb56475faf
  1. 854
      docs/plans/2026-02-14-hot-reload-login-redesign-impl.md

854
docs/plans/2026-02-14-hot-reload-login-redesign-impl.md

@ -0,0 +1,854 @@
# 后端热重载 + 登录方式改造 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add Air hot reload for backend development, and change login from email to phone/username with JWT email removal.
**Architecture:** Two independent changes — (1) Air config file + .gitignore, zero code changes; (2) LoginRequest.Email→Account, new Model queries FindOneByPhone/FindOneByUsername, backend regex auto-detect, JWT Claims remove Email, frontend login form adapt.
**Tech Stack:** Air (hot reload), go-zero, GORM, React, TypeScript
---
### Task 1: Air 热重载配置
**Files:**
- Create: `backend/.air.toml`
- Create: `backend/.gitignore`
**Step 1: Install Air**
Run: `go install github.com/air-verse/air@latest`
Expected: Binary installed to `$GOPATH/bin/air`
**Step 2: Create `.air.toml`**
Create `backend/.air.toml`:
```toml
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/base.exe base.go"
bin = "./tmp/base.exe -f etc/base-api.yaml"
include_ext = ["go", "yaml"]
exclude_dir = ["tmp", "vendor", "tests"]
delay = 1000
[log]
time = false
[misc]
clean_on_exit = true
```
**Step 3: Create `backend/.gitignore`**
Create `backend/.gitignore`:
```
tmp/
```
**Step 4: Verify Air works**
Run: `cd backend && air`
Expected: Compiles and starts server on :8888, logs show "Starting server"
Kill with Ctrl+C after confirming.
**Step 5: Commit**
```bash
git add backend/.air.toml backend/.gitignore
git commit -m "feat: add Air hot reload config for backend development"
```
---
### Task 2: Model 层 — 新增 FindOneByPhone 和 FindOneByUsername
**Files:**
- Modify: `backend/model/user_model.go`
**Step 1: Add FindOneByPhone and FindOneByUsername**
Add to `backend/model/user_model.go` after `FindOneByEmail` (after line 59):
```go
// FindOneByPhone 根据手机号查询用户
func FindOneByPhone(ctx context.Context, db *gorm.DB, phone string) (*User, error) {
var user User
result := db.WithContext(ctx).Where("phone = ?", phone).First(&user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return nil, result.Error
}
return &user, nil
}
// FindOneByUsername 根据用户名查询用户
func FindOneByUsername(ctx context.Context, db *gorm.DB, username string) (*User, error) {
var user User
result := db.WithContext(ctx).Where("username = ?", username).First(&user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return nil, result.Error
}
return &user, nil
}
```
**Step 2: Verify build**
Run: `cd backend && go build ./...`
Expected: No errors
**Step 3: Commit**
```bash
git add backend/model/user_model.go
git commit -m "feat: add FindOneByPhone and FindOneByUsername model queries"
```
---
### Task 3: JWT — 移除 Email 字段
**Files:**
- Modify: `backend/internal/util/jwt/jwt.go`
- Modify: `backend/internal/util/jwt/jwt_test.go`
**Step 1: Update Claims struct and GenerateToken**
In `backend/internal/util/jwt/jwt.go`:
Replace Claims struct (lines 18-24):
```go
type Claims struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
```
Replace GenerateToken signature (line 27):
```go
func GenerateToken(userId int64, username, role string) (string, error) {
```
Replace Claims initialization (lines 28-32):
```go
claims := Claims{
UserID: userId,
Username: username,
Role: role,
```
**Step 2: Update jwt_test.go**
Replace entire `backend/internal/util/jwt/jwt_test.go`:
```go
package jwt
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateToken_ContainsRole(t *testing.T) {
token, err := GenerateToken(1, "testuser", "admin")
require.NoError(t, err)
require.NotEmpty(t, token)
claims, err := ParseToken(token)
require.NoError(t, err)
assert.Equal(t, int64(1), claims.UserID)
assert.Equal(t, "testuser", claims.Username)
assert.Equal(t, "admin", claims.Role)
}
func TestGenerateToken_SuperAdminRole(t *testing.T) {
token, err := GenerateToken(99, "admin", "super_admin")
require.NoError(t, err)
claims, err := ParseToken(token)
require.NoError(t, err)
assert.Equal(t, "super_admin", claims.Role)
}
func TestGenerateToken_EmptyRole(t *testing.T) {
token, err := GenerateToken(1, "user", "")
require.NoError(t, err)
claims, err := ParseToken(token)
require.NoError(t, err)
assert.Equal(t, "", claims.Role)
}
```
**Step 3: Run JWT tests**
Run: `cd backend && go test -v ./internal/util/jwt/...`
Expected: 3/3 PASS
**Step 4: Commit**
```bash
git add backend/internal/util/jwt/jwt.go backend/internal/util/jwt/jwt_test.go
git commit -m "refactor: remove email from JWT Claims"
```
---
### Task 4: Auth Middleware — 移除 email context
**Files:**
- Modify: `backend/internal/middleware/authmiddleware.go`
**Step 1: Remove email context line**
In `backend/internal/middleware/authmiddleware.go`, delete line 43:
```go
ctx = context.WithValue(ctx, "email", claims.Email)
```
**Step 2: Verify build**
Run: `cd backend && go build ./...`
Expected: No errors
**Step 3: Commit**
```bash
git add backend/internal/middleware/authmiddleware.go
git commit -m "refactor: remove email from auth middleware context"
```
---
### Task 5: API 定义 — LoginRequest 和 RegisterRequest 改造
**Files:**
- Modify: `backend/base.api` (lines 37-40)
- Modify: `backend/internal/types/types.go` (auto-generated)
- Modify: `backend/internal/handler/routes.go` (auto-generated)
**Step 1: Update LoginRequest in base.api**
In `backend/base.api`, replace lines 37-40:
```
// 登录请求
LoginRequest {
Account string `json:"account" validate:"required"` // 手机号或用户名
Password string `json:"password" validate:"required,min=6,max=32"` // 密码
}
```
**Step 2: Update RegisterRequest in base.api**
In `backend/base.api`, replace lines 30-35:
```
// 注册请求
RegisterRequest {
Username string `json:"username" validate:"required,min=3,max=32"` // 用户名
Password string `json:"password" validate:"required,min=6,max=32"` // 密码
Phone string `json:"phone" validate:"required"` // 手机号(必填)
Email string `json:"email,optional"` // 邮箱(可选)
}
```
**Step 3: Regenerate types and routes**
Run: `cd backend && goctl api go -api base.api -dir .`
Expected: types.go and routes.go regenerated
**Step 4: Verify build**
Run: `cd backend && go build ./...`
Expected: Compile errors in loginlogic.go (req.Email → req.Account) — expected, will fix in next task
**Step 5: Commit**
```bash
git add backend/base.api backend/internal/types/types.go backend/internal/handler/routes.go
git commit -m "refactor: change LoginRequest to account field, RegisterRequest phone required"
```
---
### Task 6: Login Logic — 手机号/用户名自动识别
**Files:**
- Modify: `backend/internal/logic/auth/loginlogic.go`
**Step 1: Rewrite loginlogic.go**
Replace entire `backend/internal/logic/auth/loginlogic.go`:
```go
package auth
import (
"context"
"crypto/md5"
"fmt"
"regexp"
"github.com/youruser/base/internal/svc"
"github.com/youruser/base/internal/types"
"github.com/youruser/base/internal/util/jwt"
"github.com/youruser/base/model"
"github.com/zeromicro/go-zero/core/logx"
)
var phoneRegex = regexp.MustCompile(`^\d{11}$`)
type LoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 用户登录
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
// 根据输入自动识别:11位数字为手机号,否则为用户名
var user *model.User
if phoneRegex.MatchString(req.Account) {
user, err = model.FindOneByPhone(l.ctx, l.svcCtx.DB, req.Account)
} else {
user, err = model.FindOneByUsername(l.ctx, l.svcCtx.DB, req.Account)
}
if err != nil {
if err == model.ErrNotFound {
return &types.LoginResponse{
Code: 404,
Message: "用户不存在或密码错误",
Success: false,
}, nil
}
return nil, fmt.Errorf("查询用户失败: %v", err)
}
// SSO 用户不允许使用密码登录
if user.UserType == "casdoor" {
return &types.LoginResponse{
Code: 400,
Message: "该账号已绑定 SSO,请使用 SSO 方式登录",
Success: false,
}, nil
}
// 加密输入的密码并与数据库密码对比
inputPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.Password)))
if user.Password != inputPassword {
return &types.LoginResponse{
Code: 400,
Message: "用户不存在或密码错误",
Success: false,
}, nil
}
// 生成 Token
token, err := jwt.GenerateToken(user.Id, user.Username, user.Role)
if err != nil {
return nil, fmt.Errorf("生成Token失败: %v", err)
}
l.Infof("登录成功,userId=%d", user.Id)
return &types.LoginResponse{
Code: 200,
Message: "登录成功",
Success: true,
Token: token,
}, nil
}
```
**Step 2: Verify build**
Run: `cd backend && go build ./...`
Expected: May still have errors in other logic files — continue to next tasks
**Step 3: Commit**
```bash
git add backend/internal/logic/auth/loginlogic.go
git commit -m "feat: login by phone or username with auto-detection"
```
---
### Task 7: Register Logic — 移除邮箱必填,手机号唯一性检查
**Files:**
- Modify: `backend/internal/logic/auth/registerlogic.go`
**Step 1: Rewrite registerlogic.go**
Replace entire `backend/internal/logic/auth/registerlogic.go`:
```go
package auth
import (
"context"
"crypto/md5"
"fmt"
"github.com/youruser/base/internal/svc"
"github.com/youruser/base/internal/types"
"github.com/youruser/base/model"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 用户注册
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.UserInfo, err error) {
// 检查用户名是否已存在
_, err = model.FindOneByUsername(l.ctx, l.svcCtx.DB, req.Username)
if err == nil {
return nil, fmt.Errorf("用户名已被注册")
}
if err != model.ErrNotFound {
return nil, fmt.Errorf("检查用户名失败: %v", err)
}
// 检查手机号是否已存在
_, err = model.FindOneByPhone(l.ctx, l.svcCtx.DB, req.Phone)
if err == nil {
return nil, fmt.Errorf("手机号已被注册")
}
if err != model.ErrNotFound {
return nil, fmt.Errorf("检查手机号失败: %v", err)
}
// 创建用户模型
user := &model.User{
Username: req.Username,
Email: req.Email,
Password: fmt.Sprintf("%x", md5.Sum([]byte(req.Password))),
Phone: req.Phone,
Role: model.RoleUser,
Source: model.SourceRegister,
Status: 1,
}
// 插入数据库
id, err := model.Insert(l.ctx, l.svcCtx.DB, user)
if err != nil {
return nil, fmt.Errorf("创建用户失败: %v", err)
}
// 查询创建的用户
user, err = model.FindOne(l.ctx, l.svcCtx.DB, id)
if err != nil {
return nil, fmt.Errorf("查询用户失败: %v", err)
}
resp = &types.UserInfo{
Id: user.Id,
Username: user.Username,
Email: user.Email,
Phone: user.Phone,
Role: user.Role,
Source: user.Source,
Remark: user.Remark,
Status: int(user.Status),
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
}
l.Infof("注册成功,userId=%d", user.Id)
return resp, nil
}
```
**Step 2: Commit**
```bash
git add backend/internal/logic/auth/registerlogic.go
git commit -m "refactor: register checks username+phone uniqueness, email optional"
```
---
### Task 8: RefreshToken + SSO Logic — 更新 GenerateToken 调用
**Files:**
- Modify: `backend/internal/logic/auth/refreshtokenlogic.go:55`
- Modify: `backend/internal/logic/auth/ssologic.go:179`
**Step 1: Fix refreshtokenlogic.go**
In `backend/internal/logic/auth/refreshtokenlogic.go`, line 55, change:
```go
newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Email, user.Role)
```
to:
```go
newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Role)
```
**Step 2: Fix ssologic.go**
In `backend/internal/logic/auth/ssologic.go`, line 179, change:
```go
token, err := jwt.GenerateToken(localUser.Id, localUser.Username, localUser.Email, localUser.Role)
```
to:
```go
token, err := jwt.GenerateToken(localUser.Id, localUser.Username, localUser.Role)
```
**Step 3: Verify build**
Run: `cd backend && go build ./...`
Expected: No errors (all GenerateToken calls now match new signature)
**Step 4: Commit**
```bash
git add backend/internal/logic/auth/refreshtokenlogic.go backend/internal/logic/auth/ssologic.go
git commit -m "refactor: update GenerateToken calls to remove email param"
```
---
### Task 9: 种子超级管理员 — 改为用户名查找 + 手机号
**Files:**
- Modify: `backend/internal/svc/servicecontext.go` (seedSuperAdmin function)
**Step 1: Update seedSuperAdmin**
In `backend/internal/svc/servicecontext.go`, replace the `seedSuperAdmin` function:
```go
// seedSuperAdmin 首次启动创建超级管理员
func seedSuperAdmin(db *gorm.DB) {
ctx := context.Background()
// 先检查 admin 用户是否已存在
existing, err := model.FindOneByUsername(ctx, db, "admin")
if err == nil {
// 用户已存在,确保角色为 super_admin
if existing.Role != model.RoleSuperAdmin {
existing.Role = model.RoleSuperAdmin
existing.Source = model.SourceSystem
model.Update(ctx, db, existing)
log.Println("[Seed] Updated admin to super_admin role")
}
return
}
// 创建超级管理员
password := fmt.Sprintf("%x", md5.Sum([]byte("admin123")))
admin := &model.User{
Username: "admin",
Phone: "13800000000",
Email: "",
Password: password,
Role: model.RoleSuperAdmin,
Source: model.SourceSystem,
Remark: "系统自动创建的超级管理员",
Status: 1,
}
_, err = model.Insert(ctx, db, admin)
if err != nil {
log.Printf("[Seed] Failed to create super admin: %v", err)
return
}
log.Println("[Seed] Super admin created: admin / admin123")
}
```
**Step 2: Verify build**
Run: `cd backend && go build ./...`
Expected: No errors
**Step 3: Commit**
```bash
git add backend/internal/svc/servicecontext.go
git commit -m "refactor: seed super admin uses username lookup and phone number"
```
---
### Task 10: Go 单元测试修复 — rbac_test.go
**Files:**
- Modify: `backend/internal/logic/user/rbac_test.go`
**Step 1: Update test to not require email**
In `backend/internal/logic/user/rbac_test.go`, find all `CreateUserRequest` that have `Email` field and ensure they still work. The `CreateUserRequest` in `user.api` still has `Email` as a field (it's part of user management, not login). Check if the test needs any changes after the API type regeneration.
Run: `cd backend && go test -v ./internal/logic/user/...`
Expected: If tests pass, no changes needed. If they fail due to type changes, fix accordingly.
**Step 2: Commit (if changes needed)**
```bash
git add backend/internal/logic/user/rbac_test.go
git commit -m "fix: update RBAC tests for new type definitions"
```
---
### Task 11: 后端集成测试 — 启动并验证登录
**Step 1: Start backend**
Run: `cd backend && go run base.go -f etc/base-api.yaml`
**Step 2: Test login with username**
```bash
curl -s -X POST http://localhost:8888/api/v1/login \
-H "Content-Type: application/json" \
-d '{"account":"admin","password":"admin123"}'
```
Expected: `{"code":200,"message":"登录成功","success":true,"token":"..."}`
**Step 3: Test login with phone**
```bash
curl -s -X POST http://localhost:8888/api/v1/login \
-H "Content-Type: application/json" \
-d '{"account":"13800000000","password":"admin123"}'
```
Expected: `{"code":200,"message":"登录成功","success":true,"token":"..."}`
**Step 4: Verify JWT has no email**
Decode the token payload (base64 decode middle section) and confirm no `email` field.
**Step 5: Test register with phone**
```bash
curl -s -X POST http://localhost:8888/api/v1/register \
-H "Content-Type: application/json" \
-d '{"username":"testphone","password":"test123456","phone":"13900001111"}'
```
Expected: `{"id":...,"username":"testphone","phone":"13900001111",...}`
---
### Task 12: 前端 — TypeScript 类型更新
**Files:**
- Modify: `frontend/react-shadcn/pc/src/types/index.ts`
**Step 1: Update LoginRequest**
In `frontend/react-shadcn/pc/src/types/index.ts`, replace lines 33-36:
```typescript
export interface LoginRequest {
account: string
password: string
}
```
**Step 2: Update RegisterRequest**
Replace lines 38-43:
```typescript
export interface RegisterRequest {
username: string
password: string
phone: string
email?: string
}
```
**Step 3: Commit**
```bash
git add frontend/react-shadcn/pc/src/types/index.ts
git commit -m "refactor: frontend types - LoginRequest.account, RegisterRequest.phone required"
```
---
### Task 13: 前端 — API Client 和 AuthContext 更新
**Files:**
- Modify: `frontend/react-shadcn/pc/src/services/api.ts:70-80`
- Modify: `frontend/react-shadcn/pc/src/contexts/AuthContext.tsx`
**Step 1: Update api.ts login method**
In `frontend/react-shadcn/pc/src/services/api.ts`, the `login` method (line 70) accepts `LoginRequest` which now has `account` instead of `email`. No code change needed here since it just passes the object through — the type change in Task 12 handles it.
**Step 2: Update AuthContext login function**
In `frontend/react-shadcn/pc/src/contexts/AuthContext.tsx`:
Change interface (line 10):
```typescript
login: (account: string, password: string) => Promise<void>
```
Change login function (line 41):
```typescript
const login = async (account: string, password: string) => {
```
Change apiClient call (line 43):
```typescript
const response = await apiClient.login({ account, password })
```
Change JWT parsing fallback username (line 52, inside the if block):
```typescript
username: payload.username || account,
```
Remove email from userData (line 53-54):
```typescript
email: payload.email || '',
```
And in the catch block fallback (lines 62-66):
```typescript
const userData: User = {
id: 0,
username: account,
email: '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
```
**Step 3: Commit**
```bash
git add frontend/react-shadcn/pc/src/services/api.ts frontend/react-shadcn/pc/src/contexts/AuthContext.tsx
git commit -m "refactor: frontend auth uses account field, remove email from JWT parsing"
```
---
### Task 14: 前端 — LoginPage UI 改造
**Files:**
- Modify: `frontend/react-shadcn/pc/src/pages/LoginPage.tsx`
**Step 1: Update imports**
Replace `Mail` import with `User` (or `UserRound`):
```typescript
import { UserRound, Lock, AlertCircle, Eye, EyeOff, ArrowRight, Shield, Cpu, BarChart3 } from 'lucide-react'
```
**Step 2: Update state variable**
Line 10, change:
```typescript
const [account, setAccount] = useState('')
```
**Step 3: Update handleSubmit**
Line 53, change:
```typescript
await login(account, password)
```
**Step 4: Update email input to account input**
Replace the email Input component (lines 198-206):
```tsx
<Input
type="text"
label="手机号 / 用户名"
placeholder="请输入手机号或用户名"
value={account}
onChange={(e) => setAccount(e.target.value)}
required
leftIcon={<UserRound className="h-4 w-4" />}
disabled={isLoading}
/>
```
**Step 5: Verify frontend compiles**
Run: `cd frontend/react-shadcn/pc && npm run build`
Expected: No TypeScript errors
**Step 6: Commit**
```bash
git add frontend/react-shadcn/pc/src/pages/LoginPage.tsx
git commit -m "feat: login page accepts phone/username instead of email"
```
---
### Task 15: E2E 验证 — 全流程测试
**Step 1: Start backend and frontend**
```bash
# Terminal 1 (use air if installed, otherwise go run)
cd backend && air
# Terminal 2
cd frontend/react-shadcn/pc && npm run dev
```
**Step 2: Test login via browser**
Navigate to http://localhost:5173/login
- Enter "admin" + "admin123" → should redirect to dashboard
- Logout, enter "13800000000" + "admin123" → should redirect to dashboard
**Step 3: Test hot reload**
Edit any .go file (e.g., add a comment), save, verify Air rebuilds and restarts automatically.
**Step 4: Final commit with test config update**
Update `frontend/react-shadcn/pc/tests/config.ts` superAdmin credentials:
```typescript
superAdmin: {
account: 'admin',
password: 'admin123',
},
```
```bash
git add frontend/react-shadcn/pc/tests/config.ts
git commit -m "chore: update test config for account-based login"
```
Loading…
Cancel
Save