| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- package service
- import (
- "errors"
- "fmt"
- "regexp"
- "strings"
- "time"
- "wails-app/internal/dao"
- "wails-app/internal/global"
- "wails-app/internal/modules/payment/repository"
- "gorm.io/gorm"
- )
- // Internal payment constants and configuration values.
- const (
- PaymentCash = "cash"
- PaymentFree = "free"
- PaymentPOS = "pos"
- PaymentTicketQR = "ticket_qr"
- InputModeManualAmount = "manual_amount"
- InputModeConfirm = "confirm"
- InputModeExternal = "external"
- EntryCounter = "counter"
- EntryTicket = "ticket"
- // Scene* are kept as source-compatible aliases for older callers. New code
- // should pass an Entry* value and treat it as the payment entry, not a
- // property of the payment method.
- SceneCounter = EntryCounter
- SceneTicket = EntryTicket
- )
- var paymentCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{1,31}$`)
- type PaymentService struct {
- repo *repository.PaymentRepository
- }
- func NewPaymentService() *PaymentService {
- return &PaymentService{repo: &repository.PaymentRepository{}}
- }
- func (s *PaymentService) ListMethods() ([]dao.PaymentMethodConfig, error) {
- return s.repo.MethodList()
- }
- func (s *PaymentService) ListEntries() ([]dao.PaymentEntryConfig, error) {
- return s.repo.EntryList()
- }
- func (s *PaymentService) ListEnabledEntries() ([]dao.PaymentEntryConfig, error) {
- entries, err := s.repo.EntryList()
- if err != nil {
- return nil, err
- }
- result := make([]dao.PaymentEntryConfig, 0, len(entries))
- for _, entry := range entries {
- if entry.Enabled {
- result = append(result, entry)
- }
- }
- return result, nil
- }
- func (s *PaymentService) ListEnabledMethods(entryCode string) ([]dao.PaymentMethodConfig, error) {
- if entryCode != "" && entryCode != EntryCounter && entryCode != EntryTicket {
- // Custom entries are valid too; only reject an empty value at the API
- // boundary where an entry is required.
- if _, err := s.repo.GetEntryByCode(nil, entryCode); err != nil {
- return nil, errors.New("无效的支付入口")
- }
- }
- methods, err := s.repo.EnabledMethodList(global.GVA_DB, entryCode)
- if err == nil {
- return methods, nil
- }
- // Existing installations created before the split may not have the
- // relation tables yet. Keep their old scene data usable until startup
- // migration has completed.
- if strings.Contains(strings.ToLower(err.Error()), "no such table") {
- return s.listLegacyEnabledMethods(entryCode), nil
- }
- return nil, err
- }
- func (s *PaymentService) listLegacyEnabledMethods(entryCode string) []dao.PaymentMethodConfig {
- methods, err := s.repo.MethodList()
- if err != nil {
- return nil
- }
- result := make([]dao.PaymentMethodConfig, 0, len(methods))
- for _, method := range methods {
- if method.Enabled && containsScene(method.Scenes, entryCode) {
- result = append(result, method)
- }
- }
- return result
- }
- func normalizeCodes(codes []string) []string {
- seen := make(map[string]bool, len(codes))
- result := make([]string, 0, len(codes))
- for _, code := range codes {
- code = strings.ToLower(strings.TrimSpace(code))
- if code != "" && !seen[code] {
- seen[code] = true
- result = append(result, code)
- }
- }
- return result
- }
- func normalizeMethod(method *dao.PaymentMethodConfig) error {
- method.Code = strings.ToLower(strings.TrimSpace(method.Code))
- method.Name = strings.TrimSpace(method.Name)
- method.InputMode = strings.TrimSpace(method.InputMode)
- method.Description = strings.TrimSpace(method.Description)
- if !paymentCodePattern.MatchString(method.Code) {
- return errors.New("支付编码必须以小写字母开头,只能包含小写字母、数字和下划线,长度为2-32位")
- }
- if method.Name == "" || len([]rune(method.Name)) > 50 {
- return errors.New("支付方式名称不能为空且不能超过50个字符")
- }
- validModes := map[string]bool{InputModeManualAmount: true, InputModeConfirm: true, InputModeExternal: true}
- if !validModes[method.InputMode] {
- return errors.New("无效的金额处理方式")
- }
- method.EntryCodes = normalizeCodes(method.EntryCodes)
- // Scenes is only read during migration. Do not use it as a runtime
- // authorization source after payment entries have been configured.
- method.Scenes = normalizeCodes(method.Scenes)
- return nil
- }
- func (s *PaymentService) CreateMethod(method *dao.PaymentMethodConfig) error {
- if err := normalizeMethod(method); err != nil {
- return err
- }
- method.IsSystem = false
- err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
- if err := s.repo.CreateMethodTx(tx, method); err != nil {
- if strings.Contains(strings.ToLower(err.Error()), "unique") {
- return errors.New("支付编码已存在")
- }
- return fmt.Errorf("新增支付方式失败: %w", err)
- }
- if len(method.EntryCodes) > 0 {
- if err := s.validateMethodEntryCodes(tx, method.EntryCodes); err != nil {
- return err
- }
- return s.repo.ReplaceMethodEntries(tx, method.Code, method.EntryCodes)
- }
- return nil
- })
- return err
- }
- func (s *PaymentService) UpdateMethod(id uint, updates *dao.PaymentMethodConfig) error {
- current, err := s.repo.GetMethodByID(id)
- if err != nil {
- return errors.New("支付方式不存在")
- }
- updates.Code = current.Code
- updates.IsSystem = current.IsSystem
- updates.EntryCodes = nil
- if current.Code == PaymentCash {
- updates.InputMode = InputModeManualAmount
- updates.Enabled = true
- }
- if err := normalizeMethod(updates); err != nil {
- return err
- }
- updates.ID = current.ID
- return s.repo.UpdateMethod(updates)
- }
- func (s *PaymentService) SetMethodEnabled(id uint, enabled bool) error {
- method, err := s.repo.GetMethodByID(id)
- if err != nil {
- return errors.New("支付方式不存在")
- }
- if method.Code == PaymentCash && !enabled {
- return errors.New("人工支付是基础功能,不能停用")
- }
- return s.repo.SetMethodEnabled(id, enabled)
- }
- func (s *PaymentService) DeleteMethod(id uint) error {
- method, err := s.repo.GetMethodByID(id)
- if err != nil {
- return errors.New("支付方式不存在")
- }
- if method.IsSystem {
- return errors.New("系统内置支付方式不能删除")
- }
- return s.repo.DeleteMethod(method)
- }
- func normalizeEntry(entry *dao.PaymentEntryConfig) error {
- entry.Code = strings.ToLower(strings.TrimSpace(entry.Code))
- entry.Name = strings.TrimSpace(entry.Name)
- entry.Description = strings.TrimSpace(entry.Description)
- if !paymentCodePattern.MatchString(entry.Code) {
- return errors.New("支付入口编码必须以小写字母开头,只能包含小写字母、数字和下划线,长度为2-32位")
- }
- if entry.Name == "" || len([]rune(entry.Name)) > 50 {
- return errors.New("支付入口名称不能为空且不能超过50个字符")
- }
- entry.MethodCodes = normalizeCodes(entry.MethodCodes)
- if !containsScene(entry.MethodCodes, PaymentCash) {
- entry.MethodCodes = append([]string{PaymentCash}, entry.MethodCodes...)
- }
- return nil
- }
- func (s *PaymentService) validateEntryCodes(db *gorm.DB, codes []string) error {
- for _, code := range codes {
- method, err := s.repo.GetMethodByCode(db, code)
- if err != nil || method.IsLegacy {
- return fmt.Errorf("支付方式“%s”不存在或不可用", code)
- }
- }
- return nil
- }
- func (s *PaymentService) validateMethodEntryCodes(db *gorm.DB, codes []string) error {
- for _, code := range codes {
- if _, err := s.repo.GetEntryByCode(db, code); err != nil {
- return fmt.Errorf("支付入口“%s”不存在", code)
- }
- }
- return nil
- }
- func (s *PaymentService) CreateEntry(entry *dao.PaymentEntryConfig) error {
- if err := normalizeEntry(entry); err != nil {
- return err
- }
- if err := s.validateEntryCodes(global.GVA_DB, entry.MethodCodes); err != nil {
- return err
- }
- entry.IsSystem = false
- return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
- if err := s.repo.CreateEntryTx(tx, entry); err != nil {
- if strings.Contains(strings.ToLower(err.Error()), "unique") {
- return errors.New("支付入口编码已存在")
- }
- return fmt.Errorf("新增支付入口失败: %w", err)
- }
- return s.repo.ReplaceEntryMethods(tx, entry.Code, entry.MethodCodes)
- })
- }
- func (s *PaymentService) UpdateEntry(id uint, updates *dao.PaymentEntryConfig) error {
- current, err := s.repo.GetEntryByID(id)
- if err != nil {
- return errors.New("支付入口不存在")
- }
- updates.Code = current.Code
- updates.IsSystem = current.IsSystem
- if err := normalizeEntry(updates); err != nil {
- return err
- }
- if err := s.validateEntryCodes(global.GVA_DB, updates.MethodCodes); err != nil {
- return err
- }
- updates.ID = current.ID
- return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
- if err := s.repo.UpdateEntryTx(tx, updates); err != nil {
- return err
- }
- return s.repo.ReplaceEntryMethods(tx, current.Code, updates.MethodCodes)
- })
- }
- func (s *PaymentService) SetEntryEnabled(id uint, enabled bool) error {
- if _, err := s.repo.GetEntryByID(id); err != nil {
- return errors.New("支付入口不存在")
- }
- return s.repo.SetEntryEnabled(id, enabled)
- }
- func (s *PaymentService) DeleteEntry(id uint) error {
- entry, err := s.repo.GetEntryByID(id)
- if err != nil {
- return errors.New("支付入口不存在")
- }
- if entry.IsSystem {
- return errors.New("系统内置支付入口不能删除")
- }
- return s.repo.DeleteEntry(entry)
- }
- func containsScene(scenes []string, scene string) bool {
- for _, item := range scenes {
- if item == scene {
- return true
- }
- }
- return false
- }
- func (s *PaymentService) ValidateMethod(db *gorm.DB, code, entryCode string) (*dao.PaymentMethodConfig, error) {
- if code == PaymentFree {
- if entryCode != EntryCounter {
- return nil, errors.New("免密放行不能用于当前支付场景")
- }
- entry, err := s.repo.GetEntryByCode(db, entryCode)
- if err == nil && !entry.Enabled {
- return nil, fmt.Errorf("支付入口“%s”已停用", entry.Name)
- }
- if err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such table") {
- return nil, errors.New("支付入口不存在")
- }
- return &dao.PaymentMethodConfig{Code: PaymentFree, Name: "免密放行", InputMode: InputModeConfirm, Enabled: true}, nil
- }
- entry, entryErr := s.repo.GetEntryByCode(db, entryCode)
- method, methodErr := s.repo.GetMethodByCode(db, code)
- if methodErr != nil {
- return nil, errors.New("支付方式不存在")
- }
- if !method.Enabled {
- return nil, fmt.Errorf("支付方式“%s”已停用", method.Name)
- }
- if entryErr != nil {
- if strings.Contains(strings.ToLower(entryErr.Error()), "no such table") && methodErr == nil {
- if containsScene(method.Scenes, entryCode) {
- return method, nil
- }
- return nil, fmt.Errorf("支付方式“%s”不适用于当前场景", method.Name)
- }
- return nil, errors.New("支付入口不存在")
- }
- if !entry.Enabled {
- return nil, fmt.Errorf("支付入口“%s”已停用", entry.Name)
- }
- allowed, err := s.repo.IsMethodAllowed(db, entryCode, code)
- if err != nil {
- if strings.Contains(strings.ToLower(err.Error()), "no such table") && containsScene(method.Scenes, entryCode) {
- return method, nil
- }
- return nil, err
- }
- if !allowed {
- return nil, fmt.Errorf("支付方式“%s”未配置到当前支付入口", method.Name)
- }
- return method, nil
- }
- func (s *PaymentService) ValidatePayment(db *gorm.DB, code, entryCode string, amount, paidAmount float64) (*dao.PaymentMethodConfig, float64, error) {
- method, err := s.ValidateMethod(db, code, entryCode)
- if err != nil {
- return nil, 0, err
- }
- if amount < 0 || paidAmount < 0 {
- return nil, 0, errors.New("支付金额不能为负数")
- }
- if code == PaymentFree {
- return method, 0, nil
- }
- if method.InputMode == InputModeManualAmount {
- if paidAmount < amount {
- return nil, 0, errors.New("实收金额不足")
- }
- return method, paidAmount, nil
- }
- return method, amount, nil
- }
- // ProcessPayment creates a payment_record and updates vehicle_record payment status
- func (s *PaymentService) ProcessPayment(recordID uint, paymentMethod string, amount float64, paidAmount float64, operatorID uint) (*dao.PaymentRecord, error) {
- return s.ProcessPaymentTx(global.GVA_DB, recordID, paymentMethod, amount, paidAmount, operatorID, EntryCounter)
- }
- // ProcessPaymentTx keeps the pre-split argument order for callers compiled
- // against the old service. The final argument is now interpreted as the
- // payment entry.
- func (s *PaymentService) ProcessPaymentTx(db *gorm.DB, recordID uint, paymentMethod string, amount float64, paidAmount float64, operatorID uint, paymentEntry string) (*dao.PaymentRecord, error) {
- return s.ProcessPaymentTxAtEntry(db, recordID, paymentEntry, paymentMethod, amount, paidAmount, operatorID)
- }
- func (s *PaymentService) ProcessPaymentTxAtEntry(db *gorm.DB, recordID uint, paymentEntry string, paymentMethod string, amount float64, paidAmount float64, operatorID uint) (*dao.PaymentRecord, error) {
- method, paidAmount, err := s.ValidatePayment(db, paymentMethod, paymentEntry, amount, paidAmount)
- if err != nil {
- return nil, err
- }
- changeAmount := 0.0
- if method.InputMode == InputModeManualAmount && paidAmount > amount {
- changeAmount = paidAmount - amount
- }
- now := time.Now()
- pr := &dao.PaymentRecord{
- RecordID: recordID,
- PaymentEntry: paymentEntry,
- PaymentMethod: paymentMethod,
- Amount: amount,
- PaidAmount: paidAmount,
- ChangeAmount: changeAmount,
- OperatorID: operatorID,
- PaidAt: now,
- }
- if err := db.Create(pr).Error; err != nil {
- return nil, fmt.Errorf("创建支付记录失败: %w", err)
- }
- // Update vehicle_record payment status
- if err := db.Model(&dao.VehicleRecord{}).Where("id = ?", recordID).Updates(map[string]interface{}{
- "payment_status": "paid",
- "payment_entry": paymentEntry,
- "payment_method": paymentMethod,
- }).Error; err != nil {
- return nil, fmt.Errorf("更新支付状态失败: %w", err)
- }
- return pr, nil
- }
- // QueryPayments 查询收费记录列表
- func (s *PaymentService) QueryPayments(q repository.PaymentListQuery) ([]repository.PaymentListResult, int64, error) {
- return s.repo.List(q)
- }
- // GetTodaySummary 获取当日收费汇总
- func (s *PaymentService) GetTodaySummary() (repository.TodaySummary, error) {
- return s.repo.GetTodaySummary()
- }
|