# 包月车管理 — 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. **Goal:** 实现包月车(月/季/年卡)办理、续费、退卡。办卡自动写 shortlist 白名单,进出场复用已有检查。 **Architecture:** 新增 `internal/modules/monthly/` 模块(DAO/service/repository/api)。办卡时创建 monthly_card + shortlist + payment_record。前端新增包月车管理页面。 --- ## 文件结构 | 文件 | 操作 | |------|------| | `internal/dao/monthly_card.go` | 新建 | | `internal/modules/monthly/api.go` | 新建 | | `internal/modules/monthly/service/service.go` | 新建 | | `internal/modules/monthly/repository/repo.go` | 新建 | | `internal/initialize/gorm.go` | 修改 | | `internal/initialize/router.go` | 修改 | | `internal/initialize/seed.go` | 修改 | | `frontend/src/view/parking/monthlyCard.vue` | 新建 | | `frontend/src/api/monthlyCard.js` | 新建 | | `frontend/src/lang/zh-CN.js` | 修改 | | `frontend/src/lang/en.js` | 修改 | --- ### Task 1: 数据层 + 后端核心 - [ ] **Step 1: 创建 DAO** Write `internal/dao/monthly_card.go`: ```go package dao import ( "time" "wails-app/internal/global" ) type MonthlyCard struct { global.GVA_MODEL VehicleID uint `gorm:"index" json:"vehicle_id"` Vehicle *Vehicle `gorm:"foreignKey:VehicleID" json:"vehicle"` CardType string `gorm:"size:20" json:"card_type"` // month / quarter / year StartDate time.Time `json:"start_date"` EndDate time.Time `json:"end_date"` Fee float64 `json:"fee"` PaymentStatus string `gorm:"size:20;default:paid" json:"payment_status"` // paid / refunded OperatorID uint `json:"operator_id"` Remark string `gorm:"size:200" json:"remark"` } func (MonthlyCard) TableName() string { return "monthly_card" } ``` - [ ] **Step 2: 注册 AutoMigrate** Edit `internal/initialize/gorm.go`, add `dao.MonthlyCard{}` to the AutoMigrate list (after `dao.Shortlist{}`): ```go dao.Shortlist{}, dao.MonthlyCard{}, ``` - [ ] **Step 3: 创建 Repository** Write `internal/modules/monthly/repository/repo.go`: ```go package repository import ( "wails-app/internal/dao" "wails-app/internal/global" ) type MonthlyCardRepository struct{} func (r *MonthlyCardRepository) Create(card *dao.MonthlyCard) error { return global.GVA_DB.Create(card).Error } func (r *MonthlyCardRepository) GetByVehicleID(vehicleID uint) (*dao.MonthlyCard, error) { var card dao.MonthlyCard err := global.GVA_DB.Where("vehicle_id = ? AND payment_status = ?", vehicleID, "paid").Preload("Vehicle").First(&card).Error if err != nil { return nil, err } return &card, nil } func (r *MonthlyCardRepository) Update(card *dao.MonthlyCard) error { return global.GVA_DB.Save(card).Error } type MonthlyCardQuery struct { PlateNumber string `form:"plate_number"` CardType string `form:"card_type"` Page int `form:"page"` PageSize int `form:"page_size"` } type MonthlyCardResult struct { ID uint `json:"id"` PlateNumber string `json:"plate_number"` CardType string `json:"card_type"` StartDate string `json:"start_date"` EndDate string `json:"end_date"` Fee float64 `json:"fee"` PaymentStatus string `json:"payment_status"` } func (r *MonthlyCardRepository) List(q MonthlyCardQuery) ([]MonthlyCardResult, int64, error) { db := global.GVA_DB.Table("monthly_card"). Select("monthly_card.id, vehicle.plate_number, monthly_card.card_type, monthly_card.start_date, monthly_card.end_date, monthly_card.fee, monthly_card.payment_status"). Joins("LEFT JOIN vehicle ON vehicle.id = monthly_card.vehicle_id") if q.PlateNumber != "" { db = db.Where("vehicle.plate_number LIKE ?", "%"+q.PlateNumber+"%") } if q.CardType != "" { db = db.Where("monthly_card.card_type = ?", q.CardType) } var total int64 db.Count(&total) if q.Page <= 0 { q.Page = 1 } if q.PageSize <= 0 { q.PageSize = 10 } offset := (q.Page - 1) * q.PageSize var results []MonthlyCardResult err := db.Order("monthly_card.id DESC").Offset(offset).Limit(q.PageSize).Scan(&results).Error return results, total, err } ``` - [ ] **Step 4: 创建 Service** Write `internal/modules/monthly/service/service.go`: ```go package service import ( "errors" "fmt" "time" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/modules/monthly/repository" paymentSvc "wails-app/internal/modules/payment/service" ) type MonthlyCardService struct { repo *repository.MonthlyCardRepository } func NewMonthlyCardService() *MonthlyCardService { return &MonthlyCardService{repo: &repository.MonthlyCardRepository{}} } func cardDays(cardType string) int { switch cardType { case "month": return 30 case "quarter": return 90 case "year": return 365 default: return 30 } } // Create 办理月卡:创建 monthly_card + shortlist 白名单 + payment_record func (s *MonthlyCardService) Create(vehicleID uint, cardType string, fee float64, operatorID uint, remark string) (*dao.MonthlyCard, error) { // 检查是否已有有效月卡 existing, _ := s.repo.GetByVehicleID(vehicleID) if existing != nil { return nil, errors.New("该车辆已有有效月卡,请先退卡或等过期后续费") } var vehicle dao.Vehicle if err := global.GVA_DB.First(&vehicle, vehicleID).Error; err != nil { return nil, errors.New("车辆不存在") } now := time.Now() startDate := now endDate := now.AddDate(0, 0, cardDays(cardType)) card := &dao.MonthlyCard{ VehicleID: vehicleID, CardType: cardType, StartDate: startDate, EndDate: endDate, Fee: fee, PaymentStatus: "paid", OperatorID: operatorID, Remark: remark, } if err := s.repo.Create(card); err != nil { return nil, fmt.Errorf("创建月卡失败: %w", err) } // 写入白名单(shortlist),到期日与月卡一致 shortlist := dao.Shortlist{ VehicleId: int(vehicleID), ListType: "白名单", ExpirationTime: &endDate, } global.GVA_DB.Create(&shortlist) // 创建 payment_record payRecord := dao.PaymentRecord{ RecordID: 0, PaymentMethod: paymentSvc.PaymentCash, Amount: fee, PaidAmount: fee, ChangeAmount: 0, OperatorID: operatorID, PaidAt: now, Remark: fmt.Sprintf("包月卡办理(%s)", cardType), } global.GVA_DB.Create(&payRecord) return card, nil } // Renew 续费:延长到期日 + 更新 shortlist func (s *MonthlyCardService) Renew(cardID uint, cardType string, fee float64, operatorID uint) error { var card dao.MonthlyCard if err := global.GVA_DB.First(&card, cardID).Error; err != nil { return errors.New("月卡记录不存在") } if card.PaymentStatus != "paid" { return errors.New("月卡已退卡,无法续费") } newEnd := card.EndDate.AddDate(0, 0, cardDays(cardType)) card.EndDate = newEnd card.Fee += fee global.GVA_DB.Save(&card) // 更新 shortlist 到期日 global.GVA_DB.Model(&dao.Shortlist{}).Where("vehicle_id = ? AND list_type = ?", card.VehicleID, "白名单"). Update("expiration_time", newEnd) return nil } // Refund 退卡:标记 refunded + 移除 shortlist func (s *MonthlyCardService) Refund(cardID uint) error { var card dao.MonthlyCard if err := global.GVA_DB.First(&card, cardID).Error; err != nil { return errors.New("月卡记录不存在") } card.PaymentStatus = "refunded" global.GVA_DB.Save(&card) // 移除白名单 global.GVA_DB.Where("vehicle_id = ? AND list_type = ?", card.VehicleID, "白名单").Unscoped().Delete(&dao.Shortlist{}) return nil } func (s *MonthlyCardService) List(q repository.MonthlyCardQuery) ([]repository.MonthlyCardResult, int64, error) { return s.repo.List(q) } ``` - [ ] **Step 5: 构建 + 提交** ```bash mkdir -p internal/modules/monthly/service internal/modules/monthly/repository go build -o build/bin/smart-parking.exe . git add internal/dao/monthly_card.go internal/modules/monthly/ internal/initialize/gorm.go git commit -m "feat: 包月车管理后端——DAO/service/repository,办卡=白名单+支付记录" ``` --- ### Task 2: API Handler + 路由注册 - [ ] **Step 1: 创建 api.go** Write `internal/modules/monthly/api.go`: ```go package monthly import ( "strconv" "github.com/gin-gonic/gin" "wails-app/internal/model/common/response" "wails-app/internal/modules/monthly/repository" "wails-app/internal/modules/monthly/service" utils "wails-app/internal/pkg" ) var monthlySvc = service.NewMonthlyCardService() type createReq struct { VehicleID uint `json:"vehicle_id"` CardType string `json:"card_type"` Fee float64 `json:"fee"` Remark string `json:"remark"` } func CreateCard(c *gin.Context) { var req createReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } card, err := monthlySvc.Create(req.VehicleID, req.CardType, req.Fee, utils.GetUserID(c), req.Remark) if err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithData(card, c) } type renewReq struct { CardID uint `json:"card_id"` CardType string `json:"card_type"` Fee float64 `json:"fee"` } func RenewCard(c *gin.Context) { var req renewReq if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } if err := monthlySvc.Renew(req.CardID, req.CardType, req.Fee, utils.GetUserID(c)); err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("续费成功", c) } func RefundCard(c *gin.Context) { id, _ := strconv.Atoi(c.Query("id")) if err := monthlySvc.Refund(uint(id)); err != nil { response.FailWithMessage(err.Error(), c) return } response.OkWithMessage("退卡成功", c) } func ListCards(c *gin.Context) { var q repository.MonthlyCardQuery if err := c.ShouldBindQuery(&q); err != nil { response.FailWithMessage(err.Error(), c) return } list, total, err := monthlySvc.List(q) 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 SetupMonthlyCardRouter(router *gin.RouterGroup) { mc := router.Group("/monthly-card") { mc.POST("/create", CreateCard) mc.POST("/renew", RenewCard) mc.DELETE("/refund", RefundCard) mc.GET("/list", ListCards) } } ``` - [ ] **Step 2: 注册路由** Edit `internal/initialize/router.go`: - Import: `"wails-app/internal/modules/monthly"` - In PrivateGroup add: `monthly.SetupMonthlyCardRouter(PrivateGroup)` - [ ] **Step 3: 构建 + 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','/monthly-card/create','POST'),('p','618','/monthly-card/create','POST'),('p','888','/monthly-card/renew','POST'),('p','618','/monthly-card/renew','POST'),('p','888','/monthly-card/refund','DELETE'),('p','618','/monthly-card/refund','DELETE'),('p','888','/monthly-card/list','GET'),('p','618','/monthly-card/list','GET');" git add internal/modules/monthly/api.go internal/initialize/router.go git commit -m "feat: 包月车管理API——create/renew/refund/list路由注册" ``` --- ### Task 3: 前端页面 + 菜单 - [ ] **Step 1: API 封装** Write `frontend/src/api/monthlyCard.js`: ```js import service from '@/utils/request' export const createMonthlyCard = (data) => service({ url: '/monthly-card/create', method: 'post', data }) export const renewMonthlyCard = (data) => service({ url: '/monthly-card/renew', method: 'post', data }) export const refundMonthlyCard = (id) => service({ url: '/monthly-card/refund?id=' + id, method: 'delete' }) export const getMonthlyCardList = (params) => service({ url: '/monthly-card/list', method: 'get', params }) ``` - [ ] **Step 2: 页面** Write `frontend/src/view/parking/monthlyCard.vue`: ```vue ``` - [ ] **Step 3: 翻译键** zh-CN.js: ```js monthlyCard: '包月车管理', monthCard: '月卡', quarterCard: '季卡', yearCard: '年卡', ``` en.js: ```js monthlyCard: 'Monthly Card', monthCard: 'Month', quarterCard: 'Quarter', yearCard: 'Year', ``` - [ ] **Step 4: 菜单** Seed + DB insert, ParentId: 20, Sort: 6: ```bash 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, 'monthlyCard', 'monthlyCard', 0, 'view/parking/monthlyCard.vue', 6, '包月车管理', 'calendar', datetime('now'), datetime('now')); INSERT INTO sys_authority_menus VALUES ('618', 33), ('888', 33), ('9527', 33);" ``` Seed.go: add menu entry + authority_menu + Casbin rules. - [ ] **Step 5: 构建 + 提交** ```bash cd frontend && npm run build && cd .. git add frontend/src/ internal/initialize/seed.go git commit -m "feat: 包月车管理前端——办理/续费/退卡页面+菜单+多语言" ```