e2e_edge_cases_test.go 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  1. package testutil
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "testing"
  7. "time"
  8. "wails-app/internal/dao"
  9. "wails-app/internal/global"
  10. "wails-app/internal/model/common/response"
  11. dt_svc "wails-app/internal/modules/digital-ticket/service"
  12. )
  13. // TestVehicleReEntry 同一车辆重复入场应被拦截
  14. func TestVehicleReEntry(t *testing.T) {
  15. env := InitTestEnv(t)
  16. createVehicle(t, env, "JingR00001", "RFID_REENTRY", 1)
  17. entryResp, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
  18. "plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY",
  19. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  20. })
  21. AssertResponse(t, entryResp, entryBody, http.StatusOK, response.SUCCESS)
  22. _, entryBody2 := env.DoPost("/vehicle/entry", map[string]interface{}{
  23. "plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY",
  24. "parking_lot_id": 1, "parking_space_id": 2, "entry_image": "test2.jpg",
  25. })
  26. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  27. json.Unmarshal(entryBody2, &result)
  28. if result.Code == response.SUCCESS {
  29. t.Fatalf("BUG: Vehicle re-entry was NOT blocked! code=%d msg=%s", result.Code, result.Msg)
  30. }
  31. fmt.Println("PASS: Re-entry attempt correctly rejected")
  32. }
  33. // TestExitWithoutEntry 未入场车辆尝试出场应返回错误
  34. func TestExitWithoutEntry(t *testing.T) {
  35. env := InitTestEnv(t)
  36. _, exitPreviewBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  37. "plate_number": "JingZ99999", "rfid_tag": "RFID_NOENTRY",
  38. })
  39. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  40. json.Unmarshal(exitPreviewBody, &result)
  41. if result.Code == response.SUCCESS {
  42. t.Fatalf("BUG: Exit preview succeeded for non-entered vehicle!")
  43. }
  44. fmt.Println("PASS: Exit without entry correctly rejected")
  45. }
  46. // TestPaymentInsufficientAmount 支付金额不足时的行为
  47. func TestPaymentInsufficientAmount(t *testing.T) {
  48. env := InitTestEnv(t)
  49. createVehicle(t, env, "JingP00001", "RFID_PAY", 1)
  50. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  51. "plate_number": "JingP00001", "rfid_tag": "RFID_PAY",
  52. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  53. })
  54. entryTime := time.Now().Add(-3 * time.Hour)
  55. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingP00001").Update("entry_time", entryTime)
  56. _, exitConfirmBody := env.DoPost("/vehicle/exit/confirm", map[string]interface{}{
  57. "plate_number": "JingP00001", "rfid_tag": "RFID_PAY",
  58. "payment_method": "cash", "paid_amount": 0.0,
  59. })
  60. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  61. json.Unmarshal(exitConfirmBody, &result)
  62. fmt.Printf("INFO: Insufficient payment response: code=%d msg=%s\n", result.Code, result.Msg)
  63. }
  64. // TestVIPExpired 过期 VIP 应恢复普通计费
  65. func TestVIPExpired(t *testing.T) {
  66. env := InitTestEnv(t)
  67. ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
  68. "owner_name": "VIP Expired Test", "owner_surname": "VIP",
  69. "owner_phone": "13900000001", "is_vip": true,
  70. "vip_expire_time": time.Now().Add(-24*time.Hour).Format(time.RFC3339),
  71. })
  72. AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
  73. var owner dao.Owner
  74. env.DB.Where("owner_phone = ?", "13900000001").First(&owner)
  75. createVehicleWithOwner(t, env, "JingV00001", "RFID_VIP_EXP", owner.ID)
  76. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  77. "plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP",
  78. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  79. })
  80. entryTime := time.Now().Add(-3 * time.Hour)
  81. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingV00001").Update("entry_time", entryTime)
  82. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  83. "plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP",
  84. })
  85. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  86. var result struct {
  87. Data struct {
  88. Fee float64 `json:"fee"`
  89. } `json:"data"`
  90. }
  91. json.Unmarshal(exitBody, &result)
  92. if result.Data.Fee == 0 {
  93. t.Fatalf("BUG: Expired VIP still getting free parking! Fee should be > 0")
  94. }
  95. fmt.Printf("PASS: Expired VIP charged correctly, fee=%.2f\n", result.Data.Fee)
  96. }
  97. // TestFreeDuration 免费时长内不应收费
  98. func TestFreeDuration(t *testing.T) {
  99. env := InitTestEnv(t)
  100. createVehicle(t, env, "JingF00001", "RFID_FREE", 1)
  101. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  102. "plate_number": "JingF00001", "rfid_tag": "RFID_FREE",
  103. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  104. })
  105. entryTime := time.Now().Add(-10 * time.Minute)
  106. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingF00001").Update("entry_time", entryTime)
  107. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  108. "plate_number": "JingF00001", "rfid_tag": "RFID_FREE",
  109. })
  110. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  111. var preview struct { Fee float64 `json:"fee"` }
  112. json.Unmarshal(exitBody, &preview)
  113. if preview.Fee != 0 {
  114. t.Fatalf("BUG: Vehicle within free duration should have zero fee, got %.2f", preview.Fee)
  115. }
  116. fmt.Println("PASS: Free duration correctly applied")
  117. }
  118. // TestDailyCap 超过每日封顶费用应封顶
  119. func TestDailyCap(t *testing.T) {
  120. env := InitTestEnv(t)
  121. createVehicle(t, env, "JingD00001", "RFID_CAP", 1)
  122. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  123. "plate_number": "JingD00001", "rfid_tag": "RFID_CAP",
  124. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  125. })
  126. entryTime := time.Now().Add(-24 * time.Hour)
  127. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingD00001").Update("entry_time", entryTime)
  128. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  129. "plate_number": "JingD00001", "rfid_tag": "RFID_CAP",
  130. })
  131. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  132. var preview struct { Fee float64 `json:"fee"` }
  133. json.Unmarshal(exitBody, &preview)
  134. if preview.Fee > 50.0 {
  135. t.Fatalf("BUG: Fee exceeds daily cap! Expected <= 50, got %.2f", preview.Fee)
  136. }
  137. fmt.Printf("PASS: Daily cap applied correctly, fee=%.2f\n", preview.Fee)
  138. }
  139. // TestNoFeeConfig 无收费配置时默认计费
  140. func TestNoFeeConfig(t *testing.T) {
  141. env := InitTestEnv(t)
  142. var normalType dao.VehicleType
  143. env.DB.Where("is_system = ? AND name != ?", true, "临时车").First(&normalType)
  144. if normalType.ID == 0 {
  145. normalType = dao.VehicleType{Name: "特殊车", Remarks: "无收费配置"}
  146. env.DB.Create(&normalType)
  147. }
  148. env.DB.Where("vehicle_type_id = ?", normalType.ID).Delete(&dao.FeeConfig{})
  149. createVehicleWithOwner(t, env, "JingN00001", "RFID_NOFEE", normalType.ID)
  150. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  151. "plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE",
  152. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  153. })
  154. entryTime := time.Now().Add(-1 * time.Hour)
  155. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingN00001").Update("entry_time", entryTime)
  156. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  157. "plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE",
  158. })
  159. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  160. var result struct {
  161. Data struct {
  162. Fee float64 `json:"fee"`
  163. } `json:"data"`
  164. }
  165. json.Unmarshal(exitBody, &result)
  166. if result.Data.Fee <= 0 {
  167. t.Fatalf("BUG: No fee config should use default rate, got fee=%.2f", result.Data.Fee)
  168. }
  169. fmt.Printf("PASS: Default fee applied correctly, fee=%.2f\n", result.Data.Fee)
  170. }
  171. // TestShortlistByPlateOrRFID 黑白名单按车牌/RFID查询
  172. func TestShortlistByPlateOrRFID(t *testing.T) {
  173. env := InitTestEnv(t)
  174. createVehicle(t, env, "JingS00001", "RFID_FILTER", 1)
  175. var vehicle dao.Vehicle
  176. env.DB.Where("plate_number = ?", "JingS00001").First(&vehicle)
  177. _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
  178. ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
  179. })
  180. resp1, body1 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{
  181. "page": 1, "pageSize": 10, "list_type": "", "plate_number": "JingS00001",
  182. })
  183. AssertResponse(t, resp1, body1, http.StatusOK, response.SUCCESS)
  184. list1 := ParsePageList(body1)
  185. if len(list1) < 1 { t.Fatalf("Expected at least 1 shortlist record by plate number") }
  186. fmt.Printf("PASS: Shortlist query by plate number returned %d records\n", len(list1))
  187. resp2, body2 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{
  188. "page": 1, "pageSize": 10, "list_type": "", "rfid_tag": "RFID_FILTER",
  189. })
  190. AssertResponse(t, resp2, body2, http.StatusOK, response.SUCCESS)
  191. list2 := ParsePageList(body2)
  192. if len(list2) < 1 { t.Fatalf("Expected at least 1 shortlist record by RFID") }
  193. fmt.Printf("PASS: Shortlist query by RFID returned %d records\n", len(list2))
  194. }
  195. // TestShortlistUpdate 黑白名单更新
  196. func TestShortlistUpdate(t *testing.T) {
  197. env := InitTestEnv(t)
  198. createVehicle(t, env, "JingU00001", "RFID_UPDATE", 1)
  199. var vehicle dao.Vehicle
  200. env.DB.Where("plate_number = ?", "JingU00001").First(&vehicle)
  201. _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
  202. ListType: "白名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
  203. })
  204. queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil)
  205. AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS)
  206. var result struct { Data []map[string]interface{} `json:"data"` }
  207. json.Unmarshal(queryBody, &result)
  208. if len(result.Data) == 0 { t.Fatalf("No shortlist found") }
  209. idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] }
  210. id := int(idVal.(float64))
  211. updateResp, updateBody := env.DoPut("/shortlist/updateShortlist", map[string]interface{}{
  212. "ID": id, "list_type": "黑名单", "vehicle_id": vehicle.ID,
  213. "expiration_time": time.Now().Add(30*24*time.Hour).Format(time.RFC3339),
  214. })
  215. AssertResponse(t, updateResp, updateBody, http.StatusOK, response.SUCCESS)
  216. updatedResp, updatedBody := env.DoGet("/shortlist/queryAllShortlists", nil)
  217. AssertResponse(t, updatedResp, updatedBody, http.StatusOK, response.SUCCESS)
  218. fmt.Println("PASS: Shortlist update successful")
  219. }
  220. // TestShortlistDelete 黑白名单删除
  221. func TestShortlistDelete(t *testing.T) {
  222. env := InitTestEnv(t)
  223. createVehicle(t, env, "JingDel001", "RFID_DELETE", 1)
  224. var vehicle dao.Vehicle
  225. env.DB.Where("plate_number = ?", "JingDel001").First(&vehicle)
  226. _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
  227. ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
  228. })
  229. queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil)
  230. AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS)
  231. var result struct { Data []map[string]interface{} `json:"data"` }
  232. json.Unmarshal(queryBody, &result)
  233. if len(result.Data) == 0 { t.Fatalf("No shortlist found") }
  234. idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] }
  235. id := int(idVal.(float64))
  236. deleteResp, deleteBody := env.DoDelete(fmt.Sprintf("/shortlist/deleteShortlist?id=%d", id))
  237. AssertResponse(t, deleteResp, deleteBody, http.StatusOK, response.SUCCESS)
  238. fmt.Println("PASS: Shortlist delete successful")
  239. }
  240. // TestOwnerVehicleRelationship 车主关联车辆查询
  241. func TestOwnerVehicleRelationship(t *testing.T) {
  242. env := InitTestEnv(t)
  243. ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
  244. "owner_name": "Rel Test Owner", "owner_surname": "REL",
  245. "owner_phone": "13911111111", "is_vip": false, "vip_expire_time": nil,
  246. })
  247. AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
  248. var owner dao.Owner
  249. env.DB.Where("owner_phone = ?", "13911111111").First(&owner)
  250. createVehicleWithOwner(t, env, "JingO00001", "RFID_OWNER", owner.ID)
  251. ownerListResp, ownerListBody := env.DoGet("/owner/list", map[string]string{"page": "1", "page_size": "10"})
  252. AssertResponse(t, ownerListResp, ownerListBody, http.StatusOK, response.SUCCESS)
  253. list := ParsePageList(ownerListBody)
  254. found := false
  255. for _, item := range list {
  256. m, ok := item.(map[string]interface{})
  257. if !ok { continue }
  258. if m["owner_phone"] == "13911111111" { found = true; break }
  259. }
  260. if !found { t.Fatalf("Owner not found in list") }
  261. fmt.Println("PASS: Owner-vehicle relationship verified")
  262. }
  263. // TestParkingLotCapacity 停车场容量检查
  264. func TestParkingLotCapacity(t *testing.T) {
  265. env := InitTestEnv(t)
  266. lotResp, lotBody := env.DoPost("/parking/lot/create", map[string]interface{}{
  267. "lot_code": "TEST_EMPTY", "lot_name": "Empty Lot",
  268. "capacity": 0, "description": "Zero capacity test",
  269. })
  270. AssertResponse(t, lotResp, lotBody, http.StatusOK, response.SUCCESS)
  271. createVehicle(t, env, "JingC00001", "RFID_FULL", 1)
  272. _, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
  273. "plate_number": "JingC00001", "rfid_tag": "RFID_FULL",
  274. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  275. })
  276. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  277. json.Unmarshal(entryBody, &result)
  278. fmt.Printf("INFO: Entry into zero-capacity lot: code=%d msg=%s\n", result.Code, result.Msg)
  279. }
  280. // TestBoothCRUD 岗亭CRUD
  281. func TestBoothCRUD(t *testing.T) {
  282. env := InitTestEnv(t)
  283. boothResp, boothBody := env.DoPost("/parking/booth/create", map[string]interface{}{
  284. "booth_code": "BT001", "booth_name": "Test Booth",
  285. "parking_lot_id": 1, "description": "Automated test booth",
  286. })
  287. AssertResponse(t, boothResp, boothBody, http.StatusOK, response.SUCCESS)
  288. listResp, listBody := env.DoGet("/parking/booth/list", map[string]string{"page": "1", "page_size": "10"})
  289. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  290. list := ParsePageList(listBody)
  291. if len(list) < 1 { t.Fatalf("Booth list is empty") }
  292. fmt.Printf("PASS: Booth CRUD successful, total %d booths\n", len(list))
  293. }
  294. // TestChannelCRUD 通道CRUD
  295. func TestChannelCRUD(t *testing.T) {
  296. env := InitTestEnv(t)
  297. channelResp, channelBody := env.DoPost("/parking/channel/create", map[string]interface{}{
  298. "channel_code": "CH-IN-TEST", "channel_name": "Test Entrance",
  299. "direction": "in", "parking_lot_id": 1, "booth_id": 1,
  300. "allow_temporary": true, "description": "Automated test channel",
  301. })
  302. AssertResponse(t, channelResp, channelBody, http.StatusOK, response.SUCCESS)
  303. listResp, listBody := env.DoGet("/parking/channel/list", map[string]string{"page": "1", "page_size": "10"})
  304. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  305. list := ParsePageList(listBody)
  306. if len(list) < 1 { t.Fatalf("Channel list is empty") }
  307. fmt.Printf("PASS: Channel CRUD successful, total %d channels\n", len(list))
  308. }
  309. // TestDeviceCRUD 设备CRUD
  310. func TestDeviceCRUD(t *testing.T) {
  311. env := InitTestEnv(t)
  312. deviceResp, deviceBody := env.DoPost("/parking/device/create", map[string]interface{}{
  313. "device_code": "DEV001", "device_name": "Test Device",
  314. "device_type": "TCP", "connect_type": "tcp", "ip_address": "127.0.0.1",
  315. "port": 9000, "is_active": true, "parking_lot_id": 1, "channel_id": 1,
  316. "description": "Automated test device",
  317. })
  318. AssertResponse(t, deviceResp, deviceBody, http.StatusOK, response.SUCCESS)
  319. listResp, listBody := env.DoGet("/parking/device/list", map[string]string{"page": "1", "page_size": "10"})
  320. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  321. list := ParsePageList(listBody)
  322. if len(list) < 1 { t.Fatalf("Device list is empty") }
  323. fmt.Printf("PASS: Device CRUD successful, total %d devices\n", len(list))
  324. }
  325. // TestVehicleTypeCRUD 自定义车辆类型CRUD
  326. func TestVehicleTypeCRUD(t *testing.T) {
  327. env := InitTestEnv(t)
  328. typeResp, typeBody := env.DoPost("/vehicle/type/create", map[string]interface{}{
  329. "name": "新能源车", "remarks": "测试新能源车辆类型", "is_system": false,
  330. })
  331. AssertResponse(t, typeResp, typeBody, http.StatusOK, response.SUCCESS)
  332. listResp, listBody := env.DoGet("/vehicle/type/all", nil)
  333. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  334. list := ParseDataList(listBody)
  335. if len(list) < 1 { t.Fatalf("Vehicle type list is empty") }
  336. fmt.Printf("PASS: Vehicle type CRUD successful, total %d types\n", len(list))
  337. }
  338. // ===================== Helper functions =====================
  339. // TestVehicleUpdate 车辆信息更新
  340. func TestVehicleUpdate(t *testing.T) {
  341. env := InitTestEnv(t)
  342. createVehicle(t, env, "JingUPD001", "RFID_UPDATE_TEST", 1)
  343. var v dao.Vehicle
  344. env.DB.Where("plate_number = ?", "JingUPD001").First(&v)
  345. resp, body := env.DoPut("/vehicle/update", map[string]interface{}{
  346. "ID": v.ID, "plate_number": "JingUPD001", "vehicle_type_id": 1,
  347. "vehicle_brand": "UpdatedBrand",
  348. "vehicle_color": "Blue", "rfid_tag": "RFID_UPDATE_TEST",
  349. })
  350. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  351. fmt.Println("PASS: Vehicle update successful")
  352. }
  353. // TestVehicleDelete 车辆删除
  354. func TestVehicleDelete(t *testing.T) {
  355. env := InitTestEnv(t)
  356. createVehicle(t, env, "JingDEL001", "RFID_DELETE_TEST", 1)
  357. var v dao.Vehicle
  358. env.DB.Where("plate_number = ?", "JingDEL001").First(&v)
  359. resp, body := env.DoDelete(fmt.Sprintf("/vehicle/delete?id=%d", v.ID))
  360. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  361. fmt.Println("PASS: Vehicle delete successful")
  362. }
  363. // TestVehicleList 车辆分页列表
  364. func TestVehicleList(t *testing.T) {
  365. env := InitTestEnv(t)
  366. createVehicle(t, env, "JingLST001", "RFID_LIST_TEST", 1)
  367. resp, body := env.DoGet("/vehicle/list", map[string]string{"page": "1", "page_size": "10"})
  368. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  369. list := ParsePageList(body)
  370. if len(list) < 1 { t.Fatalf("Vehicle list is empty") }
  371. fmt.Printf("PASS: Vehicle list returned %d records\n", len(list))
  372. }
  373. // TestOwnerUpdate 业主信息更新
  374. func TestOwnerUpdate(t *testing.T) {
  375. env := InitTestEnv(t)
  376. ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
  377. "owner_name": "Update Test", "owner_surname": "Upd",
  378. "owner_phone": "13900000999", "is_vip": false,
  379. })
  380. AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
  381. var o dao.Owner
  382. env.DB.Where("owner_phone = ?", "13900000999").First(&o)
  383. updResp, updBody := env.DoPut("/owner/update", map[string]interface{}{
  384. "ID": o.ID, "owner_name": "Updated Name", "owner_surname": "Upd2",
  385. "owner_phone": "13900000999", "is_vip": true,
  386. })
  387. AssertResponse(t, updResp, updBody, http.StatusOK, response.SUCCESS)
  388. fmt.Println("PASS: Owner update successful")
  389. }
  390. // TestOwnerGetByPhone 按手机号查询业主
  391. func TestOwnerGetByPhone(t *testing.T) {
  392. env := InitTestEnv(t)
  393. _, _ = env.DoPost("/owner/create", map[string]interface{}{
  394. "owner_name": "Phone Query", "owner_surname": "Ph",
  395. "owner_phone": "13900000888", "is_vip": false,
  396. })
  397. resp, body := env.DoGet("/owner/get-by-phone", map[string]string{"phone": "13900000888"})
  398. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  399. fmt.Println("PASS: Owner query by phone successful")
  400. }
  401. // TestFeeConfigCreate 收费配置创建
  402. func TestFeeConfigCreate(t *testing.T) {
  403. env := InitTestEnv(t)
  404. // 创建一个新的车辆类型并为其创建收费配置
  405. typeResp, typeBody := env.DoPost("/vehicle/type/create", map[string]interface{}{
  406. "name": "FeeConfigTest", "remarks": "Fee config test type", "is_system": false,
  407. })
  408. AssertResponse(t, typeResp, typeBody, http.StatusOK, response.SUCCESS)
  409. var vt2 dao.VehicleType
  410. env.DB.Where("name = ?", "FeeConfigTest").First(&vt2)
  411. resp, body := env.DoPost("/vehicle/fee-config/create", map[string]interface{}{
  412. "vehicle_type_id": vt2.ID, "start_time": 0, "start_fee": 0,
  413. "unit_time": 60, "unit_fee": 10, "daily_max_fee": 80,
  414. })
  415. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  416. fmt.Println("PASS: Fee config create successful")
  417. }
  418. // TestParkingLotUpdate 停车场信息更新
  419. func TestParkingLotUpdate(t *testing.T) {
  420. env := InitTestEnv(t)
  421. var lot dao.ParkingLot
  422. env.DB.Where("lot_code = ?", "TEST001").First(&lot)
  423. resp, body := env.DoPut("/parking/lot/update", map[string]interface{}{
  424. "ID": lot.ID, "lot_code": "TEST001", "lot_name": "Updated Lot",
  425. "capacity": 200, "description": "Updated desc",
  426. })
  427. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  428. fmt.Println("PASS: Parking lot update successful")
  429. }
  430. // ===================== 月租车管理模块测试 =====================
  431. // TestMonthlyCardCreate 办理月卡
  432. func TestMonthlyCardCreate(t *testing.T) {
  433. env := InitTestEnv(t)
  434. createVehicle(t, env, "JingMC001", "RFID_MC001", 1)
  435. var v dao.Vehicle
  436. env.DB.Where("plate_number = ?", "JingMC001").First(&v)
  437. resp, body := env.DoPost("/monthly-card/create", map[string]interface{}{
  438. "vehicle_id": v.ID, "card_type": "month", "fee": 300.0, "remark": "test month card",
  439. })
  440. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  441. var card dao.MonthlyCard
  442. env.DB.Where("vehicle_id = ?", v.ID).First(&card)
  443. if card.ID == 0 { t.Fatalf("Monthly card not created in DB") }
  444. if card.PaymentStatus != "paid" { t.Fatalf("Expected paid, got %s", card.PaymentStatus) }
  445. // Verify shortlist (whitelist) was created
  446. var sl dao.Shortlist
  447. env.DB.Where("vehicle_id = ? AND list_type = ?", v.ID, "白名单").First(&sl)
  448. if sl.ID == 0 { t.Fatalf("Shortlist not created") }
  449. fmt.Println("PASS: Monthly card created successfully")
  450. }
  451. // TestMonthlyCardRenew 月卡续费
  452. func TestMonthlyCardRenew(t *testing.T) {
  453. env := InitTestEnv(t)
  454. createVehicle(t, env, "JingMC002", "RFID_MC002", 1)
  455. var v dao.Vehicle
  456. env.DB.Where("plate_number = ?", "JingMC002").First(&v)
  457. _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
  458. "vehicle_id": v.ID, "card_type": "month", "fee": 300.0,
  459. })
  460. var card dao.MonthlyCard
  461. env.DB.Where("vehicle_id = ?", v.ID).First(&card)
  462. origEnd := card.EndDate
  463. resp, body := env.DoPost("/monthly-card/renew", map[string]interface{}{
  464. "card_id": card.ID, "card_type": "month", "fee": 300.0,
  465. })
  466. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  467. env.DB.Where("vehicle_id = ?", v.ID).First(&card)
  468. expectedEnd := origEnd.AddDate(0, 0, 30)
  469. if !card.EndDate.Equal(expectedEnd) && !card.EndDate.After(expectedEnd.Add(-time.Hour*23)) {
  470. t.Fatalf("EndDate not extended correctly: orig=%v, new=%v", origEnd, card.EndDate)
  471. }
  472. fmt.Println("PASS: Monthly card renewed successfully")
  473. }
  474. // TestMonthlyCardRefund 月卡退卡
  475. func TestMonthlyCardRefund(t *testing.T) {
  476. env := InitTestEnv(t)
  477. createVehicle(t, env, "JingMC003", "RFID_MC003", 1)
  478. var v dao.Vehicle
  479. env.DB.Where("plate_number = ?", "JingMC003").First(&v)
  480. _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
  481. "vehicle_id": v.ID, "card_type": "month", "fee": 300.0,
  482. })
  483. var card dao.MonthlyCard
  484. env.DB.Where("vehicle_id = ?", v.ID).First(&card)
  485. resp, body := env.DoDelete(fmt.Sprintf("/monthly-card/refund?id=%d", card.ID))
  486. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  487. env.DB.First(&card, card.ID)
  488. if card.PaymentStatus != "refunded" { t.Fatalf("Expected refunded, got %s", card.PaymentStatus) }
  489. var count int64
  490. env.DB.Model(&dao.Shortlist{}).Where("vehicle_id = ? AND list_type = ?", v.ID, "白名单").Count(&count)
  491. if count != 0 { t.Fatalf("Shortlist should be removed after refund, found %d", count) }
  492. fmt.Println("PASS: Monthly card refunded successfully")
  493. }
  494. // TestMonthlyCardList 月卡列表查询
  495. func TestMonthlyCardList(t *testing.T) {
  496. env := InitTestEnv(t)
  497. createVehicle(t, env, "JingMC004", "RFID_MC004", 1)
  498. var v dao.Vehicle
  499. env.DB.Where("plate_number = ?", "JingMC004").First(&v)
  500. _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
  501. "vehicle_id": v.ID, "card_type": "quarter", "fee": 800.0,
  502. })
  503. resp, body := env.DoGet("/monthly-card/list", map[string]string{"page": "1", "page_size": "10"})
  504. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  505. list := ParsePageList(body)
  506. if len(list) < 1 { t.Fatalf("Monthly card list is empty") }
  507. resp, body2 := env.DoGet("/monthly-card/list", map[string]string{
  508. "page": "1", "page_size": "10", "card_type": "quarter",
  509. })
  510. AssertResponse(t, resp, body2, http.StatusOK, response.SUCCESS)
  511. list2 := ParsePageList(body2)
  512. if len(list2) < 1 { t.Fatalf("Filtered list by card_type is empty") }
  513. fmt.Printf("PASS: Monthly card list returned %d records\\n", len(list))
  514. }
  515. // TestMonthlyCardDuplicate 重复办卡拦截
  516. func TestMonthlyCardDuplicate(t *testing.T) {
  517. env := InitTestEnv(t)
  518. createVehicle(t, env, "JingMC005", "RFID_MC005", 1)
  519. var v dao.Vehicle
  520. env.DB.Where("plate_number = ?", "JingMC005").First(&v)
  521. _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
  522. "vehicle_id": v.ID, "card_type": "month", "fee": 300.0,
  523. })
  524. _, body2 := env.DoPost("/monthly-card/create", map[string]interface{}{
  525. "vehicle_id": v.ID, "card_type": "year", "fee": 3000.0,
  526. })
  527. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  528. json.Unmarshal(body2, &result)
  529. if result.Code == response.SUCCESS {
  530. t.Fatalf("BUG: Duplicate monthly card creation was NOT blocked!")
  531. }
  532. fmt.Printf("PASS: Duplicate card creation correctly rejected: %s\\n", result.Msg)
  533. }
  534. // TestMonthlyCardNonexistentRefund 不存在的月卡退卡
  535. func TestMonthlyCardNonexistentRefund(t *testing.T) {
  536. env := InitTestEnv(t)
  537. _, body := env.DoDelete("/monthly-card/refund?id=99999")
  538. // Should fail with error code
  539. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  540. json.Unmarshal(body, &result)
  541. if result.Code == response.SUCCESS {
  542. t.Fatalf("BUG: Refunding non-existent card was NOT blocked!")
  543. }
  544. // HTTP status may vary, just check code != 0
  545. fmt.Printf("PASS: Non-existent card refund correctly rejected: code=%d msg=%s\\n", result.Code, result.Msg)
  546. }
  547. // TestMonthlyCardDifferentTypes 不同套餐类型
  548. func TestMonthlyCardDifferentTypes(t *testing.T) {
  549. env := InitTestEnv(t)
  550. createVehicle(t, env, "JingMC006", "RFID_MC006", 1)
  551. var v dao.Vehicle
  552. env.DB.Where("plate_number = ?", "JingMC006").First(&v)
  553. resp, body := env.DoPost("/monthly-card/create", map[string]interface{}{
  554. "vehicle_id": v.ID, "card_type": "year", "fee": 3000.0,
  555. })
  556. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  557. var card dao.MonthlyCard
  558. env.DB.Where("vehicle_id = ?", v.ID).First(&card)
  559. duration := card.EndDate.Sub(card.StartDate).Hours() / 24
  560. if duration < 360 || duration > 370 {
  561. t.Fatalf("Year card should be ~365 days, got %.0f days", duration)
  562. }
  563. fmt.Printf("PASS: Year card duration = %.0f days\\n", duration)
  564. }
  565. // ===================== 数字票模块测试 =====================
  566. func createTicket(t *testing.T, env *TestEnv, plate, trigger string) (string, uint) {
  567. t.Helper()
  568. createVehicle(t, env, plate, "RFID_"+plate, 1)
  569. var v dao.Vehicle
  570. env.DB.Where("plate_number = ?", plate).First(&v)
  571. rec := dao.VehicleRecord{PlateNumber: plate, RFIDTag: "RFID_" + plate, EntryTime: time.Now(), ParkingLotID: 1}
  572. env.DB.Create(&rec)
  573. var svc = new(dt_svc.TicketService)
  574. ticket, err := svc.Create(plate, trigger, rec.ID)
  575. if err != nil {
  576. t.Fatalf("Failed to create ticket: %v", err)
  577. }
  578. return ticket.TicketNo, ticket.ID
  579. }
  580. // TestDigitalTicketDetail DT01: 查询数字票详情
  581. func TestDigitalTicketDetail(t *testing.T) {
  582. env := InitTestEnv(t)
  583. ticketNo, _ := createTicket(t, env, "DT001", "manual")
  584. resp, body := env.DoGet("/digital-ticket/"+ticketNo, nil)
  585. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  586. data := ParseDataMap(body)
  587. if data["state"] != "pending_payment" {
  588. t.Fatalf("Expected state=pending_payment, got %v", data["state"])
  589. }
  590. if data["ticket_no"] != ticketNo {
  591. t.Fatalf("TicketNo mismatch")
  592. }
  593. if data["trigger_mode"] != "manual" {
  594. t.Fatalf("TriggerMode mismatch")
  595. }
  596. fmt.Println("PASS: Digital ticket detail query successful")
  597. }
  598. // TestDigitalTicketPay DT02: 支付数字票
  599. // payment entry is explicit because entry and method are independent settings.
  600. func TestDigitalTicketPay(t *testing.T) {
  601. env := InitTestEnv(t)
  602. ticketNo, _ := createTicket(t, env, "DT002", "plate_recognition")
  603. resp, body := env.DoPost("/digital-ticket/"+ticketNo+"/pay", map[string]interface{}{
  604. "payment_entry": "ticket", "payment_method": "cash", "payment_order_no": "PO002", "paid_amount": 20.0,
  605. })
  606. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  607. fmt.Println("PASS: Pay API works correctly")
  608. }
  609. // TestDigitalTicketExit DT03: 离场验证
  610. func TestDigitalTicketExit(t *testing.T) {
  611. env := InitTestEnv(t)
  612. ticketNo, _ := createTicket(t, env, "DT003", "rfid")
  613. // 支付
  614. _, _ = env.DoPost("/digital-ticket/"+ticketNo+"/pay", map[string]interface{}{
  615. "payment_entry": "ticket", "payment_method": "cash", "payment_order_no": "PO003", "paid_amount": 15.0,
  616. })
  617. // 支付成功后离场
  618. resp, body := env.DoPost("/digital-ticket/"+ticketNo+"/exit", nil)
  619. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  620. fmt.Println("PASS: Pay → Exit flow works correctly")
  621. }
  622. // TestDigitalTicketList DT04: 数字票列表
  623. func TestDigitalTicketList(t *testing.T) {
  624. env := InitTestEnv(t)
  625. createTicket(t, env, "DT004", "manual")
  626. resp, body := env.DoGet("/digital-ticket/list", map[string]string{"page": "1", "page_size": "10"})
  627. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  628. list := ParsePageList(body)
  629. if len(list) < 1 {
  630. t.Fatalf("Ticket list is empty")
  631. }
  632. fmt.Printf("PASS: Digital ticket list returned %d records\n", len(list))
  633. }
  634. // TestDigitalTicketFullFlow DT05: 完整流程
  635. // payment and exit should advance the digital-ticket lifecycle independently.
  636. func TestDigitalTicketFullFlow(t *testing.T) {
  637. env := InitTestEnv(t)
  638. ticketNo, _ := createTicket(t, env, "DT005", "manual")
  639. // Pay
  640. resp1, body1 := env.DoPost("/digital-ticket/"+ticketNo+"/pay", map[string]interface{}{
  641. "payment_entry": "ticket", "payment_method": "cash", "payment_order_no": "PO005", "paid_amount": 25.0,
  642. })
  643. AssertResponse(t, resp1, body1, http.StatusOK, response.SUCCESS)
  644. // Exit
  645. resp2, body2 := env.DoPost("/digital-ticket/"+ticketNo+"/exit", nil)
  646. AssertResponse(t, resp2, body2, http.StatusOK, response.SUCCESS)
  647. // Verify state
  648. _, getBody := env.DoGet("/digital-ticket/"+ticketNo, nil)
  649. data := ParseDataMap(getBody)
  650. if data["state"] != "exited" {
  651. t.Fatalf("Expected exited, got state=%v", data["state"])
  652. }
  653. fmt.Println("PASS: Full flow (pay → exit) works correctly")
  654. }
  655. // TestDigitalTicketInvalidExit DT06: 未支付直接离场被拒
  656. func TestDigitalTicketInvalidExit(t *testing.T) {
  657. env := InitTestEnv(t)
  658. ticketNo, _ := createTicket(t, env, "DT006", "manual")
  659. _, body := env.DoPost("/digital-ticket/"+ticketNo+"/exit", nil)
  660. var result struct{ Code int `json:"code"`; Msg string `json:"msg"` }
  661. json.Unmarshal(body, &result)
  662. if result.Code == response.SUCCESS {
  663. t.Fatalf("BUG: Exit without payment was NOT blocked!")
  664. }
  665. fmt.Printf("PASS: Exit without payment correctly rejected: %s\n", result.Msg)
  666. }
  667. // TestDigitalTicketNonexistent DT08: 不存在的票号
  668. func TestDigitalTicketNonexistent(t *testing.T) {
  669. env := InitTestEnv(t)
  670. _, body := env.DoGet("/digital-ticket/FAKE", nil)
  671. var result struct{ Code int `json:"code"`; Msg string `json:"msg"` }
  672. json.Unmarshal(body, &result)
  673. if result.Code == response.SUCCESS {
  674. t.Fatalf("BUG: Non-existent ticket returned success!")
  675. }
  676. fmt.Printf("PASS: Non-existent ticket correctly rejected: %s\n", result.Msg)
  677. }
  678. // TestDigitalTicketListFilter DT09+10: 列表过滤
  679. func TestDigitalTicketListFilter(t *testing.T) {
  680. env := InitTestEnv(t)
  681. createTicket(t, env, "DTFILTER", "manual")
  682. resp, body := env.DoGet("/digital-ticket/list", map[string]string{
  683. "page": "1", "page_size": "10", "plate_number": "DTFILTER",
  684. })
  685. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  686. list := ParsePageList(body)
  687. if len(list) < 1 {
  688. t.Fatalf("Filter by plate returned empty")
  689. }
  690. fmt.Printf("PASS: Ticket list filter returned %d records\n", len(list))
  691. }
  692. // TestDigitalTicketEmptyList DT13: 空列表
  693. func TestDigitalTicketListPagination(t *testing.T) {
  694. env := InitTestEnv(t)
  695. createTicket(t, env, "DTPAGE", "manual")
  696. resp, body := env.DoGet("/digital-ticket/list", map[string]string{"page": "1", "page_size": "5"})
  697. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  698. list := ParsePageList(body)
  699. if len(list) < 1 {
  700. t.Fatalf("Paginated list is empty")
  701. }
  702. fmt.Printf("PASS: Ticket list pagination returned %d records\n", len(list))
  703. }
  704. func createVehicleWithOwner(t *testing.T, env *TestEnv, plateNumber, rfidTag string, ownerID uint) {
  705. t.Helper()
  706. _, body := env.DoPost("/vehicle/create", map[string]interface{}{
  707. "plate_number": plateNumber, "vehicle_type_id": 1,
  708. "vehicle_brand": "TestBrand", "vehicle_color": "TestColor",
  709. "rfid_tag": rfidTag, "owner_id": ownerID,
  710. })
  711. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  712. json.Unmarshal(body, &result)
  713. if result.Code != response.SUCCESS {
  714. t.Fatalf("Failed to create vehicle with owner: %s", result.Msg)
  715. }
  716. }
  717. // ===================== 交接班对账 (Shift) 测试 =====================
  718. //
  719. // 注意:由于 TestEnv 是全局缓存的,所有测试共享同一个数据库和操作员,
  720. // 每个 shift 测试必须清除前序测试遗留的当班记录。
  721. // closeActiveShift 清除操作员活动中当班(DB直接操作,不经过 API)
  722. func closeActiveShift(env *TestEnv) {
  723. env.DB.Model(&dao.ShiftRecord{}).
  724. Where("operator_id = ? AND status = ?", env.UserID, "active").
  725. Update("status", "closed")
  726. }
  727. // TestShiftStart SH01: 正常接班
  728. func TestShiftStart(t *testing.T) {
  729. env := InitTestEnv(t)
  730. closeActiveShift(env) // 清理前序状态
  731. resp, body := env.DoPost("/shift/start", map[string]interface{}{
  732. "starting_cash": 100.0,
  733. })
  734. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  735. // 验证 DB 中创建了当班记录
  736. var rec dao.ShiftRecord
  737. env.DB.Where("operator_id = ? AND status = ?", env.UserID, "active").First(&rec)
  738. if rec.ID == 0 {
  739. t.Fatalf("Shift record not created in DB")
  740. }
  741. if rec.StartingCash != 100.0 {
  742. t.Fatalf("Expected starting_cash=100, got %.2f", rec.StartingCash)
  743. }
  744. fmt.Printf("PASS: Shift start successful, recordID=%d\n", rec.ID)
  745. }
  746. // TestShiftEnd SH02: 正常交班(含当班现金收款统计与差异计算)
  747. func TestShiftEnd(t *testing.T) {
  748. env := InitTestEnv(t)
  749. closeActiveShift(env)
  750. // 1. 接班
  751. startResp, startBody := env.DoPost("/shift/start", map[string]interface{}{
  752. "starting_cash": 100.0,
  753. })
  754. AssertResponse(t, startResp, startBody, http.StatusOK, response.SUCCESS)
  755. // 2. 伪造一笔现金收款记录(模拟当班期间收款50元)
  756. now := time.Now()
  757. env.DB.Exec(`INSERT INTO payment_record
  758. (record_id, payment_method, amount, paid_amount, change_amount, operator_id, paid_at, created_at, updated_at)
  759. VALUES (9999, 'cash', 50.0, 50.0, 0, ?, ?, ?, ?)`,
  760. env.UserID, now, now, now)
  761. // 3. 交班 — 接班100 + 当班收款50 = 应交150,实交150 → 差异0
  762. resp, body := env.DoPost("/shift/end", map[string]interface{}{
  763. "actual_cash": 150.0,
  764. "remark": "test shift end",
  765. })
  766. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  767. // 4. 验证 DB 记录
  768. var rec dao.ShiftRecord
  769. env.DB.Where("operator_id = ? AND status = ?", env.UserID, "closed").Order("id DESC").First(&rec)
  770. if rec.ID == 0 {
  771. t.Fatalf("Shift record not updated in DB")
  772. }
  773. if rec.CollectedCash != 50.0 {
  774. t.Fatalf("Expected collected_cash=50, got %.2f", rec.CollectedCash)
  775. }
  776. if rec.ExpectedTotal != 150.0 {
  777. t.Fatalf("Expected expected_total=150, got %.2f", rec.ExpectedTotal)
  778. }
  779. if rec.Difference != 0.0 {
  780. t.Fatalf("Expected difference=0, got %.2f", rec.Difference)
  781. }
  782. if rec.Remark != "test shift end" {
  783. t.Fatalf("Remark mismatch: expected 'test shift end', got '%s'", rec.Remark)
  784. }
  785. fmt.Printf("PASS: Shift end successful, collected=%.2f expected=%.2f diff=%.2f\n",
  786. rec.CollectedCash, rec.ExpectedTotal, rec.Difference)
  787. }
  788. // TestShiftDoubleStart SH03: 重复接班被拦截
  789. func TestShiftDoubleStart(t *testing.T) {
  790. env := InitTestEnv(t)
  791. closeActiveShift(env)
  792. // 第一次接班 — 成功
  793. startResp, startBody := env.DoPost("/shift/start", map[string]interface{}{
  794. "starting_cash": 100.0,
  795. })
  796. AssertResponse(t, startResp, startBody, http.StatusOK, response.SUCCESS)
  797. // 第二次接班 — 应被拦截
  798. _, body := env.DoPost("/shift/start", map[string]interface{}{
  799. "starting_cash": 200.0,
  800. })
  801. var result struct {
  802. Code int `json:"code"`
  803. Msg string `json:"msg"`
  804. }
  805. json.Unmarshal(body, &result)
  806. if result.Code == response.SUCCESS {
  807. t.Fatalf("BUG: Double shift start was NOT blocked! code=%d msg=%s", result.Code, result.Msg)
  808. }
  809. fmt.Printf("PASS: Double start correctly rejected: msg=%s\n", result.Msg)
  810. }
  811. // TestShiftEndWithoutStart SH04: 未接班直接交班失败
  812. func TestShiftEndWithoutStart(t *testing.T) {
  813. env := InitTestEnv(t)
  814. closeActiveShift(env) // 确保没有当班记录
  815. resp, body := env.DoPost("/shift/end", map[string]interface{}{
  816. "actual_cash": 100.0,
  817. "remark": "no start",
  818. })
  819. // 期望返回错误(无当班记录)
  820. AssertResponse(t, resp, body, http.StatusOK, response.ERROR)
  821. var result struct {
  822. Code int `json:"code"`
  823. Msg string `json:"msg"`
  824. }
  825. json.Unmarshal(body, &result)
  826. fmt.Printf("PASS: End without start correctly rejected: code=%d msg=%s\n", result.Code, result.Msg)
  827. }
  828. // TestShiftCurrent SH05: 查询当前当班信息
  829. func TestShiftCurrent(t *testing.T) {
  830. env := InitTestEnv(t)
  831. closeActiveShift(env)
  832. // 先接班
  833. _, _ = env.DoPost("/shift/start", map[string]interface{}{
  834. "starting_cash": 200.0,
  835. })
  836. // 查询当班
  837. resp, body := env.DoGet("/shift/current", nil)
  838. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  839. var result struct {
  840. Code int `json:"code"`
  841. Data map[string]interface{} `json:"data"`
  842. Msg string `json:"msg"`
  843. }
  844. json.Unmarshal(body, &result)
  845. if result.Data == nil {
  846. t.Fatalf("Shift current data is nil")
  847. }
  848. startCash, ok := result.Data["starting_cash"].(float64)
  849. if !ok || startCash != 200.0 {
  850. t.Fatalf("Expected starting_cash=200, got %v", result.Data["starting_cash"])
  851. }
  852. fmt.Printf("PASS: Shift current query successful, starting_cash=%.2f\n", startCash)
  853. }
  854. // TestShiftList SH06: 交接班记录列表
  855. func TestShiftList(t *testing.T) {
  856. env := InitTestEnv(t)
  857. closeActiveShift(env)
  858. // 接班 → 交班 → 产生一条历史记录
  859. _, _ = env.DoPost("/shift/start", map[string]interface{}{"starting_cash": 50.0})
  860. _, _ = env.DoPost("/shift/end", map[string]interface{}{"actual_cash": 50.0, "remark": "list test"})
  861. // 查询列表
  862. resp, body := env.DoGet("/shift/list", map[string]string{
  863. "page": "1",
  864. "page_size": "10",
  865. })
  866. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  867. list := ParsePageList(body)
  868. if len(list) == 0 {
  869. t.Fatalf("Shift list is empty after creating a record")
  870. }
  871. fmt.Printf("PASS: Shift list returned %d records\n", len(list))
  872. }
  873. // ===================== 收入报表 (Revenue Report) 测试 =====================
  874. // createParkingPayment 辅助函数:创建一笔车辆进出+支付记录(用于报表测试)
  875. func createParkingPayment(t *testing.T, env *TestEnv, plateNumber, rfidTag string, amount float64, method string) {
  876. t.Helper()
  877. createVehicle(t, env, plateNumber, rfidTag, 1)
  878. entryResp, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
  879. "plate_number": plateNumber,
  880. "rfid_tag": rfidTag,
  881. "parking_lot_id": 1,
  882. "parking_space_id": 1,
  883. "entry_image": "test_revenue_entry.jpg",
  884. })
  885. AssertResponse(t, entryResp, entryBody, http.StatusOK, response.SUCCESS)
  886. // 修改入场时间为数小时前,以确保产生费用
  887. entryTime := time.Now().Add(-3 * time.Hour)
  888. env.DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", plateNumber).Update("entry_time", entryTime)
  889. exitResp, exitBody := env.DoPost("/vehicle/exit/confirm", map[string]interface{}{
  890. "plate_number": plateNumber,
  891. "rfid_tag": rfidTag,
  892. "payment_method": method,
  893. "paid_amount": amount,
  894. })
  895. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  896. }
  897. // TestRevenueReportBasic RR01: 基础收入报表查询(无参数)
  898. // BUG: 后端 RevenueReport 当 start_date/end_date 为空时,SQL 条件为
  899. // paid_at >= '' AND paid_at <= ' 23:59:59'
  900. // 导致所有记录被过滤,data 返回 null。
  901. func TestRevenueReportBasic(t *testing.T) {
  902. env := InitTestEnv(t)
  903. createParkingPayment(t, env, "JingREV001", "RFID_REV001", 15.0, "cash")
  904. resp, body := env.DoGet("/report/revenue", nil)
  905. // HTTP 层面是 200
  906. if resp.StatusCode != http.StatusOK {
  907. t.Fatalf("Expected 200, got %d", resp.StatusCode)
  908. }
  909. var result struct {
  910. Code int `json:"code"`
  911. Data json.RawMessage `json:"data"`
  912. Msg string `json:"msg"`
  913. }
  914. json.Unmarshal(body, &result)
  915. // BUG: 无日期参数时 data 应为有数据,但后端返回 null
  916. if result.Code != response.SUCCESS {
  917. t.Fatalf("Revenue report failed: code=%d msg=%s", result.Code, result.Msg)
  918. }
  919. if result.Data == nil || string(result.Data) == "null" {
  920. t.Fatalf("BUG: Revenue report with no date params returns null data! "+
  921. "Cause: SQL condition 'paid_at >= \"\" AND paid_at <= \" 23:59:59\"' filters out all records. "+
  922. "Fix: when start/end_date are empty, omit the WHERE clause or default to today.")
  923. }
  924. fmt.Printf("PASS: Revenue report basic query successful, data=%s\n", string(result.Data))
  925. }
  926. // TestRevenueReportDateRange RR02: 指定日期范围的收入报表
  927. func TestRevenueReportDateRange(t *testing.T) {
  928. env := InitTestEnv(t)
  929. createParkingPayment(t, env, "JingREV002", "RFID_REV002", 20.0, "cash")
  930. today := time.Now().Format("2006-01-02")
  931. resp, body := env.DoGet("/report/revenue", map[string]string{
  932. "start_date": today,
  933. "end_date": today,
  934. })
  935. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  936. var result struct {
  937. Code int `json:"code"`
  938. Data json.RawMessage `json:"data"`
  939. Msg string `json:"msg"`
  940. }
  941. json.Unmarshal(body, &result)
  942. if len(result.Data) == 0 || string(result.Data) == "null" {
  943. t.Fatalf("Revenue report with date range returned empty")
  944. }
  945. fmt.Printf("PASS: Revenue report with date range successful, data=%s\n", string(result.Data))
  946. }
  947. // TestRevenueReportWithLotFilter RR03: 指定停车场的收入报表
  948. func TestRevenueReportWithLotFilter(t *testing.T) {
  949. env := InitTestEnv(t)
  950. createParkingPayment(t, env, "JingREV003", "RFID_REV003", 25.0, "cash")
  951. today := time.Now().Format("2006-01-02")
  952. resp, body := env.DoGet("/report/revenue", map[string]string{
  953. "parking_lot_id": "1",
  954. "start_date": today,
  955. "end_date": today,
  956. })
  957. AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
  958. var result struct {
  959. Code int `json:"code"`
  960. Data json.RawMessage `json:"data"`
  961. Msg string `json:"msg"`
  962. }
  963. json.Unmarshal(body, &result)
  964. if len(result.Data) == 0 || string(result.Data) == "null" {
  965. t.Fatalf("Revenue report with lot filter returned empty")
  966. }
  967. fmt.Printf("PASS: Revenue report with lot filter successful, data=%s\n", string(result.Data))
  968. }