Files

264 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// paygateway 支付网关 alloc(从 main.go 拆分,均衡分配算法)
package main
import (
"context"
"fmt"
"paygateway/internal/account"
"paygateway/internal/ledgermon"
"paygateway/internal/logger"
"paygateway/internal/personal"
"paygateway/internal/pool"
"paygateway/internal/store"
"time"
)
func decryptDeviceSecret(encHex string) (string, error) {
if encHex == "" {
return "", fmt.Errorf("设备密钥为空")
}
data, err := account.Decrypt(encHex)
if err != nil {
return "", err
}
return string(data), nil
}
// autoRegisterDevice 首连自动注册设备(极端情况:仅有固定域名 + 可访问公网即可接入)。
// 监听端首次启动未配置时自动生成 device_id + secret,心跳携带 register_secret 完成注册。
// 返回注册后的设备(含解密密钥)。
func (a *App) resolveLedgerDevices(ctx context.Context) ([]ledgermon.MonitoredDevice, error) {
devs, err := a.personalDB.ListDevices(ctx)
if err != nil {
return nil, err
}
adb := adbMgr()
var out []ledgermon.MonitoredDevice
for _, d := range devs {
serial := deviceADBSerial(d)
if serial == "" {
continue
}
// 实时 adb 可达判定(含自动 adb connect 重连;~1s,reconcile 30s 周期可接受)
if !adb.Online(ctx, serial) {
continue
}
for _, id := range store.DeviceAccountIDs(ctx, a.personalDB, d.ID, d.AccountID) {
acct := a.accountPool.GetByID(id)
if acct == nil {
continue
}
provider, ok := ledgerProviderOf(acct.Channel)
if !ok {
continue // 非个人免签通道(沙箱/企业)无账本页可监听
}
out = append(out, ledgermon.MonitoredDevice{
DeviceID: d.DeviceID,
Serial: serial,
Provider: provider,
AccountID: id,
UserID: ledgerUserSpaceOf(ctx, a.personalDB, d.DeviceID, provider, acct.QRContent),
})
}
}
return out, nil
}
// deviceADBSerial 解析设备 adb serial(USB host[:port] 或 WiFi host:port),不可达返回空串。
func deviceADBSerial(d *personal.DeviceEncrypted) string {
if d.AdbHost != "" {
if d.AdbPort > 0 {
return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort)
}
return d.AdbHost
}
if d.AdbWifiHost != "" && d.AdbWifiPort > 0 {
return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort)
}
return ""
}
// accountDeviceOf 账户 → 设备 ID 映射(WAIT_PAY 池设备维度子池用)。
// 遍历全部设备找账户归属(主账户或附加账户);一对多时取首个(业务上账户唯一绑定一台收款设备)。
// 返回 (deviceID, found)。低频(下单时调用一次),全量遍历可接受。
func accountDeviceOf(ctx context.Context, pdb store.PersonalStore, accountID int64) (string, bool) {
devs, err := pdb.ListDevices(ctx)
if err != nil {
logger.Warn("main", "设备列表查询失败(订单归入兜底桶,Reconcile 兜底)", "err", err)
return "", false
}
for _, d := range devs {
for _, id := range store.DeviceAccountIDs(ctx, pdb, d.ID, d.AccountID) {
if id == accountID {
return d.DeviceID, true
}
}
}
return "", false
}
// devCandidate 均衡分配候选设备(含负载与健康度,供两层评分)
type devCandidate struct {
deviceID string
serial string
load int // 设备 WAIT_PAY 待确认总数(设备负载)
health float64 // 设备 poll 成功率(0~1)
acctIDs []int64 // 设备绑定账户(主+附加)
loadOverRatio bool // 负载进入缓冲带(ratio×cap ≤ load < cap):评分降权
}
// deviceLoadCapOf 计算设备订单承载上限:配置优先;0=按公式自动算。
// 公式:(MAX窗口 - 支付操作预留) / (单次poll × 每账户最低配额1) × 排队修正。
// 上限即"在用户可接受确认延迟内设备能扛的订单数"(PLAN-DYNAMIC-CLOSE-ALLOC §4.5)。
func deviceLoadCapOf(cfg *Config, dynMax time.Duration) int {
if cfg.DeviceLoadCap > 0 {
return cfg.DeviceLoadCap
}
// 与 main 的退化逻辑保持一致:动态窗口配置非法(MIN≤0/MAX≤0/MIN≥MAX)→ 静态窗口语义,不启用超限过滤
if dynMax <= 0 || cfg.OrderCloseMin <= 0 || cfg.OrderCloseMax <= 0 || cfg.OrderCloseMin >= cfg.OrderCloseMax {
return 0
}
payReserve := 60 * time.Second
pollCost := time.Duration(cfg.DevicePollCostMS) * time.Millisecond
if pollCost <= 0 {
pollCost = 4 * time.Second
}
n := int((dynMax - payReserve) / (pollCost * 1))
if n < 1 {
n = 1
}
return n
}
// balanceSelectAccount 均衡分配:先选设备(最闲最健康),再选设备内账户(最闲最健康)。
// 设备评分 = health × 1/(load+1);账户评分 = 健康 × 1/(pool.Count+1)。
// 设备超限(load ≥ cap)→ 排除;全部超限 → 返回 nil(下单层报错 + 告警)。
// channelName 形如 "personal.alipay"/"personal.wechat"(账户池通道过滤)。
func balanceSelectAccount(ctx context.Context, pdb store.PersonalStore,
pool *account.AccountPool, wp *pool.WaitPayPool, lm *ledgermon.Manager,
cfg *Config, channelName string) *account.Account {
if wp == nil || pool == nil {
return nil
}
devs, err := pdb.ListDevices(ctx)
if err != nil {
logger.Error("main", "均衡分配:设备列表查询失败", "err", err)
return nil
}
// 健康度快照(ledgermon 未启动 → 全部 0.9 中性)
health := map[string]float64{}
if lm != nil {
health = lm.DevicePollHealth()
}
// 收集在线设备候选(adb 可达)
var cands []devCandidate
for _, d := range devs {
serial := deviceADBSerial(d)
if serial == "" || !adbMgr().Online(ctx, serial) {
continue
}
ids := store.DeviceAccountIDs(ctx, pdb, d.ID, d.AccountID)
if len(ids) == 0 {
continue
}
st := wp.DeviceSnapshot(d.DeviceID)
load := 0
if st != nil {
load = st.Total
}
h := health[d.DeviceID]
if h <= 0 {
h = 0.9
}
cands = append(cands, devCandidate{deviceID: d.DeviceID, serial: serial, load: load, health: h, acctIDs: ids})
}
if len(cands) == 0 {
return nil
}
// 设备层:缓冲带降权(load ≥ ratio×cap → 降权)→ 超限排除(load ≥ cap)→ 评分排序
dynMax := time.Duration(cfg.OrderCloseMax) * time.Minute
capN := deviceLoadCapOf(cfg, dynMax)
softCap := 0
if capN > 0 && cfg.DeviceOverloadRatio > 0 && cfg.DeviceOverloadRatio < 1 {
softCap = int(float64(capN) * cfg.DeviceOverloadRatio)
}
usable := cands[:0]
for _, c := range cands {
if capN > 0 && c.load >= capN {
logger.Warn("main", "均衡分配:设备负载超限,跳过",
"device_id", c.deviceID, "load", c.load, "cap", capN)
continue
}
if softCap > 0 && c.load >= softCap {
c.loadOverRatio = true // 缓冲带:评分降权(最闲设备优先时让位)
}
usable = append(usable, c)
}
if len(usable) == 0 {
logger.Error("main", "均衡分配:全部设备负载超限,建议增加收款设备",
"loads", fmt.Sprintf("%+v", cands))
return nil
}
// 设备评分 argmax(最闲最健康;缓冲带内降权;评分相等 → load 小优先,再按 deviceID 字典序确定性)
best := usable[0]
bestScore := deviceScore(best)
for _, c := range usable[1:] {
s := deviceScore(c)
if s > bestScore || (s == bestScore && (c.load < best.load || (c.load == best.load && c.deviceID < best.deviceID))) {
best, bestScore = c, s
}
}
// 账户层:设备内选最闲最健康账户
allowed := make(map[int64]bool, len(best.acctIDs))
for _, id := range best.acctIDs {
allowed[id] = true
}
return pool.SelectFromWeighted(channelName, "default", allowed,
func(acct *account.Account) float64 {
return accountScore(acct, wp)
})
}
// deviceScore 设备评分:health × 1/(load+1)。
// 缓冲带内(loadOverRatio)额外 ×0.5 降权:让最闲设备优先,避免临界抖动。
func deviceScore(c devCandidate) float64 {
s := c.health / float64(c.load+1)
if c.loadOverRatio {
s *= 0.5
}
return s
}
// accountScore 账户评分:健康(状态/日限额/失败)× 1/(账户负载+1)
// 只罚短板,不叠加长处(PLAN-DYNAMIC-CLOSE-ALLOC §5.3)
func accountScore(acct *account.Account, wp *pool.WaitPayPool) float64 {
if acct == nil {
return 0
}
health := 1.0
if acct.DailyLimit > 0 {
health *= 1 - float64(acct.DailyUsed)/float64(acct.DailyLimit) // 日限额余量
if health < 0 {
health = 0
}
}
switch {
case acct.FailCount >= 3:
health *= 0.2
case acct.FailCount >= 1:
health *= 0.5
}
load := 1
if wp != nil {
load = wp.Count(acct.ID) + 1
}
return health / float64(load)
}
// deviceSerialFromDevice 解析设备 adb serial(内存态 Device 版本,镜像 deviceADBSerial 逻辑)。
// 探测链路用(dispatchLedgerRows 拿到的是 personalDeviceCtx 的 *personal.Device)。