service.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package service
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "gorm.io/gorm"
  9. "time"
  10. "wails-app/internal/dao"
  11. "wails-app/internal/global"
  12. "wails-app/internal/modules/digital-ticket/repository"
  13. )
  14. const (
  15. StateCreated = "created"
  16. StatePendingPayment = "pending_payment"
  17. StatePaid = "paid"
  18. StateExited = "exited"
  19. StateExpired = "expired"
  20. )
  21. type TicketService struct {
  22. repo *repository.TicketRepository
  23. }
  24. func NewTicketService() *TicketService {
  25. return &TicketService{repo: &repository.TicketRepository{}}
  26. }
  27. func genTicketNo() string {
  28. b := make([]byte, 16)
  29. rand.Read(b)
  30. return hex.EncodeToString(b)
  31. }
  32. func appendEvent(log string, event map[string]interface{}) string {
  33. var events []map[string]interface{}
  34. json.Unmarshal([]byte(log), &events)
  35. event["at"] = time.Now().Format(time.RFC3339)
  36. events = append(events, event)
  37. data, _ := json.Marshal(events)
  38. return string(data)
  39. }
  40. // Create 创建数字票(所有进场方式统一入口)
  41. func (s *TicketService) Create(plateNumber, triggerMode string, recordID uint) (*dao.DigitalTicket, error) {
  42. return s.CreateTx(global.GVA_DB, plateNumber, triggerMode, recordID)
  43. }
  44. func (s *TicketService) CreateTx(db *gorm.DB, plateNumber, triggerMode string, recordID uint) (*dao.DigitalTicket, error) {
  45. log := appendEvent("[]", map[string]interface{}{"event": "created", "trigger": triggerMode})
  46. ticket := &dao.DigitalTicket{
  47. TicketNo: genTicketNo(), PlateNumber: plateNumber, TriggerMode: triggerMode,
  48. State: StatePendingPayment, VehicleRecordID: recordID, EntryTime: time.Now(), EventLog: log,
  49. }
  50. if err := s.repo.CreateTx(db, ticket); err != nil {
  51. return nil, err
  52. }
  53. return ticket, nil
  54. }
  55. // Transition 状态转换(校验合法性,原子更新)
  56. func (s *TicketService) Transition(id uint, toState string, extra map[string]interface{}) error {
  57. return s.TransitionTx(global.GVA_DB, id, toState, extra)
  58. }
  59. func (s *TicketService) TransitionTx(db *gorm.DB, id uint, toState string, extra map[string]interface{}) error {
  60. ticket, err := s.repo.GetByIDTx(db, id)
  61. if err != nil {
  62. return errors.New("票不存在")
  63. }
  64. validTransitions := map[string][]string{
  65. StateCreated: {StatePendingPayment},
  66. StatePendingPayment: {StatePaid, StateExpired},
  67. StatePaid: {StateExited, StateExpired},
  68. }
  69. allowed := false
  70. for _, s := range validTransitions[ticket.State] {
  71. if s == toState {
  72. allowed = true
  73. break
  74. }
  75. }
  76. if !allowed {
  77. return fmt.Errorf("状态转换不合法: %s → %s", ticket.State, toState)
  78. }
  79. updates := map[string]interface{}{}
  80. if toState == StatePaid {
  81. now := time.Now()
  82. updates["paid_at"] = now
  83. }
  84. if toState == StateExited {
  85. now := time.Now()
  86. updates["exit_time"] = now
  87. }
  88. // 只允许表中存在的字段写入数据库,其余仅记录到事件日志
  89. dbFields := map[string]bool{"payment_entry": true, "payment_method": true, "payment_order_no": true, "fee": true}
  90. for k, v := range extra {
  91. if dbFields[k] {
  92. updates[k] = v
  93. }
  94. }
  95. event := map[string]interface{}{"event": toState}
  96. for k, v := range extra {
  97. event[k] = v
  98. }
  99. updates["event_log"] = appendEvent(ticket.EventLog, event)
  100. return s.repo.UpdateStateTx(db, id, toState, updates)
  101. }
  102. // MarkPaidAndExitedTx updates the ticket lifecycle in the caller's
  103. // transaction. Payment entry and method are stored as payment metadata only;
  104. // they do not define the ticket itself.
  105. func (s *TicketService) MarkPaidAndExitedTx(db *gorm.DB, recordID uint, paymentEntry, paymentMethod, paymentOrderNo string, paidAmount, fee float64) error {
  106. ticket, err := s.repo.GetByVehicleRecordIDTx(db, recordID)
  107. if err != nil {
  108. return errors.New("车辆对应的数字票不存在")
  109. }
  110. if ticket.State == StatePendingPayment {
  111. extra := map[string]interface{}{
  112. "payment_entry": paymentEntry,
  113. "payment_method": paymentMethod,
  114. "payment_order_no": paymentOrderNo,
  115. "paid_amount": paidAmount,
  116. "fee": fee,
  117. }
  118. if err := s.TransitionTx(db, ticket.ID, StatePaid, extra); err != nil {
  119. return err
  120. }
  121. ticket.State = StatePaid
  122. }
  123. if ticket.State == StatePaid {
  124. return s.TransitionTx(db, ticket.ID, StateExited, nil)
  125. }
  126. if ticket.State == StateExited {
  127. return nil
  128. }
  129. return fmt.Errorf("数字票状态不允许出场: %s", ticket.State)
  130. }
  131. func (s *TicketService) GetByTicketNo(no string) (*dao.DigitalTicket, error) {
  132. return s.repo.GetByTicketNo(no)
  133. }
  134. func (s *TicketService) List(q repository.TicketListQuery) ([]dao.DigitalTicket, int64, error) {
  135. return s.repo.List(q)
  136. }