21 KiB
后端热重载 + 登录方式改造 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:
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
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):
// 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
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):
type Claims struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
Replace GenerateToken signature (line 27):
func GenerateToken(userId int64, username, role string) (string, error) {
Replace Claims initialization (lines 28-32):
claims := Claims{
UserID: userId,
Username: username,
Role: role,
Step 2: Update jwt_test.go
Replace entire backend/internal/util/jwt/jwt_test.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
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:
ctx = context.WithValue(ctx, "email", claims.Email)
Step 2: Verify build
Run: cd backend && go build ./...
Expected: No errors
Step 3: Commit
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
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:
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
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:
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
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:
newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Email, user.Role)
to:
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:
token, err := jwt.GenerateToken(localUser.Id, localUser.Username, localUser.Email, localUser.Role)
to:
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
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:
// 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
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)
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
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
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
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:
export interface LoginRequest {
account: string
password: string
}
Step 2: Update RegisterRequest
Replace lines 38-43:
export interface RegisterRequest {
username: string
password: string
phone: string
email?: string
}
Step 3: Commit
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):
login: (account: string, password: string) => Promise<void>
Change login function (line 41):
const login = async (account: string, password: string) => {
Change apiClient call (line 43):
const response = await apiClient.login({ account, password })
Change JWT parsing fallback username (line 52, inside the if block):
username: payload.username || account,
Remove email from userData (line 53-54):
email: payload.email || '',
And in the catch block fallback (lines 62-66):
const userData: User = {
id: 0,
username: account,
email: '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
Step 3: Commit
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):
import { UserRound, Lock, AlertCircle, Eye, EyeOff, ArrowRight, Shield, Cpu, BarChart3 } from 'lucide-react'
Step 2: Update state variable
Line 10, change:
const [account, setAccount] = useState('')
Step 3: Update handleSubmit
Line 53, change:
await login(account, password)
Step 4: Update email input to account input
Replace the email Input component (lines 198-206):
<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
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
# 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:
superAdmin: {
account: 'admin',
password: 'admin123',
},
git add frontend/react-shadcn/pc/tests/config.ts
git commit -m "chore: update test config for account-based login"