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 |
修改 |
Write internal/dao/digital_ticket.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" }
Edit internal/initialize/gorm.go, add after dao.MonthlyCard{}:
dao.MonthlyCard{},
dao.DigitalTicket{},
Write internal/modules/digital-ticket/repository/repo.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
}
Write internal/modules/digital-ticket/service/service.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)
}
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状态机+事件日志"
Write internal/modules/digital-ticket/api.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.
Edit internal/initialize/router.go, import "wails-app/internal/modules/digital-ticket", add:
digital_ticket.SetupDigitalTicketRouter(PrivateGroup)
Edit internal/service/vehicle/vehicle.go. After creating the vehicle_record (after err = global.GVA_DB.Create(&record).Error and error check), add:
// 创建数字票
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".
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集成——所有入场统一创建数字票"