Files
junhong_cmp_fiber/internal/query/packagetrafficalert/query.go
break d5bcda94fe feat(套餐真流量预警): AUG26-004 真流量预警规则、达量扫描通知与导出
新增 000228 迁移:规则表 tb_package_traffic_alert_rule(每套餐商品至多一条,无软删除,package_id
非部分唯一约束)、达量预警快照表 tb_package_traffic_alert(以主套餐使用记录 + 阈值快照为唯一键,
触发时冻结用量、额度、比例、阈值、到期时间、归属与资产快照),并为 tb_package_usage 新增扫描
范围部分索引 idx_package_usage_alert_scope;down 在预警表存在数据时阻断回滚。

新增规则维护接口 GET/POST/PUT /api/admin/package-traffic-alert-rules(仅超级管理员与平台账号):
创建校验套餐存在且真流量额度大于零,阈值为 1%~100% 的两位小数;修改只影响后续扫描,不回填也
不改写既有预警快照;全部写操作记录操作者、前后值与时间。

新增每日 06:00(Asia/Shanghai)扫描任务 package:traffic:alert:scan,与套餐临期扫描共用 data_cleanup
队列:按资产汇总当前有效套餐的真流量,分子取使用记录真已用量、分母取使用记录真总量快照,命中
主套餐规则阈值时在同一事务创建预警与可靠通知事件;重复执行以唯一冲突视为已处理,不重复投递,
不建停机锁、不调用运营商。

新增预警列表、详情与异步导出 GET /api/admin/package-traffic-alerts、GET /api/admin/package-traffic-alerts/:id、
POST /api/admin/package-traffic-alerts/export,列表与详情一律读冻结快照;新增通知类型
package.traffic.alert 与受控目标 package_traffic_alert_detail,目标解析仅对超级管理员与平台账号
返回可跳转,越权与不存在统一按资源不可见处理。

同步 OpenAPI(cmd/gendocs、cmd/api/docs.go、pkg/openapi/handlers.go)、审计动作与资源注册、上下文
健康检查证据;归档变更并同步 package-traffic-alert 主 Spec。
2026-09-16 17:05:55 +08:00

547 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package packagetrafficalert 提供套餐真流量预警规则与达量预警的只读投影。
// Query 只做筛选、分页与 DTO 投影,不修改任何状态;越权与不存在统一按资源不可见处理。
package packagetrafficalert
import (
"context"
"strings"
"time"
"gorm.io/gorm"
domainpackagetrafficalert "github.com/break/junhong_cmp_fiber/internal/domain/packagetrafficalert"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// shanghaiLocation 是剩余天数推算使用的上海时区。
var shanghaiLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
// Query 查询套餐真流量预警规则与达量预警。
type Query struct {
db *gorm.DB
// now 可在验证时替换,默认使用系统时间。
now func() time.Time
}
// NewQuery 创建套餐真流量预警查询。
func NewQuery(db *gorm.DB) *Query {
return &Query{db: db, now: func() time.Time { return time.Now().UTC() }}
}
// ListRules 分页查询套餐真流量预警规则。
// 规则列表返回套餐名称与商品当前真流量额度,供维护页核对配置合法性;商品额度不作为预警分母。
func (q *Query) ListRules(ctx context.Context, request dto.ListPackageTrafficAlertRuleRequest) (*dto.PackageTrafficAlertRuleListResponse, error) {
if q == nil || q.db == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "套餐真流量预警查询尚未配置")
}
if err := requirePlatformOperator(ctx); err != nil {
return nil, err
}
page, pageSize := normalizePage(request.Page, request.PageSize)
var rows []ruleRow
query := q.db.WithContext(ctx).Table("tb_package_traffic_alert_rule AS r").
Joins("LEFT JOIN tb_package AS p ON p.id = r.package_id AND p.deleted_at IS NULL")
if request.PackageID != nil {
query = query.Where("r.package_id = ?", *request.PackageID)
}
if request.Enabled != nil {
query = query.Where("r.enabled = ?", boolToStatus(*request.Enabled))
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐真流量预警规则总数失败")
}
if err := query.Select("r.id, r.package_id, r.threshold_percent, r.enabled, r.remark, r.updated_at, " +
"COALESCE(p.package_name, '') AS package_name, COALESCE(p.real_data_mb, 0) AS real_data_mb").
Order("r.updated_at DESC, r.id DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Scan(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐真流量预警规则列表失败")
}
items := make([]dto.PackageTrafficAlertRuleItem, 0, len(rows))
for _, row := range rows {
items = append(items, dto.PackageTrafficAlertRuleItem{
ID: row.ID,
PackageID: row.PackageID,
PackageName: row.PackageName,
RealDataMB: row.RealDataMB,
ThresholdPercent: row.ThresholdPercent,
Enabled: row.Enabled == constants.StatusEnabled,
EnabledName: enabledStatusName(row.Enabled),
Remark: row.Remark,
UpdatedAt: row.UpdatedAt,
})
}
return &dto.PackageTrafficAlertRuleListResponse{Items: items, Total: total, Page: page, Size: pageSize}, nil
}
type ruleRow struct {
ID uint `gorm:"column:id"`
PackageID uint `gorm:"column:package_id"`
PackageName string `gorm:"column:package_name"`
RealDataMB int64 `gorm:"column:real_data_mb"`
ThresholdPercent float64 `gorm:"column:threshold_percent"`
Enabled int `gorm:"column:enabled"`
Remark string `gorm:"column:remark"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
// ListAlerts 分页查询套餐真流量达量预警。
// 先应用既有资产数据范围(当前对超级管理员与平台无实际过滤,保留为冻结语义与未来放开的前置),
// 再按套餐、店铺、业务员、资产/卡标识、阈值、触发时间与通知投递结果筛选。
func (q *Query) ListAlerts(ctx context.Context, request dto.ListPackageTrafficAlertRequest) (*dto.PackageTrafficAlertListResponse, error) {
if q == nil || q.db == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "套餐真流量预警查询尚未配置")
}
if err := requirePlatformOperator(ctx); err != nil {
return nil, err
}
page, pageSize := normalizePage(request.Page, request.PageSize)
query := q.applyAlertFilters(ctx, q.db.WithContext(ctx).Model(&model.PackageTrafficAlert{}), request)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐真流量达量预警总数失败")
}
var alerts []*model.PackageTrafficAlert
if err := query.Order("triggered_at DESC, id DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&alerts).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐真流量达量预警列表失败")
}
items, err := q.projectAlerts(ctx, alerts, false)
if err != nil {
return nil, err
}
return &dto.PackageTrafficAlertListResponse{Items: items, Total: total, Page: page, Size: pageSize}, nil
}
// GetAlert 查询单条达量预警详情。
// 越权与不存在统一返回资源不可见错误,不形成可枚举差异。
func (q *Query) GetAlert(ctx context.Context, alertID uint) (*dto.PackageTrafficAlertDetailResponse, error) {
if q == nil || q.db == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "套餐真流量预警查询尚未配置")
}
if alertID == 0 {
return nil, errors.New(errors.CodeInvalidParam)
}
if err := requirePlatformOperator(ctx); err != nil {
return nil, err
}
var alert model.PackageTrafficAlert
err := q.db.WithContext(ctx).Model(&model.PackageTrafficAlert{}).
Where("id = ?", alertID).
Scopes(func(scopeQuery *gorm.DB) *gorm.DB {
return applyAssetDataScope(ctx, scopeQuery, "tb_package_traffic_alert.shop_id_snapshot")
}).
First(&alert).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, invisibleAlertError()
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐真流量达量预警详情失败")
}
items, projectErr := q.projectAlerts(ctx, []*model.PackageTrafficAlert{&alert}, true)
if projectErr != nil {
return nil, projectErr
}
detail := &dto.PackageTrafficAlertDetailResponse{
PackageTrafficAlertItem: items[0],
NotificationEventID: alert.NotificationEventID,
}
detail.NotificationSummary = notificationSummary(items[0].NotificationStatus)
detail.ShopChangedSinceTrigger, detail.OwnerChangedSinceTrigger = q.detectOwnershipDrift(ctx, &alert)
return detail, nil
}
// applyAlertFilters 应用数据范围与筛选条件。
func (q *Query) applyAlertFilters(ctx context.Context, query *gorm.DB, request dto.ListPackageTrafficAlertRequest) *gorm.DB {
query = applyAssetDataScope(ctx, query, "tb_package_traffic_alert.shop_id_snapshot")
if request.PackageID != nil {
query = query.Where("tb_package_traffic_alert.package_id = ?", *request.PackageID)
}
if request.ShopID != nil {
query = query.Where("tb_package_traffic_alert.shop_id_snapshot = ?", *request.ShopID)
}
if request.BusinessOwnerAccountID != nil {
query = query.Where("tb_package_traffic_alert.business_owner_account_id_snapshot = ?", *request.BusinessOwnerAccountID)
}
if request.AssetType != "" {
query = query.Where("tb_package_traffic_alert.asset_type = ?", request.AssetType)
}
if keyword := strings.TrimSpace(request.AssetIdentifier); keyword != "" {
pattern := "%" + keyword + "%"
query = query.Where("(tb_package_traffic_alert.asset_identifier_snapshot ILIKE ? "+
"OR tb_package_traffic_alert.card_identifier_snapshot ILIKE ? "+
"OR tb_package_traffic_alert.counterpart_identifier_snapshot ILIKE ?)", pattern, pattern, pattern)
}
if request.ThresholdPercent != nil {
query = query.Where("tb_package_traffic_alert.threshold_percent_snapshot = ?",
domainpackagetrafficalert.NormalizeThresholdPercent(*request.ThresholdPercent))
}
if request.StartTime != nil {
query = query.Where("tb_package_traffic_alert.triggered_at >= ?", request.StartTime.UTC())
}
if request.EndTime != nil {
query = query.Where("tb_package_traffic_alert.triggered_at <= ?", request.EndTime.UTC())
}
return applyNotificationStatusFilter(query, request.NotificationStatus)
}
// projectAlerts 批量投影预警列表项。
// 冻结快照直接读预警行通知投递结果由可靠通知事件、Outbox 状态与站内通知事实交叉推导;
// 用户组按冻结的业务员账号实时推导(账号不变则稳定),不写入店铺表。
func (q *Query) projectAlerts(ctx context.Context, alerts []*model.PackageTrafficAlert, withReadState bool) ([]dto.PackageTrafficAlertItem, error) {
items := make([]dto.PackageTrafficAlertItem, 0, len(alerts))
if len(alerts) == 0 {
return items, nil
}
eventIDs := make([]string, 0, len(alerts))
ownerIDs := make([]uint, 0, len(alerts))
for _, alert := range alerts {
if alert.NotificationEventID != "" {
eventIDs = append(eventIDs, alert.NotificationEventID)
}
if alert.BusinessOwnerAccountIDSnapshot != nil && *alert.BusinessOwnerAccountIDSnapshot > 0 {
ownerIDs = append(ownerIDs, *alert.BusinessOwnerAccountIDSnapshot)
}
}
outboxStates, err := q.loadOutboxStates(ctx, eventIDs)
if err != nil {
return nil, err
}
notificationStates, err := q.loadNotificationStates(ctx, eventIDs)
if err != nil {
return nil, err
}
groupNames, err := q.loadBusinessUserGroupNames(ctx, ownerIDs)
if err != nil {
return nil, err
}
now := q.now()
for _, alert := range alerts {
item := dto.PackageTrafficAlertItem{
ID: alert.ID,
PackageUsageID: alert.PackageUsageID,
PackageID: alert.PackageID,
PackageName: alert.PackageNameSnapshot,
AssetType: alert.AssetType,
AssetID: alert.AssetID,
AssetIdentifier: alert.AssetIdentifierSnapshot,
CardIdentifier: alert.CardIdentifierSnapshot,
CounterpartIdentifier: alert.CounterpartIdentifierSnapshot,
DeviceType: alert.DeviceTypeSnapshot,
DeviceModel: alert.DeviceModelSnapshot,
UsedMB: alert.UsedMBSnapshot,
LimitMB: alert.LimitMBSnapshot,
UsagePercent: alert.UsagePercentSnapshot,
ThresholdPercent: alert.ThresholdPercentSnapshot,
ExpiresAt: alert.ExpiresAtSnapshot,
TriggeredAt: alert.TriggeredAt,
ShopName: alert.ShopNameSnapshot,
BusinessOwnerName: alert.BusinessOwnerNameSnapshot,
BusinessUserGroupNames: []string{},
}
if alert.ShopIDSnapshot > 0 {
shopID := alert.ShopIDSnapshot
item.ShopID = &shopID
}
if alert.BusinessOwnerAccountIDSnapshot != nil && *alert.BusinessOwnerAccountIDSnapshot > 0 {
ownerID := *alert.BusinessOwnerAccountIDSnapshot
item.BusinessOwnerAccountID = &ownerID
if names, ok := groupNames[ownerID]; ok {
item.BusinessUserGroupNames = names
}
}
if alert.ExpiresAtSnapshot != nil {
days := daysUntil(*alert.ExpiresAtSnapshot, now)
item.DaysRemaining = &days
}
status, deliveredAt, readAt, expiresAt := resolveNotificationState(alert, outboxStates, notificationStates)
item.NotificationStatus = status
item.NotificationStatusName = constants.GetPackageTrafficAlertNotifyStatusName(status)
item.NotificationDeliveredAt = deliveredAt
if withReadState {
item.NotificationReadAt = readAt
item.NotificationExpiresAt = expiresAt
}
items = append(items, item)
}
return items, nil
}
type outboxState struct {
Status int `gorm:"column:status"`
DeliveredAt *time.Time `gorm:"column:delivered_at"`
}
// loadOutboxStates 按事件ID批量读取 Outbox 状态。
func (q *Query) loadOutboxStates(ctx context.Context, eventIDs []string) (map[string]outboxState, error) {
result := make(map[string]outboxState, len(eventIDs))
if len(eventIDs) == 0 {
return result, nil
}
var rows []struct {
EventID string `gorm:"column:event_id"`
Status int `gorm:"column:status"`
DeliveredAt *time.Time `gorm:"column:delivered_at"`
}
if err := q.db.WithContext(ctx).Table("tb_outbox_event").
Select("event_id, status, delivered_at").
Where("event_id IN ?", eventIDs).
Scan(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询预警通知事件状态失败")
}
for _, row := range rows {
result[row.EventID] = outboxState{Status: row.Status, DeliveredAt: row.DeliveredAt}
}
return result, nil
}
type notificationState struct {
ReadAt *time.Time `gorm:"column:read_at"`
ExpiresAt *time.Time `gorm:"column:expires_at"`
}
// loadNotificationStates 按事件ID批量读取站内通知事实。
func (q *Query) loadNotificationStates(ctx context.Context, eventIDs []string) (map[string]notificationState, error) {
result := make(map[string]notificationState, len(eventIDs))
if len(eventIDs) == 0 {
return result, nil
}
var rows []struct {
EventID string `gorm:"column:event_id"`
ReadAt *time.Time `gorm:"column:read_at"`
ExpiresAt *time.Time `gorm:"column:expires_at"`
}
if err := q.db.WithContext(ctx).Table("tb_notification").
Select("event_id, read_at, expires_at").
Where("event_id IN ?", eventIDs).
Scan(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询预警站内通知失败")
}
for _, row := range rows {
result[row.EventID] = notificationState{ReadAt: row.ReadAt, ExpiresAt: row.ExpiresAt}
}
return result, nil
}
// loadBusinessUserGroupNames 按业务员账号批量推导当前所属业务用户组名称。
func (q *Query) loadBusinessUserGroupNames(ctx context.Context, accountIDs []uint) (map[uint][]string, error) {
result := make(map[uint][]string, len(accountIDs))
if len(accountIDs) == 0 {
return result, nil
}
var rows []struct {
AccountID uint `gorm:"column:account_id"`
GroupName string `gorm:"column:group_name"`
}
if err := q.db.WithContext(ctx).Table("tb_business_user_group_member AS m").
Select("m.account_id, g.name AS group_name").
Joins("JOIN tb_business_user_group AS g ON g.id = m.business_user_group_id AND g.deleted_at IS NULL").
Where("m.account_id IN ? AND m.deleted_at IS NULL", accountIDs).
Order("g.sort_order ASC, g.id ASC").
Scan(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询业务员业务用户组失败")
}
for _, row := range rows {
result[row.AccountID] = append(result[row.AccountID], row.GroupName)
}
return result, nil
}
// detectOwnershipDrift 判断资产当前归属店铺与店铺当前业务员是否已偏离触发快照。
func (q *Query) detectOwnershipDrift(ctx context.Context, alert *model.PackageTrafficAlert) (bool, bool) {
currentShopID, err := q.currentAssetShopID(ctx, alert.AssetType, alert.AssetID)
if err != nil || currentShopID == 0 {
return false, false
}
shopChanged := currentShopID != alert.ShopIDSnapshot
var shopOwner struct {
BusinessOwnerAccountID *uint `gorm:"column:business_owner_account_id"`
}
if err := q.db.WithContext(ctx).Table("tb_shop").
Select("business_owner_account_id").
Where("id = ?", currentShopID).
Scan(&shopOwner).Error; err != nil {
return shopChanged, false
}
ownerID := shopOwner.BusinessOwnerAccountID
ownerChanged := !sameOptionalID(ownerID, alert.BusinessOwnerAccountIDSnapshot)
return shopChanged, ownerChanged
}
// currentAssetShopID 查询资产当前所属店铺ID。
func (q *Query) currentAssetShopID(ctx context.Context, assetType string, assetID uint) (uint, error) {
if assetID == 0 {
return 0, nil
}
table := "tb_iot_card"
if assetType == constants.AssetTypeDevice {
table = "tb_device"
}
var shopID *uint
if err := q.db.WithContext(ctx).Table(table).
Select("shop_id").
Where("id = ? AND deleted_at IS NULL", assetID).
Scan(&shopID).Error; err != nil {
return 0, err
}
if shopID == nil {
return 0, nil
}
return *shopID, nil
}
// applyNotificationStatusFilter 按通知投递结果筛选。
// 结果由通知事件、Outbox 状态与站内通知事实推导,因此筛选必须与投影同口径。
func applyNotificationStatusFilter(query *gorm.DB, status *int) *gorm.DB {
if status == nil {
return query
}
const hasNotification = "EXISTS (SELECT 1 FROM tb_notification AS n WHERE n.event_id = tb_package_traffic_alert.notification_event_id)"
const outboxStatusExpr = `(SELECT oe.status FROM tb_outbox_event AS oe WHERE oe.event_id = tb_package_traffic_alert.notification_event_id)`
switch *status {
case constants.PackageTrafficAlertNotifyNoBusinessOwner:
return query.Where("tb_package_traffic_alert.notification_event_id = ''")
case constants.PackageTrafficAlertNotifyNotified:
return query.Where("tb_package_traffic_alert.notification_event_id <> ''").Where(hasNotification)
case constants.PackageTrafficAlertNotifyPending:
return query.Where("tb_package_traffic_alert.notification_event_id <> ''").
Where("NOT "+hasNotification).
Where(outboxStatusExpr+" IN ?", []int{constants.OutboxStatusPending, constants.OutboxStatusDelivering})
case constants.PackageTrafficAlertNotifyFailed:
return query.Where("tb_package_traffic_alert.notification_event_id <> ''").
Where("NOT "+hasNotification).
Where(outboxStatusExpr+" = ?", constants.OutboxStatusFailed)
case constants.PackageTrafficAlertNotifyRecipientGone:
return query.Where("tb_package_traffic_alert.notification_event_id <> ''").
Where("NOT "+hasNotification).
Where(outboxStatusExpr+" = ?", constants.OutboxStatusDelivered)
default:
return query
}
}
// resolveNotificationState 推导单条预警的通知投递结果,口径由 constants 统一定义。
func resolveNotificationState(alert *model.PackageTrafficAlert, outboxStates map[string]outboxState,
notificationStates map[string]notificationState) (int, *time.Time, *time.Time, *time.Time) {
if alert.NotificationEventID == "" {
return constants.ResolvePackageTrafficAlertNotifyStatus(false, nil, false), nil, nil, nil
}
state, hasNotification := notificationStates[alert.NotificationEventID]
var outboxStatus *int
var deliveredAt *time.Time
if outbox, ok := outboxStates[alert.NotificationEventID]; ok {
status := outbox.Status
outboxStatus = &status
deliveredAt = outbox.DeliveredAt
}
status := constants.ResolvePackageTrafficAlertNotifyStatus(true, outboxStatus, hasNotification)
if status == constants.PackageTrafficAlertNotifyNotified {
return status, deliveredAt, state.ReadAt, state.ExpiresAt
}
if status == constants.PackageTrafficAlertNotifyRecipientGone {
return status, deliveredAt, nil, nil
}
return status, nil, nil, nil
}
// notificationSummary 返回详情页的通知补充说明。
func notificationSummary(status int) string {
switch status {
case constants.PackageTrafficAlertNotifyNoBusinessOwner:
return "触发时店铺无有效业务员,只保存预警且不补发通知"
case constants.PackageTrafficAlertNotifyRecipientGone:
return "通知事件已投递,但接收人账号在投递时已失效,未生成站内通知"
case constants.PackageTrafficAlertNotifyFailed:
return "通知事件投递失败,已进入既有可靠投递恢复"
default:
return ""
}
}
// requirePlatformOperator 要求调用者为超级管理员或平台账号;其他账号统一按资源不可见处理。
func requirePlatformOperator(ctx context.Context) error {
userType := middleware.GetUserTypeFromContext(ctx)
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
return invisibleAlertError()
}
return nil
}
// applyAssetDataScope 应用既有资产数据范围。
// 范围为空表示不受限(超级管理员与平台账号当前无实际过滤),保留为冻结语义与未来放开的前置;
// 列名必须显式给出,因为预警行冻结的是 shop_id_snapshot而不是通用的 shop_id 列。
func applyAssetDataScope(ctx context.Context, query *gorm.DB, column string) *gorm.DB {
shopIDs := middleware.GetSubordinateShopIDs(ctx)
if len(shopIDs) == 0 {
return query
}
return query.Where(column+" IN ?", shopIDs)
}
// invisibleAlertError 返回与既有资源不可见一致的统一错误,避免越权与不存在形成可枚举差异。
func invisibleAlertError() error {
return errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage)
}
// daysUntil 按上海自然日计算剩余天数,负数表示已过期。
func daysUntil(expiresAt time.Time, now time.Time) int {
days := dateInShanghai(expiresAt).Sub(dateInShanghai(now))
return int(days.Hours() / 24)
}
// dateInShanghai 归一化到上海时区的自然日零点。
func dateInShanghai(value time.Time) time.Time {
local := value.In(shanghaiLocation)
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghaiLocation)
}
// normalizePage 归一化分页参数并执行默认值与上限。
func normalizePage(page, pageSize int) (int, int) {
if page <= 0 {
page = constants.DefaultPage
}
if pageSize <= 0 {
pageSize = constants.DefaultPageSize
}
if pageSize > constants.MaxPageSize {
pageSize = constants.MaxPageSize
}
return page, pageSize
}
// boolToStatus 把对外启停布尔映射为既有整型状态。
func boolToStatus(enabled bool) int {
if enabled {
return constants.StatusEnabled
}
return constants.StatusDisabled
}
// enabledStatusName 返回启停状态的中文名称。
func enabledStatusName(status int) string {
if status == constants.StatusEnabled {
return "启用"
}
return "停用"
}
// sameOptionalID 判断两个可空账号ID是否指向同一非空账号。
func sameOptionalID(left, right *uint) bool {
leftID, rightID := uint(0), uint(0)
if left != nil {
leftID = *left
}
if right != nil {
rightID = *right
}
return leftID == rightID
}