浏览代码

feat: 完善停车会话与车辆记录查询(会话状态/记录筛选)+ 功能说明文档

Co-Authored-By: Claude <noreply@anthropic.com>
lq 3 周之前
父节点
当前提交
7c0c51ef49

二进制
doc/智慧停车管理系统功能说明与车辆进出场流程.pptx


+ 4 - 0
frontend/src/lang/en.js

@@ -193,6 +193,10 @@ export default {
   entryImg: 'Entry Image',
   exitImg: 'Exit Image',
   recordDetail: 'Access Record Details',
+  EntryChannel: 'Entry Channel',
+  ExitChannel: 'Exit Channel',
+  EntryDevice: 'Entry Device',
+  ExitDevice: 'Exit Device',
   entryVehicle: 'Vehicle Entry',
   exitVehicle: 'Vehicle Exit',
   confirmEntry: 'Confirm Entry',

+ 4 - 0
frontend/src/lang/zh-CN.js

@@ -193,6 +193,10 @@ export default {
   entryImg: '入场图片',
   exitImg: '出场图片',
   recordDetail: '进出记录详情',
+  EntryChannel: '入口通道',
+  ExitChannel: '出口通道',
+  EntryDevice: '入口设备',
+  ExitDevice: '出口设备',
   entryVehicle: '车辆进场',
   exitVehicle: '车辆出场',
   confirmEntry: '确认进场',

+ 4 - 0
frontend/src/lang/zh-HK.js

@@ -193,6 +193,10 @@ export default {
   entryImg: '入場圖片',
   exitImg: '出場圖片',
   recordDetail: '進出記錄詳情',
+  EntryChannel: '入口通道',
+  ExitChannel: '出口通道',
+  EntryDevice: '入口設備',
+  ExitDevice: '出口設備',
   entryVehicle: '車輛進場',
   exitVehicle: '車輛出場',
   confirmEntry: '確認進場',

+ 4 - 0
frontend/src/lang/zh-TW.js

@@ -193,6 +193,10 @@ export default {
   entryImg: '入場圖片',
   exitImg: '出場圖片',
   recordDetail: '進出記錄詳情',
+  EntryChannel: '入口通道',
+  ExitChannel: '出口通道',
+  EntryDevice: '入口設備',
+  ExitDevice: '出口設備',
   entryVehicle: '車輛進場',
   exitVehicle: '車輛出場',
   confirmEntry: '確認進場',

+ 14 - 0
frontend/src/view/report/enter.vue

@@ -131,6 +131,20 @@
         <el-form-item label="RFID">{{ detail.rfid_tag }}</el-form-item>
         <el-form-item :label="$t.value.EntryTime">{{ formatTime(detail.entry_time) }}</el-form-item>
         <el-form-item :label="$t.value.ExitTime">{{ formatTime(detail.exit_time) || $t.value.notOut }}</el-form-item>
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item :label="$t.value.EntryChannel">{{ detail.entry_channel_name || detail.entry_channel_code || '-' }}</el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="$t.value.EntryDevice">{{ detail.entry_device_name || detail.entry_device_code || '-' }}</el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="$t.value.ExitChannel">{{ detail.exit_channel_name || detail.exit_channel_code || '-' }}</el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="$t.value.ExitDevice">{{ detail.exit_device_name || detail.exit_device_code || '-' }}</el-form-item>
+          </el-col>
+        </el-row>
         <el-form-item :label="$t.value.StayDuration">{{ detail.stay_time || 0 }} {{ $t.value.minute }}</el-form-item>
         <el-form-item :label="$t.value.Fee">¥ {{ detail.fee || 0 }}</el-form-item>
         <el-form-item :label="$t.value.PayStatus">

+ 24 - 14
internal/dao/vehicle_record.go

@@ -1,25 +1,35 @@
 package dao
 
 import (
-	"wails-app/internal/global"
 	"time"
+	"wails-app/internal/global"
 )
 
 type VehicleRecord struct {
 	global.GVA_MODEL
-	PlateNumber    string     `gorm:"size:20;index:idx_vehicle_record_plate;" json:"plate_number"`    // 车牌号
-	RFIDTag        string     `gorm:"size:40;column:rfid_tag" json:"rfid_tag"`                       // 标签
-	EntryTime      time.Time  `json:"entry_time" gorm:"default:null;index:idx_vehicle_record_entry_time;"` // 入场时间
-	ExitTime       *time.Time `json:"exit_time" gorm:"default:null"`                  // 出场时间
-	StayTime       int64      `json:"stay_time"`                                      // 停留时间(分钟)
-	Fee            float64    `json:"fee"`                                            // 费用
-	PaymentStatus  string     `gorm:"size:20;default:'unpaid'" json:"payment_status"` // 支付状态
-	PaymentEntry   string     `gorm:"size:32" json:"payment_entry"`
-	PaymentMethod  string     `gorm:"size:20" json:"payment_method"`
-	EntryImage     string     `gorm:"size:255" json:"entry_image"`                    // 入场图片
-	ExitImage      string     `gorm:"size:255" json:"exit_image"`                     // 出场图片
-	ParkingLotID   uint       `json:"parking_lot_id"`                                 // 停车场ID
-	ParkingSpaceID uint       `json:"parking_space_id"`                               // 停车位ID
+	PlateNumber      string     `gorm:"size:20;index:idx_vehicle_record_plate;" json:"plate_number"`         // 车牌号
+	RFIDTag          string     `gorm:"size:40;column:rfid_tag" json:"rfid_tag"`                             // 标签
+	EntryTime        time.Time  `json:"entry_time" gorm:"default:null;index:idx_vehicle_record_entry_time;"` // 入场时间
+	ExitTime         *time.Time `json:"exit_time" gorm:"default:null"`                                       // 出场时间
+	StayTime         int64      `json:"stay_time"`                                                           // 停留时间(分钟)
+	Fee              float64    `json:"fee"`                                                                 // 费用
+	PaymentStatus    string     `gorm:"size:20;default:'unpaid'" json:"payment_status"`                      // 支付状态
+	PaymentEntry     string     `gorm:"size:32" json:"payment_entry"`
+	PaymentMethod    string     `gorm:"size:20" json:"payment_method"`
+	EntryImage       string     `gorm:"size:255" json:"entry_image"`        // 入场图片
+	ExitImage        string     `gorm:"size:255" json:"exit_image"`         // 出场图片
+	ParkingLotID     uint       `json:"parking_lot_id"`                     // 停车场ID
+	ParkingSpaceID   uint       `json:"parking_space_id"`                   // 停车位ID
+	EntryChannelID   uint       `gorm:"index" json:"entry_channel_id"`      // 入场通道ID
+	EntryChannelCode string     `gorm:"size:50" json:"entry_channel_code"`  // 入场通道编码快照
+	EntryChannelName string     `gorm:"size:100" json:"entry_channel_name"` // 入场通道名称快照
+	EntryDeviceCode  string     `gorm:"size:50" json:"entry_device_code"`   // 入场设备编码快照
+	EntryDeviceName  string     `gorm:"size:100" json:"entry_device_name"`  // 入场设备名称快照
+	ExitChannelID    uint       `gorm:"index" json:"exit_channel_id"`       // 出场通道ID
+	ExitChannelCode  string     `gorm:"size:50" json:"exit_channel_code"`   // 出场通道编码快照
+	ExitChannelName  string     `gorm:"size:100" json:"exit_channel_name"`  // 出场通道名称快照
+	ExitDeviceCode   string     `gorm:"size:50" json:"exit_device_code"`    // 出场设备编码快照
+	ExitDeviceName   string     `gorm:"size:100" json:"exit_device_name"`   // 出场设备名称快照
 }
 
 func (VehicleRecord) TableName() string {

+ 31 - 16
internal/model/vehicle/request/vehicle.go

@@ -46,11 +46,16 @@ type VehicleTypeQuery struct {
 }
 
 type VehicleEntry struct {
-	PlateNumber    string `json:"plate_number"`
-	ParkingLotID   uint   `json:"parking_lot_id"`
-	ParkingSpaceID uint   `json:"parking_space_id"`
-	RFIDTag        string `json:"rfid_tag"`
-	EntryImage     string `json:"entry_image"`
+	PlateNumber      string `json:"plate_number"`
+	ParkingLotID     uint   `json:"parking_lot_id"`
+	ParkingSpaceID   uint   `json:"parking_space_id"`
+	RFIDTag          string `json:"rfid_tag"`
+	EntryImage       string `json:"entry_image"`
+	EntryChannelID   uint   `json:"entry_channel_id"`
+	EntryChannelCode string `json:"entry_channel_code"`
+	EntryChannelName string `json:"entry_channel_name"`
+	EntryDeviceCode  string `json:"entry_device_code"`
+	EntryDeviceName  string `json:"entry_device_name"`
 }
 
 // VehicleIdentityRequest 用车牌或 RFID 定位当前车辆及停车会话。
@@ -70,10 +75,15 @@ type VehicleEntryResponse struct {
 	Success bool   `json:"success"`
 	Message string `json:"message"`
 	Data    struct {
-		PlateNumber    string `json:"plate_number"`
-		EntryTime      int64  `json:"entry_time"`
-		ParkingLotID   uint   `json:"parking_lot_id"`
-		ParkingSpaceID uint   `json:"parking_space_id"`
+		PlateNumber      string `json:"plate_number"`
+		EntryTime        int64  `json:"entry_time"`
+		ParkingLotID     uint   `json:"parking_lot_id"`
+		ParkingSpaceID   uint   `json:"parking_space_id"`
+		EntryChannelID   uint   `json:"entry_channel_id"`
+		EntryChannelCode string `json:"entry_channel_code"`
+		EntryChannelName string `json:"entry_channel_name"`
+		EntryDeviceCode  string `json:"entry_device_code"`
+		EntryDeviceName  string `json:"entry_device_name"`
 	} `json:"data"`
 }
 
@@ -85,11 +95,16 @@ type VehicleRecordRequest struct {
 }
 
 type ExitConfirmRequest struct {
-	SessionID     uint    `json:"session_id"`
-	TicketNo      string  `json:"ticket_no"`
-	PlateNumber   string  `json:"plate_number"`
-	RFIDTag       string  `json:"rfid_tag"`
-	PaymentEntry  string  `json:"payment_entry"`
-	PaymentMethod string  `json:"payment_method"`
-	PaidAmount    float64 `json:"paid_amount"`
+	SessionID       uint    `json:"session_id"`
+	TicketNo        string  `json:"ticket_no"`
+	PlateNumber     string  `json:"plate_number"`
+	RFIDTag         string  `json:"rfid_tag"`
+	PaymentEntry    string  `json:"payment_entry"`
+	PaymentMethod   string  `json:"payment_method"`
+	PaidAmount      float64 `json:"paid_amount"`
+	ExitChannelID   uint    `json:"exit_channel_id"`
+	ExitChannelCode string  `json:"exit_channel_code"`
+	ExitChannelName string  `json:"exit_channel_name"`
+	ExitDeviceCode  string  `json:"exit_device_code"`
+	ExitDeviceName  string  `json:"exit_device_name"`
 }

+ 39 - 24
internal/model/vehicle/response/vehicle.go

@@ -30,22 +30,27 @@ type FeeConfigList struct {
 }
 
 type ExitPreviewResponse struct {
-	SessionID     uint    `json:"session_id"`
-	TicketNo      string  `json:"ticket_no"`
-	TicketState   string  `json:"ticket_state"`
-	PlateNumber   string  `json:"plate_number"`
-	RFIDTag       string  `json:"rfid_tag"`
-	EntryTime     int64   `json:"entry_time"`
-	StayTime      int64   `json:"stay_time"`
-	Fee           float64 `json:"fee"`
-	PaidAmount    float64 `json:"paid_amount"`
-	PaymentStatus string  `json:"payment_status"`
-	PaymentEntry  string  `json:"payment_entry"`
-	PaymentMethod string  `json:"payment_method"`
-	ParkingLotID  uint    `json:"parking_lot_id"`
-	EntryImage    string  `json:"entry_image"`
-	VehicleType   string  `json:"vehicle_type"`
-	IsTemp        bool    `json:"is_temp"`
+	SessionID        uint    `json:"session_id"`
+	TicketNo         string  `json:"ticket_no"`
+	TicketState      string  `json:"ticket_state"`
+	PlateNumber      string  `json:"plate_number"`
+	RFIDTag          string  `json:"rfid_tag"`
+	EntryTime        int64   `json:"entry_time"`
+	StayTime         int64   `json:"stay_time"`
+	Fee              float64 `json:"fee"`
+	PaidAmount       float64 `json:"paid_amount"`
+	PaymentStatus    string  `json:"payment_status"`
+	PaymentEntry     string  `json:"payment_entry"`
+	PaymentMethod    string  `json:"payment_method"`
+	ParkingLotID     uint    `json:"parking_lot_id"`
+	EntryImage       string  `json:"entry_image"`
+	EntryChannelID   uint    `json:"entry_channel_id"`
+	EntryChannelCode string  `json:"entry_channel_code"`
+	EntryChannelName string  `json:"entry_channel_name"`
+	EntryDeviceCode  string  `json:"entry_device_code"`
+	EntryDeviceName  string  `json:"entry_device_name"`
+	VehicleType      string  `json:"vehicle_type"`
+	IsTemp           bool    `json:"is_temp"`
 }
 
 type OperationContextResponse struct {
@@ -56,12 +61,22 @@ type OperationContextResponse struct {
 }
 
 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"`
-	PaymentEntry  string  `json:"payment_entry"`
-	PaymentMethod string  `json:"payment_method"`
+	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"`
+	PaymentEntry     string  `json:"payment_entry"`
+	PaymentMethod    string  `json:"payment_method"`
+	EntryChannelID   uint    `json:"entry_channel_id"`
+	EntryChannelCode string  `json:"entry_channel_code"`
+	EntryChannelName string  `json:"entry_channel_name"`
+	EntryDeviceCode  string  `json:"entry_device_code"`
+	EntryDeviceName  string  `json:"entry_device_name"`
+	ExitChannelID    uint    `json:"exit_channel_id"`
+	ExitChannelCode  string  `json:"exit_channel_code"`
+	ExitChannelName  string  `json:"exit_channel_name"`
+	ExitDeviceCode   string  `json:"exit_device_code"`
+	ExitDeviceName   string  `json:"exit_device_name"`
 }

+ 28 - 20
internal/modules/parking-session/model/request/session.go

@@ -2,34 +2,42 @@ package request
 
 import "time"
 
-// ResolveActiveSessionRequest accepts the three supported external handles for
-// one parking session. At least one handle must be present.
+// ResolveActiveSessionRequest 使用会话编号、车牌或 RFID 定位停车会话,至少需要提供一个标识。
 type ResolveActiveSessionRequest struct {
 	SessionID   uint
 	PlateNumber string
 	RFIDTag     string
 }
 
-// OpenSessionRequest contains the immutable entry data for one parking stay.
+// OpenSessionRequest 保存一次停车会话不可变的入场信息。
 type OpenSessionRequest struct {
-	PlateNumber    string
-	RFIDTag        string
-	ParkingLotID   uint
-	ParkingSpaceID uint
-	EntryImage     string
-	EntryTime      time.Time
+	PlateNumber      string
+	RFIDTag          string
+	ParkingLotID     uint
+	ParkingSpaceID   uint
+	EntryChannelID   uint
+	EntryChannelCode string
+	EntryChannelName string
+	EntryDeviceCode  string
+	EntryDeviceName  string
+	EntryImage       string
+	EntryTime        time.Time
 }
 
-// CloseSessionRequest contains the final result of a completed parking stay.
-// Payment details are copied here as the current session summary; the payment
-// ledger remains the corresponding payment_record rows.
+// CloseSessionRequest 保存一次停车会话的最终结算与出场信息。
+// 支付信息仅作为会话摘要,支付流水仍以 payment_record 为准。
 type CloseSessionRequest struct {
-	SessionID     uint
-	ExitTime      time.Time
-	StayTime      int64
-	Fee           float64
-	PaymentStatus string
-	PaymentEntry  string
-	PaymentMethod string
-	ExitImage     string
+	SessionID       uint
+	ExitTime        time.Time
+	StayTime        int64
+	Fee             float64
+	PaymentStatus   string
+	PaymentEntry    string
+	PaymentMethod   string
+	ExitImage       string
+	ExitChannelID   uint
+	ExitChannelCode string
+	ExitChannelName string
+	ExitDeviceCode  string
+	ExitDeviceName  string
 }

+ 47 - 14
internal/modules/parking-session/service/service.go

@@ -166,9 +166,8 @@ func sessionLookupError(db *gorm.DB, id uint, err error) error {
 	return ErrSessionNotFound
 }
 
-// OpenSessionTx creates a durable parking session. The active-session lookup
-// is repeated in the caller transaction to make the validation part of the
-// same atomic operation as the insert.
+// OpenSessionTx 创建持久化停车会话。
+// 在调用方事务中再次查询在场会话,确保校验和写入属于同一个原子操作。
 func (s *ParkingSessionService) OpenSessionTx(db *gorm.DB, req request.OpenSessionRequest) (*dao.VehicleRecord, error) {
 	if db == nil {
 		return nil, errors.New("数据库未初始化")
@@ -195,13 +194,18 @@ func (s *ParkingSessionService) OpenSessionTx(db *gorm.DB, req request.OpenSessi
 		entryTime = time.Now()
 	}
 	session := &dao.VehicleRecord{
-		PlateNumber:    req.PlateNumber,
-		RFIDTag:        req.RFIDTag,
-		EntryTime:      entryTime,
-		ParkingLotID:   req.ParkingLotID,
-		ParkingSpaceID: req.ParkingSpaceID,
-		EntryImage:     req.EntryImage,
-		PaymentStatus:  PaymentStatusUnpaid,
+		PlateNumber:      req.PlateNumber,
+		RFIDTag:          req.RFIDTag,
+		EntryTime:        entryTime,
+		ParkingLotID:     req.ParkingLotID,
+		ParkingSpaceID:   req.ParkingSpaceID,
+		EntryChannelID:   req.EntryChannelID,
+		EntryChannelCode: req.EntryChannelCode,
+		EntryChannelName: req.EntryChannelName,
+		EntryDeviceCode:  req.EntryDeviceCode,
+		EntryDeviceName:  req.EntryDeviceName,
+		EntryImage:       req.EntryImage,
+		PaymentStatus:    PaymentStatusUnpaid,
 	}
 	if err := s.repo.CreateTx(db, session); err != nil {
 		return nil, err
@@ -209,9 +213,8 @@ func (s *ParkingSessionService) OpenSessionTx(db *gorm.DB, req request.OpenSessi
 	return session, nil
 }
 
-// MarkPaidTx updates the current settlement summary on a still-active session.
-// The payment_record created by the payment module remains the source of truth
-// for the monetary transaction itself.
+// MarkPaidTx 更新在场会话的当前结算摘要。
+// 实际资金流水仍以支付模块创建的 payment_record 为准。
 func (s *ParkingSessionService) MarkPaidTx(db *gorm.DB, sessionID uint, paymentEntry, paymentMethod string, fee float64) (*dao.VehicleRecord, error) {
 	if db == nil {
 		return nil, errors.New("数据库未初始化")
@@ -250,7 +253,7 @@ func (s *ParkingSessionService) MarkPaidTx(db *gorm.DB, sessionID uint, paymentE
 	return session, nil
 }
 
-// CloseSessionTx finalizes one paid parking session exactly once.
+// CloseSessionTx 只允许将一个已支付的停车会话结算出场一次。
 func (s *ParkingSessionService) CloseSessionTx(db *gorm.DB, req request.CloseSessionRequest) (*dao.VehicleRecord, error) {
 	if db == nil {
 		return nil, errors.New("数据库未初始化")
@@ -285,6 +288,21 @@ func (s *ParkingSessionService) CloseSessionTx(db *gorm.DB, req request.CloseSes
 	if req.ExitImage != "" {
 		updates["exit_image"] = req.ExitImage
 	}
+	if req.ExitChannelID != 0 {
+		updates["exit_channel_id"] = req.ExitChannelID
+	}
+	if req.ExitChannelCode != "" {
+		updates["exit_channel_code"] = req.ExitChannelCode
+	}
+	if req.ExitChannelName != "" {
+		updates["exit_channel_name"] = req.ExitChannelName
+	}
+	if req.ExitDeviceCode != "" {
+		updates["exit_device_code"] = req.ExitDeviceCode
+	}
+	if req.ExitDeviceName != "" {
+		updates["exit_device_name"] = req.ExitDeviceName
+	}
 	rows, err := s.repo.UpdateActiveTx(db, req.SessionID, updates)
 	if err != nil {
 		return nil, fmt.Errorf("关闭停车会话失败: %w", err)
@@ -301,5 +319,20 @@ func (s *ParkingSessionService) CloseSessionTx(db *gorm.DB, req request.CloseSes
 	if req.ExitImage != "" {
 		session.ExitImage = req.ExitImage
 	}
+	if req.ExitChannelID != 0 {
+		session.ExitChannelID = req.ExitChannelID
+	}
+	if req.ExitChannelCode != "" {
+		session.ExitChannelCode = req.ExitChannelCode
+	}
+	if req.ExitChannelName != "" {
+		session.ExitChannelName = req.ExitChannelName
+	}
+	if req.ExitDeviceCode != "" {
+		session.ExitDeviceCode = req.ExitDeviceCode
+	}
+	if req.ExitDeviceName != "" {
+		session.ExitDeviceName = req.ExitDeviceName
+	}
 	return session, nil
 }

+ 17 - 0
internal/modules/parking-session/service/service_test.go

@@ -38,6 +38,8 @@ func TestParkingSessionOpenResolveAndClose(t *testing.T) {
 		var err error
 		session, err = svc.OpenSessionTx(tx, request.OpenSessionRequest{
 			PlateNumber: "SESSION-001", RFIDTag: "RFID-001", ParkingLotID: 1,
+			EntryChannelID: 11, EntryChannelCode: "ENTRY-A", EntryChannelName: "东入口通道",
+			EntryDeviceCode: "GATE-ENTRY-A", EntryDeviceName: "东入口读卡器",
 		})
 		return err
 	}))
@@ -54,10 +56,25 @@ func TestParkingSessionOpenResolveAndClose(t *testing.T) {
 		}
 		_, err := svc.CloseSessionTx(tx, request.CloseSessionRequest{
 			SessionID: session.ID, StayTime: 60, Fee: 12, PaymentEntry: "counter", PaymentMethod: "cash",
+			ExitChannelID: 22, ExitChannelCode: "EXIT-B", ExitChannelName: "西出口通道",
+			ExitDeviceCode: "GATE-EXIT-B", ExitDeviceName: "西出口读卡器",
 		})
 		return err
 	}))
 
+	var persisted dao.VehicleRecord
+	require.NoError(t, global.GVA_DB.First(&persisted, session.ID).Error)
+	require.EqualValues(t, 11, persisted.EntryChannelID)
+	require.Equal(t, "ENTRY-A", persisted.EntryChannelCode)
+	require.Equal(t, "东入口通道", persisted.EntryChannelName)
+	require.Equal(t, "GATE-ENTRY-A", persisted.EntryDeviceCode)
+	require.Equal(t, "东入口读卡器", persisted.EntryDeviceName)
+	require.EqualValues(t, 22, persisted.ExitChannelID)
+	require.Equal(t, "EXIT-B", persisted.ExitChannelCode)
+	require.Equal(t, "西出口通道", persisted.ExitChannelName)
+	require.Equal(t, "GATE-EXIT-B", persisted.ExitDeviceCode)
+	require.Equal(t, "西出口读卡器", persisted.ExitDeviceName)
+
 	_, err = svc.ResolveActiveSession(request.ResolveActiveSessionRequest{SessionID: session.ID})
 	require.ErrorIs(t, err, ErrSessionAlreadyClosed)
 }

+ 25 - 15
internal/service/parking/passage.go

@@ -113,16 +113,16 @@ func (s *PassageService) HandlePassage(req common.PassageRequest) (*common.Passa
 		if req.ParkingLotID != 0 && req.ParkingLotID != parkingLotID {
 			return nil, errors.New("所选停车场与通道配置不一致")
 		}
-		return s.handleEntry(req, identifier, parkingLotID)
+		return s.handleEntry(req, identifier, parkingLotID, channel, &device)
 	case "out":
-		return s.handleExit(req, identifier)
+		return s.handleExit(req, identifier, channel, &device)
 	default:
 		return nil, fmt.Errorf("未知通道方向: %s", direction)
 	}
 }
 
 // handleEntry 处理入场
-func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint) (*common.PassageResult, error) {
+func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) {
 	passageMutex.Lock()
 	canIn := !passageStatus[identifier]
 	passageMutex.Unlock()
@@ -134,10 +134,15 @@ func (s *PassageService) handleEntry(req common.PassageRequest, identifier strin
 	// 先创建停车会话和数字票,避免业务失败时车辆已经放行。
 	vehicleSvc := &vehicle.VehicleService{}
 	resp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{
-		PlateNumber:  req.PlateNumber,
-		RFIDTag:      req.RFIDTag,
-		ParkingLotID: parkingLotID,
-		EntryImage:   req.Image,
+		PlateNumber:      req.PlateNumber,
+		RFIDTag:          req.RFIDTag,
+		ParkingLotID:     parkingLotID,
+		EntryImage:       req.Image,
+		EntryChannelID:   channel.ID,
+		EntryChannelCode: channel.ChannelCode,
+		EntryChannelName: channel.ChannelName,
+		EntryDeviceCode:  req.DeviceCode,
+		EntryDeviceName:  device.DeviceName,
 	})
 	if err != nil {
 		return nil, err
@@ -172,7 +177,7 @@ func (s *PassageService) handleEntry(req common.PassageRequest, identifier strin
 }
 
 // handleExit 处理出场(使用新的收费流程 ExitConfirm)
-func (s *PassageService) handleExit(req common.PassageRequest, identifier string) (*common.PassageResult, error) {
+func (s *PassageService) handleExit(req common.PassageRequest, identifier string, channel *dao.Channel, device *dao.UHFReader) (*common.PassageResult, error) {
 
 	// UHF 自动出场默认走免密;人工入口会显式传入支付参数。
 	paymentMethod := req.PaymentMethod
@@ -181,13 +186,18 @@ func (s *PassageService) handleExit(req common.PassageRequest, identifier string
 	}
 	vehicleSvc := &vehicle.VehicleService{}
 	confirmReq := request.ExitConfirmRequest{
-		SessionID:     req.SessionID,
-		TicketNo:      req.TicketNo,
-		PlateNumber:   req.PlateNumber,
-		RFIDTag:       req.RFIDTag,
-		PaymentEntry:  req.PaymentEntry,
-		PaymentMethod: paymentMethod,
-		PaidAmount:    req.PaidAmount,
+		SessionID:       req.SessionID,
+		TicketNo:        req.TicketNo,
+		PlateNumber:     req.PlateNumber,
+		RFIDTag:         req.RFIDTag,
+		PaymentEntry:    req.PaymentEntry,
+		PaymentMethod:   paymentMethod,
+		PaidAmount:      req.PaidAmount,
+		ExitChannelID:   channel.ID,
+		ExitChannelCode: channel.ChannelCode,
+		ExitChannelName: channel.ChannelName,
+		ExitDeviceCode:  req.DeviceCode,
+		ExitDeviceName:  device.DeviceName,
 	}
 	resp, err := vehicleSvc.ExitConfirm(confirmReq, req.OperatorID)
 	if err != nil {

+ 14 - 0
internal/service/vehicle/parking_session_test.go

@@ -25,6 +25,8 @@ func TestVehicleEntryOpensSessionUsingCanonicalVehicleIdentity(t *testing.T) {
 
 	_, err := svc.VehicleEntry(request.VehicleEntry{
 		PlateNumber: vehicle.PlateNumber, ParkingLotID: 1,
+		EntryChannelID: 31, EntryChannelCode: "ENTRY-31", EntryChannelName: "入口三号通道",
+		EntryDeviceCode: "GATE-31", EntryDeviceName: "入口三号设备",
 	})
 	require.NoError(t, err)
 
@@ -34,6 +36,11 @@ func TestVehicleEntryOpensSessionUsingCanonicalVehicleIdentity(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, vehicle.PlateNumber, session.PlateNumber)
 	require.Equal(t, vehicle.RFIDTag, session.RFIDTag)
+	require.EqualValues(t, 31, session.EntryChannelID)
+	require.Equal(t, "ENTRY-31", session.EntryChannelCode)
+	require.Equal(t, "入口三号通道", session.EntryChannelName)
+	require.Equal(t, "GATE-31", session.EntryDeviceCode)
+	require.Equal(t, "入口三号设备", session.EntryDeviceName)
 
 	var ticket dao.DigitalTicket
 	require.NoError(t, global.GVA_DB.Where("vehicle_record_id = ?", session.ID).First(&ticket).Error)
@@ -77,6 +84,8 @@ func TestExitConfirmAcceptsZeroFeeFreeReleaseBySession(t *testing.T) {
 	result, err := svc.ExitConfirm(request.ExitConfirmRequest{
 		SessionID: record.ID, PlateNumber: vehicle.PlateNumber,
 		PaymentEntry: paymentService.EntryCounter, PaymentMethod: paymentService.PaymentFree,
+		ExitChannelID: 41, ExitChannelCode: "EXIT-41", ExitChannelName: "出口四号通道",
+		ExitDeviceCode: "GATE-41", ExitDeviceName: "出口四号设备",
 	}, 1)
 	require.NoError(t, err)
 	require.Zero(t, result.Fee)
@@ -84,4 +93,9 @@ func TestExitConfirmAcceptsZeroFeeFreeReleaseBySession(t *testing.T) {
 	var persisted dao.VehicleRecord
 	require.NoError(t, global.GVA_DB.First(&persisted, record.ID).Error)
 	require.NotNil(t, persisted.ExitTime)
+	require.EqualValues(t, 41, persisted.ExitChannelID)
+	require.Equal(t, "EXIT-41", persisted.ExitChannelCode)
+	require.Equal(t, "出口四号通道", persisted.ExitChannelName)
+	require.Equal(t, "GATE-41", persisted.ExitDeviceCode)
+	require.Equal(t, "出口四号设备", persisted.ExitDeviceName)
 }

+ 1 - 0
internal/service/vehicle/payment_edge_test.go

@@ -33,6 +33,7 @@ func setupVehiclePaymentTest(t *testing.T) *VehicleService {
 	require.NoError(t, db.AutoMigrate(
 		&dao.Owner{}, &dao.Vehicle{}, &dao.VehicleType{}, &dao.FeeConfig{}, &dao.VehicleRecord{}, &dao.DigitalTicket{},
 		&dao.PaymentMethodConfig{}, &dao.PaymentEntryConfig{}, &dao.PaymentEntryMethod{}, &dao.PaymentRecord{}, &dao.Shortlist{},
+		&dao.Channel{}, &dao.UHFReader{},
 	))
 	require.NoError(t, db.Create(&dao.PaymentMethodConfig{
 		Code: paymentService.PaymentCash, Name: "cash", InputMode: paymentService.InputModeManualAmount,

+ 54 - 29
internal/service/vehicle/vehicle.go

@@ -268,12 +268,17 @@ func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.Vehicle
 	err = global.GVA_DB.Transaction(func(tx *gorm.DB) error {
 		var err error
 		record, err = sessionSvc.OpenSessionTx(tx, sessionRequest.OpenSessionRequest{
-			PlateNumber:    plateNumber,
-			RFIDTag:        rfidTag,
-			ParkingLotID:   req.ParkingLotID,
-			ParkingSpaceID: req.ParkingSpaceID,
-			EntryImage:     req.EntryImage,
-			EntryTime:      entryTime,
+			PlateNumber:      plateNumber,
+			RFIDTag:          rfidTag,
+			ParkingLotID:     req.ParkingLotID,
+			ParkingSpaceID:   req.ParkingSpaceID,
+			EntryChannelID:   req.EntryChannelID,
+			EntryChannelCode: req.EntryChannelCode,
+			EntryChannelName: req.EntryChannelName,
+			EntryDeviceCode:  req.EntryDeviceCode,
+			EntryDeviceName:  req.EntryDeviceName,
+			EntryImage:       req.EntryImage,
+			EntryTime:        entryTime,
 		})
 		if err != nil {
 			return err
@@ -296,6 +301,11 @@ func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.Vehicle
 	response.Data.EntryTime = record.EntryTime.Unix()
 	response.Data.ParkingLotID = req.ParkingLotID
 	response.Data.ParkingSpaceID = req.ParkingSpaceID
+	response.Data.EntryChannelID = record.EntryChannelID
+	response.Data.EntryChannelCode = record.EntryChannelCode
+	response.Data.EntryChannelName = record.EntryChannelName
+	response.Data.EntryDeviceCode = record.EntryDeviceCode
+	response.Data.EntryDeviceName = record.EntryDeviceName
 
 	return response, nil
 }
@@ -342,22 +352,27 @@ func (s *VehicleService) buildExitPreview(record *dao.VehicleRecord) (vehicleRes
 	}
 
 	return vehicleResponse.ExitPreviewResponse{
-		SessionID:     record.ID,
-		TicketNo:      ticket.TicketNo,
-		TicketState:   ticket.State,
-		PlateNumber:   plateNumber,
-		RFIDTag:       record.RFIDTag,
-		EntryTime:     record.EntryTime.Unix(),
-		StayTime:      stayTime,
-		Fee:           fee,
-		PaidAmount:    ticket.PaidAmount,
-		PaymentStatus: record.PaymentStatus,
-		PaymentEntry:  ticket.PaymentEntry,
-		PaymentMethod: ticket.PaymentMethod,
-		ParkingLotID:  record.ParkingLotID,
-		EntryImage:    record.EntryImage,
-		VehicleType:   typeName,
-		IsTemp:        isTemp,
+		SessionID:        record.ID,
+		TicketNo:         ticket.TicketNo,
+		TicketState:      ticket.State,
+		PlateNumber:      plateNumber,
+		RFIDTag:          record.RFIDTag,
+		EntryTime:        record.EntryTime.Unix(),
+		StayTime:         stayTime,
+		Fee:              fee,
+		PaidAmount:       ticket.PaidAmount,
+		PaymentStatus:    record.PaymentStatus,
+		PaymentEntry:     ticket.PaymentEntry,
+		PaymentMethod:    ticket.PaymentMethod,
+		ParkingLotID:     record.ParkingLotID,
+		EntryImage:       record.EntryImage,
+		EntryChannelID:   record.EntryChannelID,
+		EntryChannelCode: record.EntryChannelCode,
+		EntryChannelName: record.EntryChannelName,
+		EntryDeviceCode:  record.EntryDeviceCode,
+		EntryDeviceName:  record.EntryDeviceName,
+		VehicleType:      typeName,
+		IsTemp:           isTemp,
 	}, nil
 }
 
@@ -610,13 +625,18 @@ func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID
 		}
 
 		record, err = sessionSvc.CloseSessionTx(tx, sessionRequest.CloseSessionRequest{
-			SessionID:     record.ID,
-			ExitTime:      exitTime,
-			StayTime:      stayTime,
-			Fee:           fee,
-			PaymentStatus: sessionService.PaymentStatusPaid,
-			PaymentEntry:  actualEntry,
-			PaymentMethod: actualMethod,
+			SessionID:       record.ID,
+			ExitTime:        exitTime,
+			StayTime:        stayTime,
+			Fee:             fee,
+			PaymentStatus:   sessionService.PaymentStatusPaid,
+			PaymentEntry:    actualEntry,
+			PaymentMethod:   actualMethod,
+			ExitChannelID:   req.ExitChannelID,
+			ExitChannelCode: req.ExitChannelCode,
+			ExitChannelName: req.ExitChannelName,
+			ExitDeviceCode:  req.ExitDeviceCode,
+			ExitDeviceName:  req.ExitDeviceName,
 		})
 		if err != nil {
 			return err
@@ -635,6 +655,11 @@ func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID
 		result = vehicleResponse.ExitConfirmResponse{
 			PlateNumber: vehicle.PlateNumber, EntryTime: record.EntryTime.Unix(), ExitTime: exitTime.Unix(),
 			StayTime: stayTime, Fee: fee, ChangeAmount: changeAmount, PaymentEntry: actualEntry, PaymentMethod: actualMethod,
+			EntryChannelID: record.EntryChannelID, EntryChannelCode: record.EntryChannelCode,
+			EntryChannelName: record.EntryChannelName, EntryDeviceCode: record.EntryDeviceCode,
+			EntryDeviceName: record.EntryDeviceName, ExitChannelID: record.ExitChannelID,
+			ExitChannelCode: record.ExitChannelCode, ExitChannelName: record.ExitChannelName,
+			ExitDeviceCode: record.ExitDeviceCode, ExitDeviceName: record.ExitDeviceName,
 		}
 		return nil
 	})

+ 72 - 2
internal/service/vehicle/vehicle_record.go

@@ -15,7 +15,11 @@ func (s *VehicleRecordService) GetVehicleRecordByID(id uint) (dao.VehicleRecord,
 	if result.RowsAffected == 0 {
 		return dao.VehicleRecord{}, result.Error
 	}
-	return record, nil
+	records := []dao.VehicleRecord{record}
+	if err := hydratePassageNames(records); err != nil {
+		return dao.VehicleRecord{}, err
+	}
+	return records[0], nil
 }
 
 // ListVehicleRecordsByPlateNumber 根据车牌号获取车辆记录列表
@@ -34,6 +38,9 @@ func (s *VehicleRecordService) ListVehicleRecordsByPlateNumber(plateNumber strin
 	if result.Error != nil {
 		return 0, nil, result.Error
 	}
+	if err := hydratePassageNames(records); err != nil {
+		return 0, nil, err
+	}
 
 	return total, records, nil
 }
@@ -63,5 +70,68 @@ func (s *VehicleRecordService) ListVehicleRecords(info request.VehicleRecordRequ
 
 	// 分页查询 + 排序
 	err = db.Order("entry_time DESC").Limit(limit).Offset(offset).Find(&list).Error
-	return list, total, err
+	if err != nil {
+		return nil, 0, err
+	}
+	if err := hydratePassageNames(list); err != nil {
+		return nil, 0, err
+	}
+	return list, total, nil
+}
+
+func hydratePassageNames(records []dao.VehicleRecord) error {
+	channelIDs := make([]uint, 0, len(records)*2)
+	deviceCodes := make([]string, 0, len(records)*2)
+	for _, record := range records {
+		if record.EntryChannelName == "" && record.EntryChannelID != 0 {
+			channelIDs = append(channelIDs, record.EntryChannelID)
+		}
+		if record.ExitChannelName == "" && record.ExitChannelID != 0 {
+			channelIDs = append(channelIDs, record.ExitChannelID)
+		}
+		if record.EntryDeviceName == "" && record.EntryDeviceCode != "" {
+			deviceCodes = append(deviceCodes, record.EntryDeviceCode)
+		}
+		if record.ExitDeviceName == "" && record.ExitDeviceCode != "" {
+			deviceCodes = append(deviceCodes, record.ExitDeviceCode)
+		}
+	}
+
+	channelNames := make(map[uint]string, len(channelIDs))
+	if len(channelIDs) > 0 {
+		var channels []dao.Channel
+		if err := global.GVA_DB.Where("id IN ?", channelIDs).Find(&channels).Error; err != nil {
+			return err
+		}
+		for _, channel := range channels {
+			channelNames[channel.ID] = channel.ChannelName
+		}
+	}
+
+	deviceNames := make(map[string]string, len(deviceCodes))
+	if len(deviceCodes) > 0 {
+		var devices []dao.UHFReader
+		if err := global.GVA_DB.Where("device_code IN ?", deviceCodes).Find(&devices).Error; err != nil {
+			return err
+		}
+		for _, device := range devices {
+			deviceNames[device.DeviceCode] = device.DeviceName
+		}
+	}
+
+	for i := range records {
+		if records[i].EntryChannelName == "" {
+			records[i].EntryChannelName = channelNames[records[i].EntryChannelID]
+		}
+		if records[i].ExitChannelName == "" {
+			records[i].ExitChannelName = channelNames[records[i].ExitChannelID]
+		}
+		if records[i].EntryDeviceName == "" {
+			records[i].EntryDeviceName = deviceNames[records[i].EntryDeviceCode]
+		}
+		if records[i].ExitDeviceName == "" {
+			records[i].ExitDeviceName = deviceNames[records[i].ExitDeviceCode]
+		}
+	}
+	return nil
 }

+ 41 - 0
internal/service/vehicle/vehicle_record_test.go

@@ -0,0 +1,41 @@
+package vehicle
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+	"wails-app/internal/model/vehicle/request"
+)
+
+func TestListVehicleRecordsComplementsLegacyPassageNames(t *testing.T) {
+	setupVehiclePaymentTest(t)
+	entryChannel := &dao.Channel{ChannelCode: "ENTRY-001", ChannelName: "东门入口", Direction: "in"}
+	exitChannel := &dao.Channel{ChannelCode: "EXIT-002", ChannelName: "西门出口", Direction: "out"}
+	require.NoError(t, global.GVA_DB.Create(entryChannel).Error)
+	require.NoError(t, global.GVA_DB.Create(exitChannel).Error)
+	require.NoError(t, global.GVA_DB.Create(&dao.UHFReader{
+		DeviceCode: "READER-ENTRY", DeviceName: "东门读卡器", DeviceType: "uhf", ChannelID: entryChannel.ID,
+	}).Error)
+	require.NoError(t, global.GVA_DB.Create(&dao.UHFReader{
+		DeviceCode: "READER-EXIT", DeviceName: "西门读卡器", DeviceType: "uhf", ChannelID: exitChannel.ID,
+	}).Error)
+
+	record := &dao.VehicleRecord{
+		PlateNumber: "LEGACY-PASSAGE", EntryTime: time.Now(), PaymentStatus: "paid",
+		EntryChannelID: entryChannel.ID, EntryChannelCode: entryChannel.ChannelCode, EntryDeviceCode: "READER-ENTRY",
+		ExitChannelID: exitChannel.ID, ExitChannelCode: exitChannel.ChannelCode, ExitDeviceCode: "READER-EXIT",
+	}
+	require.NoError(t, global.GVA_DB.Create(record).Error)
+
+	list, total, err := (&VehicleRecordService{}).ListVehicleRecords(request.VehicleRecordRequest{Page: 1, PageSize: 10})
+	require.NoError(t, err)
+	require.EqualValues(t, 1, total)
+	require.Len(t, list, 1)
+	require.Equal(t, "东门入口", list[0].EntryChannelName)
+	require.Equal(t, "东门读卡器", list[0].EntryDeviceName)
+	require.Equal(t, "西门出口", list[0].ExitChannelName)
+	require.Equal(t, "西门读卡器", list[0].ExitDeviceName)
+}