vehicle.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. package vehicle
  2. import (
  3. "errors"
  4. "fmt"
  5. "time"
  6. "wails-app/internal/dao"
  7. "wails-app/internal/global"
  8. "wails-app/internal/model/vehicle/request"
  9. vehicleResponse "wails-app/internal/model/vehicle/response"
  10. ticketService "wails-app/internal/modules/digital-ticket/service"
  11. sessionRequest "wails-app/internal/modules/parking-session/model/request"
  12. sessionService "wails-app/internal/modules/parking-session/service"
  13. paymentService "wails-app/internal/modules/payment/service"
  14. "gorm.io/gorm"
  15. )
  16. type VehicleService struct{}
  17. // buildPlateOrRfidQuery 按车牌或RFID查询,RFID为空时仅用车牌(避免 rfid_tag=” 匹配所有无标签车辆)
  18. func buildPlateOrRfidQuery(db *gorm.DB, plateNumber, rfidTag string) *gorm.DB {
  19. if rfidTag != "" {
  20. return db.Where("plate_number = ? OR rfid_tag = ?", plateNumber, rfidTag)
  21. }
  22. return db.Where("plate_number = ?", plateNumber)
  23. }
  24. // getVehicleForSessionTx 根据停车会话中保存的车辆身份定位车辆档案。
  25. // 优先同时匹配车牌和 RFID;若历史会话未完整保存 RFID,则依次按 RFID、车牌降级查询。
  26. func getVehicleForSessionTx(db *gorm.DB, session *dao.VehicleRecord) (dao.Vehicle, error) {
  27. if db == nil || session == nil {
  28. return dao.Vehicle{}, errors.New("车辆会话不存在")
  29. }
  30. var vehicle dao.Vehicle
  31. if session.PlateNumber != "" && session.RFIDTag != "" {
  32. result := db.Preload("VehicleType").Preload("Owner").
  33. Where("plate_number = ? AND rfid_tag = ?", session.PlateNumber, session.RFIDTag).
  34. First(&vehicle)
  35. if result.Error == nil {
  36. return vehicle, nil
  37. }
  38. if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
  39. return dao.Vehicle{}, result.Error
  40. }
  41. }
  42. if session.RFIDTag != "" {
  43. result := db.Preload("VehicleType").Preload("Owner").Where("rfid_tag = ?", session.RFIDTag).First(&vehicle)
  44. if result.Error == nil {
  45. return vehicle, nil
  46. }
  47. if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
  48. return dao.Vehicle{}, result.Error
  49. }
  50. }
  51. if session.PlateNumber != "" {
  52. result := db.Preload("VehicleType").Preload("Owner").Where("plate_number = ?", session.PlateNumber).First(&vehicle)
  53. if result.Error == nil {
  54. return vehicle, nil
  55. }
  56. if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
  57. return dao.Vehicle{}, result.Error
  58. }
  59. }
  60. return dao.Vehicle{}, errors.New("车辆不存在")
  61. }
  62. // CreateVehicle 创建车辆信息
  63. func (s *VehicleService) CreateVehicle(req request.VehicleCreate) error {
  64. // 检查车牌号是否已存在
  65. var existingVehicle dao.Vehicle
  66. result := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).First(&existingVehicle)
  67. if result.RowsAffected > 0 {
  68. return errors.New("车牌号已存在")
  69. }
  70. // 检查车辆类型是否存在
  71. var vehicleType dao.VehicleType
  72. result = global.GVA_DB.First(&vehicleType, req.VehicleTypeID)
  73. if result.RowsAffected == 0 {
  74. return errors.New("车辆类型不存在")
  75. }
  76. // 处理临时车辆的情况
  77. var ownerId *uint
  78. if req.OwnerId > 0 {
  79. ownerId = &req.OwnerId
  80. }
  81. vehicle := dao.Vehicle{
  82. PlateNumber: req.PlateNumber,
  83. RFIDTag: req.RFIDTag,
  84. VehicleTypeID: req.VehicleTypeID,
  85. VehicleBrand: req.VehicleBrand,
  86. VehicleColor: req.VehicleColor,
  87. OwnerId: ownerId,
  88. }
  89. return global.GVA_DB.Create(&vehicle).Error
  90. }
  91. // UpdateVehicle 更新车辆信息
  92. func (s *VehicleService) UpdateVehicle(req request.VehicleUpdate) error {
  93. var vehicle dao.Vehicle
  94. result := global.GVA_DB.First(&vehicle, req.ID)
  95. if result.RowsAffected == 0 {
  96. return errors.New("车辆不存在")
  97. }
  98. // 检查车牌号是否被其他车辆使用
  99. var existingVehicle dao.Vehicle
  100. result = buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("id != ?", req.ID).First(&existingVehicle)
  101. fmt.Println("result", result.RowsAffected)
  102. if result.RowsAffected > 0 {
  103. return errors.New("车牌号已被其他车辆使用")
  104. }
  105. // 检查车辆类型是否存在
  106. var vehicleType dao.VehicleType
  107. result = global.GVA_DB.First(&vehicleType, req.VehicleTypeID)
  108. if result.RowsAffected == 0 {
  109. return errors.New("车辆类型不存在")
  110. }
  111. // 处理临时车辆的情况
  112. var ownerId *uint
  113. if req.OwnerId > 0 {
  114. ownerId = &req.OwnerId
  115. } else {
  116. ownerId = nil
  117. }
  118. err := global.GVA_DB.Model(&vehicle).Updates(map[string]interface{}{
  119. "plate_number": req.PlateNumber,
  120. "rfid_tag": req.RFIDTag,
  121. "vehicle_type_id": req.VehicleTypeID,
  122. "vehicle_brand": req.VehicleBrand,
  123. "vehicle_color": req.VehicleColor,
  124. "owner_id": ownerId,
  125. }).Error
  126. return err
  127. }
  128. // GetVehicleByID 根据ID获取车辆信息
  129. func (s *VehicleService) GetVehicleByID(id uint) (dao.Vehicle, error) {
  130. var vehicle dao.Vehicle
  131. result := global.GVA_DB.First(&vehicle, id)
  132. if result.RowsAffected == 0 {
  133. return dao.Vehicle{}, errors.New("车辆不存在")
  134. }
  135. return vehicle, nil
  136. }
  137. // GetVehicleByPlateNumber 根据车牌号或RFID获取车辆信息
  138. func (s *VehicleService) GetVehicleByPlateNumber(plateNumber, RFIDTag string) (dao.Vehicle, error) {
  139. var vehicle dao.Vehicle
  140. if plateNumber != "" && RFIDTag != "" {
  141. exactQuery := global.GVA_DB.Preload("VehicleType").Preload("Owner").Where("plate_number = ? AND rfid_tag = ?", plateNumber, RFIDTag)
  142. if result := exactQuery.First(&vehicle); result.Error == nil {
  143. return vehicle, nil
  144. }
  145. }
  146. query := global.GVA_DB.Preload("VehicleType").Preload("Owner")
  147. if RFIDTag != "" {
  148. query = query.Where("plate_number = ? OR rfid_tag = ?", plateNumber, RFIDTag)
  149. } else {
  150. query = query.Where("plate_number = ?", plateNumber)
  151. }
  152. result := query.First(&vehicle)
  153. if result.RowsAffected == 0 {
  154. return dao.Vehicle{}, errors.New("车辆不存在")
  155. }
  156. return vehicle, nil
  157. }
  158. func (s *VehicleService) QueryAllVehicles() ([]dao.Vehicle, error) {
  159. var vehicles []dao.Vehicle
  160. err := global.GVA_DB.Find(&vehicles).Error
  161. if err != nil {
  162. return vehicles, err
  163. }
  164. return vehicles, nil
  165. }
  166. // ListVehicles 获取车辆列表
  167. func (s *VehicleService) ListVehicles(req request.VehicleQuery) (int64, []dao.Vehicle, error) {
  168. var vehicles []dao.Vehicle
  169. var total int64
  170. query := global.GVA_DB.Model(&dao.Vehicle{})
  171. if req.PlateNumber != "" {
  172. query = query.Where("plate_number LIKE ?", "%"+req.PlateNumber+"%")
  173. }
  174. if req.VehicleTypeID > 0 {
  175. query = query.Where("vehicle_type_id = ?", req.VehicleTypeID)
  176. }
  177. if req.OwnerId > 0 {
  178. query = query.Where("owner_id = ?", req.OwnerId)
  179. }
  180. // 计算总数
  181. query.Count(&total)
  182. // 分页查询
  183. offset := (req.Page - 1) * req.PageSize
  184. result := query.Offset(offset).Limit(req.PageSize).Preload("Owner").Preload("VehicleType").Find(&vehicles)
  185. if result.Error != nil {
  186. return 0, nil, result.Error
  187. }
  188. return total, vehicles, nil
  189. }
  190. // DeleteVehicle 删除车辆
  191. func (s *VehicleService) DeleteVehicle(id uint) error {
  192. result := global.GVA_DB.Delete(&dao.Vehicle{}, id)
  193. if result.RowsAffected == 0 {
  194. return errors.New("车辆不存在")
  195. }
  196. return result.Error
  197. }
  198. // VehicleEntry 车辆入场
  199. func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.VehicleEntryResponse, error) {
  200. // 黑名单检查
  201. var shortlistSvc ShortlistService
  202. if isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag); isBlack {
  203. return request.VehicleEntryResponse{}, errors.New("车辆在黑名单中,禁止入场")
  204. }
  205. // 检查车辆是否存在
  206. vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  207. if err != nil {
  208. // 如果车辆不存在,创建新车辆,自动归类为"临时车"
  209. var tempType dao.VehicleType
  210. if global.GVA_DB.Where("is_system = ?", true).First(&tempType).Error != nil {
  211. return request.VehicleEntryResponse{}, errors.New("系统错误:临时车类型未配置")
  212. }
  213. createReq := request.VehicleCreate{
  214. PlateNumber: req.PlateNumber,
  215. RFIDTag: req.RFIDTag,
  216. VehicleTypeID: tempType.ID,
  217. OwnerId: 0,
  218. }
  219. err = s.CreateVehicle(createReq)
  220. if err != nil {
  221. return request.VehicleEntryResponse{}, err
  222. }
  223. vehicle, _ = s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  224. }
  225. // 在同一事务中创建停车会话和数字票,避免出现只有会话或只有数字票的不完整入场数据。
  226. entryTime := time.Now()
  227. plateNumber := vehicle.PlateNumber
  228. if plateNumber == "" {
  229. plateNumber = req.PlateNumber
  230. }
  231. rfidTag := req.RFIDTag
  232. if rfidTag == "" {
  233. rfidTag = vehicle.RFIDTag
  234. }
  235. var record *dao.VehicleRecord
  236. ticketSvc := ticketService.NewTicketService()
  237. sessionSvc := sessionService.NewParkingSessionService()
  238. triggerMode := "manual"
  239. if req.RFIDTag != "" {
  240. triggerMode = "rfid"
  241. }
  242. err = global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  243. var err error
  244. record, err = sessionSvc.OpenSessionTx(tx, sessionRequest.OpenSessionRequest{
  245. PlateNumber: plateNumber,
  246. RFIDTag: rfidTag,
  247. ParkingLotID: req.ParkingLotID,
  248. ParkingSpaceID: req.ParkingSpaceID,
  249. EntryChannelID: req.EntryChannelID,
  250. EntryChannelCode: req.EntryChannelCode,
  251. EntryChannelName: req.EntryChannelName,
  252. EntryDeviceCode: req.EntryDeviceCode,
  253. EntryDeviceName: req.EntryDeviceName,
  254. EntryImage: req.EntryImage,
  255. EntryTime: entryTime,
  256. })
  257. if err != nil {
  258. return err
  259. }
  260. if _, err := ticketSvc.CreateTx(tx, plateNumber, triggerMode, record.ID); err != nil {
  261. return fmt.Errorf("创建数字票失败: %w", err)
  262. }
  263. return tx.Model(&dao.Vehicle{}).Where("id = ?", vehicle.ID).
  264. Update("last_entry_time", entryTime).Error
  265. })
  266. if err != nil {
  267. return request.VehicleEntryResponse{}, err
  268. }
  269. response := request.VehicleEntryResponse{
  270. Success: true,
  271. Message: "车辆入场成功",
  272. }
  273. response.Data.PlateNumber = plateNumber
  274. response.Data.EntryTime = record.EntryTime.Unix()
  275. response.Data.ParkingLotID = req.ParkingLotID
  276. response.Data.ParkingSpaceID = req.ParkingSpaceID
  277. response.Data.EntryChannelID = record.EntryChannelID
  278. response.Data.EntryChannelCode = record.EntryChannelCode
  279. response.Data.EntryChannelName = record.EntryChannelName
  280. response.Data.EntryDeviceCode = record.EntryDeviceCode
  281. response.Data.EntryDeviceName = record.EntryDeviceName
  282. return response, nil
  283. }
  284. func (s *VehicleService) buildExitPreview(record *dao.VehicleRecord) (vehicleResponse.ExitPreviewResponse, error) {
  285. if record == nil {
  286. return vehicleResponse.ExitPreviewResponse{}, errors.New("停车会话不存在")
  287. }
  288. ticketSvc := ticketService.NewTicketService()
  289. ticket, err := ticketSvc.GetByVehicleRecordIDTx(global.GVA_DB, record.ID)
  290. if err != nil {
  291. return vehicleResponse.ExitPreviewResponse{}, errors.New("停车会话对应的数字票不存在")
  292. }
  293. vehicle, err := getVehicleForSessionTx(global.GVA_DB, record)
  294. if err != nil {
  295. return vehicleResponse.ExitPreviewResponse{}, err
  296. }
  297. stayTime := int64(time.Since(record.EntryTime).Minutes())
  298. if stayTime < 0 {
  299. stayTime = 0
  300. }
  301. fee := s.calculateFee(&vehicle, stayTime)
  302. var shortlistSvc ShortlistService
  303. _, isWhite := shortlistSvc.CheckVehicleShortlist(record.PlateNumber, record.RFIDTag)
  304. if isWhite {
  305. fee = 0
  306. }
  307. if ticket.State == ticketService.StatePaid {
  308. fee = ticket.Fee
  309. }
  310. typeName := ""
  311. isTemp := false
  312. if vehicle.VehicleType != nil {
  313. typeName = vehicle.VehicleType.Name
  314. isTemp = vehicle.VehicleType.IsSystem
  315. }
  316. plateNumber := record.PlateNumber
  317. if plateNumber == "" {
  318. plateNumber = vehicle.PlateNumber
  319. }
  320. return vehicleResponse.ExitPreviewResponse{
  321. SessionID: record.ID,
  322. TicketNo: ticket.TicketNo,
  323. TicketState: ticket.State,
  324. PlateNumber: plateNumber,
  325. RFIDTag: record.RFIDTag,
  326. EntryTime: record.EntryTime.Unix(),
  327. StayTime: stayTime,
  328. Fee: fee,
  329. PaidAmount: ticket.PaidAmount,
  330. PaymentStatus: record.PaymentStatus,
  331. PaymentEntry: ticket.PaymentEntry,
  332. PaymentMethod: ticket.PaymentMethod,
  333. ParkingLotID: record.ParkingLotID,
  334. EntryImage: record.EntryImage,
  335. EntryChannelID: record.EntryChannelID,
  336. EntryChannelCode: record.EntryChannelCode,
  337. EntryChannelName: record.EntryChannelName,
  338. EntryDeviceCode: record.EntryDeviceCode,
  339. EntryDeviceName: record.EntryDeviceName,
  340. VehicleType: typeName,
  341. IsTemp: isTemp,
  342. }, nil
  343. }
  344. // GetOperationContext 根据车辆身份判断下一步应执行进场还是出场。
  345. func (s *VehicleService) GetOperationContext(req request.VehicleIdentityRequest) (vehicleResponse.OperationContextResponse, error) {
  346. sessionSvc := sessionService.NewParkingSessionService()
  347. record, err := sessionSvc.ResolveActiveSession(sessionRequest.ResolveActiveSessionRequest{
  348. PlateNumber: req.PlateNumber,
  349. RFIDTag: req.RFIDTag,
  350. })
  351. if err == nil {
  352. preview, previewErr := s.buildExitPreview(record)
  353. if previewErr != nil {
  354. return vehicleResponse.OperationContextResponse{}, previewErr
  355. }
  356. vehicle, vehicleErr := getVehicleForSessionTx(global.GVA_DB, record)
  357. if vehicleErr != nil {
  358. return vehicleResponse.OperationContextResponse{}, vehicleErr
  359. }
  360. return vehicleResponse.OperationContextResponse{
  361. Action: "exit",
  362. VehicleFound: true,
  363. Vehicle: &vehicle,
  364. Exit: &preview,
  365. }, nil
  366. }
  367. if !errors.Is(err, sessionService.ErrSessionNotFound) {
  368. return vehicleResponse.OperationContextResponse{}, err
  369. }
  370. vehicle, vehicleErr := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  371. if vehicleErr != nil {
  372. return vehicleResponse.OperationContextResponse{Action: "entry", VehicleFound: false}, nil
  373. }
  374. return vehicleResponse.OperationContextResponse{
  375. Action: "entry",
  376. VehicleFound: true,
  377. Vehicle: &vehicle,
  378. }, nil
  379. }
  380. // ExitPreview 费用预览(不修改数据库,仅查询和计算费用)。
  381. func (s *VehicleService) ExitPreview(req request.ExitPreviewRequest) (vehicleResponse.ExitPreviewResponse, error) {
  382. sessionSvc := sessionService.NewParkingSessionService()
  383. record, err := sessionSvc.ResolveActiveSession(sessionRequest.ResolveActiveSessionRequest{
  384. SessionID: req.SessionID,
  385. PlateNumber: req.PlateNumber,
  386. RFIDTag: req.RFIDTag,
  387. })
  388. if err != nil {
  389. return vehicleResponse.ExitPreviewResponse{}, errors.New("车辆未入场或已出场")
  390. }
  391. return s.buildExitPreview(record)
  392. }
  393. // PayDigitalTicket settles a digital ticket and its vehicle record atomically.
  394. // A successful ticket payment is therefore visible to exit processing,
  395. // payment reports, and shift reconciliation immediately.
  396. func (s *VehicleService) PayDigitalTicket(ticketNo, paymentEntry, paymentMethod, paymentOrderNo string, paidAmount float64, operatorID uint) error {
  397. if ticketNo == "" {
  398. return errors.New("数字票号不能为空")
  399. }
  400. if paymentEntry == "" {
  401. return errors.New("数字票支付必须指定支付入口")
  402. }
  403. ticketSvc := ticketService.NewTicketService()
  404. paymentSvc := paymentService.NewPaymentService()
  405. sessionSvc := sessionService.NewParkingSessionService()
  406. return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  407. ticket, err := ticketSvc.GetByTicketNoTx(tx, ticketNo)
  408. if err != nil {
  409. return errors.New("票不存在")
  410. }
  411. if ticket.State != ticketService.StatePendingPayment {
  412. return fmt.Errorf("数字票当前状态不允许支付: %s", ticket.State)
  413. }
  414. record, err := sessionSvc.ResolveActiveSessionTx(tx, sessionRequest.ResolveActiveSessionRequest{
  415. SessionID: ticket.VehicleRecordID,
  416. })
  417. if err != nil {
  418. return errors.New("车辆未入场或已出场")
  419. }
  420. vehicle, err := getVehicleForSessionTx(tx, record)
  421. if err != nil {
  422. return err
  423. }
  424. stayTime := int64(time.Since(record.EntryTime).Minutes())
  425. if stayTime < 0 {
  426. stayTime = 0
  427. }
  428. fee := s.calculateFee(&vehicle, stayTime)
  429. var shortlistSvc ShortlistService
  430. _, isWhite := shortlistSvc.CheckVehicleShortlist(record.PlateNumber, record.RFIDTag)
  431. if isWhite {
  432. fee = 0
  433. }
  434. payment, err := paymentSvc.ProcessPaymentTxAtEntry(tx, record.ID, paymentEntry, paymentMethod, fee, paidAmount, operatorID)
  435. if err != nil {
  436. return err
  437. }
  438. extra := map[string]interface{}{
  439. "payment_entry": paymentEntry, "payment_method": paymentMethod, "payment_order_no": paymentOrderNo,
  440. "paid_amount": payment.PaidAmount, "fee": fee,
  441. }
  442. return ticketSvc.TransitionTx(tx, ticket.ID, ticketService.StatePaid, extra)
  443. })
  444. }
  445. func resolveExitRecordTx(tx *gorm.DB, ticketSvc *ticketService.TicketService, req request.ExitConfirmRequest) (*dao.VehicleRecord, *dao.DigitalTicket, error) {
  446. sessionSvc := sessionService.NewParkingSessionService()
  447. var record *dao.VehicleRecord
  448. var ticket *dao.DigitalTicket
  449. var err error
  450. if req.SessionID != 0 {
  451. record, err = sessionSvc.ResolveActiveSessionTx(tx, sessionRequest.ResolveActiveSessionRequest{SessionID: req.SessionID})
  452. if err == nil {
  453. ticket, err = ticketSvc.GetByVehicleRecordIDTx(tx, record.ID)
  454. }
  455. if err == nil && req.TicketNo != "" && ticket.TicketNo != req.TicketNo {
  456. return nil, nil, errors.New("停车会话与数字票不匹配")
  457. }
  458. } else if req.TicketNo != "" {
  459. ticket, err = ticketSvc.GetByTicketNoTx(tx, req.TicketNo)
  460. if err != nil {
  461. return nil, nil, errors.New("票不存在")
  462. }
  463. record, err = sessionSvc.ResolveActiveSessionTx(tx, sessionRequest.ResolveActiveSessionRequest{
  464. SessionID: ticket.VehicleRecordID,
  465. })
  466. } else {
  467. record, err = sessionSvc.ResolveActiveSessionTx(tx, sessionRequest.ResolveActiveSessionRequest{
  468. PlateNumber: req.PlateNumber,
  469. RFIDTag: req.RFIDTag,
  470. })
  471. if err == nil {
  472. ticket, err = ticketSvc.GetByVehicleRecordIDTx(tx, record.ID)
  473. }
  474. }
  475. if err != nil {
  476. return nil, nil, errors.New("车辆未入场、已出场或数字票不存在")
  477. }
  478. return record, ticket, nil
  479. }
  480. func ensurePrepaidPaymentTx(tx *gorm.DB, ticket *dao.DigitalTicket, recordID uint) (*dao.PaymentRecord, error) {
  481. var payment dao.PaymentRecord
  482. err := tx.Where("record_id = ?", recordID).Order("id DESC").First(&payment).Error
  483. if err == nil {
  484. return &payment, nil
  485. }
  486. if !errors.Is(err, gorm.ErrRecordNotFound) {
  487. return nil, err
  488. }
  489. if ticket.PaymentEntry == "" || ticket.PaymentMethod == "" {
  490. return nil, errors.New("已支付数字票缺少支付明细")
  491. }
  492. paidAt := time.Now()
  493. if ticket.PaidAt != nil {
  494. paidAt = *ticket.PaidAt
  495. }
  496. changeAmount := ticket.PaidAmount - ticket.Fee
  497. if changeAmount < 0 {
  498. changeAmount = 0
  499. }
  500. payment = dao.PaymentRecord{
  501. RecordID: recordID, PaymentEntry: ticket.PaymentEntry, PaymentMethod: ticket.PaymentMethod,
  502. Amount: ticket.Fee, PaidAmount: ticket.PaidAmount, ChangeAmount: changeAmount,
  503. PaidAt: paidAt, Remark: "由历史数字票支付记录补建",
  504. }
  505. if err := tx.Create(&payment).Error; err != nil {
  506. return nil, err
  507. }
  508. return &payment, nil
  509. }
  510. // ExitConfirm is the only exit settlement path. It either creates a new
  511. // payment or consumes an existing paid digital ticket, then updates the vehicle
  512. // record, vehicle statistics, and ticket state in one transaction.
  513. func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID uint) (vehicleResponse.ExitConfirmResponse, error) {
  514. paymentEntry := req.PaymentEntry
  515. if paymentEntry == "" {
  516. paymentEntry = paymentService.EntryCounter
  517. }
  518. var result vehicleResponse.ExitConfirmResponse
  519. err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  520. ticketSvc := ticketService.NewTicketService()
  521. sessionSvc := sessionService.NewParkingSessionService()
  522. record, ticket, err := resolveExitRecordTx(tx, ticketSvc, req)
  523. if err != nil {
  524. return err
  525. }
  526. var shortlistSvc ShortlistService
  527. isBlack, isWhite := shortlistSvc.CheckVehicleShortlist(record.PlateNumber, record.RFIDTag)
  528. if isBlack {
  529. return errors.New("车辆在黑名单中,禁止出场")
  530. }
  531. vehicle, err := getVehicleForSessionTx(tx, record)
  532. if err != nil {
  533. return err
  534. }
  535. exitTime := time.Now()
  536. stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
  537. if stayTime < 0 {
  538. stayTime = 0
  539. }
  540. fee := s.calculateFee(&vehicle, stayTime)
  541. if isWhite {
  542. fee = 0
  543. }
  544. actualEntry := paymentEntry
  545. actualMethod := req.PaymentMethod
  546. paidAmount := req.PaidAmount
  547. changeAmount := 0.0
  548. if ticket.State == ticketService.StatePaid {
  549. payment, err := ensurePrepaidPaymentTx(tx, ticket, record.ID)
  550. if err != nil {
  551. return err
  552. }
  553. fee = payment.Amount
  554. paidAmount = payment.PaidAmount
  555. changeAmount = payment.ChangeAmount
  556. actualEntry = payment.PaymentEntry
  557. actualMethod = payment.PaymentMethod
  558. } else if ticket.State == ticketService.StatePendingPayment {
  559. isTemp := vehicle.VehicleType != nil && vehicle.VehicleType.IsSystem
  560. if fee > 0 && isTemp && !isWhite && req.PaymentMethod == paymentService.PaymentFree {
  561. return errors.New("临时车辆不能使用免密放行")
  562. }
  563. paymentSvc := paymentService.NewPaymentService()
  564. payment, err := paymentSvc.ProcessPaymentTxAtEntry(tx, record.ID, paymentEntry, req.PaymentMethod, fee, req.PaidAmount, operatorID)
  565. if err != nil {
  566. return err
  567. }
  568. paidAmount = payment.PaidAmount
  569. changeAmount = payment.ChangeAmount
  570. } else {
  571. return fmt.Errorf("数字票状态不允许出场: %s", ticket.State)
  572. }
  573. record, err = sessionSvc.CloseSessionTx(tx, sessionRequest.CloseSessionRequest{
  574. SessionID: record.ID,
  575. ExitTime: exitTime,
  576. StayTime: stayTime,
  577. Fee: fee,
  578. PaymentStatus: sessionService.PaymentStatusPaid,
  579. PaymentEntry: actualEntry,
  580. PaymentMethod: actualMethod,
  581. ExitChannelID: req.ExitChannelID,
  582. ExitChannelCode: req.ExitChannelCode,
  583. ExitChannelName: req.ExitChannelName,
  584. ExitDeviceCode: req.ExitDeviceCode,
  585. ExitDeviceName: req.ExitDeviceName,
  586. })
  587. if err != nil {
  588. return err
  589. }
  590. vehicle.LastExitTime = &exitTime
  591. vehicle.TotalStayTime += stayTime
  592. vehicle.TotalFee += fee
  593. if err := tx.Save(&vehicle).Error; err != nil {
  594. return err
  595. }
  596. if err := ticketSvc.MarkPaidAndExitedTx(tx, record.ID, actualEntry, actualMethod, "", paidAmount, fee); err != nil {
  597. return err
  598. }
  599. result = vehicleResponse.ExitConfirmResponse{
  600. PlateNumber: vehicle.PlateNumber, EntryTime: record.EntryTime.Unix(), ExitTime: exitTime.Unix(),
  601. StayTime: stayTime, Fee: fee, ChangeAmount: changeAmount, PaymentEntry: actualEntry, PaymentMethod: actualMethod,
  602. EntryChannelID: record.EntryChannelID, EntryChannelCode: record.EntryChannelCode,
  603. EntryChannelName: record.EntryChannelName, EntryDeviceCode: record.EntryDeviceCode,
  604. EntryDeviceName: record.EntryDeviceName, ExitChannelID: record.ExitChannelID,
  605. ExitChannelCode: record.ExitChannelCode, ExitChannelName: record.ExitChannelName,
  606. ExitDeviceCode: record.ExitDeviceCode, ExitDeviceName: record.ExitDeviceName,
  607. }
  608. return nil
  609. })
  610. if err != nil {
  611. return vehicleResponse.ExitConfirmResponse{}, err
  612. }
  613. return result, nil
  614. }
  615. // calculateFee 计算停车费用
  616. func (s *VehicleService) calculateFee(vehicle *dao.Vehicle, stayTime int64) float64 {
  617. return ticketService.CalculateFee(vehicle, stayTime)
  618. }
  619. // InParkVehicle 在场车辆信息
  620. type InParkVehicle struct {
  621. RecordID uint `json:"record_id"`
  622. PlateNumber string `json:"plate_number"`
  623. VehicleType string `json:"vehicle_type"`
  624. OwnerName string `json:"owner_name"`
  625. OwnerPhone string `json:"owner_phone"`
  626. EntryTime string `json:"entry_time"`
  627. StayMinutes int64 `json:"stay_minutes"`
  628. ParkingLot string `json:"parking_lot"`
  629. ParkingSpace string `json:"parking_space"`
  630. }
  631. // GetInParkVehicles 获取所有在场车辆
  632. func (s *VehicleService) GetInParkVehicles(lotID uint, plateNumber string) ([]InParkVehicle, error) {
  633. var results []InParkVehicle
  634. now := time.Now()
  635. query := global.GVA_DB.Table("vehicle_record").
  636. Select(`vehicle_record.id as record_id, vehicle_record.plate_number, vehicle_type.name as vehicle_type,
  637. owner.owner_name, owner.owner_phone, vehicle_record.entry_time,
  638. parking_lot.lot_name as parking_lot`).
  639. Joins("LEFT JOIN vehicle ON vehicle.plate_number = vehicle_record.plate_number").
  640. Joins("LEFT JOIN vehicle_type ON vehicle_type.id = vehicle.vehicle_type_id").
  641. Joins("LEFT JOIN owner ON owner.id = vehicle.owner_id").
  642. Joins("LEFT JOIN parking_lot ON parking_lot.id = vehicle_record.parking_lot_id").
  643. Where("vehicle_record.exit_time IS NULL")
  644. if lotID > 0 {
  645. query = query.Where("vehicle_record.parking_lot_id = ?", lotID)
  646. }
  647. if plateNumber != "" {
  648. query = query.Where("vehicle_record.plate_number LIKE ?", "%"+plateNumber+"%")
  649. }
  650. err := query.Order("vehicle_record.entry_time ASC").Scan(&results).Error
  651. if err != nil {
  652. return nil, err
  653. }
  654. for i := range results {
  655. // Parse entry time and calculate stay duration
  656. t, parseErr := time.Parse("2006-01-02T15:04:05-07:00", results[i].EntryTime)
  657. if parseErr != nil {
  658. t, _ = time.Parse("2006-01-02 15:04:05", results[i].EntryTime)
  659. }
  660. results[i].StayMinutes = int64(now.Sub(t).Minutes())
  661. if results[i].StayMinutes < 0 {
  662. results[i].StayMinutes = 0
  663. }
  664. }
  665. return results, nil
  666. }