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:
@@ -15,11 +15,11 @@ import (
|
||||
|
||||
func TestStockInHandler_FullFlow(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI001")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Main")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Test Beer")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI001")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Test Beer")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 1. 创建入库单(草稿)
|
||||
@@ -59,13 +59,13 @@ func TestStockInHandler_FullFlow(t *testing.T) {
|
||||
|
||||
// 5. 验证库存变化
|
||||
var inv model.Inventory
|
||||
db.Where("hotel_id = ? AND warehouse_id = ? AND product_id = ?",
|
||||
hotel.ID, warehouse.ID, product.ID).First(&inv)
|
||||
db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?",
|
||||
shop.ID, warehouse.ID, product.ID).First(&inv)
|
||||
assert.Equal(t, float64(10), inv.Quantity)
|
||||
|
||||
// 6. 验证库存流水
|
||||
var logs []model.InventoryLog
|
||||
db.Where("hotel_id = ? AND product_id = ?", hotel.ID, product.ID).Find(&logs)
|
||||
db.Where("shop_id = ? AND product_id = ?", shop.ID, product.ID).Find(&logs)
|
||||
require.Len(t, logs, 1)
|
||||
assert.Equal(t, "in", logs[0].Direction)
|
||||
assert.Equal(t, float64(10), logs[0].Quantity)
|
||||
@@ -73,11 +73,11 @@ func TestStockInHandler_FullFlow(t *testing.T) {
|
||||
|
||||
func TestStockInHandler_List(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI002")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Wine")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI002")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Wine")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建 2 个入库单
|
||||
@@ -99,11 +99,11 @@ func TestStockInHandler_List(t *testing.T) {
|
||||
|
||||
func TestStockInHandler_Submit_WrongStatus(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI003")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Gin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI003")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Gin")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建
|
||||
@@ -126,11 +126,11 @@ func TestStockInHandler_Submit_WrongStatus(t *testing.T) {
|
||||
|
||||
func TestStockInHandler_Approve_NotPending(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI004")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Rum")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI004")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Rum")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建但不提交(状态是 draft)
|
||||
@@ -150,11 +150,11 @@ func TestStockInHandler_Approve_NotPending(t *testing.T) {
|
||||
|
||||
func TestStockInHandler_Reject(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI005")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Tequila")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI005")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Tequila")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建并提交
|
||||
@@ -180,9 +180,9 @@ func TestStockInHandler_Reject(t *testing.T) {
|
||||
|
||||
func TestStockInHandler_GetNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI006")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI006")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-in/orders/99999", token, nil)
|
||||
@@ -191,12 +191,12 @@ func TestStockInHandler_GetNotFound(t *testing.T) {
|
||||
|
||||
func TestStockInHandler_TotalAmount(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SI007")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product1 := testutil.CreateTestProduct(db, hotel.ID, "ProductA")
|
||||
product2 := testutil.CreateTestProduct(db, hotel.ID, "ProductB")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
shop := testutil.CreateTestShop(db, "SI007")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product1 := testutil.CreateTestProduct(db, shop.ID, "ProductA")
|
||||
product2 := testutil.CreateTestProduct(db, shop.ID, "ProductB")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
@@ -211,3 +211,136 @@ func TestStockInHandler_TotalAmount(t *testing.T) {
|
||||
data := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, float64(110), data["total_amount"])
|
||||
}
|
||||
|
||||
func TestStockInHandler_NoAuth(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-in/orders", "", nil)
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
|
||||
w = makeRequest(r, "POST", "/api/v1/stock-in/orders", "", map[string]interface{}{
|
||||
"warehouse_id": 1,
|
||||
})
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
|
||||
func TestStockInHandler_TenantIsolation(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
|
||||
shopA := testutil.CreateTestShop(db, "SI_A")
|
||||
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
|
||||
warehouseA := testutil.CreateTestWarehouse(db, shopA.ID, "WA")
|
||||
productA := testutil.CreateTestProduct(db, shopA.ID, "BeerA")
|
||||
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
|
||||
|
||||
shopB := testutil.CreateTestShop(db, "SI_B")
|
||||
userB := testutil.CreateTestUser(db, shopB.ID, "adminB", "pass", "admin")
|
||||
tokenB := getAuthToken(userB.ID, shopB.ID, "admin")
|
||||
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 门店 A 创建入库单
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", tokenA, map[string]interface{}{
|
||||
"warehouse_id": warehouseA.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": productA.ID, "quantity": 5.0},
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
orderAID := extractID(w)
|
||||
|
||||
// 门店 B 看不到门店 A 的订单
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders", tokenB, nil)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(0), resp["total"].(float64))
|
||||
|
||||
// 门店 B 不能获取门店 A 的订单详情
|
||||
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderAID), tokenB, nil)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// 门店 B 不能提交门店 A 的订单
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderAID), tokenB, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
// 门店 B 不能审核门店 A 的订单
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderAID), tokenB, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestStockInHandler_Create_MissingWarehouse(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI008")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 缺少 warehouse_id,应该返回 400
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{},
|
||||
})
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestStockInHandler_Reject_NotPending(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI009")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Whiskey")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建但不提交(draft 状态)
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product.ID, "quantity": 5.0},
|
||||
},
|
||||
})
|
||||
orderID := extractID(w)
|
||||
|
||||
// 直接驳回(应该失败,因为是 draft 状态)
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/reject", orderID), token, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestStockInHandler_List_FilterByStatus(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI010")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Vodka")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建一个 draft 和一个 pending 订单
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 5.0}},
|
||||
})
|
||||
draftOrderID := extractID(w)
|
||||
|
||||
w = makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 3.0}},
|
||||
})
|
||||
pendingOrderID := extractID(w)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", pendingOrderID), token, nil)
|
||||
_ = draftOrderID
|
||||
|
||||
// 过滤 pending 状态
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=pending", token, nil)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
|
||||
// 过滤 draft 状态
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=draft", token, nil)
|
||||
resp = parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user