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 |
修改 |
Write internal/dao/shift_record.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{}):
dao.DigitalTicket{},
dao.ShiftRecord{},
Write internal/modules/shift/repository/repo.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:
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) }
Write internal/modules/shift/api.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:
shift.SetupShiftRouter(PrivateGroup)
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: 交接班模块——接班/交班/当班统计/差异计算"
Write internal/modules/report/service/service.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:
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:
report.SetupReportRouter(PrivateGroup)
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"
Write frontend/src/view/parking/shiftRecord.vue:
<template>
<div>
<el-card v-if="current" class="active-shift">
<template #header>当班中</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="接班时间">{{ current.start_time }}</el-descriptions-item>
<el-descriptions-item label="接班现金">¥{{ current.starting_cash.toFixed(2) }}</el-descriptions-item>
<el-descriptions-item label="当班收款">¥{{ current.collected_cash.toFixed(2) }}</el-descriptions-item>
<el-descriptions-item label="已收笔数">--</el-descriptions-item>
<el-descriptions-item label="应交总额"><b>¥{{ (current.starting_cash + current.collected_cash).toFixed(2) }}</b></el-descriptions-item>
</el-descriptions>
<div style="margin-top:16px">
<el-input-number v-model="endForm.actual_cash" :min="0" :precision="2" placeholder="实交现金" style="width:200px" />
<el-button type="danger" style="margin-left:12px" @click="doEnd">交班</el-button>
</div>
</el-card>
<el-card v-else>
<el-form inline>
<el-form-item label="接班现金">
<el-input-number v-model="startForm.starting_cash" :min="0" :precision="2" style="width:200px" />
</el-form-item>
<el-form-item><el-button type="primary" @click="doStart">接班</el-button></el-form-item>
</el-form>
</el-card>
<el-table :data="list" border style="margin-top:16px">
<el-table-column label="接班时间" prop="start_time" width="170" />
<el-table-column label="交班时间" prop="end_time" width="170" />
<el-table-column label="接班现金" width="100"><template #default="{row}">¥{{ row.starting_cash.toFixed(2) }}</template></el-table-column>
<el-table-column label="当班收款" width="100"><template #default="{row}">¥{{ row.collected_cash.toFixed(2) }}</template></el-table-column>
<el-table-column label="应交" width="100"><template #default="{row}">¥{{ row.expected_total.toFixed(2) }}</template></el-table-column>
<el-table-column label="实交" width="100"><template #default="{row}">¥{{ row.actual_total.toFixed(2) }}</template></el-table-column>
<el-table-column label="差异" width="100"><template #default="{row}"><span :style="{color:row.difference<0?'#f56c6c':'#67c23a'}">¥{{ row.difference.toFixed(2) }}</span></template></el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { startShift, endShift, getCurrentShift, getShiftList } from '@/api/shift'
const current = ref(null), list = ref([])
const startForm = reactive({ starting_cash: 0 })
const endForm = reactive({ actual_cash: 0 })
async function fetch() {
const r = await getCurrentShift(); if (r.code === 0) current.value = r.data; else current.value = null
const l = await getShiftList({ page: 1, page_size: 20 }); if (l.code === 0) { list.value = l.data.list || []; list.value.forEach(s => s.collected_cash = s.collected_cash || 0) }
}
async function doStart() { const r = await startShift(startForm); if (r.code === 0) { ElMessage.success('接班成功'); fetch() } else ElMessage.error(r.msg) }
async function doEnd() { const r = await endShift(endForm); if (r.code === 0) { ElMessage.success(`交班完成,差异¥${r.data.difference.toFixed(2)}`); fetch() } else ElMessage.error(r.msg) }
onMounted(fetch)
</script>
Write frontend/src/api/shift.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 })
Write frontend/src/view/report/revenue.vue:
<template>
<div>
<el-form inline>
<el-form-item label="日期">
<el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="停车场">
<el-select v-model="lotId" clearable placeholder="全部" style="width:180px">
<el-option v-for="l in lotList" :key="l.ID" :label="l.lot_name" :value="l.ID" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetch">{{ $t.value.Search }}</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border show-summary :summary-method="summaries">
<el-table-column label="日期" prop="date" width="120" />
<el-table-column label="停车场" prop="parking_lot" width="140" />
<el-table-column label="现金" width="100"><template #default="{row}">¥{{ row.cash.toFixed(2) }}</template></el-table-column>
<el-table-column label="免密" width="100"><template #default="{row}">¥{{ row.free.toFixed(2) }}</template></el-table-column>
<el-table-column label="总收入" width="120"><template #default="{row}">¥{{ row.total.toFixed(2) }}</template></el-table-column>
<el-table-column label="笔数" prop="count" width="80" />
</el-table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getRevenueReport } from '@/api/report'
import { obtainPullOverList } from '@/api/pullOver'
const dateRange = ref([]), lotId = ref(null), list = ref([]), lotList = ref([])
function summaries({ columns, data }) {
const sums = ['合计', '', 0, 0, 0, 0]
data.forEach(r => { sums[2] += r.cash; sums[3] += r.free; sums[4] += r.total; sums[5] += r.count })
return columns.map((c, i) => i >= 2 ? (i === 5 ? sums[i] : '¥' + sums[i].toFixed(2)) : sums[i])
}
async function fetch() {
const [s, e] = dateRange.value && dateRange.value.length === 2 ? dateRange.value : ['', '']
const r = await getRevenueReport({ start_date: s, end_date: e, parking_lot_id: lotId.value || '' })
if (r.code === 0) list.value = r.data || []
}
onMounted(async () => {
const today = new Date().toISOString().slice(0, 10)
dateRange.value = [today, today]
const l = await obtainPullOverList({ page: 1, pageSize: 100 })
if (l.code === 0) lotList.value = l.data?.list || []
fetch()
})
</script>
Write frontend/src/api/report.js:
import service from '@/utils/request'
export const getRevenueReport = (params) => service({ url: '/report/revenue', method: 'get', params })
DB inserts:
# 交接班: 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:
shiftRecord: '交接班管理',
revenueReport: '收入报表',
en.js:
shiftRecord: 'Shift Record',
revenueReport: 'Revenue Report',
cd frontend && npm run build && cd ..
git add frontend/src/ internal/initialize/seed.go
git commit -m "feat: 交接班前端+收入报表+菜单+多语言"