You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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"