For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在出场环节增加收费结算:临时车现金收款+找零,月租/VIP免密放行,预留POS/扫码扩展
Architecture: 新增 internal/modules/payment/ 模块(api.go + service + repository + model),拆分 VehicleExit 为 preview/confirm 两步,前端 entryExit.vue 出场 Tab 加收费 UI
Tech Stack: Go 1.25 + Gin + GORM + SQLite, Vue 3 + Element Plus
| 文件 | 操作 | 职责 |
|---|---|---|
internal/modules/payment/api.go |
新建 | HTTP Handler(ExitPreview, ExitConfirm)+ Router Setup |
internal/modules/payment/service/service.go |
新建 | PaymentService:处理支付、创建支付记录 |
internal/modules/payment/repository/repo.go |
新建 | PaymentRepository:payment_record 表 CRUD |
internal/modules/payment/model/request/request.go |
新建 | 请求 DTO |
internal/modules/payment/model/response/response.go |
新建 | 响应 DTO |
internal/dao/payment_record.go |
新建 | GORM 实体 PaymentRecord |
internal/dao/vehicle_record.go |
修改 | 加 PaymentMethod 字段 |
internal/service/vehicle/vehicle.go |
修改 | Vehicle Exit 返回费用不下文,新增 ExitPreview/ExitConfirm |
internal/initialize/router.go |
修改 | 注册 payment 路由 |
internal/initialize/seed.go |
修改 | AutoMigrate PaymentRecord |
frontend/src/view/parking/entryExit.vue |
修改 | 出场 Tab 加收费 UI |
frontend/src/api/vehicle.js |
修改 | 新增 exitPreview, exitConfirm API |
frontend/src/lang/zh-CN.js |
修改 | 新增翻译键 |
frontend/src/lang/en.js |
修改 | 新增翻译键 |
Files:
internal/dao/payment_record.goModify: internal/dao/vehicle_record.go
[ ] Step 1: 创建 payment_record DAO
package dao
import "wails-app/internal/global"
import "time"
type PaymentRecord struct {
global.GVA_MODEL
RecordID uint `gorm:"index" json:"record_id"`
PaymentMethod string `gorm:"size:20" json:"payment_method"` // cash / free / pos / wechat / alipay
Amount float64 `json:"amount"` // 应收
PaidAmount float64 `json:"paid_amount"` // 实收
ChangeAmount float64 `json:"change_amount"` // 找零
OperatorID uint `json:"operator_id"` // 收费员
PaidAt time.Time `json:"paid_at"` // 支付时间
Remark string `gorm:"size:200" json:"remark"`
}
func (PaymentRecord) TableName() string {
return "payment_record"
}
在 VehicleRecord 结构体的 PaymentStatus 行之后新增:
PaymentMethod string `gorm:"size:20" json:"payment_method"` // 支付方式
在 SeedSystemData 函数的 db := global.GVA_DB 之后加:
db.AutoMigrate(&dao.PaymentRecord{})
cd lc_garage
sqlite3 "$APPDATA/smart-parking/lc_garage.db" "ALTER TABLE vehicle_record ADD COLUMN payment_method TEXT DEFAULT '';"
cd lc_garage && go build -o build/bin/smart-parking.exe .
git add internal/dao/payment_record.go internal/dao/vehicle_record.go internal/initialize/seed.go
git commit -m "feat: 新增payment_record表和vehicle_record.payment_method字段"
Files:
internal/modules/payment/model/request/request.gointernal/modules/payment/model/response/response.gointernal/modules/payment/repository/repo.goCreate: internal/modules/payment/service/service.go
[ ] Step 1: 请求 DTO
// internal/modules/payment/model/request/request.go
package request
type ExitPreviewRequest struct {
PlateNumber string `json:"plate_number"`
RFIDTag string `json:"rfid_tag"`
}
type ExitConfirmRequest struct {
PlateNumber string `json:"plate_number"`
RFIDTag string `json:"rfid_tag"`
PaymentMethod string `json:"payment_method"` // cash / free
PaidAmount float64 `json:"paid_amount"` // 现金实收,free 时为 0
}
// internal/modules/payment/model/response/response.go
package response
type ExitPreviewResponse struct {
PlateNumber string `json:"plate_number"`
EntryTime int64 `json:"entry_time"`
StayTime int64 `json:"stay_time"`
Fee float64 `json:"fee"`
VehicleType string `json:"vehicle_type"`
IsTemp bool `json:"is_temp"`
}
type ExitConfirmResponse struct {
PlateNumber string `json:"plate_number"`
EntryTime int64 `json:"entry_time"`
ExitTime int64 `json:"exit_time"`
StayTime int64 `json:"stay_time"`
Fee float64 `json:"fee"`
ChangeAmount float64 `json:"change_amount"`
PaymentMethod string `json:"payment_method"`
}
// internal/modules/payment/repository/repo.go
package repository
import (
"wails-app/internal/dao"
"wails-app/internal/global"
)
type PaymentRepository struct{}
func (r *PaymentRepository) Create(record *dao.PaymentRecord) error {
return global.GVA_DB.Create(record).Error
}
func (r *PaymentRepository) GetByRecordID(recordID uint) (*dao.PaymentRecord, error) {
var record dao.PaymentRecord
err := global.GVA_DB.Where("record_id = ?", recordID).First(&record).Error
if err != nil {
return nil, err
}
return &record, nil
}
// internal/modules/payment/service/service.go
package service
import (
"fmt"
"time"
"wails-app/internal/dao"
"wails-app/internal/global"
"wails-app/internal/modules/payment/repository"
)
const (
PaymentCash = "cash"
PaymentFree = "free"
PaymentPOS = "pos"
PaymentWechat = "wechat"
PaymentAlipay = "alipay"
)
type PaymentService struct {
repo *repository.PaymentRepository
}
func NewPaymentService() *PaymentService {
return &PaymentService{repo: &repository.PaymentRepository{}}
}
// ProcessPayment 处理支付:创建 payment_record,更新 vehicle_record
func (s *PaymentService) ProcessPayment(recordID uint, paymentMethod string, amount float64, paidAmount float64, operatorID uint) (*dao.PaymentRecord, error) {
changeAmount := paidAmount - amount
if changeAmount < 0 {
changeAmount = 0
}
paymentRecord := &dao.PaymentRecord{
RecordID: recordID,
PaymentMethod: paymentMethod,
Amount: amount,
PaidAmount: paidAmount,
ChangeAmount: changeAmount,
OperatorID: operatorID,
PaidAt: time.Now(),
}
if err := s.repo.Create(paymentRecord); err != nil {
return nil, fmt.Errorf("创建支付记录失败: %w", err)
}
// 更新 vehicle_record
global.GVA_DB.Model(&dao.VehicleRecord{}).Where("id = ?", recordID).Updates(map[string]interface{}{
"payment_status": "paid",
"payment_method": paymentMethod,
})
return paymentRecord, nil
}
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/
git commit -m "feat: 创建payment模块——model/repository/service层"
Files:
internal/service/vehicle/vehicle.goCreate: internal/modules/payment/api.go
[ ] Step 1: vehicle.go 新增 ExitPreview 方法
在 internal/service/vehicle/vehicle.go 的 VehicleExit 函数后面新增:
// ExitPreview 费用预览(不修改数据库)
func (s *VehicleService) ExitPreview(req request.VehicleEntry) (paymentResponse.ExitPreviewResponse, error) {
var record dao.VehicleRecord
if err := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL", req.PlateNumber, req.RFIDTag).First(&record).Error; err != nil {
return paymentResponse.ExitPreviewResponse{}, errors.New("车辆未入场或已出场")
}
vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
if err != nil {
return paymentResponse.ExitPreviewResponse{}, err
}
stayTime := int64(time.Now().Sub(record.EntryTime).Minutes())
if stayTime < 0 {
stayTime = 0
}
fee := s.calculateFee(&vehicle, stayTime)
typeName := ""
isTemp := false
if vehicle.VehicleType != nil {
typeName = vehicle.VehicleType.Name
isTemp = vehicle.VehicleType.IsSystem
}
return paymentResponse.ExitPreviewResponse{
PlateNumber: vehicle.PlateNumber,
EntryTime: record.EntryTime.Unix(),
StayTime: stayTime,
Fee: fee,
VehicleType: typeName,
IsTemp: isTemp,
}, nil
}
需要在文件头部新增 import:
"wails-app/internal/model/common"
"wails-app/internal/model/vehicle/response"
"wails-app/internal/modules/payment/model/response" as paymentResponse
Wait, that would create an import cycle. Let me think about this differently.
The response DTO for ExitPreview is in internal/modules/payment/model/response/. If internal/service/vehicle/vehicle.go imports from internal/modules/payment/model/response/, that should be fine — modules/payment/model/response doesn't import service/vehicle.
Actually let me check — will there be a cycle? modules/payment/model/response only imports standard library. No cycle.
But wait, internal/service/vehicle/vehicle.go already imports internal/model/vehicle/request for request.VehicleEntry. If I add an import for internal/modules/payment/model/response, that's a new dependency. Should be fine.
Actually, I need to check if there's an internal/model/vehicle/response/ too. Let me not add confusion and just put the ExitPreview DTOs in the existing internal/model/vehicle/ location. This way the vehicle service doesn't need to import from modules/payment.
Hmm, but the design says new modules should go in modules/payment. Let me put the request/response DTOs for the payment-related operations in the payment module, and have the vehicle service's ExitPreview method use these types.
The import internal/modules/payment/model/response should be fine — it's just data types, no cycles.
Actually, re-reading the import structure:
service/vehicle imports model/vehicle/request and model/vehicle/responsemodules/payment/model/response import, it's a new leaf dependencymodules/payment/model/response imports nothing from the service layerNo cycle. Let me proceed.
But I also need to reuse request.VehicleEntry for ExitPreviewRequest. Actually, I could just use the existing request.VehicleEntry as the preview request (since it takes plate_number + rfid_tag). The ExitConfirmRequest has payment_method + paid_amount so it needs to be a new type.
Let me simplify:
ExitPreview uses existing request.VehicleEntry as inputExitConfirm uses a new type with payment infoLet me rewrite the plan accordingly. I'll define ExitConfirmRequest in internal/model/vehicle/request/vehicle.go (old location) to keep things simpler, and define the response types there too.
Actually, this is getting complicated trying to balance old/new code. Let me just take the pragmatic approach: put the new DTOs for exit preview/confirm in the existing internal/model/vehicle/request/vehicle.go and internal/model/vehicle/response/ since they're closely tied to the vehicle module. The payment module (models, repository, service) handles the payment_record specific logic.
This way:
This is more practical. Let me rewrite the plan.
Actually, the modules/payment/model is still useful because the payment module's own request/response types (like payment creation) might differ from the exit flow. Let me just put:
Vehicle-related DTOs (exit preview, exit confirm req/resp) → internal/model/vehicle/request/ and internal/model/vehicle/response/
Payment module DTOs → internal/modules/payment/model/ (for payment-specific operations)
Let me rewrite this task more cleanly.<|end▁of▁thinking|>
<||DSML||tool_calls> <||DSML||invoke name="Write"> <||DSML||parameter name="content" string="true"># 收费结算模块 — 实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在出场环节增加收费结算:临时车现金收款+找零,月租/VIP免密放行,预留POS/扫码扩展
Architecture: 新增 internal/modules/payment/ 模块(含 PaymentService + PaymentRepository + 常量),拆分 VehicleExit 为 preview/confirm。Exit 相关 DTO 就近放在 internal/model/vehicle/ 下,避免跨层导入。
Tech Stack: Go 1.25 + Gin + GORM + SQLite, Vue 3 + Element Plus
| 文件 | 操作 | 职责 |
|---|---|---|
internal/dao/payment_record.go |
新建 | GORM 实体 |
internal/dao/vehicle_record.go |
修改 | +PaymentMethod 字段 |
internal/modules/payment/service/service.go |
新建 | 支付常量 + ProcessPayment |
internal/modules/payment/repository/repo.go |
新建 | payment_record CRUD |
internal/model/vehicle/request/vehicle.go |
修改 | +ExitConfirmRequest |
internal/model/vehicle/response/vehicle.go |
修改 | +ExitPreviewResponse, ExitConfirmResponse |
internal/service/vehicle/vehicle.go |
修改 | +ExitPreview(), +ExitConfirm() |
internal/modules/payment/api.go |
新建 | Handler + SetupPaymentRouter |
internal/initialize/router.go |
修改 | 注册 payment 路由 |
frontend/src/view/parking/entryExit.vue |
修改 | 出场 Tab 加收费 UI |
frontend/src/api/vehicle.js |
修改 | +exitPreview, +exitConfirm |
frontend/src/lang/zh-CN.js |
修改 | 翻译键 |
frontend/src/lang/en.js |
修改 | 翻译键 |
Files: Create internal/dao/payment_record.go, Modify internal/dao/vehicle_record.go
Write file internal/dao/payment_record.go:
package dao
import (
"time"
"wails-app/internal/global"
)
type PaymentRecord struct {
global.GVA_MODEL
RecordID uint `gorm:"index" json:"record_id"`
PaymentMethod string `gorm:"size:20" json:"payment_method"`
Amount float64 `json:"amount"`
PaidAmount float64 `json:"paid_amount"`
ChangeAmount float64 `json:"change_amount"`
OperatorID uint `json:"operator_id"`
PaidAt time.Time `json:"paid_at"`
Remark string `gorm:"size:200" json:"remark"`
}
func (PaymentRecord) TableName() string {
return "payment_record"
}
Edit internal/dao/vehicle_record.go,在 PaymentStatus 之后加一行:
PaymentMethod string `gorm:"size:20" json:"payment_method"`
cd lc_garage && go build -o build/bin/smart-parking.exe .
git add internal/dao/payment_record.go internal/dao/vehicle_record.go
git commit -m "feat: 新增payment_record实体,vehicle_record加payment_method"
Files: Create internal/modules/payment/service/service.go, Create internal/modules/payment/repository/repo.go
Write internal/modules/payment/repository/repo.go:
package repository
import (
"wails-app/internal/dao"
"wails-app/internal/global"
)
type PaymentRepository struct{}
func (r *PaymentRepository) Create(record *dao.PaymentRecord) error {
return global.GVA_DB.Create(record).Error
}
Write internal/modules/payment/service/service.go:
package service
import (
"fmt"
"time"
"wails-app/internal/dao"
"wails-app/internal/global"
"wails-app/internal/modules/payment/repository"
)
// 支付方式常量
const (
PaymentCash = "cash"
PaymentFree = "free"
PaymentPOS = "pos"
PaymentWechat = "wechat"
PaymentAlipay = "alipay"
)
type PaymentService struct {
repo *repository.PaymentRepository
}
func NewPaymentService() *PaymentService {
return &PaymentService{repo: &repository.PaymentRepository{}}
}
func (s *PaymentService) ProcessPayment(recordID uint, paymentMethod string, amount float64, paidAmount float64, operatorID uint) (*dao.PaymentRecord, error) {
changeAmount := 0.0
if paymentMethod == PaymentCash && paidAmount > amount {
changeAmount = paidAmount - amount
}
now := time.Now()
paymentRecord := &dao.PaymentRecord{
RecordID: recordID,
PaymentMethod: paymentMethod,
Amount: amount,
PaidAmount: paidAmount,
ChangeAmount: changeAmount,
OperatorID: operatorID,
PaidAt: now,
}
if err := s.repo.Create(paymentRecord); err != nil {
return nil, fmt.Errorf("创建支付记录失败: %w", err)
}
// 更新 vehicle_record 支付状态
global.GVA_DB.Model(&dao.VehicleRecord{}).Where("id = ?", recordID).Updates(map[string]interface{}{
"payment_status": "paid",
"payment_method": paymentMethod,
})
return paymentRecord, nil
}
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/
git commit -m "feat: 创建payment模块——支付服务+仓储层"
Files: Modify internal/model/vehicle/request/vehicle.go, Modify internal/model/vehicle/response/vehicle.go, Create if not exists internal/model/vehicle/response/vehicle.go, Modify internal/service/vehicle/vehicle.go
Edit internal/model/vehicle/request/vehicle.go,在文件末尾新增:
type ExitConfirmRequest struct {
PlateNumber string `json:"plate_number"`
RFIDTag string `json:"rfid_tag"`
PaymentMethod string `json:"payment_method"`
PaidAmount float64 `json:"paid_amount"`
}
Check if internal/model/vehicle/response/vehicle.go exists. If not, create it:
package response
type ExitPreviewResponse struct {
PlateNumber string `json:"plate_number"`
EntryTime int64 `json:"entry_time"`
StayTime int64 `json:"stay_time"`
Fee float64 `json:"fee"`
VehicleType string `json:"vehicle_type"`
IsTemp bool `json:"is_temp"`
}
type ExitConfirmResponse struct {
PlateNumber string `json:"plate_number"`
EntryTime int64 `json:"entry_time"`
ExitTime int64 `json:"exit_time"`
StayTime int64 `json:"stay_time"`
Fee float64 `json:"fee"`
ChangeAmount float64 `json:"change_amount"`
PaymentMethod string `json:"payment_method"`
}
If the file already exists, add these types to it.
In internal/service/vehicle/vehicle.go,在 VehicleExit 函数后面新增:
// ExitPreview 费用预览(不修改数据库)
func (s *VehicleService) ExitPreview(req request.VehicleEntry) (vehicleResponse.ExitPreviewResponse, error) {
var record dao.VehicleRecord
err := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL", req.PlateNumber, req.RFIDTag).First(&record).Error
if err != nil {
return vehicleResponse.ExitPreviewResponse{}, errors.New("车辆未入场或已出场")
}
vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
if err != nil {
return vehicleResponse.ExitPreviewResponse{}, err
}
now := time.Now()
stayTime := int64(now.Sub(record.EntryTime).Minutes())
if stayTime < 0 {
stayTime = 0
}
fee := s.calculateFee(&vehicle, stayTime)
typeName := ""
isTemp := false
if vehicle.VehicleType != nil {
typeName = vehicle.VehicleType.Name
isTemp = vehicle.VehicleType.IsSystem
}
return vehicleResponse.ExitPreviewResponse{
PlateNumber: vehicle.PlateNumber,
EntryTime: record.EntryTime.Unix(),
StayTime: stayTime,
Fee: fee,
VehicleType: typeName,
IsTemp: isTemp,
}, nil
}
需要在 vehicle.go 头部 import 中新增:
vehicleResponse "wails-app/internal/model/vehicle/response"
在 ExitPreview 之后新增:
// ExitConfirm 确认出场 + 支付
func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID uint) (vehicleResponse.ExitConfirmResponse, error) {
var record dao.VehicleRecord
err := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL", req.PlateNumber, req.RFIDTag).First(&record).Error
if err != nil {
return vehicleResponse.ExitConfirmResponse{}, errors.New("车辆未入场或已出场")
}
vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
if err != nil {
return vehicleResponse.ExitConfirmResponse{}, err
}
exitTime := time.Now()
stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
if stayTime < 0 {
stayTime = 0
}
fee := s.calculateFee(&vehicle, stayTime)
// 更新出场记录
record.ExitTime = &exitTime
record.StayTime = stayTime
record.Fee = fee
record.PaymentMethod = req.PaymentMethod
global.GVA_DB.Save(&record)
// 更新车辆统计
vehicle.LastExitTime = &exitTime
vehicle.TotalStayTime += stayTime
vehicle.TotalFee += fee
global.GVA_DB.Save(&vehicle)
// 创建支付记录
paymentSvc := paymentService.NewPaymentService()
payRecord, err := paymentSvc.ProcessPayment(record.ID, req.PaymentMethod, fee, req.PaidAmount, operatorID)
if err != nil {
return vehicleResponse.ExitConfirmResponse{}, err
}
changeAmount := 0.0
if payRecord != nil {
changeAmount = payRecord.ChangeAmount
}
return vehicleResponse.ExitConfirmResponse{
PlateNumber: vehicle.PlateNumber,
EntryTime: record.EntryTime.Unix(),
ExitTime: exitTime.Unix(),
StayTime: stayTime,
Fee: fee,
ChangeAmount: changeAmount,
PaymentMethod: req.PaymentMethod,
}, nil
}
需要在 vehicle.go 头部新增 import:
paymentService "wails-app/internal/modules/payment/service"
go build -o build/bin/smart-parking.exe .
git add internal/model/vehicle/ internal/service/vehicle/vehicle.go
git commit -m "feat: 拆分VehicleExit为ExitPreview+ExitConfirm——预览费用+确认出场支付"
Files: Create internal/modules/payment/api.go, Modify internal/initialize/router.go
Write internal/modules/payment/api.go:
package payment
import (
"github.com/gin-gonic/gin"
"wails-app/internal/global"
"wails-app/internal/model/common/response"
"wails-app/internal/model/vehicle/request"
"wails-app/internal/service"
)
var vehicleSvc = service.ServiceGroupApp.VehicleServiceGroup.VehicleService
// ExitPreview 费用预览
func ExitPreview(c *gin.Context) {
var req request.VehicleEntry
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
resp, err := vehicleSvc.ExitPreview(req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData(resp, c)
}
// ExitConfirm 确认出场+支付
func ExitConfirm(c *gin.Context) {
var req request.ExitConfirmRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 从 JWT 获取操作员 ID
operatorID := utils.GetUserID(c)
resp, err := vehicleSvc.ExitConfirm(req, operatorID)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData(resp, c)
}
// SetupPaymentRouter 注册支付路由
func SetupPaymentRouter(router *gin.RouterGroup) {
paymentGroup := router.Group("/vehicle")
{
paymentGroup.POST("/exit/preview", ExitPreview)
paymentGroup.POST("/exit/confirm", ExitConfirm)
}
}
Wait, I need to think about how to get the operator ID from JWT. Let me check the existing middleware/utils:<|end▁of▁thinking|>
<||DSML||tool_calls> <||DSML||invoke name="Write"> <||DSML||parameter name="content" string="true"># 收费结算模块 — 实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
Goal: 在出场环节增加收费结算:临时车现金收款+找零,月租/VIP免密放行,预留POS/扫码扩展
Architecture: 新增 internal/modules/payment/ 模块。拆分 VehicleExit 为 preview/confirm 两步。前端 entryExit.vue 加收费 UI。
Tech Stack: Go 1.25 + Gin + GORM + SQLite, Vue 3 + Element Plus
| 文件 | 操作 | 职责 |
|---|---|---|
internal/dao/payment_record.go |
新建 | PaymentRecord GORM 实体 |
internal/dao/vehicle_record.go |
修改 | +PaymentMethod 字段 |
internal/modules/payment/service/service.go |
新建 | 支付常量 + ProcessPayment |
internal/modules/payment/repository/repo.go |
新建 | payment_record CRUD |
internal/model/vehicle/request/vehicle.go |
修改 | +ExitConfirmRequest |
internal/model/vehicle/response/vehicle.go |
新建 | ExitPreviewResponse, ExitConfirmResponse |
internal/service/vehicle/vehicle.go |
修改 | +ExitPreview(), +ExitConfirm() |
internal/modules/payment/api.go |
新建 | Handler + SetupPaymentRouter |
internal/initialize/router.go |
修改 | 注册路由 |
frontend/src/view/parking/entryExit.vue |
修改 | 出场 Tab 收费 UI |
frontend/src/api/vehicle.js |
修改 | +exitPreview, +exitConfirm |
frontend/src/lang/zh-CN.js |
修改 | 翻译键 |
frontend/src/lang/en.js |
修改 | 翻译键 |
Files: Create internal/dao/payment_record.go, Modify internal/dao/vehicle_record.go
// internal/dao/payment_record.go
package dao
import (
"time"
"wails-app/internal/global"
)
type PaymentRecord struct {
global.GVA_MODEL
RecordID uint `gorm:"index" json:"record_id"`
PaymentMethod string `gorm:"size:20" json:"payment_method"`
Amount float64 `json:"amount"`
PaidAmount float64 `json:"paid_amount"`
ChangeAmount float64 `json:"change_amount"`
OperatorID uint `json:"operator_id"`
PaidAt time.Time `json:"paid_at"`
Remark string `gorm:"size:200" json:"remark"`
}
func (PaymentRecord) TableName() string { return "payment_record" }
在 internal/dao/vehicle_record.go 的 PaymentStatus 之后加:
PaymentMethod string `gorm:"size:20" json:"payment_method"`
sqlite3 "$APPDATA/smart-parking/lc_garage.db" "ALTER TABLE vehicle_record ADD COLUMN payment_method TEXT DEFAULT '';"
go build -o build/bin/smart-parking.exe .
git add internal/dao/payment_record.go internal/dao/vehicle_record.go
git commit -m "feat: 新增payment_record实体,vehicle_record加payment_method"
Files: Create internal/modules/payment/service/service.go, internal/modules/payment/repository/repo.go
// internal/modules/payment/repository/repo.go
package repository
import (
"wails-app/internal/dao"
"wails-app/internal/global"
)
type PaymentRepository struct{}
func (r *PaymentRepository) Create(record *dao.PaymentRecord) error {
return global.GVA_DB.Create(record).Error
}
// internal/modules/payment/service/service.go
package service
import (
"fmt"
"time"
"wails-app/internal/dao"
"wails-app/internal/global"
"wails-app/internal/modules/payment/repository"
)
const (
PaymentCash = "cash"
PaymentFree = "free"
PaymentPOS = "pos"
PaymentWechat = "wechat"
PaymentAlipay = "alipay"
)
type PaymentService struct {
repo *repository.PaymentRepository
}
func NewPaymentService() *PaymentService {
return &PaymentService{repo: &repository.PaymentRepository{}}
}
func (s *PaymentService) ProcessPayment(recordID uint, paymentMethod string, amount float64, paidAmount float64, operatorID uint) (*dao.PaymentRecord, error) {
changeAmount := 0.0
if paymentMethod == PaymentCash && paidAmount > amount {
changeAmount = paidAmount - amount
}
now := time.Now()
pr := &dao.PaymentRecord{
RecordID: recordID, PaymentMethod: paymentMethod,
Amount: amount, PaidAmount: paidAmount,
ChangeAmount: changeAmount, OperatorID: operatorID, PaidAt: now,
}
if err := s.repo.Create(pr); err != nil {
return nil, fmt.Errorf("创建支付记录失败: %w", err)
}
global.GVA_DB.Model(&dao.VehicleRecord{}).Where("id = ?", recordID).Updates(map[string]interface{}{
"payment_status": "paid", "payment_method": paymentMethod,
})
return pr, nil
}
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/
git commit -m "feat: 创建payment模块——支付服务+仓储层"
Files: Modify internal/model/vehicle/request/vehicle.go, Create internal/model/vehicle/response/vehicle.go, Modify internal/service/vehicle/vehicle.go
在 internal/model/vehicle/request/vehicle.go 末尾新增:
type ExitConfirmRequest struct {
PlateNumber string `json:"plate_number"`
RFIDTag string `json:"rfid_tag"`
PaymentMethod string `json:"payment_method"`
PaidAmount float64 `json:"paid_amount"`
}
创建 internal/model/vehicle/response/vehicle.go:
package response
type ExitPreviewResponse struct {
PlateNumber string `json:"plate_number"`
EntryTime int64 `json:"entry_time"`
StayTime int64 `json:"stay_time"`
Fee float64 `json:"fee"`
VehicleType string `json:"vehicle_type"`
IsTemp bool `json:"is_temp"`
}
type ExitConfirmResponse struct {
PlateNumber string `json:"plate_number"`
EntryTime int64 `json:"entry_time"`
ExitTime int64 `json:"exit_time"`
StayTime int64 `json:"stay_time"`
Fee float64 `json:"fee"`
ChangeAmount float64 `json:"change_amount"`
PaymentMethod string `json:"payment_method"`
}
在 internal/service/vehicle/vehicle.go 的 VehicleExit 函数之后新增。文件头部新增 import:
vehicleResponse "wails-app/internal/model/vehicle/response"
func (s *VehicleService) ExitPreview(req request.VehicleEntry) (vehicleResponse.ExitPreviewResponse, error) {
var record dao.VehicleRecord
if err := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL",
req.PlateNumber, req.RFIDTag).First(&record).Error; err != nil {
return vehicleResponse.ExitPreviewResponse{}, errors.New("车辆未入场或已出场")
}
vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
if err != nil {
return vehicleResponse.ExitPreviewResponse{}, err
}
now := time.Now()
stayTime := int64(now.Sub(record.EntryTime).Minutes())
if stayTime < 0 { stayTime = 0 }
fee := s.calculateFee(&vehicle, stayTime)
typeName, isTemp := "", false
if vehicle.VehicleType != nil {
typeName = vehicle.VehicleType.Name
isTemp = vehicle.VehicleType.IsSystem
}
return vehicleResponse.ExitPreviewResponse{
PlateNumber: vehicle.PlateNumber, EntryTime: record.EntryTime.Unix(),
StayTime: stayTime, Fee: fee, VehicleType: typeName, IsTemp: isTemp,
}, nil
}
继续在 vehicle.go 新增(需 import paymentService "wails-app/internal/modules/payment/service"):
func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID uint) (vehicleResponse.ExitConfirmResponse, error) {
var record dao.VehicleRecord
if err := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL",
req.PlateNumber, req.RFIDTag).First(&record).Error; err != nil {
return vehicleResponse.ExitConfirmResponse{}, errors.New("车辆未入场或已出场")
}
vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
if err != nil {
return vehicleResponse.ExitConfirmResponse{}, err
}
exitTime := time.Now()
stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
if stayTime < 0 { stayTime = 0 }
fee := s.calculateFee(&vehicle, stayTime)
record.ExitTime = &exitTime
record.StayTime = stayTime
record.Fee = fee
record.PaymentMethod = req.PaymentMethod
global.GVA_DB.Save(&record)
vehicle.LastExitTime = &exitTime
vehicle.TotalStayTime += stayTime
vehicle.TotalFee += fee
global.GVA_DB.Save(&vehicle)
paymentSvc := paymentService.NewPaymentService()
pr, err := paymentSvc.ProcessPayment(record.ID, req.PaymentMethod, fee, req.PaidAmount, operatorID)
if err != nil {
return vehicleResponse.ExitConfirmResponse{}, err
}
changeAmount := 0.0
if pr != nil { changeAmount = pr.ChangeAmount }
return vehicleResponse.ExitConfirmResponse{
PlateNumber: vehicle.PlateNumber, EntryTime: record.EntryTime.Unix(),
ExitTime: exitTime.Unix(), StayTime: stayTime, Fee: fee,
ChangeAmount: changeAmount, PaymentMethod: req.PaymentMethod,
}, nil
}
go build -o build/bin/smart-parking.exe .
git add internal/model/vehicle/ internal/service/vehicle/vehicle.go
git commit -m "feat: 拆分VehicleExit——ExitPreview费用预览+ExitConfirm确认出场支付"
Files: Create internal/modules/payment/api.go, Modify internal/initialize/router.go
// internal/modules/payment/api.go
package payment
import (
"github.com/gin-gonic/gin"
"wails-app/internal/model/common/response"
"wails-app/internal/model/vehicle/request"
utils "wails-app/internal/pkg"
"wails-app/internal/service"
)
var vehicleSvc = service.ServiceGroupApp.VehicleServiceGroup.VehicleService
func ExitPreview(c *gin.Context) {
var req request.VehicleEntry
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
resp, err := vehicleSvc.ExitPreview(req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData(resp, c)
}
func ExitConfirm(c *gin.Context) {
var req request.ExitConfirmRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
resp, err := vehicleSvc.ExitConfirm(req, utils.GetUserID(c))
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData(resp, c)
}
func SetupPaymentRouter(router *gin.RouterGroup) {
pg := router.Group("/vehicle")
{
pg.POST("/exit/preview", ExitPreview)
pg.POST("/exit/confirm", ExitConfirm)
}
}
在 internal/initialize/router.go 头部 import 加:
"wails-app/internal/modules/payment"
在 PrivateGroup 块内加:
payment.SetupPaymentRouter(PrivateGroup)
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/api.go internal/initialize/router.go
git commit -m "feat: 注册支付API路由——/vehicle/exit/preview + /vehicle/exit/confirm"
Files: Modify frontend/src/api/vehicle.js, frontend/src/view/parking/entryExit.vue, frontend/src/lang/zh-CN.js, frontend/src/lang/en.js
在 frontend/src/api/vehicle.js 末尾新增:
// 出场费用预览
export const exitPreview = (data) => {
return service({ url: '/vehicle/exit/preview', method: 'post', data })
}
// 确认出场并支付
export const exitConfirm = (data) => {
return service({ url: '/vehicle/exit/confirm', method: 'post', data })
}
修改 confirmExit 函数逻辑:
// 1. 查询后→调用 preview 显示费用
async function queryVehicle() {
// ...existing query logic...
if (res.code === 0 && res.data) {
vehicleInfo.value = res.data
vehicleLoaded.value = true
// 出场模式:预览费用
if (activeTab.value === 'exit') {
await previewExitFee()
}
}
}
async function previewExitFee() {
try {
const res = await exitPreview({
plate_number: queryForm.plate_number.trim(),
rfid_tag: queryForm.rfid_tag.trim()
})
if (res.code === 0) {
exitData.value = res.data
}
} catch (e) { /* ignore preview errors */ }
}
// 2. 确认出场 → 调 confirm API
async function confirmExit() {
if (exitData.value.is_temp && paymentMethod.value === 'cash' && paidAmount.value < exitData.value.fee) {
ElMessage.warning(t.value.insufficientAmount)
return
}
await ElMessageBox.confirm(t.value.confirmExitMsg, t.value.confirmExitTitle, {
confirmButtonText: t.value.Confirm, cancelButtonText: t.value.Cancel, type: 'warning'
})
if (submitting.value) return
submitting.value = true
try {
const res = await exitConfirm({
plate_number: queryForm.plate_number.trim(),
rfid_tag: queryForm.rfid_tag.trim(),
payment_method: exitData.value.is_temp ? paymentMethod.value : 'free',
paid_amount: exitData.value.is_temp ? paidAmount.value : 0
})
if (res.code === 0) {
const d = res.data
const detail = t.value.exitRecordDetail
.replace('{entryTime}', new Date(d.entry_time * 1000).toLocaleString('zh-CN'))
.replace('{duration}', formatDuration(d.stay_time))
.replace('{fee}', d.fee.toFixed(2))
ElMessage.success(`${t.value.exitSuccessMsg}!${detail}`)
resetForm()
} else {
ElMessage.error(res.msg || t.value.exitFailed)
}
} catch (e) {
ElMessage.error(e.message || t.value.exitFailed)
} finally {
submitting.value = false
}
}
修改出场操作区,查询到车辆后根据 exitData.is_temp 显示:
<!-- 出场操作区 -->
<div v-if="activeTab === 'exit' && exitData" class="action-area">
<el-divider />
<el-descriptions :column="2" border>
<el-descriptions-item :label="$t.value.EntryTime">
{{ formatTime(exitData.entry_time) }}
</el-descriptions-item>
<el-descriptions-item :label="$t.value.StayDuration">
{{ formatDuration(exitData.stay_time) }}
</el-descriptions-item>
<el-descriptions-item :label="$t.value.Fee">
<span class="fee-amount">¥{{ exitData.fee.toFixed(2) }}</span>
</el-descriptions-item>
<el-descriptions-item :label="$t.value.VehicleType">
{{ exitData.vehicle_type || '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 临时车:支付方式 -->
<div v-if="exitData.is_temp" class="payment-section">
<p class="section-label">{{ $t.value.paymentMethod }}:</p>
<el-radio-group v-model="paymentMethod">
<el-radio-button value="cash">{{ $t.value.cashPayment }}</el-radio-button>
<el-radio-button value="wechat" disabled>{{ $t.value.wechatPayment }}</el-radio-button>
<el-radio-button value="pos" disabled>{{ $t.value.posPayment }}</el-radio-button>
</el-radio-group>
<!-- 现金收款 -->
<div v-if="paymentMethod === 'cash'" class="cash-input">
<span class="cash-label">{{ $t.value.cashReceived }}:</span>
<el-input-number v-model="paidAmount" :min="0" :precision="1" style="width: 160px" />
<span class="cash-label"> {{ $t.value.change }}: ¥{{ Math.max(0, paidAmount - exitData.fee).toFixed(1) }}</span>
</div>
</div>
<div style="margin-top: 16px">
<el-button type="danger" icon="Check" :loading="submitting" @click="confirmExit">
{{ exitData.is_temp ? $t.value.confirmPayAndExit : $t.value.confirmExitAndPay }}
</el-button>
</div>
</div>
需要新增的响应式变量:
const exitData = ref(null)
const paymentMethod = ref('cash')
const paidAmount = ref(0)
zh-CN.js 新增:
paymentMethod: '支付方式',
cashPayment: '现金',
wechatPayment: '扫码支付',
posPayment: 'POS刷卡',
cashReceived: '实收金额',
change: '找零',
confirmPayAndExit: '确认收款并放行',
insufficientAmount: '实收金额不足',
en.js 新增:
paymentMethod: 'Payment Method',
cashPayment: 'Cash',
wechatPayment: 'QR Code',
posPayment: 'POS Card',
cashReceived: 'Received',
change: 'Change',
confirmPayAndExit: 'Confirm & Release',
insufficientAmount: 'Insufficient amount',
cd frontend && npm run build
cd .. && go build -o build/bin/smart-parking.exe .
git add frontend/src/
git commit -m "feat: 出场Tab增加收费结算UI——现金收款+找零+免密放行"
wails dev
| 场景 | 预期 |
|---|---|
| 已出场车辆再出场 | "车辆未入场或已出场" |
| 实收不足 | "实收金额不足" |
| 免费车辆 + free | 直接放行,paid_amount=0 |
git add -A && git commit -m "verify: 收费结算端到端验证通过"