소스 검색

feat: 数字票模块——DAO/repo/service,5状态机+事件日志

lq 1 개월 전
부모
커밋
dcec5d9da8

+ 25 - 0
internal/dao/digital_ticket.go

@@ -0,0 +1,25 @@
+package dao
+
+import (
+	"time"
+	"wails-app/internal/global"
+)
+
+type DigitalTicket struct {
+	global.GVA_MODEL
+	TicketNo        string     `gorm:"size:64;uniqueIndex" json:"ticket_no"`
+	PlateNumber     string     `gorm:"size:20" json:"plate_number"`
+	TriggerMode     string     `gorm:"size:20" json:"trigger_mode"`
+	State           string     `gorm:"size:20;default:created" json:"state"`
+	VehicleRecordID uint       `json:"vehicle_record_id"`
+	EntryTime       time.Time  `json:"entry_time"`
+	ExitTime        *time.Time `json:"exit_time"`
+	Fee             float64    `json:"fee"`
+	PaymentMethod   string     `gorm:"size:20" json:"payment_method"`
+	PaymentOrderNo  string     `gorm:"size:64" json:"payment_order_no"`
+	PaidAt          *time.Time `json:"paid_at"`
+	ExpireAt        *time.Time `json:"expire_at"`
+	EventLog        string     `gorm:"type:text" json:"event_log"`
+}
+
+func (DigitalTicket) TableName() string { return "digital_ticket" }

+ 1 - 0
internal/initialize/gorm.go

@@ -60,6 +60,7 @@ func RegisterTables() {
 		dao.Camera{},
 		dao.Shortlist{},
 		dao.MonthlyCard{},
+		dao.DigitalTicket{},
 	)
 	if err != nil {
 		global.GVA_LOG.Error("register table failed", zap.Error(err))

+ 63 - 0
internal/modules/digital-ticket/repository/repo.go

@@ -0,0 +1,63 @@
+package repository
+
+import (
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+)
+
+type TicketRepository struct{}
+
+func (r *TicketRepository) Create(ticket *dao.DigitalTicket) error {
+	return global.GVA_DB.Create(ticket).Error
+}
+
+func (r *TicketRepository) GetByTicketNo(ticketNo string) (*dao.DigitalTicket, error) {
+	var t dao.DigitalTicket
+	err := global.GVA_DB.Where("ticket_no = ?", ticketNo).First(&t).Error
+	if err != nil {
+		return nil, err
+	}
+	return &t, nil
+}
+
+func (r *TicketRepository) GetByID(id uint) (*dao.DigitalTicket, error) {
+	var t dao.DigitalTicket
+	err := global.GVA_DB.First(&t, id).Error
+	if err != nil {
+		return nil, err
+	}
+	return &t, nil
+}
+
+func (r *TicketRepository) UpdateState(id uint, state string, updates map[string]interface{}) error {
+	updates["state"] = state
+	return global.GVA_DB.Model(&dao.DigitalTicket{}).Where("id = ?", id).Updates(updates).Error
+}
+
+type TicketListQuery struct {
+	PlateNumber string `form:"plate_number"`
+	State       string `form:"state"`
+	Page        int    `form:"page"`
+	PageSize    int    `form:"page_size"`
+}
+
+func (r *TicketRepository) List(q TicketListQuery) ([]dao.DigitalTicket, int64, error) {
+	db := global.GVA_DB.Model(&dao.DigitalTicket{})
+	if q.PlateNumber != "" {
+		db = db.Where("plate_number LIKE ?", "%"+q.PlateNumber+"%")
+	}
+	if q.State != "" {
+		db = db.Where("state = ?", q.State)
+	}
+	var total int64
+	db.Count(&total)
+	if q.Page <= 0 {
+		q.Page = 1
+	}
+	if q.PageSize <= 0 {
+		q.PageSize = 10
+	}
+	var list []dao.DigitalTicket
+	err := db.Order("id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&list).Error
+	return list, total, err
+}

+ 110 - 0
internal/modules/digital-ticket/service/service.go

@@ -0,0 +1,110 @@
+package service
+
+import (
+	"crypto/rand"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"time"
+	"wails-app/internal/dao"
+	"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) {
+	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.Create(ticket); err != nil {
+		return nil, err
+	}
+	return ticket, nil
+}
+
+// Transition 状态转换(校验合法性,原子更新)
+func (s *TicketService) Transition(id uint, toState string, extra map[string]interface{}) error {
+	ticket, err := s.repo.GetByID(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
+	}
+	for k, v := range extra {
+		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.UpdateState(id, toState, updates)
+}
+
+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)
+}