You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

189 lines
6.2 KiB

package console
/*
账号数据作用域(后端强制隔离)。
登录时把账号绑定(apps/products/regions,CSV)写进 JWT,tokenValid 解析后放进 gin.Context。
每个数据接口用 scopeOf(c) 取出作用域,对查询/结果强制按绑定收敛——无论前端传什么、是否传 X-App-Id,
代理(3)/运营(4) 都只能拿到自己绑定范围内的数据;超管(1)/管理员(2) 不受限。
语义(与前端既有行为一致):某维度绑定为空 = 该维度不限制;非空 = 仅限列表内。
*/
import (
"strconv"
"strings"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
type acctScope struct {
unlimited bool // 超管/管理员:不受任何限制
apps []string // 绑定应用名(空=应用维度不限)
products []uint32 // 绑定产品 id(空=产品维度不限)
regions []string // 绑定区域代码(空=区域维度不限)
// 渠道分成体系的归属绑定(P3 起):
// 品牌商账号(代理,3):brandId 非 0,可见该品牌商及其名下全部渠道商的数据;
// 渠道商账号(5) :brandId + channelId 都有,只可见自己这一个渠道商的数据。
// 与 apps/products 一样是「非空才限制」,故超管/管理员天然不受影响。
brandId uint32
channelId string
}
func ctxStr(c *gin.Context, k string) string {
if v, ok := c.Get(k); ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
func splitCSV(s string) []string {
out := make([]string, 0)
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func splitCSVU32(s string) []uint32 {
out := make([]uint32, 0)
for _, p := range splitCSV(s) {
if n, err := strconv.ParseUint(p, 10, 32); err == nil {
out = append(out, uint32(n))
}
}
return out
}
func containsStr(list []string, v string) bool {
for _, x := range list {
if x == v {
return true
}
}
return false
}
// scopeOf 从已鉴权的请求上下文解析当前账号的数据作用域。
func scopeOf(c *gin.Context) acctScope {
switch currentIdentity(c) {
case pb.Identity_Admin, pb.Identity_Manager:
return acctScope{unlimited: true} // 超管/管理员不受工厂/产品/应用限制
default:
return acctScope{
apps: splitCSV(ctxStr(c, "apps")),
products: splitCSVU32(ctxStr(c, "products")),
regions: splitCSV(ctxStr(c, "regions")),
brandId: ctxU32(c, "brandid"),
channelId: ctxStr(c, "channelid"),
}
}
}
// ctxU32 从上下文取 uint32(tokenValid 写入)。
func ctxU32(c *gin.Context, k string) uint32 {
if v, ok := c.Get(k); ok {
if n, ok := v.(uint32); ok {
return n
}
}
return 0
}
// allowBrand 该账号是否可见某品牌商的数据。品牌商/渠道商账号只可见自己绑定的品牌商。
func (s acctScope) allowBrand(id uint32) bool {
return s.unlimited || s.brandId == 0 || s.brandId == id
}
// allowChannel 该账号是否可见某渠道商的数据。渠道商账号只可见自己那一个。
// 品牌商账号不在此限制(它看的是名下全部渠道商,由 allowBrand + 列表过滤把关)。
func (s acctScope) allowChannel(id string) bool {
return s.unlimited || s.channelId == "" || s.channelId == id
}
// filterChannels 把渠道商列表收敛到账号可见范围:渠道商账号只剩自己,品牌商账号只剩
// 本品牌商名下的 + 全局渠道商。
//
// ⚠️ brandid==0 是「全局渠道商」(后台新增渠道商已不再选品牌商),对所有账号可见。
// 少了这个判断,品牌商账号会看到一个空列表——allowBrand(0) 在 brandId 非 0 时恒为 false。
func (s acctScope) filterChannels(in []*pb.DBChannel) []*pb.DBChannel {
if s.unlimited || (s.brandId == 0 && s.channelId == "") {
return in
}
out := make([]*pb.DBChannel, 0, len(in))
for _, ch := range in {
if (ch.Brandid == 0 || s.allowBrand(ch.Brandid)) && s.allowChannel(ch.Id) {
out = append(out, ch)
}
}
return out
}
func (s acctScope) allowApp(name string) bool {
return s.unlimited || len(s.apps) == 0 || containsStr(s.apps, name)
}
func (s acctScope) allowProduct(id uint32) bool {
if s.unlimited || len(s.products) == 0 {
return true
}
for _, p := range s.products {
if p == id {
return true
}
}
return false
}
// filterProducts 把产品列表收敛到绑定范围内。
func (s acctScope) filterProducts(in []*pb.DBProduct) []*pb.DBProduct {
if s.unlimited || len(s.products) == 0 {
return in
}
out := make([]*pb.DBProduct, 0, len(in))
for _, p := range in {
if s.allowProduct(p.Id) {
out = append(out, p)
}
}
return out
}
// statsClause 返回账号作用域的统计过滤子句(与前端下钻条件 AND,强制收敛、防越权)。
//
// 口径:按"最具体的绑定"收敛——有产品绑定就**只按产品**,否则按应用。这样对绑定产品的代理,
// 「全部产品」(不传产品) 与「选该产品」(传 product_id) 落到同一过滤 product_id IN(绑定),结果一致。
// 注意:登录/用户/订单等"应用级"指标记在 product_id=0,按产品收敛后对代理显示 0(它们不挂产品);
// apps 绑定改为只用于"用户查询"页(X-App-Id 校验),不参与仪表盘统计。
// 区域再 AND 上(含未归属 region=”,让产品级空区域行能通过)。超管/管理员或完全无绑定 → 返回空。
func (s acctScope) statsClause() (string, []interface{}) {
if s.unlimited {
return "", nil
}
conds := make([]string, 0, 2)
args := make([]interface{}, 0, 2)
// 渠道商账号:直接锁死到自己那一个渠道商,比产品/应用维度都具体,优先于它们。
// (品牌商账号的收敛在调用方按「本品牌商名下渠道商集合」注入,见 statsChannelClause。)
if s.channelId != "" {
conds = append(conds, "channel_id = ?")
args = append(args, s.channelId)
} else if len(s.products) > 0 {
conds = append(conds, "product_id IN ?")
args = append(args, s.products)
} else if len(s.apps) > 0 {
conds = append(conds, "app_id IN ?")
args = append(args, s.apps)
}
if len(s.regions) > 0 {
conds = append(conds, "(region IN ? OR region = '')")
args = append(args, s.regions)
}
return strings.Join(conds, " AND "), args
}