Files
junhong_cmp_fiber/internal/service/polling/permission.go
break 5ed6b39deb
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕
- 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败
- 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影
- 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序
- 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列
- 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名
- 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
2026-09-18 15:34:29 +08:00

130 lines
4.9 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"
"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"
)
// 本文件承载人工轮询入口共享的权限判定:既有人工触发与人工优先入队必须使用同一套语义,
// 因此判定只在此处实现一次,调用方不得各自内联,避免权限口径漂移。
// pollingUserTypePermission 检查用户类型是否有手动轮询权限。
// 返回 skip=true 表示超级管理员/平台用户直接放行;
// 返回 err!=nil 表示企业账号无权限;
// 返回 skip=false, err=nil 表示代理账号需继续细粒度检查。
func pollingUserTypePermission(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
}
// pollingDeniedMessage 是优先轮询入口对外统一的拒绝文案:越权与不存在共用,不产生可枚举差异。
const pollingDeniedMessage = "无权限操作该资源或资源不存在"
// PriorityPollingReadScope 返回卡轮询优先项读侧可访问的店铺快照范围。
//
// 判定顺序与人工入队共用同一用户类型分类pollingUserTypePermission
// 1. 超级管理员与平台账号 → 不受限(返回 nil
// 2. 企业账号 → 拒绝;
// 3. 其余(代理)→ 取认证中间件预计算的下级店铺集合集合缺失nil是「未预计算」而不是
// 「不受限」,一律按拒绝处理,绝不让非代理身份或缺失范围回退成全量读取。
func PriorityPollingReadScope(ctx context.Context) ([]uint, error) {
skip, err := pollingUserTypePermission(ctx)
if err != nil {
// 企业账号同样不允许读取优先轮询事实;对外只暴露统一的不可枚举拒绝文案。
return nil, errors.New(errors.CodeForbidden, pollingDeniedMessage)
}
if skip {
return nil, nil
}
shopIDs := middleware.GetSubordinateShopIDs(ctx)
if shopIDs == nil {
return nil, errors.New(errors.CodeForbidden, pollingDeniedMessage)
}
return shopIDs, nil
}
// requirePriorityPollingAdmin 判定当前账号是否为可执行人工关闭的超级管理员或平台账号。
// 代理账号即使对该卡有数据权限也不能关闭优先项:关闭是不可逆的队列出口,
// 与人工入队相比是更窄的授权面。拒绝一律返回统一文案,不暴露资源是否存在。
func requirePriorityPollingAdmin(ctx context.Context) error {
skip, err := pollingUserTypePermission(ctx)
if err != nil {
return errors.New(errors.CodeForbidden, pollingDeniedMessage)
}
if !skip {
return errors.New(errors.CodeForbidden, pollingDeniedMessage)
}
return nil
}
// canManagePollingCard 检查用户是否有权管理单张卡。
func canManagePollingCard(ctx context.Context, iotCardStore *postgres.IotCardStore, cardID uint) error {
skip, err := pollingUserTypePermission(ctx)
if err != nil || skip {
return err
}
// 代理账号只能管理自己店铺及下级店铺的卡
card, err := 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)
}
// canManagePollingCards 检查用户是否有权管理多张卡。
func canManagePollingCards(ctx context.Context, iotCardStore *postgres.IotCardStore, cardIDs []uint) error {
skip, err := pollingUserTypePermission(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 := 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
}