1 changed files with 854 additions and 0 deletions
@ -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…
Reference in new issue