repo.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package repository
  2. import (
  3. "gorm.io/gorm"
  4. "wails-app/internal/dao"
  5. "wails-app/internal/global"
  6. )
  7. type TicketRepository struct{}
  8. func (r *TicketRepository) Create(ticket *dao.DigitalTicket) error {
  9. return r.CreateTx(global.GVA_DB, ticket)
  10. }
  11. func (r *TicketRepository) CreateTx(db *gorm.DB, ticket *dao.DigitalTicket) error {
  12. return db.Create(ticket).Error
  13. }
  14. func (r *TicketRepository) GetByTicketNo(ticketNo string) (*dao.DigitalTicket, error) {
  15. return r.GetByTicketNoTx(global.GVA_DB, ticketNo)
  16. }
  17. func (r *TicketRepository) GetByTicketNoTx(db *gorm.DB, ticketNo string) (*dao.DigitalTicket, error) {
  18. var t dao.DigitalTicket
  19. err := db.Where("ticket_no = ?", ticketNo).First(&t).Error
  20. if err != nil {
  21. return nil, err
  22. }
  23. return &t, nil
  24. }
  25. func (r *TicketRepository) GetByID(id uint) (*dao.DigitalTicket, error) {
  26. return r.GetByIDTx(global.GVA_DB, id)
  27. }
  28. func (r *TicketRepository) GetByIDTx(db *gorm.DB, id uint) (*dao.DigitalTicket, error) {
  29. var t dao.DigitalTicket
  30. err := db.First(&t, id).Error
  31. if err != nil {
  32. return nil, err
  33. }
  34. return &t, nil
  35. }
  36. func (r *TicketRepository) UpdateState(id uint, state string, updates map[string]interface{}) error {
  37. return r.UpdateStateTx(global.GVA_DB, id, state, updates)
  38. }
  39. func (r *TicketRepository) GetByVehicleRecordID(recordID uint) (*dao.DigitalTicket, error) {
  40. return r.GetByVehicleRecordIDTx(global.GVA_DB, recordID)
  41. }
  42. func (r *TicketRepository) GetByVehicleRecordIDTx(db *gorm.DB, recordID uint) (*dao.DigitalTicket, error) {
  43. var t dao.DigitalTicket
  44. err := db.Where("vehicle_record_id = ?", recordID).Order("id DESC").First(&t).Error
  45. if err != nil {
  46. return nil, err
  47. }
  48. return &t, nil
  49. }
  50. func (r *TicketRepository) UpdateStateTx(db *gorm.DB, id uint, state string, updates map[string]interface{}) error {
  51. updates["state"] = state
  52. return db.Model(&dao.DigitalTicket{}).Where("id = ?", id).Updates(updates).Error
  53. }
  54. // UpdateStateIfCurrentTx changes a ticket only when it is still in the state
  55. // observed by the caller. This makes payment and exit transitions safe when
  56. // two requests race for the same ticket.
  57. func (r *TicketRepository) UpdateStateIfCurrentTx(db *gorm.DB, id uint, fromState, toState string, updates map[string]interface{}) (bool, error) {
  58. if updates == nil {
  59. updates = make(map[string]interface{})
  60. }
  61. updates["state"] = toState
  62. result := db.Model(&dao.DigitalTicket{}).
  63. Where("id = ? AND state = ?", id, fromState).
  64. Updates(updates)
  65. return result.RowsAffected == 1, result.Error
  66. }
  67. type TicketListQuery struct {
  68. PlateNumber string `form:"plate_number"`
  69. State string `form:"state"`
  70. Page int `form:"page"`
  71. PageSize int `form:"page_size"`
  72. }
  73. type TicketSummary struct {
  74. Total int64 `json:"total"`
  75. Pending int64 `json:"pending"`
  76. Paid int64 `json:"paid"`
  77. Exited int64 `json:"exited"`
  78. Expired int64 `json:"expired"`
  79. }
  80. func (r *TicketRepository) List(q TicketListQuery) ([]dao.DigitalTicket, int64, error) {
  81. db := ticketQuery(q, true)
  82. var total int64
  83. db.Count(&total)
  84. if q.Page <= 0 {
  85. q.Page = 1
  86. }
  87. if q.PageSize <= 0 {
  88. q.PageSize = 10
  89. }
  90. var list []dao.DigitalTicket
  91. err := db.Order("digital_ticket.id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&list).Error
  92. return list, total, err
  93. }
  94. // Summary returns counts for the complete filtered result set. Pagination and
  95. // the selected state tab are intentionally ignored so the dashboard totals do
  96. // not change when the user moves between pages.
  97. func (r *TicketRepository) Summary(q TicketListQuery) (TicketSummary, error) {
  98. db := ticketQuery(q, false)
  99. var rows []struct {
  100. State string
  101. Count int64
  102. }
  103. if err := db.Select("digital_ticket.state, COUNT(*) AS count").Group("digital_ticket.state").Scan(&rows).Error; err != nil {
  104. return TicketSummary{}, err
  105. }
  106. var summary TicketSummary
  107. for _, row := range rows {
  108. summary.Total += row.Count
  109. switch row.State {
  110. case "created", "pending_payment":
  111. summary.Pending += row.Count
  112. case "paid":
  113. summary.Paid += row.Count
  114. case "exited":
  115. summary.Exited += row.Count
  116. case "expired":
  117. summary.Expired += row.Count
  118. }
  119. }
  120. return summary, nil
  121. }
  122. // ticketQuery applies filters shared by the paged list and the unpaged
  123. // summary. A left join keeps old tickets searchable even when their copied
  124. // plate_number column is empty and the vehicle record still has the plate.
  125. func ticketQuery(q TicketListQuery, includeState bool) *gorm.DB {
  126. db := global.GVA_DB.Model(&dao.DigitalTicket{})
  127. if q.PlateNumber != "" {
  128. like := "%" + q.PlateNumber + "%"
  129. db = db.Joins("LEFT JOIN vehicle_record ON vehicle_record.id = digital_ticket.vehicle_record_id").
  130. Where("(digital_ticket.plate_number LIKE ? OR vehicle_record.plate_number LIKE ?)", like, like)
  131. }
  132. if includeState && q.State != "" {
  133. if q.State == "pending_payment" {
  134. db = db.Where("digital_ticket.state IN ?", []string{"created", "pending_payment"})
  135. } else {
  136. db = db.Where("digital_ticket.state = ?", q.State)
  137. }
  138. }
  139. return db
  140. }