e2e_edge_cases_test.go 44 KB

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