# 收费结算模块 — 实现计划 > **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
{{ formatTime(exitData.entry_time) }} {{ formatDuration(exitData.stay_time) }} ¥{{ exitData.fee.toFixed(2) }} {{ exitData.vehicle_type || '-' }}
{{ $t.value.cashPayment }} {{ $t.value.wechatPayment }} {{ $t.value.posPayment }}
{{ $t.value.cashReceived }}: {{ $t.value.change }}: ¥{{ Math.max(0, paidAmount - exitData.fee).toFixed(1) }}
{{ exitData.is_temp ? $t.value.confirmPayAndExit : $t.value.confirmExitAndPay }}
``` 需要新增的响应式变量: ```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: 收费结算端到端验证通过" ```