Ver Fonte

feat: 交接班模块+收入报表模块——接班/交班/统计/按日汇总

lq há 1 mês atrás
pai
commit
ec60584ad4

+ 22 - 0
internal/dao/shift_record.go

@@ -0,0 +1,22 @@
+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" }

+ 1 - 0
internal/initialize/gorm.go

@@ -61,6 +61,7 @@ func RegisterTables() {
 		dao.Shortlist{},
 		dao.Shortlist{},
 		dao.MonthlyCard{},
 		dao.MonthlyCard{},
 		dao.DigitalTicket{},
 		dao.DigitalTicket{},
+		dao.ShiftRecord{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		global.GVA_LOG.Error("register table failed", zap.Error(err))
 		global.GVA_LOG.Error("register table failed", zap.Error(err))

+ 8 - 0
internal/initialize/router.go

@@ -10,6 +10,8 @@ import (
 	"wails-app/internal/router/owner"
 	"wails-app/internal/router/owner"
 	"wails-app/internal/modules/digital-ticket"
 	"wails-app/internal/modules/digital-ticket"
 	"wails-app/internal/modules/monthly"
 	"wails-app/internal/modules/monthly"
+	"wails-app/internal/modules/report"
+	"wails-app/internal/modules/shift"
 	"wails-app/internal/modules/payment"
 	"wails-app/internal/modules/payment"
 	"wails-app/internal/router/parking"
 	"wails-app/internal/router/parking"
 	"wails-app/internal/router/vehicle"
 	"wails-app/internal/router/vehicle"
@@ -117,6 +119,12 @@ func Routers() *gin.Engine {
 		// 数字票路由
 		// 数字票路由
 		digital_ticket.SetupDigitalTicketRouter(PrivateGroup)
 		digital_ticket.SetupDigitalTicketRouter(PrivateGroup)
 
 
+		// 交接班路由
+		shift.SetupShiftRouter(PrivateGroup)
+
+		// 收入报表路由
+		report.SetupReportRouter(PrivateGroup)
+
 		// UHF读写器路由
 		// UHF读写器路由
 		//uhf.SetupUHFRouter(PrivateGroup)
 		//uhf.SetupUHFRouter(PrivateGroup)
 	}
 	}

+ 25 - 0
internal/modules/report/api.go

@@ -0,0 +1,25 @@
+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)
+}

+ 31 - 0
internal/modules/report/service/service.go

@@ -0,0 +1,31 @@
+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
+}

+ 79 - 0
internal/modules/shift/api.go

@@ -0,0 +1,79 @@
+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.OkWithData(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)
+}

+ 45 - 0
internal/modules/shift/repository/repo.go

@@ -0,0 +1,45 @@
+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
+}

+ 69 - 0
internal/modules/shift/service/service.go

@@ -0,0 +1,69 @@
+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{}}
+}
+
+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
+}
+
+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)
+}