# 数字票(Digital Ticket)— 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. **Goal:** 引入数字票统一抽象,所有进场方式创建 digital_ticket,5 状态机驱动停车流程 **Architecture:** 新增 `internal/modules/digital-ticket/` 模块。VehicleEntry 调用 CreateTicket 创建票。状态转换原子化,事件日志 JSON 追加。 --- ## 文件结构 | 文件 | 操作 | |------|------| | `internal/dao/digital_ticket.go` | 新建 | | `internal/modules/digital-ticket/api.go` | 新建 | | `internal/modules/digital-ticket/service/service.go` | 新建 | | `internal/modules/digital-ticket/repository/repo.go` | 新建 | | `internal/service/vehicle/vehicle.go` | 修改 | | `internal/initialize/gorm.go` | 修改 | | `internal/initialize/router.go` | 修改 | --- ### Task 1: 数据层 + 服务层 - [ ] **Step 1: DAO** Write `internal/dao/digital_ticket.go`: ```go 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"` // rfid / plate_recognition / manual 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"` // JSON array } func (DigitalTicket) TableName() string { return "digital_ticket" } ``` - [ ] **Step 2: AutoMigrate** Edit `internal/initialize/gorm.go`, add after `dao.MonthlyCard{}`: ```go dao.MonthlyCard{}, dao.DigitalTicket{}, ``` - [ ] **Step 3: Repository** Write `internal/modules/digital-ticket/repository/repo.go`: ```go 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) 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 } ``` - [ ] **Step 4: Service** Write `internal/modules/digital-ticket/service/service.go`: ```go package service import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "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) 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{}{"at": time.Now().Format(time.RFC3339), "event": "created", "trigger": triggerMode}) ticket := &dao.DigitalTicket{ TicketNo: genTicketNo(), PlateNumber: plateNumber, TriggerMode: triggerMode, State: StateCreated, VehicleRecordID: recordID, EntryTime: time.Now(), EventLog: log, } if err := s.repo.Create(ticket); err != nil { return nil, err } // created → pending_payment s.Transition(ticket.ID, StatePendingPayment, nil) return ticket, nil } // Transition 状态转换(原子化,校验合法性) func (s *TicketService) Transition(id uint, toState string, extra map[string]interface{}) error { ticket, err := s.repo.GetByTicketNo(fmt.Sprintf("%d", id)) if err != nil { ticket = &dao.DigitalTicket{Model: global.GVA_MODEL{ID: id}} } validTransitions := map[string][]string{ StateCreated: {StatePendingPayment}, StatePendingPayment: {StatePaid, StateExpired}, StatePaid: {StateExited, StateExpired}, StateExited: {}, StateExpired: {}, } allowed := false for _, s := range validTransitions[ticket.State] { if s == toState { allowed = true; break } } if !allowed { return errors.New("状态转换不合法: " + ticket.State + " → " + toState) } updates := map[string]interface{}{} if toState == StatePaid { updates["paid_at"] = time.Now() } if toState == StateExited { now := time.Now(); updates["exit_time"] = now } if toState == StateExpired { updates["expire_at"] = time.Now() } for k, v := range extra { updates[k] = v } // 追加事件日志 event := map[string]interface{}{"at": time.Now().Format(time.RFC3339), "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) } ``` - [ ] **Step 5: 编译+提交** ```bash mkdir -p internal/modules/digital-ticket/service internal/modules/digital-ticket/repository go build -o build/bin/smart-parking.exe . git add internal/dao/digital_ticket.go internal/modules/digital-ticket/ internal/initialize/gorm.go git commit -m "feat: 数字票模块——DAO/repo/service,5状态机+事件日志" ``` --- ### Task 2: API + 路由 + 集成 - [ ] **Step 1: api.go** Write `internal/modules/digital-ticket/api.go`: ```go package digital_ticket import ( "github.com/gin-gonic/gin" "wails-app/internal/model/common/response" "wails-app/internal/modules/digital-ticket/repository" "wails-app/internal/modules/digital-ticket/service" ) var ticketSvc = service.NewTicketService() func GetTicket(c *gin.Context) { t, err := ticketSvc.GetByTicketNo(c.Param("ticketNo")) if err != nil { response.FailWithMessage("票不存在", c); return } response.OkWithData(t, c) } func CalculateFee(c *gin.Context) { t, err := ticketSvc.GetByTicketNo(c.Param("ticketNo")) if err != nil { response.FailWithMessage("票不存在", c); return } if err := ticketSvc.Transition(t.ID, service.StatePendingPayment, nil); err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithMessage("计费完成", c) } type payReq struct { PaymentMethod string `json:"payment_method"` PaymentOrderNo string `json:"payment_order_no"` PaidAmount float64 `json:"paid_amount"` } func PayTicket(c *gin.Context) { var req payReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c); return } t, err := ticketSvc.GetByTicketNo(c.Param("ticketNo")) if err != nil { response.FailWithMessage("票不存在", c); return } extra := map[string]interface{}{ "payment_method": req.PaymentMethod, "payment_order_no": req.PaymentOrderNo, "fee": t.Fee, "paid_amount": req.PaidAmount, } if err := ticketSvc.Transition(t.ID, service.StatePaid, extra); err != nil { response.FailWithMessage(err.Error(), c); return } // 同时更新 payment_method 和 payment_order_no global.GVA_DB.Model(t).Updates(map[string]interface{}{ "payment_method": req.PaymentMethod, "payment_order_no": req.PaymentOrderNo, }) response.OkWithMessage("支付成功", c) } func ExitTicket(c *gin.Context) { t, err := ticketSvc.GetByTicketNo(c.Param("ticketNo")) if err != nil { response.FailWithMessage("票不存在", c); return } if err := ticketSvc.Transition(t.ID, service.StateExited, nil); err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithMessage("已离场", c) } func ListTickets(c *gin.Context) { var q repository.TicketListQuery if err := c.ShouldBindQuery(&q); err != nil { response.FailWithMessage(err.Error(), c); return } list, total, err := ticketSvc.List(q) if err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithDetailed(response.PageResult{List: list, Total: total, Page: q.Page, PageSize: q.PageSize}, "查询成功", c) } func SetupDigitalTicketRouter(router *gin.RouterGroup) { dt := router.Group("/digital-ticket") dt.GET("/:ticketNo", GetTicket) dt.POST("/:ticketNo/calculate", CalculateFee) dt.POST("/:ticketNo/pay", PayTicket) dt.POST("/:ticketNo/exit", ExitTicket) dt.GET("/list", ListTickets) } ``` Need to add `"wails-app/internal/global"` import for `PayTicket`. - [ ] **Step 2: 注册路由** Edit `internal/initialize/router.go`, import `"wails-app/internal/modules/digital-ticket"`, add: ```go digital_ticket.SetupDigitalTicketRouter(PrivateGroup) ``` - [ ] **Step 3: VehicleEntry 集成** Edit `internal/service/vehicle/vehicle.go`. After creating the vehicle_record (after `err = global.GVA_DB.Create(&record).Error` and error check), add: ```go // 创建数字票 ticketSvc := ticket_service.NewTicketService() triggerMode := "manual" if req.RFIDTag != "" { triggerMode = "rfid" } ticketSvc.Create(req.PlateNumber, triggerMode, record.ID) ``` Need import `ticket_service "wails-app/internal/modules/digital-ticket/service"`. - [ ] **Step 4: Casbin + 编译+提交** ```bash go build -o build/bin/smart-parking.exe . sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype,v0,v1,v2) VALUES ('p','888','/digital-ticket','GET'),('p','618','/digital-ticket','GET'),('p','888','/digital-ticket','POST'),('p','618','/digital-ticket','POST');" git add internal/modules/digital-ticket/api.go internal/initialize/router.go internal/service/vehicle/vehicle.go git commit -m "feat: 数字票API+VehicleEntry集成——所有入场统一创建数字票" ```