vehicle.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  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. paymentService "wails-app/internal/modules/payment/service"
  12. "gorm.io/gorm"
  13. )
  14. type VehicleService struct{}
  15. // buildPlateOrRfidQuery 按车牌或RFID查询,RFID为空时仅用车牌(避免 rfid_tag=” 匹配所有无标签车辆)
  16. func buildPlateOrRfidQuery(db *gorm.DB, plateNumber, rfidTag string) *gorm.DB {
  17. if rfidTag != "" {
  18. return db.Where("plate_number = ? OR rfid_tag = ?", plateNumber, rfidTag)
  19. }
  20. return db.Where("plate_number = ?", plateNumber)
  21. }
  22. // CreateVehicle 创建车辆信息
  23. func (s *VehicleService) CreateVehicle(req request.VehicleCreate) error {
  24. // 检查车牌号是否已存在
  25. var existingVehicle dao.Vehicle
  26. result := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).First(&existingVehicle)
  27. if result.RowsAffected > 0 {
  28. return errors.New("车牌号已存在")
  29. }
  30. // 检查车辆类型是否存在
  31. var vehicleType dao.VehicleType
  32. result = global.GVA_DB.First(&vehicleType, req.VehicleTypeID)
  33. if result.RowsAffected == 0 {
  34. return errors.New("车辆类型不存在")
  35. }
  36. // 处理临时车辆的情况
  37. var ownerId *uint
  38. if req.OwnerId > 0 {
  39. ownerId = &req.OwnerId
  40. }
  41. vehicle := dao.Vehicle{
  42. PlateNumber: req.PlateNumber,
  43. RFIDTag: req.RFIDTag,
  44. VehicleTypeID: req.VehicleTypeID,
  45. VehicleBrand: req.VehicleBrand,
  46. VehicleColor: req.VehicleColor,
  47. OwnerId: ownerId,
  48. }
  49. return global.GVA_DB.Create(&vehicle).Error
  50. }
  51. // UpdateVehicle 更新车辆信息
  52. func (s *VehicleService) UpdateVehicle(req request.VehicleUpdate) error {
  53. var vehicle dao.Vehicle
  54. result := global.GVA_DB.First(&vehicle, req.ID)
  55. if result.RowsAffected == 0 {
  56. return errors.New("车辆不存在")
  57. }
  58. // 检查车牌号是否被其他车辆使用
  59. var existingVehicle dao.Vehicle
  60. result = buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("id != ?", req.ID).First(&existingVehicle)
  61. fmt.Println("result", result.RowsAffected)
  62. if result.RowsAffected > 0 {
  63. return errors.New("车牌号已被其他车辆使用")
  64. }
  65. // 检查车辆类型是否存在
  66. var vehicleType dao.VehicleType
  67. result = global.GVA_DB.First(&vehicleType, req.VehicleTypeID)
  68. if result.RowsAffected == 0 {
  69. return errors.New("车辆类型不存在")
  70. }
  71. // 处理临时车辆的情况
  72. var ownerId *uint
  73. if req.OwnerId > 0 {
  74. ownerId = &req.OwnerId
  75. } else {
  76. ownerId = nil
  77. }
  78. err := global.GVA_DB.Model(&vehicle).Updates(map[string]interface{}{
  79. "plate_number": req.PlateNumber,
  80. "rfid_tag": req.RFIDTag,
  81. "vehicle_type_id": req.VehicleTypeID,
  82. "vehicle_brand": req.VehicleBrand,
  83. "vehicle_color": req.VehicleColor,
  84. "owner_id": ownerId,
  85. }).Error
  86. return err
  87. }
  88. // GetVehicleByID 根据ID获取车辆信息
  89. func (s *VehicleService) GetVehicleByID(id uint) (dao.Vehicle, error) {
  90. var vehicle dao.Vehicle
  91. result := global.GVA_DB.First(&vehicle, id)
  92. if result.RowsAffected == 0 {
  93. return dao.Vehicle{}, errors.New("车辆不存在")
  94. }
  95. return vehicle, nil
  96. }
  97. // GetVehicleByPlateNumber 根据车牌号或RFID获取车辆信息
  98. func (s *VehicleService) GetVehicleByPlateNumber(plateNumber, RFIDTag string) (dao.Vehicle, error) {
  99. var vehicle dao.Vehicle
  100. query := global.GVA_DB.Preload("VehicleType")
  101. if RFIDTag != "" {
  102. query = query.Where("plate_number = ? OR rfid_tag = ?", plateNumber, RFIDTag)
  103. } else {
  104. query = query.Where("plate_number = ?", plateNumber)
  105. }
  106. result := query.First(&vehicle)
  107. if result.RowsAffected == 0 {
  108. return dao.Vehicle{}, errors.New("车辆不存在")
  109. }
  110. return vehicle, nil
  111. }
  112. func (s *VehicleService) QueryAllVehicles() ([]dao.Vehicle, error) {
  113. var vehicles []dao.Vehicle
  114. err := global.GVA_DB.Find(&vehicles).Error
  115. if err != nil {
  116. return vehicles, err
  117. }
  118. return vehicles, nil
  119. }
  120. // ListVehicles 获取车辆列表
  121. func (s *VehicleService) ListVehicles(req request.VehicleQuery) (int64, []dao.Vehicle, error) {
  122. var vehicles []dao.Vehicle
  123. var total int64
  124. query := global.GVA_DB.Model(&dao.Vehicle{})
  125. if req.PlateNumber != "" {
  126. query = query.Where("plate_number LIKE ?", "%"+req.PlateNumber+"%")
  127. }
  128. if req.VehicleTypeID > 0 {
  129. query = query.Where("vehicle_type_id = ?", req.VehicleTypeID)
  130. }
  131. if req.OwnerId > 0 {
  132. query = query.Where("owner_id = ?", req.OwnerId)
  133. }
  134. // 计算总数
  135. query.Count(&total)
  136. // 分页查询
  137. offset := (req.Page - 1) * req.PageSize
  138. result := query.Offset(offset).Limit(req.PageSize).Preload("Owner").Preload("VehicleType").Find(&vehicles)
  139. if result.Error != nil {
  140. return 0, nil, result.Error
  141. }
  142. return total, vehicles, nil
  143. }
  144. // DeleteVehicle 删除车辆
  145. func (s *VehicleService) DeleteVehicle(id uint) error {
  146. result := global.GVA_DB.Delete(&dao.Vehicle{}, id)
  147. if result.RowsAffected == 0 {
  148. return errors.New("车辆不存在")
  149. }
  150. return result.Error
  151. }
  152. // VehicleEntry 车辆入场
  153. func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.VehicleEntryResponse, error) {
  154. // 黑名单检查
  155. var shortlistSvc ShortlistService
  156. if isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag); isBlack {
  157. return request.VehicleEntryResponse{}, errors.New("车辆在黑名单中,禁止入场")
  158. }
  159. // 检查车辆是否存在
  160. vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  161. if err != nil {
  162. // 如果车辆不存在,创建新车辆,自动归类为"临时车"
  163. var tempType dao.VehicleType
  164. if global.GVA_DB.Where("is_system = ?", true).First(&tempType).Error != nil {
  165. return request.VehicleEntryResponse{}, errors.New("系统错误:临时车类型未配置")
  166. }
  167. createReq := request.VehicleCreate{
  168. PlateNumber: req.PlateNumber,
  169. VehicleTypeID: tempType.ID,
  170. OwnerId: 0,
  171. }
  172. err = s.CreateVehicle(createReq)
  173. if err != nil {
  174. return request.VehicleEntryResponse{}, err
  175. }
  176. vehicle, _ = s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  177. }
  178. // 检查是否有未出场的记录
  179. var existingRecord dao.VehicleRecord
  180. result := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&existingRecord)
  181. if result.RowsAffected > 0 {
  182. return request.VehicleEntryResponse{}, errors.New("车辆已入场,未出场")
  183. }
  184. // Create the vehicle record and its digital ticket atomically. A vehicle
  185. // entry without a ticket would make every later payment flow ambiguous.
  186. entryTime := time.Now()
  187. record := dao.VehicleRecord{
  188. PlateNumber: req.PlateNumber,
  189. RFIDTag: req.RFIDTag,
  190. EntryTime: entryTime,
  191. ParkingLotID: req.ParkingLotID,
  192. ParkingSpaceID: req.ParkingSpaceID,
  193. EntryImage: req.EntryImage,
  194. PaymentStatus: "unpaid",
  195. }
  196. ticketSvc := ticketService.NewTicketService()
  197. triggerMode := "manual"
  198. if req.RFIDTag != "" {
  199. triggerMode = "rfid"
  200. }
  201. err = global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  202. var activeRecord dao.VehicleRecord
  203. if result := buildPlateOrRfidQuery(tx, req.PlateNumber, req.RFIDTag).
  204. Where("exit_time IS NULL").First(&activeRecord); result.RowsAffected > 0 {
  205. return errors.New("车辆已入场,未出场")
  206. }
  207. if err := tx.Create(&record).Error; err != nil {
  208. return err
  209. }
  210. if _, err := ticketSvc.CreateTx(tx, req.PlateNumber, triggerMode, record.ID); err != nil {
  211. return fmt.Errorf("创建数字票失败: %w", err)
  212. }
  213. return tx.Model(&dao.Vehicle{}).Where("id = ?", vehicle.ID).
  214. Update("last_entry_time", entryTime).Error
  215. })
  216. if err != nil {
  217. return request.VehicleEntryResponse{}, err
  218. }
  219. response := request.VehicleEntryResponse{
  220. Success: true,
  221. Message: "车辆入场成功",
  222. }
  223. response.Data.PlateNumber = req.PlateNumber
  224. response.Data.EntryTime = entryTime.Unix()
  225. response.Data.ParkingLotID = req.ParkingLotID
  226. response.Data.ParkingSpaceID = req.ParkingSpaceID
  227. return response, nil
  228. }
  229. // VehicleExit 车辆出场
  230. func (s *VehicleService) VehicleExit(req request.VehicleExit) (request.VehicleExitResponse, error) {
  231. // 1. 查询未出场记录
  232. var record dao.VehicleRecord
  233. if err := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error; err != nil {
  234. return request.VehicleExitResponse{}, errors.New("车辆未入场或已出场")
  235. }
  236. // 2. 获取车辆信息
  237. vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  238. if err != nil {
  239. return request.VehicleExitResponse{}, err
  240. }
  241. // 3. 计算停留时长 & 费用
  242. exitTime := time.Now()
  243. stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
  244. if stayTime < 0 {
  245. stayTime = 0
  246. }
  247. fee := s.calculateFee(&vehicle, stayTime)
  248. // 4. 更新出场记录
  249. record.ExitTime = &exitTime
  250. record.StayTime = stayTime
  251. record.Fee = fee
  252. record.ExitImage = req.ExitImage
  253. record.PaymentStatus = "paid"
  254. if err := global.GVA_DB.Save(&record).Error; err != nil {
  255. return request.VehicleExitResponse{}, err
  256. }
  257. // 5. 更新车辆统计信息
  258. vehicle.LastExitTime = &exitTime
  259. vehicle.TotalStayTime += stayTime
  260. vehicle.TotalFee += fee
  261. global.GVA_DB.Save(&vehicle)
  262. // 6. 完全匹配你的匿名结构体返回(正确写法)
  263. var response request.VehicleExitResponse
  264. response.Success = true
  265. response.Message = "车辆出场成功"
  266. response.Data.PlateNumber = record.PlateNumber
  267. response.Data.EntryTime = record.EntryTime.Unix()
  268. response.Data.ExitTime = exitTime.Unix()
  269. response.Data.StayTime = stayTime
  270. response.Data.Fee = fee
  271. response.Data.PaymentStatus = record.PaymentStatus
  272. return response, nil
  273. }
  274. // ExitPreview 费用预览(不修改数据库,仅查询+计算费用)
  275. func (s *VehicleService) ExitPreview(req request.VehicleEntry) (vehicleResponse.ExitPreviewResponse, error) {
  276. var record dao.VehicleRecord
  277. err := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error
  278. if err != nil {
  279. return vehicleResponse.ExitPreviewResponse{}, errors.New("车辆未入场或已出场")
  280. }
  281. vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
  282. if err != nil {
  283. return vehicleResponse.ExitPreviewResponse{}, err
  284. }
  285. now := time.Now()
  286. stayTime := int64(now.Sub(record.EntryTime).Minutes())
  287. if stayTime < 0 {
  288. stayTime = 0
  289. }
  290. fee := s.calculateFee(&vehicle, stayTime)
  291. var shortlistSvc ShortlistService
  292. _, isWhite := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag)
  293. if isWhite {
  294. fee = 0
  295. }
  296. typeName := ""
  297. isTemp := false
  298. if vehicle.VehicleType != nil {
  299. typeName = vehicle.VehicleType.Name
  300. isTemp = vehicle.VehicleType.IsSystem
  301. }
  302. return vehicleResponse.ExitPreviewResponse{
  303. PlateNumber: vehicle.PlateNumber,
  304. EntryTime: record.EntryTime.Unix(),
  305. StayTime: stayTime,
  306. Fee: fee,
  307. VehicleType: typeName,
  308. IsTemp: isTemp,
  309. }, nil
  310. }
  311. // ExitConfirm 确认出场 + 支付(更新车辆记录 + 创建支付记录)
  312. func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID uint) (vehicleResponse.ExitConfirmResponse, error) {
  313. // 黑名单检查
  314. var shortlistSvc ShortlistService
  315. isBlack, isWhite := shortlistSvc.CheckVehicleShortlist(req.PlateNumber, req.RFIDTag)
  316. if isBlack {
  317. return vehicleResponse.ExitConfirmResponse{}, errors.New("车辆在黑名单中,禁止出场")
  318. }
  319. paymentEntry := req.PaymentEntry
  320. if paymentEntry == "" {
  321. paymentEntry = paymentService.EntryCounter
  322. }
  323. var result vehicleResponse.ExitConfirmResponse
  324. err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  325. var record dao.VehicleRecord
  326. if err := buildPlateOrRfidQuery(tx, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error; err != nil {
  327. return errors.New("车辆未入场或已出场")
  328. }
  329. var vehicle dao.Vehicle
  330. query := tx.Preload("VehicleType")
  331. query = buildPlateOrRfidQuery(query, req.PlateNumber, req.RFIDTag)
  332. if err := query.First(&vehicle).Error; err != nil {
  333. return errors.New("车辆不存在")
  334. }
  335. exitTime := time.Now()
  336. stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
  337. if stayTime < 0 {
  338. stayTime = 0
  339. }
  340. fee := s.calculateFee(&vehicle, stayTime)
  341. if isWhite {
  342. fee = 0
  343. }
  344. isTemp := vehicle.VehicleType != nil && vehicle.VehicleType.IsSystem
  345. if isTemp && !isWhite && req.PaymentMethod == paymentService.PaymentFree {
  346. return errors.New("临时车辆不能使用免密放行")
  347. }
  348. paymentSvc := paymentService.NewPaymentService()
  349. if _, _, err := paymentSvc.ValidatePayment(tx, req.PaymentMethod, paymentEntry, fee, req.PaidAmount); err != nil {
  350. return err
  351. }
  352. record.ExitTime = &exitTime
  353. record.StayTime = stayTime
  354. record.Fee = fee
  355. record.PaymentEntry = paymentEntry
  356. record.PaymentMethod = req.PaymentMethod
  357. if err := tx.Save(&record).Error; err != nil {
  358. return err
  359. }
  360. vehicle.LastExitTime = &exitTime
  361. vehicle.TotalStayTime += stayTime
  362. vehicle.TotalFee += fee
  363. if err := tx.Save(&vehicle).Error; err != nil {
  364. return err
  365. }
  366. pr, err := paymentSvc.ProcessPaymentTxAtEntry(tx, record.ID, paymentEntry, req.PaymentMethod, fee, req.PaidAmount, operatorID)
  367. if err != nil {
  368. return err
  369. }
  370. ticketSvc := ticketService.NewTicketService()
  371. if err := ticketSvc.MarkPaidAndExitedTx(tx, record.ID, paymentEntry, req.PaymentMethod, "", pr.PaidAmount, fee); err != nil {
  372. return err
  373. }
  374. result = vehicleResponse.ExitConfirmResponse{
  375. PlateNumber: vehicle.PlateNumber, EntryTime: record.EntryTime.Unix(), ExitTime: exitTime.Unix(),
  376. StayTime: stayTime, Fee: fee, ChangeAmount: pr.ChangeAmount, PaymentEntry: paymentEntry, PaymentMethod: req.PaymentMethod,
  377. }
  378. return nil
  379. })
  380. if err != nil {
  381. return vehicleResponse.ExitConfirmResponse{}, err
  382. }
  383. return result, nil
  384. }
  385. // calculateFee 计算停车费用
  386. func (s *VehicleService) calculateFee(vehicle *dao.Vehicle, stayTime int64) float64 {
  387. // 1. 根据车辆类型ID获取收费配置(唯一)
  388. var feeConfig dao.FeeConfig
  389. if err := global.GVA_DB.Where("vehicle_type_id = ?", vehicle.VehicleTypeID).First(&feeConfig).Error; err != nil {
  390. // 无配置 → 默认计费:0.1元/分钟
  391. return float64(stayTime) * 0.1
  392. }
  393. // 配置存在但全为 0 → 同样回退默认费率
  394. if feeConfig.UnitFee == 0 && feeConfig.StartFee == 0 && feeConfig.DailyMaxFee == 0 {
  395. return float64(stayTime) * 0.1
  396. }
  397. // 2. 判断VIP有效性(必须IsVip=true、过期时间未到、且过期时间非零值)
  398. isVIP := false
  399. if vehicle.OwnerId != nil {
  400. var owner dao.Owner
  401. if err := global.GVA_DB.First(&owner, *vehicle.OwnerId).Error; err == nil {
  402. now := time.Now()
  403. isVIP = owner.IsVip && owner.VipExpireTime.After(now) && !owner.VipExpireTime.IsZero()
  404. }
  405. }
  406. // 3. VIP 免费
  407. if isVIP && feeConfig.IsVIPFree {
  408. return 0
  409. }
  410. // ===================== 修复这里 =====================
  411. // 4. 在免费时长内 → 直接 0 元
  412. if stayTime <= int64(feeConfig.StartTime) {
  413. return 0
  414. }
  415. // 5. 超过免费时长 → 开始计费
  416. extraTime := stayTime - int64(feeConfig.StartTime)
  417. extraUnits := extraTime / int64(feeConfig.UnitTime)
  418. if extraTime%int64(feeConfig.UnitTime) > 0 {
  419. extraUnits++
  420. }
  421. // 起步费 + 超时费用
  422. fee := feeConfig.StartFee + float64(extraUnits)*feeConfig.UnitFee
  423. // 6. 每日封顶
  424. if feeConfig.DailyMaxFee > 0 && fee > feeConfig.DailyMaxFee {
  425. fee = feeConfig.DailyMaxFee
  426. }
  427. // 7. VIP 折扣
  428. if isVIP {
  429. fee *= feeConfig.VIPDiscount
  430. }
  431. return fee
  432. }
  433. // InParkVehicle 在场车辆信息
  434. type InParkVehicle struct {
  435. RecordID uint `json:"record_id"`
  436. PlateNumber string `json:"plate_number"`
  437. VehicleType string `json:"vehicle_type"`
  438. OwnerName string `json:"owner_name"`
  439. OwnerPhone string `json:"owner_phone"`
  440. EntryTime string `json:"entry_time"`
  441. StayMinutes int64 `json:"stay_minutes"`
  442. ParkingLot string `json:"parking_lot"`
  443. ParkingSpace string `json:"parking_space"`
  444. }
  445. // GetInParkVehicles 获取所有在场车辆
  446. func (s *VehicleService) GetInParkVehicles(lotID uint, plateNumber string) ([]InParkVehicle, error) {
  447. var results []InParkVehicle
  448. now := time.Now()
  449. query := global.GVA_DB.Table("vehicle_record").
  450. Select(`vehicle_record.id as record_id, vehicle_record.plate_number, vehicle_type.name as vehicle_type,
  451. owner.owner_name, owner.owner_phone, vehicle_record.entry_time,
  452. parking_lot.lot_name as parking_lot`).
  453. Joins("LEFT JOIN vehicle ON vehicle.plate_number = vehicle_record.plate_number").
  454. Joins("LEFT JOIN vehicle_type ON vehicle_type.id = vehicle.vehicle_type_id").
  455. Joins("LEFT JOIN owner ON owner.id = vehicle.owner_id").
  456. Joins("LEFT JOIN parking_lot ON parking_lot.id = vehicle_record.parking_lot_id").
  457. Where("vehicle_record.exit_time IS NULL")
  458. if lotID > 0 {
  459. query = query.Where("vehicle_record.parking_lot_id = ?", lotID)
  460. }
  461. if plateNumber != "" {
  462. query = query.Where("vehicle_record.plate_number LIKE ?", "%"+plateNumber+"%")
  463. }
  464. err := query.Order("vehicle_record.entry_time ASC").Scan(&results).Error
  465. if err != nil {
  466. return nil, err
  467. }
  468. for i := range results {
  469. // Parse entry time and calculate stay duration
  470. t, parseErr := time.Parse("2006-01-02T15:04:05-07:00", results[i].EntryTime)
  471. if parseErr != nil {
  472. t, _ = time.Parse("2006-01-02 15:04:05", results[i].EntryTime)
  473. }
  474. results[i].StayMinutes = int64(now.Sub(t).Minutes())
  475. if results[i].StayMinutes < 0 {
  476. results[i].StayMinutes = 0
  477. }
  478. }
  479. return results, nil
  480. }