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.
 
 
 
 
 
 

211 lines
8.3 KiB

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 比较点分版本号,a<b 返回 -1、相等 0、a>b 返回 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
}