# 后端热重载 + 登录方式改造 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 ``` 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 setAccount(e.target.value)} required leftIcon={} 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" ```