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