fix(backend): JWT config mapstructure tag 修复 + 模型从 hotel 重构为 shop

- 修复 JWTConfig 缺少 mapstructure tag 导致 access_expire_min 解析为 0,
  token 签发即过期,所有 API 请求返回 401
- 全部 config struct 补齐 mapstructure tag(secret/dsn/hmac_secret 等)
- 模型层从 hotel/HotelID 统一重命名为 shop/ShopID
- 删除旧 migrations(001-004),新增 001_init 综合迁移文件
- 更新 schema.sql、testutil、handler/service/model 相关引用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-07 22:20:12 +08:00
parent 37112d6599
commit 31ea370cea
50 changed files with 2675 additions and 951 deletions
+141 -20
View File
@@ -22,11 +22,11 @@ func init() {
func newTestAuthRouter(t *testing.T) (*gin.Engine, *gin.Engine) {
db := testutil.SetupTestDB()
hotel := testutil.CreateTestHotel(db, "AUTHTEST")
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
testutil.CreateTestUser(db, hotel.ID, "disabled", "password123", "operator")
shop := testutil.CreateTestShop(db, "AUTHTEST")
testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
testutil.CreateTestUser(db, shop.ID, "disabled", "password123", "operator")
// 禁用该用户
db.Exec("UPDATE users SET is_active = 0 WHERE username = 'disabled' AND hotel_id = ?", hotel.ID)
db.Exec("UPDATE users SET is_active = 0 WHERE username = 'disabled' AND shop_id = ?", shop.ID)
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
@@ -39,8 +39,8 @@ func newTestAuthRouter(t *testing.T) (*gin.Engine, *gin.Engine) {
func TestAuthHandler_Login_Success(t *testing.T) {
db := testutil.SetupTestDB()
hotel := testutil.CreateTestHotel(db, "AH001")
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
shop := testutil.CreateTestShop(db, "AH001")
testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
@@ -48,9 +48,9 @@ func TestAuthHandler_Login_Success(t *testing.T) {
r.POST("/api/v1/auth/login", h.Login)
body := map[string]string{
"hotel_code": "AH001",
"username": "admin",
"password": "password123",
"shop_code": "AH001",
"username": "admin",
"password": "password123",
}
bodyBytes, _ := json.Marshal(body)
@@ -70,8 +70,8 @@ func TestAuthHandler_Login_Success(t *testing.T) {
func TestAuthHandler_Login_WrongPassword(t *testing.T) {
db := testutil.SetupTestDB()
hotel := testutil.CreateTestHotel(db, "AH002")
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
shop := testutil.CreateTestShop(db, "AH002")
testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
@@ -79,9 +79,9 @@ func TestAuthHandler_Login_WrongPassword(t *testing.T) {
r.POST("/api/v1/auth/login", h.Login)
body := map[string]string{
"hotel_code": "AH002",
"username": "admin",
"password": "wrongpassword",
"shop_code": "AH002",
"username": "admin",
"password": "wrongpassword",
}
bodyBytes, _ := json.Marshal(body)
@@ -102,7 +102,7 @@ func TestAuthHandler_Login_MissingFields(t *testing.T) {
// 缺少必填字段
body := map[string]string{
"hotel_code": "AH003",
"shop_code": "AH003",
}
bodyBytes, _ := json.Marshal(body)
@@ -116,8 +116,8 @@ func TestAuthHandler_Login_MissingFields(t *testing.T) {
func TestAuthHandler_Refresh_Success(t *testing.T) {
db := testutil.SetupTestDB()
hotel := testutil.CreateTestHotel(db, "AH004")
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
shop := testutil.CreateTestShop(db, "AH004")
testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
@@ -127,9 +127,9 @@ func TestAuthHandler_Refresh_Success(t *testing.T) {
// 先登录获取 token
loginBody := map[string]string{
"hotel_code": "AH004",
"username": "admin",
"password": "password123",
"shop_code": "AH004",
"username": "admin",
"password": "password123",
}
loginBytes, _ := json.Marshal(loginBody)
w := httptest.NewRecorder()
@@ -173,3 +173,124 @@ func TestAuthHandler_Refresh_InvalidToken(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestAuthHandler_Login_DisabledUser(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "AH005")
user := testutil.CreateTestUser(db, shop.ID, "disabled_user", "password123", "operator")
db.Model(user).Update("is_active", false)
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
r := gin.New()
r.POST("/api/v1/auth/login", h.Login)
body := map[string]string{
"shop_code": "AH005",
"username": "disabled_user",
"password": "password123",
}
bodyBytes, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(bodyBytes))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestAuthHandler_Login_WrongShopCode(t *testing.T) {
db := testutil.SetupTestDB()
testutil.CreateTestShop(db, "AH006")
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
r := gin.New()
r.POST("/api/v1/auth/login", h.Login)
body := map[string]string{
"shop_code": "NONEXISTENT",
"username": "admin",
"password": "password123",
}
bodyBytes, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(bodyBytes))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestAuthHandler_Login_EmptyBody(t *testing.T) {
db := testutil.SetupTestDB()
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
r := gin.New()
r.POST("/api/v1/auth/login", h.Login)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer([]byte("{}")))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestAuthHandler_Refresh_MissingToken(t *testing.T) {
db := testutil.SetupTestDB()
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
r := gin.New()
r.POST("/api/v1/auth/refresh", h.Refresh)
body := map[string]string{}
bodyBytes, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/auth/refresh", bytes.NewBuffer(bodyBytes))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
// 缺少 refresh_token,应返回 4xx
assert.True(t, w.Code >= 400 && w.Code < 500)
}
func TestAuthHandler_Login_ResponseContainsUserInfo(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "AH007")
testutil.CreateTestUser(db, shop.ID, "manager", "password123", "admin")
svc := service.NewAuthService(db)
h := NewAuthHandler(svc)
r := gin.New()
r.POST("/api/v1/auth/login", h.Login)
body := map[string]string{
"shop_code": "AH007",
"username": "manager",
"password": "password123",
}
bodyBytes, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(bodyBytes))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
data := resp["data"].(map[string]interface{})
// token 不为空
assert.NotEmpty(t, data["access_token"])
assert.NotEmpty(t, data["refresh_token"])
// 包含 shop_id
assert.NotNil(t, data["shop_id"])
// 包含用户信息
userInfo, ok := data["user"].(map[string]interface{})
if ok {
assert.Equal(t, "manager", userInfo["username"])
}
}