package comm import ( "strconv" "strings" ) // 渠道分发配置(表 TableChannelAppCfg,存 console 共享库 postgres)。 // // 与 svcpool.go / agentcfg.go / calltranslate.go 同一模式:console 后台读写(管理端 CRUD), // 业务服务(home 的 user 模块)运行时按 (app_name=? OR app_name='') 直读、下发客户端。 // // 取代旧的 channelapp 表(存各应用业务库 mysql、主键只有 channel、只有一个 version 字段): // 现在按 app_name 分作用域统一收在 console 主库,且版本信息扩展为「线上版本 / 最低支持版本 / // 强制更新 / 更新日志 / 审核中版本 / 上架状态」,够客户端做强更与审核期功能开关判断。 // 渠道枚举(与 pb.UserChannel 一致,此处冗余一份便于 comm 层独立使用)。 const ( ChannelUnknown int32 = 0 ChannelChinaHuawei int32 = 1 ChannelChinaXiaomi int32 = 2 ChannelChinaOppo int32 = 3 ChannelChinaVivo int32 = 4 ChannelChinaHonor int32 = 5 ChannelOfficial int32 = 100 ChannelGlobalApple int32 = 101 ChannelGlobalGoogle int32 = 102 ) // 上架状态。 const ( ChannelStatusDraft int32 = 0 // 草稿:后台配着,不下发 ChannelStatusOnline int32 = 1 // 已上架 ChannelStatusReviewing int32 = 2 // 审核中:ReviewVersion 那一版正在过审 ChannelStatusOffline int32 = 3 // 已下架 ) // 支付渠道取值(PayChannels 逗号分隔)。与 comm.ModuleCatalog 里「支付」分组的模块一一对应, // 后台据此校验「开了某支付渠道但对应凭据没配」。 const ( PayWechat = "wxpay" PayAlipay = "alipay" PayPaypal = "paypal" PayApple = "apple" PayGoogle = "google" ) // PayChannelModule 支付渠道 → 业务功能配置里的模块键(app_module_config.module)。 var PayChannelModule = map[string]string{ PayWechat: "wechatpay", PayAlipay: "alipay", PayPaypal: "paypal", PayApple: "appleiap", PayGoogle: "googleiap", } // ChannelApp 一个应用在一个分发渠道上的配置。(app_name, channel) 唯一。 type ChannelApp struct { RowId uint64 `gorm:"primaryKey;autoIncrement;column:row_id" json:"row_id"` AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_channel_app,priority:1" json:"app_name"` // ''=全局默认 Channel int32 `gorm:"column:channel;uniqueIndex:uidx_channel_app,priority:2" json:"channel"` // 版本 Version string `gorm:"column:version;size:32" json:"version"` // 当前线上版本 MinVersion string `gorm:"column:min_version;size:32" json:"min_version"` // 最低支持版本,低于它的客户端必须更新 ForceUpdate bool `gorm:"column:force_update" json:"force_update"` // 强制更新总开关(低于 MinVersion 时才有意义) Changelog string `gorm:"column:changelog" json:"changelog"` Address string `gorm:"column:address" json:"address"` // 下载/商店地址 ReviewVersion string `gorm:"column:review_version;size:32" json:"review_version"` // 审核中的版本号;客户端报这个版本时可隐藏敏感功能 Status int32 `gorm:"column:status" json:"status"` // 见 ChannelStatus* Description string `gorm:"column:description" json:"description"` // 能力开关(沿用旧 DBChannelApp 语义) Tourists bool `gorm:"column:tourists" json:"tourists"` // 允许游客登录(总开关) TouristsVersion string `gorm:"column:touristsversion;size:32" json:"touristsversion"` // 游客登录生效版本;非空时只有该版本客户端可见,其余版本一律隐藏 AllowSkipDeviceBinding bool `gorm:"column:allowskipdevicebinding" json:"allowskipdevicebinding"` // 允许跳过设备绑定 PayChannels string `gorm:"column:paychannels;size:128" json:"paychannels"` // 逗号分隔,见 Pay* 常量 Enable bool `gorm:"column:enable" json:"enable"` Createtime int64 `gorm:"column:createtime" json:"createtime"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } // SplitPayChannels 拆分支付渠道串。 func SplitPayChannels(s string) []string { out := make([]string, 0, 4) for _, x := range strings.Split(s, ",") { if v := strings.TrimSpace(x); v != "" { out = append(out, v) } } return out } // CompareVersion 比较点分版本号,ab 返回 1。 // // 逐段按整数比较而非字符串比较——"1.10.0" 必须大于 "1.9.0",字符串比较会判反。 // 段数不同时短的按 0 补齐("1.2" == "1.2.0");非数字段(如 "1.2.0-beta" 的 "0-beta") // 取其前导数字,取不到按 0 处理,保证脏数据不会 panic 也不会误判成强更。 func CompareVersion(a, b string) int { as, bs := strings.Split(strings.TrimSpace(a), "."), strings.Split(strings.TrimSpace(b), ".") n := len(as) if len(bs) > n { n = len(bs) } for i := 0; i < n; i++ { x, y := 0, 0 if i < len(as) { x = leadingInt(as[i]) } if i < len(bs) { y = leadingInt(bs[i]) } if x != y { if x < y { return -1 } return 1 } } return 0 } // leadingInt 取字符串前导的十进制数字,没有则 0("0-beta" → 0、"12rc1" → 12)。 func leadingInt(s string) int { s = strings.TrimSpace(s) end := 0 for end < len(s) && s[end] >= '0' && s[end] <= '9' { end++ } if end == 0 { return 0 } n, err := strconv.Atoi(s[:end]) if err != nil { return 0 } return n } // NeedForceUpdate 判断某客户端版本是否必须更新:开了强更开关、配了最低版本、且客户端版本低于它。 // 客户端版本为空(老客户端没上报)时不强更,避免把所有人挡在门外。 func (c *ChannelApp) NeedForceUpdate(clientVersion string) bool { if c == nil || !c.ForceUpdate || strings.TrimSpace(c.MinVersion) == "" { return false } if strings.TrimSpace(clientVersion) == "" { return false } return CompareVersion(clientVersion, c.MinVersion) < 0 } // IsReviewing 判断某客户端版本是否正处于「送审版本」——客户端据此隐藏未过审的功能。 func (c *ChannelApp) IsReviewing(clientVersion string) bool { if c == nil || strings.TrimSpace(c.ReviewVersion) == "" { return false } return CompareVersion(clientVersion, c.ReviewVersion) == 0 } // ShowTourists 判断某客户端版本是否该看到「游客登录」入口。 // // 判定一律在服务端做(同 NeedForceUpdate/IsReviewing):客户端只拿结论,不拿规则。 // 各端各写一遍版本比较必然出现实现不一致,而这个开关直接关系到应用商店过审,判错代价很高。 // // 语义是「白名单」:**只有被点名的那一个版本可见,其余一律隐藏**。 // - Tourists=false → 一律隐藏(总开关关掉) // - Tourists=true 且 TouristsVersion="" → 一律隐藏(没点名任何版本 = 谁都不放行) // - Tourists=true 且 TouristsVersion=X → 只有版本号等于 X 的客户端可见 // // 送审场景:只给正在审核的那一版开游客登录,线上其它版本不受影响。 // // ⚠️ 留空按「全隐藏」而非「全放行」处理,是刻意选的失败方向:这个开关一旦判宽, // 后果是不该露出游客入口的版本露了出去(过审风险、白嫖开户礼);判严只是入口少显示。 // 后台 saveChannelApp 会拦住「勾了开关却没填版本」这种等于没开的配置,避免运营踩空。 // // 客户端没上报版本时同样隐藏——点名了版本却认不出来是哪一版,只能从严。 func (c *ChannelApp) ShowTourists(clientVersion string) bool { if c == nil || !c.Tourists { return false } want := strings.TrimSpace(c.TouristsVersion) if want == "" { return false } if strings.TrimSpace(clientVersion) == "" { return false } return CompareVersion(clientVersion, want) == 0 } // PickChannelApp 从「应用行 + 全局行」里挑出该渠道生效的一条:应用行覆盖全局行,都没有返回 nil。 // 与 agent / 第三方服务下发同语义(应用优先、无则回退全局)。 func PickChannelApp(rows []*ChannelApp, channel int32) *ChannelApp { var global, scoped *ChannelApp for _, r := range rows { if r == nil || r.Channel != channel { continue } if r.AppName == "" { global = r } else { scoped = r } } if scoped != nil { return scoped } return global }