Files

169 lines
5.2 KiB
Go

package quota
import (
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
ErrUnavailable = errors.New("配额配置不可读取")
ErrExceeded = errors.New("当前配额已满")
ErrInvalid = errors.New("配额配置不符合要求")
ErrVersionConflict = errors.New("配额已被其他用户修改,请刷新后重试")
)
type Service struct{ DB *gorm.DB }
func (s Service) Overview(request PageRequest) (Overview, error) {
if s.DB == nil {
return Overview{}, ErrUnavailable
}
request.Keyword = strings.TrimSpace(request.Keyword)
if request.Status != "" && request.Status != "active" && request.Status != "pending" && request.Status != "disabled" {
return Overview{}, ErrInvalid
}
used, err := occupiedCount(s.DB)
if err != nil {
return Overview{}, fmt.Errorf("count quota occupancy: %w", err)
}
now := time.Now().UTC()
summary := Summary{Used: int(used), ReadStatus: ReadStatusUnreadable, LastReadAt: now}
var setting Setting
if err = s.DB.First(&setting, "id = ?", SettingID).Error; err == nil && validLimit(setting.Limit) {
summary.Limit = setting.Limit
summary.Remaining = setting.Limit - int(used)
if summary.Remaining < 0 {
summary.Remaining = 0
}
summary.ReadStatus = ReadStatusReadable
summary.Source = setting.Source
summary.Version = setting.Version
summary.LastChanged = setting.UpdatedAt
}
query := s.DB.Table("sense_devices").Select("id, name, location, status, version, updated_at")
if request.Keyword != "" {
pattern := "%" + strings.ToLower(request.Keyword) + "%"
query = query.Where("LOWER(name) LIKE ? OR LOWER(location) LIKE ?", pattern, pattern)
}
if request.Status != "" {
query = query.Where("status = ?", request.Status)
}
var count int64
if err = query.Count(&count).Error; err != nil {
return Overview{}, fmt.Errorf("count quota devices: %w", err)
}
pageSize := request.GetPageSize()
if pageSize > 100 {
pageSize = 100
}
var rows []DeviceOccupancy
if err = query.Order("updated_at DESC").Limit(pageSize).Offset((request.GetPageIndex() - 1) * pageSize).Scan(&rows).Error; err != nil {
return Overview{}, fmt.Errorf("list quota devices: %w", err)
}
for index := range rows {
rows[index].Occupied = rows[index].Status != "disabled"
}
return Overview{Summary: summary, List: rows, Count: count, Tiers: deliveryTiers(summary.Limit)}, nil
}
func (s Service) Update(request UpdateRequest) (Summary, error) {
request.Reason = strings.TrimSpace(request.Reason)
if s.DB == nil || !validLimit(request.Limit) || request.Version < 1 || request.Reason == "" || len([]rune(request.Reason)) > 512 {
return Summary{}, ErrInvalid
}
err := s.DB.Transaction(func(tx *gorm.DB) error {
setting, err := lockSetting(tx)
if err != nil {
return err
}
if setting.Version != request.Version {
return ErrVersionConflict
}
now := time.Now().UTC()
result := tx.Model(&Setting{}).Where("id = ? AND version = ?", SettingID, request.Version).Updates(map[string]any{
"limit": request.Limit, "source": "database", "version": request.Version + 1,
"update_by": request.UpdateBy, "updated_at": now,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrVersionConflict
}
return tx.Create(&Change{ID: uuid.NewString(), OldLimit: setting.Limit, NewLimit: request.Limit, Reason: request.Reason, ChangedBy: request.UpdateBy, CreatedAt: now}).Error
})
if err != nil {
return Summary{}, err
}
overview, err := s.Overview(PageRequest{})
return overview.Summary, err
}
func ReadLimit(db *gorm.DB) (int, error) {
if db == nil {
return 0, ErrUnavailable
}
var setting Setting
if err := db.First(&setting, "id = ?", SettingID).Error; err != nil || !validLimit(setting.Limit) {
return 0, ErrUnavailable
}
return setting.Limit, nil
}
func WithAvailableSlot(db *gorm.DB, write func(*gorm.DB) error) error {
if db == nil {
return ErrUnavailable
}
return db.Transaction(func(tx *gorm.DB) error {
setting, err := lockSetting(tx)
if err != nil {
return err
}
used, err := occupiedCount(tx)
if err != nil {
return fmt.Errorf("count quota occupancy: %w", err)
}
if used >= int64(setting.Limit) {
return ErrExceeded
}
return write(tx)
})
}
func lockSetting(tx *gorm.DB) (Setting, error) {
var setting Setting
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&setting, "id = ?", SettingID).Error
if err != nil || !validLimit(setting.Limit) {
return Setting{}, ErrUnavailable
}
return setting, nil
}
func occupiedCount(db *gorm.DB) (int64, error) {
var count int64
err := db.Table("sense_devices").Where("status <> ?", "disabled").Count(&count).Error
return count, err
}
func validLimit(limit int) bool { return limit >= 1 && limit <= 100000 }
func deliveryTiers(configured int) []Tier {
tiers := []Tier{
{Limit: 16, Validation: "verified", DeliveryStatus: "默认学校试点交付档位"},
{Limit: 32, Validation: "unverified", DeliveryStatus: "需完成目标硬件压测后启用"},
{Limit: 64, Validation: "unverified", DeliveryStatus: "不作单机容量承诺"},
{Limit: 128, Validation: "unverified", DeliveryStatus: "当前不在交付承诺范围"},
}
for index := range tiers {
tiers[index].Configured = tiers[index].Limit == configured
}
return tiers
}