# 收费结算模块 — 实现计划

> **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**

```go
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` 行之后新增：

```go
PaymentMethod  string     `gorm:"size:20" json:"payment_method"`                   // 支付方式
```

- [ ] **Step 3: seed.go 中 AutoMigrate**

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

```go
db.AutoMigrate(&dao.PaymentRecord{})
```

- [ ] **Step 4: 数据库 ALTER TABLE（现有数据库）**

```bash
cd lc_garage
sqlite3 "$APPDATA/smart-parking/lc_garage.db" "ALTER TABLE vehicle_record ADD COLUMN payment_method TEXT DEFAULT '';"
```

- [ ] **Step 5: 编译验证 + 提交**

```bash
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**

```go
// 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**

```go
// 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**

```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
}

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**

```go
// 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: 编译验证 + 提交**

```bash
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.go` 的 `VehicleExit` 函数后面新增：

```go
// 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：
```go
"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`:

```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` 之后加一行：

```go
PaymentMethod  string     `gorm:"size:20" json:"payment_method"`
```

- [ ] **Step 3: 编译 + 提交**

```bash
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`:

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

```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: 编译 + 提交**

```bash
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`，在文件末尾新增：

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

```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"`
}
```

If the file already exists, add these types to it.

- [ ] **Step 3: vehicle.go 新增 ExitPreview**

In `internal/service/vehicle/vehicle.go`，在 `VehicleExit` 函数后面新增：

```go
// 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 中新增：
```go
vehicleResponse "wails-app/internal/model/vehicle/response"
```

- [ ] **Step 4: vehicle.go 新增 ExitConfirm**

在 `ExitPreview` 之后新增：

```go
// 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：
```go
paymentService "wails-app/internal/modules/payment/service"
```

- [ ] **Step 5: 编译 + 提交**

```bash
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`:

```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**

```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" }
```

- [ ] **Step 2: vehicle_record 加字段**

在 `internal/dao/vehicle_record.go` 的 `PaymentStatus` 之后加：

```go
PaymentMethod  string     `gorm:"size:20" json:"payment_method"`
```

- [ ] **Step 3: 现有数据库迁移**

```bash
sqlite3 "$APPDATA/smart-parking/lc_garage.db" "ALTER TABLE vehicle_record ADD COLUMN payment_method TEXT DEFAULT '';"
```

- [ ] **Step 4: 编译 + 提交**

```bash
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**

```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
}
```

- [ ] **Step 2: Service（含支付常量）**

```go
// 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: 编译 + 提交**

```bash
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` 末尾新增：

```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`：

```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.go` 的 `VehicleExit` 函数之后新增。文件头部新增 import：

```go
vehicleResponse "wails-app/internal/model/vehicle/response"
```

```go
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"`）：

```go
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: 编译 + 提交**

```bash
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**

```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 加：

```go
"wails-app/internal/modules/payment"
```

在 `PrivateGroup` 块内加：

```go
payment.SetupPaymentRouter(PrivateGroup)
```

- [ ] **Step 3: 编译 + 提交**

```bash
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` 末尾新增：

```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` 函数逻辑：

```js
// 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` 显示：

```html
<!-- 出场操作区 -->
<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>
```

需要新增的响应式变量：
```js
const exitData = ref(null)
const paymentMethod = ref('cash')
const paidAmount = ref(0)
```

- [ ] **Step 4: 翻译键**

zh-CN.js 新增：
```js
paymentMethod: '支付方式',
cashPayment: '现金',
wechatPayment: '扫码支付',
posPayment: 'POS刷卡',
cashReceived: '实收金额',
change: '找零',
confirmPayAndExit: '确认收款并放行',
insufficientAmount: '实收金额不足',
```

en.js 新增：
```js
paymentMethod: 'Payment Method',
cashPayment: 'Cash',
wechatPayment: 'QR Code',
posPayment: 'POS Card',
cashReceived: 'Received',
change: 'Change',
confirmPayAndExit: 'Confirm & Release',
insufficientAmount: 'Insufficient amount',
```

- [ ] **Step 5: 编译 + 提交**

```bash
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: 启动**

```bash
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: 提交**

```bash
git add -A && git commit -m "verify: 收费结算端到端验证通过"
```
