package console import ( "time" "yunyan/comm" "yunyan/lego/sys/postgres" "yunyan/pb" "golang.org/x/crypto/bcrypt" ) // Account 后台账号,存 console 主库(postgres,console_account 表)。 // 角色 Identity:1 超管 / 2 管理员 / 4 运营 / 5 渠道商(语义见 pb.Identity)。 // 3 代理商(品牌商) 已于 2026-09-12 下线,存量行由 migrateBrandAccountsToOperator 处置。 // 超管引导账号(yaml AdminAccount/AdminPassword)不入表,account_id=0;表里存的是后台新建的账号。 type Account struct { Id uint32 `gorm:"primaryKey;autoIncrement;column:id" json:"id"` Username string `gorm:"column:username;size:64;uniqueIndex" json:"username"` Password string `gorm:"column:password;size:128" json:"-"` // bcrypt 哈希,绝不回传前端 Identity pb.Identity `gorm:"column:identity" json:"identity"` // 角色 Enabled bool `gorm:"column:enabled" json:"enabled"` // 停用的不能登录 Remark string `gorm:"column:remark;size:255" json:"remark"` // 作用域绑定(仅代理/运营有意义;超管/管理员忽略,拥有全部权限)。均为 CSV,登录时回传前端做限制。 Access string `gorm:"column:access;type:text" json:"access"` // 可访问页面 id 列表,如 "dashboard,product" Apps string `gorm:"column:apps;type:text" json:"apps"` // 绑定应用名称列表(与 stats_global_day.app_id 一致),如 "Voitrans,Echomeet" Regions string `gorm:"column:regions;type:text" json:"regions"` // 绑定区域代码列表,如 "cn,us" Products string `gorm:"column:products;type:text" json:"products"` // 绑定产品 id 列表,如 "45058,45059" // 渠道分成体系的归属绑定:渠道商(5) 填 Channelid,只可见自己这一个渠道商的数据。 // 与 Apps/Products/Regions 一样随 JWT 下发,请求级由 scopeOf 强制收敛,前端传参不可越权。 // // ⚠️ 2026-09-12 品牌商(identity=3)下线,这里原本还有个 Brandid。库里那一列**没有删** // (存量账号的归属线索,且 migrateBrandAccountsToOperator 要读它),只是 struct 不再映射。 Channelid string `gorm:"column:channelid;size:16;index" json:"channelid"` // 绑定渠道商 id(短码),空=不绑定 // 代理资源点余额(仅已下线的代理 identity=3 有意义,保留列与存量数据,后台不再产出这类账号): // 管理员充值入账,代理给终端用户赠送时按类原子扣减。 // 单位与赠送表单/公码奖励一致:VIP=天、翻译/会议=分钟。 BalanceVipday int64 `gorm:"column:balance_vipday" json:"balance_vipday"` // VIP天数余额 BalanceTrademin int64 `gorm:"column:balance_trademin" json:"balance_trademin"` // 翻译分钟余额 BalanceMeetmin int64 `gorm:"column:balance_meetmin" json:"balance_meetmin"` // 会议分钟余额 Createtime int64 `gorm:"column:createtime" json:"createtime"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } func (Account) TableName() string { return comm.TableConsoleAccount } // hashPassword 生成 bcrypt 哈希。 func hashPassword(plain string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) return string(b), err } // checkPassword 校验明文与 bcrypt 哈希是否匹配。 func checkPassword(hash, plain string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil } // ---- 账号表 CRUD(走 console 主库 postgres)---- func (this *modelComp) ensureAccountTable() error { if err := postgres.CreateTable(comm.TableConsoleAccount, &Account{}); err != nil { return err } // postgres CreateTable 对已存在的表跳过 AutoMigrate,故新增的绑定列需显式补(幂等)。 for _, col := range []string{"access", "apps", "regions", "products"} { sql := "ALTER TABLE " + comm.TableConsoleAccount + " ADD COLUMN IF NOT EXISTS " + col + " text DEFAULT ''" if res := postgres.Exec(sql); res.Error != nil { return res.Error } } // 代理资源点余额列(bigint),同样幂等补列。 for _, col := range []string{"balance_vipday", "balance_trademin", "balance_meetmin"} { sql := "ALTER TABLE " + comm.TableConsoleAccount + " ADD COLUMN IF NOT EXISTS " + col + " bigint DEFAULT 0" if res := postgres.Exec(sql); res.Error != nil { return res.Error } } // 渠道商账号的归属绑定列,幂等补列。 // ⚠️ brandid 那一列刻意不补也不删:品牌商 2026-09-12 下线后新库不该再有它, // 老库里那列留着是存量账号唯一的归属线索(migrateBrandAccountsToOperator 的错误提示要用)。 if res := postgres.Exec("ALTER TABLE " + comm.TableConsoleAccount + " ADD COLUMN IF NOT EXISTS channelid varchar(16) DEFAULT ''"); res.Error != nil { return res.Error } return migrateBrandAccountsToOperator() } // migrateBrandAccountsToOperator 处置存量的品牌商账号(identity=3):**禁用 + 降级为运营(4)**。 // // ⚠️ 这一步是安全必需的,不是清理洁癖。品牌商账号原先靠 acctScope.brandId 把数据收敛到 // 自家品牌;brandId 随品牌商下线一并删除后,scopeOf 对它只剩 apps/products/regions/channelid // 四维,而这类账号这四项**本来就都是空的**——按「非空才限制」的语义,它们会当场从 // 「只看自己品牌」变成「看全平台」。降级成运营也救不了(运营同样是空绑定=不限), // 所以必须同时 enabled=false,把可见性交回超管手里重新配。 // // 幂等:只改 identity=3 的行,改完就没有这类行了。 func migrateBrandAccountsToOperator() error { res := postgres.Exec( "UPDATE "+comm.TableConsoleAccount+" SET identity=?, enabled=false, updatetime=? WHERE identity=?", int32(pb.Identity_Operator), time.Now().Unix(), int32(pb.Identity_Agent)) if res.Error != nil { return res.Error } return nil } // countAccountsByChannel 统计绑定到某渠道商的账号数(渠道商列表展示「是否已开通账号」用)。 func (this *modelComp) countAccountsByChannel(channelid string) (n int64, err error) { err = postgres.Table(comm.TableConsoleAccount).Where("channelid=?", channelid).Count(&n).Error return } // agentDeductBalance 代理赠送时按类原子扣减资源点余额(CAS):任一类不足则一律不扣、返回 ok=false。 func (this *modelComp) agentDeductBalance(id uint32, vipday, trademin, meetmin, ts int64) (bool, error) { res := postgres.Exec( "UPDATE "+comm.TableConsoleAccount+" SET balance_vipday=balance_vipday-?, balance_trademin=balance_trademin-?, balance_meetmin=balance_meetmin-?, updatetime=? "+ "WHERE id=? AND balance_vipday>=? AND balance_trademin>=? AND balance_meetmin>=?", vipday, trademin, meetmin, ts, id, vipday, trademin, meetmin) if res.Error != nil { return false, res.Error } return res.RowsAffected == 1, nil } // agentAddBalance 给代理账号余额按类加 delta(充值正数 / 回滚或下调负数);结果任一类为负则一律不改、返回 ok=false。 // WHERE 限定 identity=代理,避免误给非代理账号建立余额语义。 func (this *modelComp) agentAddBalance(id uint32, vipday, trademin, meetmin, ts int64) (bool, error) { res := postgres.Exec( "UPDATE "+comm.TableConsoleAccount+" SET balance_vipday=balance_vipday+?, balance_trademin=balance_trademin+?, balance_meetmin=balance_meetmin+?, updatetime=? "+ "WHERE id=? AND identity=? AND balance_vipday+?>=0 AND balance_trademin+?>=0 AND balance_meetmin+?>=0", vipday, trademin, meetmin, ts, id, int32(pb.Identity_Agent), vipday, trademin, meetmin) if res.Error != nil { return false, res.Error } return res.RowsAffected == 1, nil } // listAccounts 返回全部账号(按 id 升序)。Password 字段 json:"-" 不会外泄。 func (this *modelComp) listAccounts() (models []*Account, err error) { models = make([]*Account, 0) err = postgres.Find(comm.TableConsoleAccount, &models, "") return } // getAccountByName 按用户名取账号(登录用)。 func (this *modelComp) getAccountByName(username string) (model *Account, err error) { model = &Account{} err = postgres.FindOne(comm.TableConsoleAccount, model, "username=?", username) return } // getAccount 按 id 取账号。 func (this *modelComp) getAccount(id uint32) (model *Account, err error) { model = &Account{} err = postgres.FindOne(comm.TableConsoleAccount, model, "id=?", id) return } // addAccount 新增账号(Insert 后 model.Id 被回填)。 func (this *modelComp) addAccount(model *Account) (err error) { err = postgres.Insert(comm.TableConsoleAccount, model) return } // saveAccount 更新账号(整行保存)。 func (this *modelComp) saveAccount(model *Account) (err error) { err = postgres.Save(comm.TableConsoleAccount, model) return } // delAccount 删除账号。 func (this *modelComp) delAccount(id uint32) (err error) { err = postgres.Delete(comm.TableConsoleAccount, "id=?", id) return }