3 Revize 31de39cd9a ... c2f44c6a34

Autor SHA1 Zpráva Datum
  lq c2f44c6a34 fix: 测试用例ExitPreview响应解析层级修正——fee在data嵌套内 před 1 měsícem
  lq 74fc4a7f11 fix: VIP过期检查加固+全零收费配置回退默认费率 před 1 měsícem
  lq c7ba8f3080 fix: 手动进出场加入黑名单检查——三入口全覆盖,CheckVehicleShortlist支持车牌查询 před 1 měsícem

+ 1 - 1
internal/service/parking/passage.go

@@ -76,7 +76,7 @@ func (s *PassageService) HandlePassage(req common.PassageRequest) (*common.Passa
 	}
 
 	// ===================== 4. 黑名单检查 =====================
-	isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.RFIDTag)
+	isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag)
 	if isBlack {
 		fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
 		return nil, fmt.Errorf("车辆在黑名单中")

+ 9 - 5
internal/service/vehicle/shortlist.go

@@ -27,16 +27,20 @@ func (shortlistService *ShortlistService) QueryShortlistByRFIDTag(rfidTag string
 	return dao.QueryShortlistByRFIDTag(rfidTag)
 }
 
-func (s *ShortlistService) CheckVehicleShortlist(rfid string) (isBlack bool, isWhite bool) {
+func (s *ShortlistService) CheckVehicleShortlist(plateNumber, rfidTag string) (isBlack bool, isWhite bool) {
 	var vehicle dao.Vehicle
-	err := global.GVA_DB.Where("rfid_tag = ?", rfid).First(&vehicle).Error
-	if err != nil {
+	query := global.GVA_DB
+	if rfidTag != "" {
+		query = query.Where("rfid_tag = ?", rfidTag)
+	} else {
+		query = query.Where("plate_number = ?", plateNumber)
+	}
+	if err := query.First(&vehicle).Error; err != nil {
 		return false, false
 	}
 
 	var shortlist dao.Shortlist
-	err = global.GVA_DB.Where("vehicle_id = ?", vehicle.ID).First(&shortlist).Error
-	if err != nil {
+	if err := global.GVA_DB.Where("vehicle_id = ?", vehicle.ID).First(&shortlist).Error; err != nil {
 		return false, false
 	}
 

+ 19 - 11
internal/service/vehicle/vehicle.go

@@ -174,6 +174,11 @@ func (s *VehicleService) DeleteVehicle(id uint) error {
 
 // VehicleEntry 车辆入场
 func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.VehicleEntryResponse, error) {
+	// 黑名单检查
+	var shortlistSvc ShortlistService
+	if isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag); isBlack {
+		return request.VehicleEntryResponse{}, errors.New("车辆在黑名单中,禁止入场")
+	}
 	// 检查车辆是否存在
 	vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
 	if err != nil {
@@ -326,46 +331,44 @@ func (s *VehicleService) ExitPreview(req request.VehicleEntry) (vehicleResponse.
 
 // ExitConfirm 确认出场 + 支付(更新车辆记录 + 创建支付记录)
 func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID uint) (vehicleResponse.ExitConfirmResponse, error) {
-var record dao.VehicleRecord
+	// 黑名单检查
+	var shortlistSvc ShortlistService
+	if isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag); isBlack {
+		return vehicleResponse.ExitConfirmResponse{}, errors.New("车辆在黑名单中,禁止出场")
+	}
+	var record dao.VehicleRecord
 	err := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error
 	if err != nil {
 		return vehicleResponse.ExitConfirmResponse{}, errors.New("车辆未入场或已出场")
 	}
-
 	vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
 	if err != nil {
 		return vehicleResponse.ExitConfirmResponse{}, err
 	}
-
 	exitTime := time.Now()
 	stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
 	if stayTime < 0 {
 		stayTime = 0
 	}
 	fee := s.calculateFee(&vehicle, stayTime)
-
 	record.ExitTime = &exitTime
 	record.StayTime = stayTime
 	record.Fee = fee
 	record.PaymentMethod = req.PaymentMethod
 	global.GVA_DB.Save(&record)
-
 	vehicle.LastExitTime = &exitTime
 	vehicle.TotalStayTime += stayTime
 	vehicle.TotalFee += fee
 	global.GVA_DB.Save(&vehicle)
-
 	paymentSvc := paymentService.NewPaymentService()
 	pr, err := paymentSvc.ProcessPayment(record.ID, req.PaymentMethod, fee, req.PaidAmount, operatorID)
 	if err != nil {
 		return vehicleResponse.ExitConfirmResponse{}, err
 	}
-
 	changeAmount := 0.0
 	if pr != nil {
 		changeAmount = pr.ChangeAmount
 	}
-
 	return vehicleResponse.ExitConfirmResponse{
 		PlateNumber:   vehicle.PlateNumber,
 		EntryTime:     record.EntryTime.Unix(),
@@ -382,16 +385,21 @@ func (s *VehicleService) calculateFee(vehicle *dao.Vehicle, stayTime int64) floa
 	// 1. 根据车辆类型ID获取收费配置(唯一)
 	var feeConfig dao.FeeConfig
 	if err := global.GVA_DB.Where("vehicle_type_id = ?", vehicle.VehicleTypeID).First(&feeConfig).Error; err != nil {
-		// 无配置 → 默认计费
+		// 无配置 → 默认计费:0.1元/分钟
+		return float64(stayTime) * 0.1
+	}
+	// 配置存在但全为 0 → 同样回退默认费率
+	if feeConfig.UnitFee == 0 && feeConfig.StartFee == 0 && feeConfig.DailyMaxFee == 0 {
 		return float64(stayTime) * 0.1
 	}
 
-	// 2. 判断VIP有效性
+	// 2. 判断VIP有效性(必须IsVip=true、过期时间未到、且过期时间非零值)
 	isVIP := false
 	if vehicle.OwnerId != nil {
 		var owner dao.Owner
 		if err := global.GVA_DB.First(&owner, *vehicle.OwnerId).Error; err == nil {
-			isVIP = owner.IsVip && time.Now().Before(owner.VipExpireTime)
+			now := time.Now()
+			isVIP = owner.IsVip && owner.VipExpireTime.After(now) && !owner.VipExpireTime.IsZero()
 		}
 	}
 

+ 376 - 0
test/testutil/e2e_edge_cases_test.go

@@ -0,0 +1,376 @@
+package testutil
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+	"wails-app/internal/model/common/response"
+)
+
+// TestVehicleReEntry 同一车辆重复入场应被拦截
+func TestVehicleReEntry(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingR00001", "RFID_REENTRY", 1)
+	entryResp, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	AssertResponse(t, entryResp, entryBody, http.StatusOK, response.SUCCESS)
+
+	_, entryBody2 := env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY",
+		"parking_lot_id": 1, "parking_space_id": 2, "entry_image": "test2.jpg",
+	})
+	var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
+	json.Unmarshal(entryBody2, &result)
+	if result.Code == response.SUCCESS {
+		t.Fatalf("BUG: Vehicle re-entry was NOT blocked! code=%d msg=%s", result.Code, result.Msg)
+	}
+	fmt.Println("PASS: Re-entry attempt correctly rejected")
+}
+
+// TestExitWithoutEntry 未入场车辆尝试出场应返回错误
+func TestExitWithoutEntry(t *testing.T) {
+	env := InitTestEnv(t)
+	_, exitPreviewBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
+		"plate_number": "JingZ99999", "rfid_tag": "RFID_NOENTRY",
+	})
+	var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
+	json.Unmarshal(exitPreviewBody, &result)
+	if result.Code == response.SUCCESS {
+		t.Fatalf("BUG: Exit preview succeeded for non-entered vehicle!")
+	}
+	fmt.Println("PASS: Exit without entry correctly rejected")
+}
+
+// TestPaymentInsufficientAmount 支付金额不足时的行为
+func TestPaymentInsufficientAmount(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingP00001", "RFID_PAY", 1)
+	_, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingP00001", "rfid_tag": "RFID_PAY",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	entryTime := time.Now().Add(-3 * time.Hour)
+	global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingP00001").Update("entry_time", entryTime)
+	_, exitConfirmBody := env.DoPost("/vehicle/exit/confirm", map[string]interface{}{
+		"plate_number": "JingP00001", "rfid_tag": "RFID_PAY",
+		"payment_method": "cash", "paid_amount": 0.0,
+	})
+	var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
+	json.Unmarshal(exitConfirmBody, &result)
+	fmt.Printf("INFO: Insufficient payment response: code=%d msg=%s\n", result.Code, result.Msg)
+}
+
+// TestVIPExpired 过期 VIP 应恢复普通计费
+func TestVIPExpired(t *testing.T) {
+	env := InitTestEnv(t)
+	ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
+		"owner_name": "VIP Expired Test", "owner_surname": "VIP",
+		"owner_phone": "13900000001", "is_vip": true,
+		"vip_expire_time": time.Now().Add(-24*time.Hour).Format(time.RFC3339),
+	})
+	AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
+
+	var owner dao.Owner
+	env.DB.Where("owner_phone = ?", "13900000001").First(&owner)
+	createVehicleWithOwner(t, env, "JingV00001", "RFID_VIP_EXP", owner.ID)
+
+	_, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	entryTime := time.Now().Add(-3 * time.Hour)
+	global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingV00001").Update("entry_time", entryTime)
+
+	exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
+		"plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP",
+	})
+	AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
+	var result struct {
+		Data struct {
+			Fee float64 `json:"fee"`
+		} `json:"data"`
+	}
+	json.Unmarshal(exitBody, &result)
+	if result.Data.Fee == 0 {
+		t.Fatalf("BUG: Expired VIP still getting free parking! Fee should be > 0")
+	}
+	fmt.Printf("PASS: Expired VIP charged correctly, fee=%.2f\n", result.Data.Fee)
+}
+
+// TestFreeDuration 免费时长内不应收费
+func TestFreeDuration(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingF00001", "RFID_FREE", 1)
+	_, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingF00001", "rfid_tag": "RFID_FREE",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	entryTime := time.Now().Add(-10 * time.Minute)
+	global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingF00001").Update("entry_time", entryTime)
+	exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
+		"plate_number": "JingF00001", "rfid_tag": "RFID_FREE",
+	})
+	AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
+	var preview struct { Fee float64 `json:"fee"` }
+	json.Unmarshal(exitBody, &preview)
+	if preview.Fee != 0 {
+		t.Fatalf("BUG: Vehicle within free duration should have zero fee, got %.2f", preview.Fee)
+	}
+	fmt.Println("PASS: Free duration correctly applied")
+}
+
+// TestDailyCap 超过每日封顶费用应封顶
+func TestDailyCap(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingD00001", "RFID_CAP", 1)
+	_, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingD00001", "rfid_tag": "RFID_CAP",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	entryTime := time.Now().Add(-24 * time.Hour)
+	global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingD00001").Update("entry_time", entryTime)
+	exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
+		"plate_number": "JingD00001", "rfid_tag": "RFID_CAP",
+	})
+	AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
+	var preview struct { Fee float64 `json:"fee"` }
+	json.Unmarshal(exitBody, &preview)
+	if preview.Fee > 50.0 {
+		t.Fatalf("BUG: Fee exceeds daily cap! Expected <= 50, got %.2f", preview.Fee)
+	}
+	fmt.Printf("PASS: Daily cap applied correctly, fee=%.2f\n", preview.Fee)
+}
+
+// TestNoFeeConfig 无收费配置时默认计费
+func TestNoFeeConfig(t *testing.T) {
+	env := InitTestEnv(t)
+	var normalType dao.VehicleType
+	env.DB.Where("is_system = ? AND name != ?", true, "临时车").First(&normalType)
+	if normalType.ID == 0 {
+		normalType = dao.VehicleType{Name: "特殊车", Remarks: "无收费配置"}
+		env.DB.Create(&normalType)
+	}
+	env.DB.Where("vehicle_type_id = ?", normalType.ID).Delete(&dao.FeeConfig{})
+	createVehicleWithOwner(t, env, "JingN00001", "RFID_NOFEE", normalType.ID)
+	_, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	entryTime := time.Now().Add(-1 * time.Hour)
+	global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingN00001").Update("entry_time", entryTime)
+	exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
+		"plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE",
+	})
+	AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
+	var result struct {
+		Data struct {
+			Fee float64 `json:"fee"`
+		} `json:"data"`
+	}
+	json.Unmarshal(exitBody, &result)
+	if result.Data.Fee <= 0 {
+		t.Fatalf("BUG: No fee config should use default rate, got fee=%.2f", result.Data.Fee)
+	}
+	fmt.Printf("PASS: Default fee applied correctly, fee=%.2f\n", result.Data.Fee)
+}
+
+// TestShortlistByPlateOrRFID 黑白名单按车牌/RFID查询
+func TestShortlistByPlateOrRFID(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingS00001", "RFID_FILTER", 1)
+	var vehicle dao.Vehicle
+	env.DB.Where("plate_number = ?", "JingS00001").First(&vehicle)
+	_, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
+		ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
+	})
+
+	resp1, body1 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{
+		"page": 1, "pageSize": 10, "list_type": "", "plate_number": "JingS00001",
+	})
+	AssertResponse(t, resp1, body1, http.StatusOK, response.SUCCESS)
+	list1 := ParsePageList(body1)
+	if len(list1) < 1 { t.Fatalf("Expected at least 1 shortlist record by plate number") }
+	fmt.Printf("PASS: Shortlist query by plate number returned %d records\n", len(list1))
+
+	resp2, body2 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{
+		"page": 1, "pageSize": 10, "list_type": "", "rfid_tag": "RFID_FILTER",
+	})
+	AssertResponse(t, resp2, body2, http.StatusOK, response.SUCCESS)
+	list2 := ParsePageList(body2)
+	if len(list2) < 1 { t.Fatalf("Expected at least 1 shortlist record by RFID") }
+	fmt.Printf("PASS: Shortlist query by RFID returned %d records\n", len(list2))
+}
+
+// TestShortlistUpdate 黑白名单更新
+func TestShortlistUpdate(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingU00001", "RFID_UPDATE", 1)
+	var vehicle dao.Vehicle
+	env.DB.Where("plate_number = ?", "JingU00001").First(&vehicle)
+	_, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
+		ListType: "白名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
+	})
+	queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil)
+	AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS)
+	var result struct { Data []map[string]interface{} `json:"data"` }
+	json.Unmarshal(queryBody, &result)
+	if len(result.Data) == 0 { t.Fatalf("No shortlist found") }
+	idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] }
+	id := int(idVal.(float64))
+	updateResp, updateBody := env.DoPut("/shortlist/updateShortlist", map[string]interface{}{
+		"ID": id, "list_type": "黑名单", "vehicle_id": vehicle.ID,
+		"expiration_time": time.Now().Add(30*24*time.Hour).Format(time.RFC3339),
+	})
+	AssertResponse(t, updateResp, updateBody, http.StatusOK, response.SUCCESS)
+	updatedResp, updatedBody := env.DoGet("/shortlist/queryAllShortlists", nil)
+	AssertResponse(t, updatedResp, updatedBody, http.StatusOK, response.SUCCESS)
+	fmt.Println("PASS: Shortlist update successful")
+}
+
+// TestShortlistDelete 黑白名单删除
+func TestShortlistDelete(t *testing.T) {
+	env := InitTestEnv(t)
+	createVehicle(t, env, "JingDel001", "RFID_DELETE", 1)
+	var vehicle dao.Vehicle
+	env.DB.Where("plate_number = ?", "JingDel001").First(&vehicle)
+	_, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
+		ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
+	})
+	queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil)
+	AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS)
+	var result struct { Data []map[string]interface{} `json:"data"` }
+	json.Unmarshal(queryBody, &result)
+	if len(result.Data) == 0 { t.Fatalf("No shortlist found") }
+	idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] }
+	id := int(idVal.(float64))
+	deleteResp, deleteBody := env.DoDelete(fmt.Sprintf("/shortlist/deleteShortlist?id=%d", id))
+	AssertResponse(t, deleteResp, deleteBody, http.StatusOK, response.SUCCESS)
+	fmt.Println("PASS: Shortlist delete successful")
+}
+
+// TestOwnerVehicleRelationship 车主关联车辆查询
+func TestOwnerVehicleRelationship(t *testing.T) {
+	env := InitTestEnv(t)
+	ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
+		"owner_name": "Rel Test Owner", "owner_surname": "REL",
+		"owner_phone": "13911111111", "is_vip": false, "vip_expire_time": nil,
+	})
+	AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
+	var owner dao.Owner
+	env.DB.Where("owner_phone = ?", "13911111111").First(&owner)
+	createVehicleWithOwner(t, env, "JingO00001", "RFID_OWNER", owner.ID)
+	ownerListResp, ownerListBody := env.DoGet("/owner/list", map[string]string{"page": "1", "page_size": "10"})
+	AssertResponse(t, ownerListResp, ownerListBody, http.StatusOK, response.SUCCESS)
+	list := ParsePageList(ownerListBody)
+	found := false
+	for _, item := range list {
+		m, ok := item.(map[string]interface{})
+		if !ok { continue }
+		if m["owner_phone"] == "13911111111" { found = true; break }
+	}
+	if !found { t.Fatalf("Owner not found in list") }
+	fmt.Println("PASS: Owner-vehicle relationship verified")
+}
+
+// TestParkingLotCapacity 停车场容量检查
+func TestParkingLotCapacity(t *testing.T) {
+	env := InitTestEnv(t)
+	lotResp, lotBody := env.DoPost("/parking/lot/create", map[string]interface{}{
+		"lot_code": "TEST_EMPTY", "lot_name": "Empty Lot",
+		"capacity": 0, "description": "Zero capacity test",
+	})
+	AssertResponse(t, lotResp, lotBody, http.StatusOK, response.SUCCESS)
+	createVehicle(t, env, "JingC00001", "RFID_FULL", 1)
+	_, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
+		"plate_number": "JingC00001", "rfid_tag": "RFID_FULL",
+		"parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
+	})
+	var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
+	json.Unmarshal(entryBody, &result)
+	fmt.Printf("INFO: Entry into zero-capacity lot: code=%d msg=%s\n", result.Code, result.Msg)
+}
+
+// TestBoothCRUD 岗亭CRUD
+func TestBoothCRUD(t *testing.T) {
+	env := InitTestEnv(t)
+	boothResp, boothBody := env.DoPost("/parking/booth/create", map[string]interface{}{
+		"booth_code": "BT001", "booth_name": "Test Booth",
+		"parking_lot_id": 1, "description": "Automated test booth",
+	})
+	AssertResponse(t, boothResp, boothBody, http.StatusOK, response.SUCCESS)
+	listResp, listBody := env.DoGet("/parking/booth/list", map[string]string{"page": "1", "page_size": "10"})
+	AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
+	list := ParsePageList(listBody)
+	if len(list) < 1 { t.Fatalf("Booth list is empty") }
+	fmt.Printf("PASS: Booth CRUD successful, total %d booths\n", len(list))
+}
+
+// TestChannelCRUD 通道CRUD
+func TestChannelCRUD(t *testing.T) {
+	env := InitTestEnv(t)
+	channelResp, channelBody := env.DoPost("/parking/channel/create", map[string]interface{}{
+		"channel_code": "CH-IN-TEST", "channel_name": "Test Entrance",
+		"direction": "in", "parking_lot_id": 1, "booth_id": 1,
+		"allow_temporary": true, "description": "Automated test channel",
+	})
+	AssertResponse(t, channelResp, channelBody, http.StatusOK, response.SUCCESS)
+	listResp, listBody := env.DoGet("/parking/channel/list", map[string]string{"page": "1", "page_size": "10"})
+	AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
+	list := ParsePageList(listBody)
+	if len(list) < 1 { t.Fatalf("Channel list is empty") }
+	fmt.Printf("PASS: Channel CRUD successful, total %d channels\n", len(list))
+}
+
+// TestDeviceCRUD 设备CRUD
+func TestDeviceCRUD(t *testing.T) {
+	env := InitTestEnv(t)
+	deviceResp, deviceBody := env.DoPost("/parking/device/create", map[string]interface{}{
+		"device_code": "DEV001", "device_name": "Test Device",
+		"device_type": "TCP", "connect_type": "tcp", "ip_address": "127.0.0.1",
+		"port": 9000, "is_active": true, "parking_lot_id": 1, "channel_id": 1,
+		"description": "Automated test device",
+	})
+	AssertResponse(t, deviceResp, deviceBody, http.StatusOK, response.SUCCESS)
+	listResp, listBody := env.DoGet("/parking/device/list", map[string]string{"page": "1", "page_size": "10"})
+	AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
+	list := ParsePageList(listBody)
+	if len(list) < 1 { t.Fatalf("Device list is empty") }
+	fmt.Printf("PASS: Device CRUD successful, total %d devices\n", len(list))
+}
+
+// TestVehicleTypeCRUD 自定义车辆类型CRUD
+func TestVehicleTypeCRUD(t *testing.T) {
+	env := InitTestEnv(t)
+	typeResp, typeBody := env.DoPost("/vehicle/type/create", map[string]interface{}{
+		"name": "新能源车", "remarks": "测试新能源车辆类型", "is_system": false,
+	})
+	AssertResponse(t, typeResp, typeBody, http.StatusOK, response.SUCCESS)
+	listResp, listBody := env.DoGet("/vehicle/type/all", nil)
+	AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
+	list := ParseDataList(listBody)
+	if len(list) < 1 { t.Fatalf("Vehicle type list is empty") }
+	fmt.Printf("PASS: Vehicle type CRUD successful, total %d types\n", len(list))
+}
+
+// ===================== Helper functions =====================
+
+func createVehicleWithOwner(t *testing.T, env *TestEnv, plateNumber, rfidTag string, ownerID uint) {
+	t.Helper()
+	_, body := env.DoPost("/vehicle/create", map[string]interface{}{
+		"plate_number": plateNumber, "vehicle_type_id": 1,
+		"vehicle_brand": "TestBrand", "vehicle_color": "TestColor",
+		"rfid_tag": rfidTag, "owner_id": ownerID,
+	})
+	var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
+	json.Unmarshal(body, &result)
+	if result.Code != response.SUCCESS {
+		t.Fatalf("Failed to create vehicle with owner: %s", result.Msg)
+	}
+}