2026-07-17-payment-settlement-plan.md 41 KB

收费结算模块 — 实现计划

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 修改 新增翻译键

Task 1: 创建 payment_record DAO 实体 + vehicle_record 加字段

Files:

  • Create: internal/dao/payment_record.go
  • Modify: 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"
}
  • Step 2: vehicle_record 加 payment_method 字段

VehicleRecord 结构体的 PaymentStatus 行之后新增:

PaymentMethod  string     `gorm:"size:20" json:"payment_method"`                   // 支付方式
  • Step 3: seed.go 中 AutoMigrate

SeedSystemData 函数的 db := global.GVA_DB 之后加:

db.AutoMigrate(&dao.PaymentRecord{})
  • Step 4: 数据库 ALTER TABLE(现有数据库)
cd lc_garage
sqlite3 "$APPDATA/smart-parking/lc_garage.db" "ALTER TABLE vehicle_record ADD COLUMN payment_method TEXT DEFAULT '';"
  • Step 5: 编译验证 + 提交
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字段"

Task 2: 创建 payment 模块(model + repository + service)

Files:

  • Create: internal/modules/payment/model/request/request.go
  • Create: internal/modules/payment/model/response/response.go
  • Create: internal/modules/payment/repository/repo.go
  • Create: 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
}
  • Step 2: 响应 DTO
// 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"`
}
  • Step 3: Repository
// 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
}
  • Step 4: Service
// 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
}
  • Step 5: 编译验证 + 提交
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/
git commit -m "feat: 创建payment模块——model/repository/service层"

Task 3: 拆分 VehicleExit 为 preview/confirm

Files:

  • Modify: internal/service/vehicle/vehicle.go
  • Create: internal/modules/payment/api.go

  • [ ] Step 1: vehicle.go 新增 ExitPreview 方法

internal/service/vehicle/vehicle.goVehicleExit 函数后面新增:

// 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/response
  • If I add modules/payment/model/response import, it's a new leaf dependency
  • modules/payment/model/response imports nothing from the service layer

No 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:

  1. ExitPreview uses existing request.VehicleEntry as input
  2. ExitConfirm uses a new type with payment info

Let 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:

  • Vehicle module changes stay self-contained (service + model changes in same package tree)
  • Payment module handles only payment_record concerns
  • No new cross-module import complexity

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 修改 翻译键

Task 1: 数据层——payment_record 实体 + vehicle_record 加字段

Files: Create internal/dao/payment_record.go, Modify internal/dao/vehicle_record.go

  • Step 1: 创建 payment_record DAO

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"
}
  • Step 2: vehicle_record 加 PaymentMethod

Edit internal/dao/vehicle_record.go,在 PaymentStatus 之后加一行:

PaymentMethod  string     `gorm:"size:20" json:"payment_method"`
  • Step 3: 编译 + 提交
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"

Task 2: 支付模块——service + repository

Files: Create internal/modules/payment/service/service.go, Create internal/modules/payment/repository/repo.go

  • Step 1: Repository

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
}
  • Step 2: Service(含支付常量)

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
}
  • Step 3: 编译 + 提交
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/
git commit -m "feat: 创建payment模块——支付服务+仓储层"

Task 3: 请求/响应 DTO + VehicleExit 拆分

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

  • Step 1: 新增请求 DTO

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"`
}
  • Step 2: 新增响应 DTO

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.

  • Step 3: vehicle.go 新增 ExitPreview

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"
  • Step 4: vehicle.go 新增 ExitConfirm

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"
  • Step 5: 编译 + 提交
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——预览费用+确认出场支付"

Task 4: API 层——Handler + 路由注册

Files: Create internal/modules/payment/api.go, Modify internal/initialize/router.go

  • Step 1: 创建 api.go(Handler + Router Setup)

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 修改 翻译键

Task 1: 数据层

Files: Create internal/dao/payment_record.go, Modify internal/dao/vehicle_record.go

  • Step 1: payment_record DAO
// 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" }
  • Step 2: vehicle_record 加字段

internal/dao/vehicle_record.goPaymentStatus 之后加:

PaymentMethod  string     `gorm:"size:20" json:"payment_method"`
  • Step 3: 现有数据库迁移
sqlite3 "$APPDATA/smart-parking/lc_garage.db" "ALTER TABLE vehicle_record ADD COLUMN payment_method TEXT DEFAULT '';"
  • Step 4: 编译 + 提交
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"

Task 2: 支付模块 service + repository

Files: Create internal/modules/payment/service/service.go, internal/modules/payment/repository/repo.go

  • Step 1: Repository
// 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
}
  • Step 2: Service(含支付常量)
// 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
}
  • Step 3: 编译 + 提交
go build -o build/bin/smart-parking.exe .
git add internal/modules/payment/
git commit -m "feat: 创建payment模块——支付服务+仓储层"

Task 3: DTO + VehicleExit 拆分

Files: Modify internal/model/vehicle/request/vehicle.go, Create internal/model/vehicle/response/vehicle.go, Modify internal/service/vehicle/vehicle.go

  • Step 1: 请求 DTO

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"`
}
  • Step 2: 响应 DTO

创建 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"`
}
  • Step 3: vehicle.go 新增 ExitPreview

internal/service/vehicle/vehicle.goVehicleExit 函数之后新增。文件头部新增 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
}
  • Step 4: vehicle.go 新增 ExitConfirm

继续在 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
}
  • Step 5: 编译 + 提交
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确认出场支付"

Task 4: API Handler + 路由注册

Files: Create internal/modules/payment/api.go, Modify internal/initialize/router.go

  • Step 1: api.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)
	}
}
  • Step 2: router.go 注册

internal/initialize/router.go 头部 import 加:

"wails-app/internal/modules/payment"

PrivateGroup 块内加:

payment.SetupPaymentRouter(PrivateGroup)
  • Step 3: 编译 + 提交
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"

Task 5: 前端——API 封装 + 收费 UI + 翻译

Files: Modify frontend/src/api/vehicle.js, frontend/src/view/parking/entryExit.vue, frontend/src/lang/zh-CN.js, frontend/src/lang/en.js

  • Step 1: vehicle.js 新增 API

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 })
}
  • Step 2: entryExit.vue 出场 Tab 改为两步

修改 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
  }
}
  • Step 3: entryExit.vue 模板加收费 UI

修改出场操作区,查询到车辆后根据 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)
  • Step 4: 翻译键

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',
  • Step 5: 编译 + 提交
cd frontend && npm run build
cd .. && go build -o build/bin/smart-parking.exe .
git add frontend/src/
git commit -m "feat: 出场Tab增加收费结算UI——现金收款+找零+免密放行"

Task 6: 端到端验证

  • Step 1: 启动
wails dev
  • Step 2: 测试免密出场
  1. 登录 → 进出场操作 → 车辆出场 Tab
  2. 输入月租/VIP车牌号 → 查询 → 确认出场
  3. 验证:直接放行,无收费界面,payment_record.payment_method = "free"
  • Step 3: 测试现金收费
  1. 切换到车辆出场 Tab
  2. 输入临时车车牌号 → 查询 → 确认费用显示
  3. 选择"现金" → 输入实收 → 确认收款并放行
  4. 验证:payment_record 记录正确,找零正确
  • Step 4: 测试边界
场景 预期
已出场车辆再出场 "车辆未入场或已出场"
实收不足 "实收金额不足"
免费车辆 + free 直接放行,paid_amount=0
  • Step 5: 提交
git add -A && git commit -m "verify: 收费结算端到端验证通过"