# 交接班 + 收入报表 — 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. **Goal:** 交接班(接班/交班/差异统计)+ 收入报表(按日+停车场汇总) **Architecture:** 新增两个独立模块 `modules/shift/` 和 `modules/report/`,共享 payment_record 数据。 --- ## 文件结构 | 文件 | 操作 | |------|------| | `internal/dao/shift_record.go` | 新建 | | `internal/modules/shift/api.go` | 新建 | | `internal/modules/shift/service/service.go` | 新建 | | `internal/modules/shift/repository/repo.go` | 新建 | | `internal/modules/report/api.go` | 新建 | | `internal/modules/report/service/service.go` | 新建 | | `internal/initialize/gorm.go` | 修改 | | `internal/initialize/router.go` | 修改 | | `internal/initialize/seed.go` | 修改 | | `frontend/src/view/parking/shiftRecord.vue` | 新建 | | `frontend/src/view/report/revenue.vue` | 新建 | | `frontend/src/api/shift.js` | 新建 | | `frontend/src/api/report.js` | 新建 | | `frontend/src/lang/zh-CN.js` | 修改 | | `frontend/src/lang/en.js` | 修改 | --- ### Task 1: 交接班——后端 - [ ] **Step 1: DAO** Write `internal/dao/shift_record.go`: ```go package dao import ( "time" "wails-app/internal/global" ) type ShiftRecord struct { global.GVA_MODEL OperatorID uint `gorm:"index" json:"operator_id"` StartTime time.Time `json:"start_time"` EndTime *time.Time `json:"end_time"` StartingCash float64 `json:"starting_cash"` CollectedCash float64 `json:"collected_cash"` ExpectedTotal float64 `json:"expected_total"` ActualTotal float64 `json:"actual_total"` Difference float64 `json:"difference"` Status string `gorm:"size:20;default:active" json:"status"` Remark string `gorm:"size:200" json:"remark"` } func (ShiftRecord) TableName() string { return "shift_record" } ``` Add to `internal/initialize/gorm.go` AutoMigrate (after `dao.DigitalTicket{}`): ```go dao.DigitalTicket{}, dao.ShiftRecord{}, ``` - [ ] **Step 2: Repository + Service** Write `internal/modules/shift/repository/repo.go`: ```go package repository import ( "wails-app/internal/dao" "wails-app/internal/global" ) type ShiftRepository struct{} func (r *ShiftRepository) Create(record *dao.ShiftRecord) error { return global.GVA_DB.Create(record).Error } func (r *ShiftRepository) Update(record *dao.ShiftRecord) error { return global.GVA_DB.Save(record).Error } func (r *ShiftRepository) GetActive(operatorID uint) (*dao.ShiftRecord, error) { var s dao.ShiftRecord err := global.GVA_DB.Where("operator_id = ? AND status = ?", operatorID, "active").First(&s).Error if err != nil { return nil, err } return &s, nil } type ShiftListQuery struct { Page int `form:"page"`; PageSize int `form:"page_size"` } func (r *ShiftRepository) List(q ShiftListQuery, operatorID uint) ([]dao.ShiftRecord, int64, error) { db := global.GVA_DB.Model(&dao.ShiftRecord{}).Where("operator_id = ?", operatorID) var total int64; db.Count(&total) if q.Page <= 0 { q.Page = 1 }; if q.PageSize <= 0 { q.PageSize = 10 } var list []dao.ShiftRecord err := db.Order("id DESC").Offset((q.Page-1)*q.PageSize).Limit(q.PageSize).Find(&list).Error return list, total, err } ``` Write `internal/modules/shift/service/service.go`: ```go package service import ( "errors" "fmt" "time" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/modules/shift/repository" ) type ShiftService struct { repo *repository.ShiftRepository } func NewShiftService() *ShiftService { return &ShiftService{repo: &repository.ShiftRepository{}} } // Start 接班 func (s *ShiftService) Start(operatorID uint, startingCash float64) (*dao.ShiftRecord, error) { active, _ := s.repo.GetActive(operatorID) if active != nil { return nil, errors.New("已有当班记录,请先交班") } rec := &dao.ShiftRecord{OperatorID: operatorID, StartTime: time.Now(), StartingCash: startingCash, Status: "active"} if err := s.repo.Create(rec); err != nil { return nil, err } return rec, nil } // End 交班 func (s *ShiftService) End(operatorID uint, actualCash float64, remark string) (*dao.ShiftRecord, error) { active, err := s.repo.GetActive(operatorID) if err != nil { return nil, errors.New("无当班记录") } // 统计当班现金收款 var collected float64 global.GVA_DB.Raw("SELECT COALESCE(SUM(amount),0) FROM payment_record WHERE payment_method='cash' AND paid_at >= ? AND paid_at < ?", active.StartTime, time.Now()).Scan(&collected) now := time.Now() expected := active.StartingCash + collected diff := actualCash - expected active.EndTime = &now active.CollectedCash = collected active.ExpectedTotal = expected active.ActualTotal = actualCash active.Difference = diff active.Status = "closed" active.Remark = remark if err := s.repo.Update(active); err != nil { return nil, err } fmt.Printf("交班完成: 接班%.2f + 当班%.2f = 应交%.2f, 实交%.2f, 差异%.2f\n", active.StartingCash, collected, expected, actualCash, diff) return active, nil } func (s *ShiftService) GetCurrent(operatorID uint) (*dao.ShiftRecord, error) { return s.repo.GetActive(operatorID) } func (s *ShiftService) List(q repository.ShiftListQuery, operatorID uint) ([]dao.ShiftRecord, int64, error) { return s.repo.List(q, operatorID) } ``` - [ ] **Step 3: API + 路由注册** Write `internal/modules/shift/api.go`: ```go package shift import ( "github.com/gin-gonic/gin" "wails-app/internal/model/common/response" "wails-app/internal/modules/shift/repository" "wails-app/internal/modules/shift/service" utils "wails-app/internal/pkg" ) var shiftSvc = service.NewShiftService() type startReq struct { StartingCash float64 `json:"starting_cash"` } type endReq struct { ActualCash float64 `json:"actual_cash"`; Remark string `json:"remark"` } func StartShift(c *gin.Context) { var req startReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c); return } rec, err := shiftSvc.Start(utils.GetUserID(c), req.StartingCash) if err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithData(rec, c) } func EndShift(c *gin.Context) { var req endReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c); return } rec, err := shiftSvc.End(utils.GetUserID(c), req.ActualCash, req.Remark) if err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithData(rec, c) } func GetCurrentShift(c *gin.Context) { rec, err := shiftSvc.GetCurrent(utils.GetUserID(c)) if err != nil { response.FailWithDetailed(gin.H{"has_active": false}, "无当班", c); return } response.OkWithData(rec, c) } func ListShifts(c *gin.Context) { var q repository.ShiftListQuery if err := c.ShouldBindQuery(&q); err != nil { response.FailWithMessage(err.Error(), c); return } list, total, err := shiftSvc.List(q, utils.GetUserID(c)) if err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithDetailed(response.PageResult{List: list, Total: total, Page: q.Page, PageSize: q.PageSize}, "查询成功", c) } func SetupShiftRouter(router *gin.RouterGroup) { sr := router.Group("/shift") sr.POST("/start", StartShift) sr.POST("/end", EndShift) sr.GET("/current", GetCurrentShift) sr.GET("/list", ListShifts) } ``` Register in `internal/initialize/router.go` — import `"wails-app/internal/modules/shift"`, add: ```go shift.SetupShiftRouter(PrivateGroup) ``` - [ ] **Step 4: Casbin + 编译+提交** ```bash go build -o build/bin/smart-parking.exe . sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype,v0,v1,v2) VALUES ('p','888','/shift/start','POST'),('p','618','/shift/start','POST'),('p','888','/shift/end','POST'),('p','618','/shift/end','POST'),('p','888','/shift/current','GET'),('p','618','/shift/current','GET'),('p','888','/shift/list','GET'),('p','618','/shift/list','GET');" git add internal/dao/shift_record.go internal/modules/shift/ internal/initialize/ git commit -m "feat: 交接班模块——接班/交班/当班统计/差异计算" ``` --- ### Task 2: 收入报表——后端 - [ ] **Step 1: Service + API** Write `internal/modules/report/service/service.go`: ```go package service import "wails-app/internal/global" type RevenueItem struct { Date string `json:"date"` ParkingLot string `json:"parking_lot"` Cash float64 `json:"cash"` Free float64 `json:"free"` Total float64 `json:"total"` Count int64 `json:"count"` } type ReportService struct{} func (s *ReportService) RevenueReport(startDate, endDate string, lotID uint) ([]RevenueItem, error) { var items []RevenueItem db := global.GVA_DB.Table("payment_record p"). Select(`DATE(p.paid_at) as date, COALESCE(pr.lot_name,'-') as parking_lot, COALESCE(SUM(CASE WHEN p.payment_method='cash' THEN p.amount ELSE 0 END),0) as cash, COALESCE(SUM(CASE WHEN p.payment_method='free' THEN p.amount ELSE 0 END),0) as free, COALESCE(SUM(p.amount),0) as total, COUNT(*) as count`). Joins("LEFT JOIN vehicle_record vr ON vr.id = p.record_id"). Joins("LEFT JOIN parking_lot pr ON pr.id = vr.parking_lot_id"). Where("p.paid_at >= ? AND p.paid_at <= ?", startDate, endDate+" 23:59:59") if lotID > 0 { db = db.Where("vr.parking_lot_id = ?", lotID) } err := db.Group("DATE(p.paid_at), vr.parking_lot_id").Order("date DESC").Scan(&items).Error return items, err } ``` Write `internal/modules/report/api.go`: ```go package report import ( "strconv" "github.com/gin-gonic/gin" "wails-app/internal/model/common/response" "wails-app/internal/modules/report/service" ) var reportSvc = service.ReportService{} func RevenueReport(c *gin.Context) { lotID, _ := strconv.Atoi(c.Query("parking_lot_id")) items, err := reportSvc.RevenueReport(c.Query("start_date"), c.Query("end_date"), uint(lotID)) if err != nil { response.FailWithMessage(err.Error(), c); return } response.OkWithData(items, c) } func SetupReportRouter(router *gin.RouterGroup) { router.GET("/report/revenue", RevenueReport) } ``` Register in `internal/initialize/router.go` — import `"wails-app/internal/modules/report"`, add: ```go report.SetupReportRouter(PrivateGroup) ``` - [ ] **Step 2: Casbin + 编译+提交** ```bash go build -o build/bin/smart-parking.exe . sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype,v0,v1,v2) VALUES ('p','888','/report/revenue','GET'),('p','618','/report/revenue','GET');" git add internal/modules/report/ internal/initialize/router.go git commit -m "feat: 收入报表——按日+停车场汇总payment_record" ``` --- ### Task 3: 前端 - [ ] **Step 1: 交接班页面** Write `frontend/src/view/parking/shiftRecord.vue`: ```vue ``` Write `frontend/src/api/shift.js`: ```js import service from '@/utils/request' export const startShift = (data) => service({ url: '/shift/start', method: 'post', data }) export const endShift = (data) => service({ url: '/shift/end', method: 'post', data }) export const getCurrentShift = () => service({ url: '/shift/current', method: 'get' }) export const getShiftList = (params) => service({ url: '/shift/list', method: 'get', params }) ``` - [ ] **Step 2: 收入报表页面** Write `frontend/src/view/report/revenue.vue`: ```vue ``` Write `frontend/src/api/report.js`: ```js import service from '@/utils/request' export const getRevenueReport = (params) => service({ url: '/report/revenue', method: 'get', params }) ``` - [ ] **Step 3: 菜单 + 翻译** DB inserts: ```bash # 交接班: ParentId=20, Sort=7 sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO sys_base_menus (menu_level,parent_id,path,name,hidden,component,sort,title,icon,created_at,updated_at) VALUES (0,20,'shiftRecord','shiftRecord',0,'view/parking/shiftRecord.vue',7,'交接班管理','clock',datetime('now'),datetime('now')); INSERT INTO sys_authority_menus VALUES ('618',34),('888',34),('9527',34);" # 收入报表: ParentId=25, Sort=3 sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO sys_base_menus (menu_level,parent_id,path,name,hidden,component,sort,title,icon,created_at,updated_at) VALUES (0,25,'revenue','revenue',0,'view/report/revenue.vue',3,'收入报表','money',datetime('now'),datetime('now')); INSERT INTO sys_authority_menus VALUES ('618',35),('888',35),('9527',35);" ``` Seed.go: add menu entries + authority_menu + Casbin rules. zh-CN.js: ```js shiftRecord: '交接班管理', revenueReport: '收入报表', ``` en.js: ```js shiftRecord: 'Shift Record', revenueReport: 'Revenue Report', ``` - [ ] **Step 4: 编译+提交** ```bash cd frontend && npm run build && cd .. git add frontend/src/ internal/initialize/seed.go git commit -m "feat: 交接班前端+收入报表+菜单+多语言" ```