Files
junhong_cmp_fiber/internal/service/polling/manual_trigger_service.go
break 5e552d99bc 收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
2026-08-05 14:30:54 +08:00

634 lines
24 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 polling
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// ManualTriggerService 手动触发服务
type ManualTriggerService struct {
logStore *postgres.PollingManualTriggerLogStore
iotCardStore *postgres.IotCardStore
db *gorm.DB
auditWriter *auditinfra.Writer
redis *redis.Client
logger *zap.Logger
}
// SetAudit 注入手动轮询任务事务与统一审计 Writer。
func (s *ManualTriggerService) SetAudit(db *gorm.DB, writer *auditinfra.Writer) {
s.db = db
s.auditWriter = writer
}
// NewManualTriggerService 创建手动触发服务实例
func NewManualTriggerService(
logStore *postgres.PollingManualTriggerLogStore,
iotCardStore *postgres.IotCardStore,
redis *redis.Client,
logger *zap.Logger,
) *ManualTriggerService {
return &ManualTriggerService{
logStore: logStore,
iotCardStore: iotCardStore,
redis: redis,
logger: logger,
}
}
// TriggerSingle 单卡手动触发
func (s *ManualTriggerService) TriggerSingle(ctx context.Context, cardID uint, taskType string, triggeredBy uint) error {
// 验证任务类型
if !isValidTaskType(taskType) {
return errors.New(errors.CodeInvalidParam, "无效的任务类型")
}
// 权限验证:检查用户是否有权管理该卡
if err := s.canManageCard(ctx, cardID); err != nil {
return err
}
cards, err := s.iotCardStore.GetByIDs(ctx, []uint{cardID})
if err != nil {
return errors.Wrap(errors.CodeInternalError, err, "查询手动轮询卡失败")
}
// 检查每日触发限制
todayCount, err := s.logStore.CountTodayTriggers(ctx, triggeredBy)
if err != nil {
s.logger.Error("查询今日触发次数失败",
zap.Uint("triggered_by", triggeredBy),
zap.String("task_type", taskType),
zap.Error(err))
return err
}
if todayCount >= 500 { // 每日最多触发500次
appErr := errors.New(errors.CodeInvalidParam, "已达到每日触发次数上限")
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerSingle, Summary: "拒绝超过每日上限的单卡手动触发",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "single", triggeredBy), DisplayName: "单卡手动触发",
OperatorID: triggeredBy, Result: constants.AuditResultDenied,
IdentitySnapshot: pollingManualAttemptIdentity(taskType, "single", 1, triggeredBy), Cards: cards,
}, appErr)
return appErr
}
// 检查去重
dedupeKey := constants.RedisPollingManualDedupeKey(taskType)
added, err := s.redis.SAdd(ctx, dedupeKey, cardID).Result()
if err != nil {
s.logger.Error("Redis去重操作失败",
zap.Uint("card_id", cardID),
zap.String("task_type", taskType),
zap.Error(err))
return err
}
if added == 0 {
appErr := errors.New(errors.CodeInvalidParam, "该卡已在手动触发队列中")
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerSingle, Summary: "拒绝重复加入手动触发队列",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "single", triggeredBy), DisplayName: "单卡手动触发",
OperatorID: triggeredBy, Result: constants.AuditResultDenied,
IdentitySnapshot: pollingManualAttemptIdentity(taskType, "single", 1, triggeredBy), Cards: cards,
}, appErr)
return appErr
}
// 设置去重 key 过期时间24小时与日限制周期对齐
s.redis.Expire(ctx, dedupeKey, 24*time.Hour)
// 创建触发日志
cardIDsJSON, _ := json.Marshal([]uint{cardID})
triggerLog := &model.PollingManualTriggerLog{
TaskType: taskType,
TriggerType: "single",
CardIDs: string(cardIDsJSON),
TotalCount: 1,
Status: constants.PollingManualTriggerStatusProcessing,
TriggeredBy: triggeredBy,
TriggeredAt: time.Now(),
}
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
if err := s.logStore.WithTx(tx).Create(ctx, triggerLog); err != nil {
return err
}
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerSingle, Summary: "单卡手动触发",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: triggerLog.ID,
ResourceKey: pollingManualTriggerKey(triggerLog.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualTriggerIdentity(triggerLog),
AfterData: map[string]any{"status": triggerLog.Status, "task_type": taskType, "trigger_type": triggerLog.TriggerType},
Cards: cards,
})
})
if err != nil {
_ = s.redis.SRem(ctx, dedupeKey, cardID).Err()
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerSingle, Summary: "单卡手动触发失败",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "single", triggeredBy), DisplayName: "单卡手动触发",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualAttemptIdentity(taskType, "single", 1, triggeredBy), Cards: cards,
}, err)
s.logger.Error("创建触发日志失败",
zap.Uint("card_id", cardID),
zap.Uint("triggered_by", triggeredBy),
zap.Error(err))
return err
}
// 加入手动触发队列(使用 List优先级高于定时轮询
queueKey := constants.RedisPollingManualQueueKey(taskType)
if err := s.redis.LPush(ctx, queueKey, cardID).Err(); err != nil {
_ = s.redis.SRem(ctx, dedupeKey, cardID).Err()
s.logger.Error("写入手动触发队列失败",
zap.Uint("card_id", cardID),
zap.String("task_type", taskType),
zap.Error(err))
return err
}
// 更新日志状态
_ = s.logStore.UpdateProgress(ctx, triggerLog.ID, 1, 1, 0)
_ = s.logStore.UpdateStatus(ctx, triggerLog.ID, constants.PollingManualTriggerStatusCompleted)
s.logger.Info("单卡手动触发成功",
zap.Uint("card_id", cardID),
zap.String("task_type", taskType),
zap.Uint("triggered_by", triggeredBy))
return nil
}
// TriggerBatch 批量手动触发
func (s *ManualTriggerService) TriggerBatch(ctx context.Context, cardIDs []uint, taskType string, triggeredBy uint) (*model.PollingManualTriggerLog, error) {
// 验证任务类型
if !isValidTaskType(taskType) {
return nil, errors.New(errors.CodeInvalidParam, "无效的任务类型")
}
// 单次最多1000张卡
if len(cardIDs) > 1000 {
return nil, errors.New(errors.CodeInvalidParam, "单次最多触发1000张卡")
}
// 权限验证:检查用户是否有权管理所有卡
if err := s.canManageCards(ctx, cardIDs); err != nil {
return nil, err
}
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询手动轮询卡失败")
}
// 检查每日触发限制
todayCount, err := s.logStore.CountTodayTriggers(ctx, triggeredBy)
if err != nil {
return nil, err
}
if todayCount >= 500 { // 每日最多触发500次
appErr := errors.New(errors.CodeInvalidParam, "已达到每日触发次数上限")
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerBatch, Summary: "拒绝超过每日上限的批量手动触发",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "batch", triggeredBy), DisplayName: "批量手动触发",
OperatorID: triggeredBy, Result: constants.AuditResultDenied,
IdentitySnapshot: pollingManualAttemptIdentity(taskType, "batch", len(cardIDs), triggeredBy), Cards: cards,
}, appErr)
return nil, appErr
}
// 创建触发日志
cardIDsJSON, _ := json.Marshal(cardIDs)
triggerLog := &model.PollingManualTriggerLog{
TaskType: taskType,
TriggerType: "batch",
CardIDs: string(cardIDsJSON),
TotalCount: len(cardIDs),
Status: constants.PollingManualTriggerStatusProcessing,
TriggeredBy: triggeredBy,
TriggeredAt: time.Now(),
}
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
if err := s.logStore.WithTx(tx).Create(ctx, triggerLog); err != nil {
return err
}
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerBatch, Summary: "批量手动触发",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: triggerLog.ID,
ResourceKey: pollingManualTriggerKey(triggerLog.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualTriggerIdentity(triggerLog),
AfterData: map[string]any{"status": triggerLog.Status, "task_type": taskType, "trigger_type": triggerLog.TriggerType},
Cards: cards,
})
})
if err != nil {
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerBatch, Summary: "批量手动触发失败",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "batch", triggeredBy), DisplayName: "批量手动触发",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualAttemptIdentity(taskType, "batch", len(cardIDs), triggeredBy), Cards: cards,
}, err)
return nil, err
}
// 异步处理批量触发
go s.processBatchTrigger(context.Background(), triggerLog.ID, cardIDs, taskType)
return triggerLog, nil
}
// processBatchTrigger 异步处理批量触发
func (s *ManualTriggerService) processBatchTrigger(ctx context.Context, logID uint, cardIDs []uint, taskType string) {
dedupeKey := constants.RedisPollingManualDedupeKey(taskType)
queueKey := constants.RedisPollingManualQueueKey(taskType)
var processedCount, successCount, failedCount int
for _, cardID := range cardIDs {
// 检查去重
added, err := s.redis.SAdd(ctx, dedupeKey, cardID).Result()
if err != nil {
failedCount++
processedCount++
continue
}
if added == 0 {
// 已在队列中,跳过
failedCount++
processedCount++
continue
}
// 加入队列
if err := s.redis.LPush(ctx, queueKey, cardID).Err(); err != nil {
failedCount++
} else {
successCount++
}
processedCount++
// 每处理100条更新一次进度
if processedCount%100 == 0 {
_ = s.logStore.UpdateProgress(ctx, logID, processedCount, successCount, failedCount)
}
}
// 设置去重 key 过期时间24小时与日限制周期对齐
s.redis.Expire(ctx, dedupeKey, 24*time.Hour)
// 更新最终状态
_ = s.logStore.UpdateProgress(ctx, logID, processedCount, successCount, failedCount)
_ = s.logStore.UpdateStatus(ctx, logID, constants.PollingManualTriggerStatusCompleted)
s.logger.Info("批量手动触发完成",
zap.Uint("log_id", logID),
zap.Int("total", len(cardIDs)),
zap.Int("success", successCount),
zap.Int("failed", failedCount))
}
// ConditionFilter 条件筛选参数
type ConditionFilter struct {
CardStatus string `json:"card_status,omitempty"` // 卡状态
CarrierCode string `json:"carrier_code,omitempty"` // 运营商代码
CardType string `json:"card_type,omitempty"` // 卡类型
ShopID *uint `json:"shop_id,omitempty"` // 店铺ID
PackageIDs []uint `json:"package_ids,omitempty"` // 套餐ID列表
EnablePolling *bool `json:"enable_polling,omitempty"` // 是否启用轮询
Limit int `json:"limit,omitempty"` // 限制数量
}
// TriggerByCondition 条件筛选触发
func (s *ManualTriggerService) TriggerByCondition(ctx context.Context, filter *ConditionFilter, taskType string, triggeredBy uint) (*model.PollingManualTriggerLog, error) {
// 验证任务类型
if !isValidTaskType(taskType) {
return nil, errors.New(errors.CodeInvalidParam, "无效的任务类型")
}
// 设置默认限制
if filter.Limit <= 0 || filter.Limit > 1000 {
filter.Limit = 1000
}
// 权限验证:代理只能筛选自己管理的店铺的卡
if err := s.applyShopPermissionFilter(ctx, filter); err != nil {
return nil, err
}
// 检查每日触发限制
todayCount, err := s.logStore.CountTodayTriggers(ctx, triggeredBy)
if err != nil {
return nil, err
}
if todayCount >= 500 { // 每日最多触发500次
appErr := errors.New(errors.CodeInvalidParam, "已达到每日触发次数上限")
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerByCondition, Summary: "拒绝超过每日上限的条件筛选触发",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "by_condition", triggeredBy), DisplayName: "条件筛选触发",
OperatorID: triggeredBy, Result: constants.AuditResultDenied,
IdentitySnapshot: pollingManualAttemptIdentity(taskType, "by_condition", 0, triggeredBy),
Metadata: map[string]any{"condition_filter_configured": true},
}, appErr)
return nil, appErr
}
// 查询符合条件的卡(已应用权限过滤)
cardIDs, err := s.queryCardsByCondition(ctx, filter)
if err != nil {
return nil, err
}
if len(cardIDs) == 0 {
return nil, errors.New(errors.CodeInvalidParam, "没有符合条件的卡")
}
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询手动轮询卡失败")
}
// 创建触发日志
filterJSON, _ := json.Marshal(filter)
cardIDsJSON, _ := json.Marshal(cardIDs)
triggerLog := &model.PollingManualTriggerLog{
TaskType: taskType,
TriggerType: "by_condition",
CardIDs: string(cardIDsJSON),
ConditionFilter: string(filterJSON),
TotalCount: len(cardIDs),
Status: constants.PollingManualTriggerStatusProcessing,
TriggeredBy: triggeredBy,
TriggeredAt: time.Now(),
}
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
if err := s.logStore.WithTx(tx).Create(ctx, triggerLog); err != nil {
return err
}
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerByCondition, Summary: "条件筛选触发",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: triggerLog.ID,
ResourceKey: pollingManualTriggerKey(triggerLog.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualTriggerIdentity(triggerLog),
AfterData: map[string]any{"status": triggerLog.Status, "task_type": taskType, "trigger_type": triggerLog.TriggerType},
Metadata: map[string]any{"condition_filter_configured": true}, Cards: cards,
})
})
if err != nil {
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualTriggerByCondition, Summary: "条件筛选触发失败",
ResourceType: constants.AuditResourcePollingManualTrigger,
ResourceKey: pollingManualAttemptKey(taskType, "by_condition", triggeredBy), DisplayName: "条件筛选触发",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualAttemptIdentity(taskType, "by_condition", len(cardIDs), triggeredBy),
Metadata: map[string]any{"condition_filter_configured": true}, Cards: cards,
}, err)
return nil, err
}
// 异步处理批量触发
go s.processBatchTrigger(context.Background(), triggerLog.ID, cardIDs, taskType)
return triggerLog, nil
}
// queryCardsByCondition 根据条件查询卡ID
func (s *ManualTriggerService) queryCardsByCondition(ctx context.Context, filter *ConditionFilter) ([]uint, error) {
// 构建查询条件并查询卡
queryFilter := &postgres.IotCardQueryFilter{
ShopID: filter.ShopID,
EnablePolling: filter.EnablePolling,
Limit: filter.Limit,
}
// 映射其他过滤条件
if filter.CardStatus != "" {
queryFilter.CardStatus = &filter.CardStatus
}
if filter.CarrierCode != "" {
queryFilter.CarrierCode = &filter.CarrierCode
}
if filter.CardType != "" {
queryFilter.CardType = &filter.CardType
}
// 调用 IotCardStore 查询
cardIDs, err := s.iotCardStore.QueryIDsByFilter(ctx, queryFilter)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询符合条件的卡失败")
}
return cardIDs, nil
}
// GetStatus 获取触发状态
func (s *ManualTriggerService) GetStatus(ctx context.Context, logID uint) (*model.PollingManualTriggerLog, error) {
return s.logStore.GetByID(ctx, logID)
}
// ListHistory 获取触发历史
func (s *ManualTriggerService) ListHistory(ctx context.Context, page, pageSize int, taskType string, triggeredBy *uint) ([]*model.PollingManualTriggerLog, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
return s.logStore.List(ctx, page, pageSize, taskType, triggeredBy)
}
// CancelTrigger 取消触发任务
func (s *ManualTriggerService) CancelTrigger(ctx context.Context, logID uint, triggeredBy uint) error {
log, err := s.logStore.GetByID(ctx, logID)
if err != nil {
return errors.Wrap(errors.CodeNotFound, err, "触发任务不存在")
}
if log.TriggeredBy != triggeredBy {
appErr := errors.New(errors.CodeForbidden, "无权限取消该任务")
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualCancelled, Summary: "拒绝取消其他账号的手动触发任务",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: log.ID,
ResourceKey: pollingManualTriggerKey(log.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, Result: constants.AuditResultDenied, IdentitySnapshot: pollingManualTriggerIdentity(log),
}, appErr)
return appErr
}
if log.Status != constants.PollingManualTriggerStatusPending && log.Status != constants.PollingManualTriggerStatusProcessing {
appErr := errors.New(errors.CodeInvalidParam, "任务已完成或已取消")
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualCancelled, Summary: "拒绝取消已结束的手动触发任务",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: log.ID,
ResourceKey: pollingManualTriggerKey(log.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, Result: constants.AuditResultDenied, IdentitySnapshot: pollingManualTriggerIdentity(log),
}, appErr)
return appErr
}
var cardIDs []uint
_ = json.Unmarshal([]byte(log.CardIDs), &cardIDs)
cards, _ := s.iotCardStore.GetByIDs(ctx, cardIDs)
err = runPollingTransaction(ctx, s.db, s.auditWriter, func(tx *gorm.DB) error {
if err := s.logStore.WithTx(tx).UpdateStatus(ctx, logID, constants.PollingManualTriggerStatusCancelled); err != nil {
return err
}
before := log.Status
log.Status = constants.PollingManualTriggerStatusCancelled
return writePollingAudit(ctx, tx, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualCancelled, Summary: "人工取消轮询任务",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: log.ID,
ResourceKey: pollingManualTriggerKey(log.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualTriggerIdentity(log),
BeforeData: map[string]any{"status": before}, AfterData: map[string]any{"status": log.Status}, Cards: cards,
})
})
if err != nil {
recordPollingFailure(ctx, s.db, s.auditWriter, auditinfra.PollingInput{
ActionCode: constants.AuditActionPollingManualCancelled, Summary: "取消手动触发任务失败",
ResourceType: constants.AuditResourcePollingManualTrigger, ResourceID: log.ID,
ResourceKey: pollingManualTriggerKey(log.ID), DisplayName: "手动轮询任务",
OperatorID: triggeredBy, IdentitySnapshot: pollingManualTriggerIdentity(log), Cards: cards,
}, err)
}
return err
}
// GetRunningTasks 获取正在运行的任务
func (s *ManualTriggerService) GetRunningTasks(ctx context.Context, triggeredBy uint) ([]*model.PollingManualTriggerLog, error) {
return s.logStore.GetRunning(ctx, triggeredBy)
}
// GetQueueSize 获取手动触发队列大小
func (s *ManualTriggerService) GetQueueSize(ctx context.Context, taskType string) (int64, error) {
queueKey := constants.RedisPollingManualQueueKey(taskType)
return s.redis.LLen(ctx, queueKey).Result()
}
func isValidTaskType(taskType string) bool {
switch taskType {
case constants.TaskTypePollingRealname,
constants.TaskTypePollingCarddata,
constants.TaskTypePollingPackage:
return true
default:
return false
}
}
// checkUserTypePermission 检查用户类型是否有手动触发权限
// 返回 skip=true 表示超级管理员/平台用户直接放行
// 返回 err!=nil 表示企业账号无权限
// 返回 skip=false, err=nil 表示代理账号需继续细粒度检查
func (s *ManualTriggerService) checkUserTypePermission(ctx context.Context) (skip bool, err error) {
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform {
return true, nil
}
if userType == constants.UserTypeEnterprise {
return false, errors.New(errors.CodeForbidden, "企业账号无权限手动触发轮询")
}
return false, nil
}
// canManageCard 检查用户是否有权管理单张卡
func (s *ManualTriggerService) canManageCard(ctx context.Context, cardID uint) error {
skip, err := s.checkUserTypePermission(ctx)
if err != nil || skip {
return err
}
// 代理账号只能管理自己店铺及下级店铺的卡
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
return errors.Wrap(errors.CodeForbidden, err, "无权限操作该资源或资源不存在")
}
// 平台卡ShopID为nil代理不能管理
if card.ShopID == nil {
return errors.New(errors.CodeForbidden, "无权限操作平台卡")
}
// 检查代理是否有权管理该店铺
return middleware.CanManageShop(ctx, *card.ShopID)
}
// canManageCards 检查用户是否有权管理多张卡
func (s *ManualTriggerService) canManageCards(ctx context.Context, cardIDs []uint) error {
skip, err := s.checkUserTypePermission(ctx)
if err != nil || skip {
return err
}
// 从 Context 获取预计算的下级店铺 ID 列表
subordinateIDs := middleware.GetSubordinateShopIDs(ctx)
if subordinateIDs == nil {
// 平台用户/超管不受限制,但这里不应该进入(前面已经检查过用户类型)
return errors.New(errors.CodeForbidden, "无权限操作")
}
// 构建可管理的店铺ID集合
allowedShopIDs := make(map[uint]bool)
for _, id := range subordinateIDs {
allowedShopIDs[id] = true
}
// 批量查询卡信息
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
if err != nil {
return errors.Wrap(errors.CodeForbidden, err, "查询卡信息失败")
}
// 验证所有卡都在可管理范围内
for _, card := range cards {
if card.ShopID == nil {
return errors.New(errors.CodeForbidden, "无权限操作平台卡")
}
if !allowedShopIDs[*card.ShopID] {
return errors.New(errors.CodeForbidden, "包含无权限操作的卡")
}
}
return nil
}
// applyShopPermissionFilter 应用店铺权限过滤(代理只能筛选自己管理的卡)
func (s *ManualTriggerService) applyShopPermissionFilter(ctx context.Context, filter *ConditionFilter) error {
skip, err := s.checkUserTypePermission(ctx)
if err != nil || skip {
return err
}
// 代理账号:限制只能查询自己店铺及下级店铺的卡
currentShopID := middleware.GetShopIDFromContext(ctx)
if currentShopID == 0 {
return errors.New(errors.CodeForbidden, "无权限操作")
}
// 如果用户指定了 ShopID验证是否在可管理范围内
if filter.ShopID != nil {
if err := middleware.CanManageShop(ctx, *filter.ShopID); err != nil {
return err
}
// 已指定有效的 ShopID无需修改
return nil
}
// 用户未指定 ShopID限制为当前用户的店铺代理只能查自己店铺的卡
// 注意:这里限制为当前店铺,而不是所有下级店铺,以避免返回过多数据
filter.ShopID = &currentShopID
return nil
}