vehicle.go 26 KB

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