package service import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "gorm.io/gorm" "time" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/modules/digital-ticket/repository" ) const ( StateCreated = "created" StatePendingPayment = "pending_payment" StatePaid = "paid" StateExited = "exited" StateExpired = "expired" ) type TicketService struct { repo *repository.TicketRepository } func NewTicketService() *TicketService { return &TicketService{repo: &repository.TicketRepository{}} } func genTicketNo() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) } func appendEvent(log string, event map[string]interface{}) string { var events []map[string]interface{} json.Unmarshal([]byte(log), &events) event["at"] = time.Now().Format(time.RFC3339) events = append(events, event) data, _ := json.Marshal(events) return string(data) } // Create 创建数字票(所有进场方式统一入口) func (s *TicketService) Create(plateNumber, triggerMode string, recordID uint) (*dao.DigitalTicket, error) { return s.CreateTx(global.GVA_DB, plateNumber, triggerMode, recordID) } func (s *TicketService) CreateTx(db *gorm.DB, plateNumber, triggerMode string, recordID uint) (*dao.DigitalTicket, error) { log := appendEvent("[]", map[string]interface{}{"event": "created", "trigger": triggerMode}) ticket := &dao.DigitalTicket{ TicketNo: genTicketNo(), PlateNumber: plateNumber, TriggerMode: triggerMode, State: StatePendingPayment, VehicleRecordID: recordID, EntryTime: time.Now(), EventLog: log, } if err := s.repo.CreateTx(db, ticket); err != nil { return nil, err } return ticket, nil } // Transition 状态转换(校验合法性,原子更新) func (s *TicketService) Transition(id uint, toState string, extra map[string]interface{}) error { return s.TransitionTx(global.GVA_DB, id, toState, extra) } func (s *TicketService) TransitionTx(db *gorm.DB, id uint, toState string, extra map[string]interface{}) error { ticket, err := s.repo.GetByIDTx(db, id) if err != nil { return errors.New("票不存在") } validTransitions := map[string][]string{ StateCreated: {StatePendingPayment}, StatePendingPayment: {StatePaid, StateExpired}, StatePaid: {StateExited, StateExpired}, } allowed := false for _, s := range validTransitions[ticket.State] { if s == toState { allowed = true break } } if !allowed { return fmt.Errorf("状态转换不合法: %s → %s", ticket.State, toState) } updates := map[string]interface{}{} if toState == StatePaid { now := time.Now() updates["paid_at"] = now } if toState == StateExited { now := time.Now() updates["exit_time"] = now } // 只允许表中存在的字段写入数据库,其余仅记录到事件日志 dbFields := map[string]bool{"payment_entry": true, "payment_method": true, "payment_order_no": true, "fee": true} for k, v := range extra { if dbFields[k] { updates[k] = v } } event := map[string]interface{}{"event": toState} for k, v := range extra { event[k] = v } updates["event_log"] = appendEvent(ticket.EventLog, event) return s.repo.UpdateStateTx(db, id, toState, updates) } // MarkPaidAndExitedTx updates the ticket lifecycle in the caller's // transaction. Payment entry and method are stored as payment metadata only; // they do not define the ticket itself. func (s *TicketService) MarkPaidAndExitedTx(db *gorm.DB, recordID uint, paymentEntry, paymentMethod, paymentOrderNo string, paidAmount, fee float64) error { ticket, err := s.repo.GetByVehicleRecordIDTx(db, recordID) if err != nil { return errors.New("车辆对应的数字票不存在") } if ticket.State == StatePendingPayment { extra := map[string]interface{}{ "payment_entry": paymentEntry, "payment_method": paymentMethod, "payment_order_no": paymentOrderNo, "paid_amount": paidAmount, "fee": fee, } if err := s.TransitionTx(db, ticket.ID, StatePaid, extra); err != nil { return err } ticket.State = StatePaid } if ticket.State == StatePaid { return s.TransitionTx(db, ticket.ID, StateExited, nil) } if ticket.State == StateExited { return nil } return fmt.Errorf("数字票状态不允许出场: %s", ticket.State) } func (s *TicketService) GetByTicketNo(no string) (*dao.DigitalTicket, error) { return s.repo.GetByTicketNo(no) } func (s *TicketService) List(q repository.TicketListQuery) ([]dao.DigitalTicket, int64, error) { return s.repo.List(q) }