lawless/docs/技术文档/TDD-06-离线挂机结算系统设计.md

1465 行
53 KiB
Markdown

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

# TDD-06 离线挂机结算系统设计
> 文档类型技术设计文档Technical Design Document
> 版本1.1
> 日期2026-07-02
> 关联文档TDD-04《数据库表结构设计》、TDD-05《API接口设计》、GDD-03《战斗系统设计》、GDD-06《经济系统设计》、GDD-07《帮派门派社交系统设计》、GDD-22《开放世界随机事件》、GDD-23《能量体系与功法相性设计》
---
## 1. 文档信息
| 项目 | 说明 |
|------|------|
| 目标 | 为《洪荒大陆》挂机手游定义离线挂机结算系统的服务端技术方案,覆盖离线时间快进、资源批量结算、ATB战斗离线结算、弟子代挂、游历事件触发、上线结算面板等核心模块。 |
| 读者 | 服务端开发Nakama/Go、数值策划、测试 |
| 技术栈 | Nakama 3.x + Go插件 + PostgreSQL 16 + Valkey + Nacos 2.x |
| 核心约束 | 无任务系统、无赛季重置、概率/机遇驱动、文字战报、ATB行动条、功法加持、能量体系非体力 |
| 游戏时间 | 现实:游戏 = 1:3 |
---
## 2. 系统总览
### 2.1 离线结算的核心循环
```
玩家下线
记录 last_online_at + 当前状态快照
服务端定时任务扫描离线玩家(每 5 分钟)
时间快进算法:计算离线期间的事件队列
批量结算:资源产出 / 弟子代挂 / 游历事件 / 战斗
生成结算结果 JSON压缩存储到 Valkey
玩家上线
拉取结算结果 → 展示结算面板 → 应用状态变更到 DB
```
### 2.2 设计原则
| 原则 | 说明 |
|------|------|
| **服务端权威** | 所有离线结算逻辑在服务端完成,客户端只负责展示结算面板 |
| **延迟结算** | 离线期间只记录事件队列,玩家上线时才批量执行结算(减少 DB 写入压力) |
| **时间快进** | 离线时间按游戏时间 1:3 换算后,以固定步长快进模拟事件触发 |
| **幂等性** | 结算结果带版本号,重复登录不会重复结算 |
| **上限兜底** | 背包满/货币上限/弟子异常等边界情况在结算时统一处理 |
---
## 3. 离线期间事件触发的服务端模拟逻辑
### 3.1 时间快进算法
离线期间不实时模拟,而是玩家上线时通过"时间快进"算法一次性计算离线期间的所有事件。
```
算法OfflineTimeWarp(character_id, last_online_at, now)
输入:
last_online_at — 玩家最后在线时间timestamptz
now — 当前时间timestamptz
输出:
event_queue — 离线期间触发的事件列表
步骤:
1. 计算离线时长(现实时间)
offline_duration_real = now - last_online_at
offline_duration_game = offline_duration_real × 3 // 现实:游戏 = 1:3
2. 限制最大离线结算时长(按境界递增,防止超长离线导致结算爆炸)
max_offline_hours = 按角色境界动态确定(见下方境界递增表)
if offline_duration_real > max_offline_hours:
offline_duration_real = max_offline_hours
offline_duration_game = max_offline_hours × 3
境界递增离线上限表:
炼气期 → 72 现实小时默认基准,约3天
筑基期 → 96 现实小时约4天
金丹期 → 120 现实小时约5天,新默认上限
元婴期及以上 → 无上限(完全离线结算,不限制时长)
// 通过 Nacos 配置可覆盖:
// offline.max_settle_hours.default = 120默认上限,金丹期
// offline.max_settle_hours.qi_refining = 72
// offline.max_settle_hours.foundation = 96
// offline.max_settle_hours.golden_core = 120
// offline.max_settle_hours.nascent_soul = 00表示无上限
3. 确定时间步长tick_interval
// 每个 tick 代表一段游戏时间,期间可能触发 0~N 个事件
tick_interval_game = 1 游戏小时 // 可通过 Nacos 配置
4. 加载玩家离线前状态快照
snapshot = load_character_snapshot(character_id)
// 包含:境界、属性、装备、功法、弟子列表、当前挂机状态
5. 加载区域事件池
zone_events = load_zone_event_pool(snapshot.zone_id, snapshot.world_tier)
// 来自 GDD-22 的区域事件配置
6. 时间快进循环
current_game_time = last_online_at × 3 // 转为游戏时间
end_game_time = now × 3
event_queue = []
while current_game_time < end_game_time:
// 6a. 检查定时触发事件
for event in zone_events:
if event.trigger_type == "periodic":
if current_game_time % event.period == 0:
event_queue.append(Event(event, current_game_time))
// 6b. 概率触发事件(游历/挂机遭遇)
for event in zone_events:
if event.trigger_type == "probability":
roll = random(0, 1)
// 概率衰减:连续触发同类事件时概率递减
decay = get_fatigue_decay(character_id, event.category)
effective_rate = event.base_rate × decay
if roll < effective_rate:
event_queue.append(Event(event, current_game_time))
// 6c. 弟子代挂事件检查
for disciple in snapshot.disciples:
if disciple.status == "dispatched":
disciple_tick_result = simulate_disciple_tick(disciple, current_game_time)
event_queue.append(disciple_tick_result)
// 6d. 推进时间
current_game_time += tick_interval_game
7. 返回 event_queue
```
### 3.2 事件队列结构
```go
type OfflineEvent struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"` // combat / gather / disciple / encounter / resource
TriggerTime int64 `json:"trigger_time"` // 游戏时间戳
ZoneID string `json:"zone_id"`
WorldTier int `json:"world_tier"`
RealmTier int `json:"realm_tier"`
Payload map[string]interface{} `json:"payload"` // 事件特定数据
Resolved bool `json:"resolved"` // 是否已结算
Result map[string]interface{} `json:"result"` // 结算结果
}
```
### 3.3 事件队列回放
```go
// 玩家上线时调用
func SettleOfflineEvents(characterID string) (*SettlementReport, error) {
// 1. 加载事件队列
events := loadEventQueue(characterID)
// 2. 按时间顺序回放
report := &SettlementReport{CharacterID: characterID}
for _, event := range events {
if event.Resolved {
continue // 已结算跳过(幂等)
}
switch event.EventType {
case "combat":
result := resolveOfflineCombat(event)
report.Battles = append(report.Battles, result)
case "gather":
result := resolveOfflineGather(event)
report.Resources = append(report.Resources, result)
case "disciple":
result := resolveDiscipleEvent(event)
report.DiscipleEvents = append(report.DiscipleEvents, result)
case "encounter":
result := resolveEncounter(event)
report.Encounters = append(report.Encounters, result)
}
event.Resolved = true
saveEventResult(event)
}
// 3. 批量应用状态变更
applySettlementToDatabase(report)
// 4. 压缩存储结算报告
saveCompressedReport(characterID, report)
return report, nil
}
```
---
## 4. 离线产出计算公式
### 4.1 挂机资源产出(接入 GDD-06 经济参数)
离线挂机产出遵循 GDD-06 §5.3 的"单位时间/风险期望产出参考"。
```
离线挂机资源产出公式:
base_output = zone_base_rate × realm_multiplier × game_hours
actual_output = base_output × quality_modifier × fatigue_decay × facility_bonus
参数说明:
zone_base_rate — 当前区域基础产出率(来自 GDD-08-附录A
realm_multiplier — 境界倍率(炼气 1.0 / 筑基 1.5 / 金丹 2.5 / 元婴 4.0 / 化神 6.5 / 合体 10.0
game_hours — 离线游戏时长(现实小时 × 3
quality_modifier — 弟子/装备品质修正1.0~2.5
fatigue_decay — 疲劳衰减系数(连续挂机同一区域,每游戏小时 -5%,最低 0.3
facility_bonus — 门派/领地设施加成1.0~2.2
```
**接入 GDD-06 各境界产出基线**
| 境界 | 1 游戏小时基础产出 | 离线挂机上限(现实 24h |
|------|-------------------|------------------------|
| 炼气期 | 3~6 铜钱 或 低品灵石碎片×0.2 | 216~432 铜钱 |
| 筑基期 | 15~30 铜钱 或 灵石×0.3~0.5 | 1080~2160 铜钱 |
| 金丹期 | 灵石中品×0.4~0.8 | 28.8~57.6 中品灵石 |
| 元婴期 | 魂晶/仙晶碎片×0.3~0.6 | 21.6~43.2 碎片 |
| 化神期 | 仙晶×0.2~0.4 | 14.4~28.8 仙晶 |
| 合体期 | 仙晶上品×0.15~0.3 | 10.8~21.6 上品仙晶 |
### 4.2 弟子代挂产出(接入 GDD-07 弟子系统)
```
弟子代挂产出公式:
disciple_output = base_output × disciple_quality × race_match × skill_match × guild_facility
参数说明:
base_output — 同 §4.1 的区域基础产出
disciple_quality — 弟子品质系数(凡品 0.8 / 良品 1.0 / 优品 1.3 / 极品 1.8 / 仙品 2.5
race_match — 种族匹配系数(地精挖矿 1.3 / 矮人锻造 1.2 / 其他 1.0
skill_match — 生活技能匹配系数(匹配 1.5 / 不匹配 1.0
guild_facility — 门派设施等级加成Lv1 1.0 / Lv2 1.2 / Lv3 1.45 / Lv4 1.8 / Lv5 2.2
```
**弟子品质与产出对照表GDD-07 §2.3.2**
| 品质 | 出现概率 | 效率加成 | 特殊能力 |
|------|---------|---------|---------|
| 凡品 | 55% | 80% | 无 |
| 良品 | 30% | 100% | 小幅降低意外率 |
| 优品 | 12% | 130% | 可学习一种生活技能加成 |
| 极品 | 2.8% | 180% | 有概率触发双倍产出 |
| 仙品 | 0.2% | 250% | 可独立触发小型奇遇 |
### 4.3 游历事件产出
```
游历事件产出公式:
explore_output = event_reward × risk_multiplier × time_efficiency × sect_bonus
参数说明:
event_reward — 事件基础奖励(来自 GDD-22 事件池配置)
risk_multiplier — 风险倍率(闲逛 1.0 / 历练 1.3 / 秘境 1.8 / 古迹 2.5 / 禁地 4.0
time_efficiency — 时间效率(离线游历按实际游戏时长折算,非满效率)
sect_bonus — 系统门派游历加成(门派等级 × 2.5%,最高 25%
```
---
## 5. 离线 ATB 战斗结算(接入 GDD-03 战斗公式)
### 5.1 离线战斗触发条件
离线期间战斗由以下场景触发:
- 游历途中遭遇野怪/精英/BossGDD-03 §6
- 弟子代挂遭遇战斗
- 被其他玩家 PVP 挑战离线战书默认接受,GDD-03 ✅14
### 5.2 服务端完整 ATB 战斗计算
离线战斗采用与在线战斗完全相同的 ATB 引擎,服务端一次性完整计算。
```go
// 离线ATB战斗结算接入 GDD-03 §三 ATB 机制)
func SimulateOfflineBattle(attacker, defender *CombatUnit, context BattleContext) *BattleResult {
// 1. 初始化战斗状态
battle := &BattleState{
Units: []*CombatUnit{attacker, defender},
Tick: 0,
MaxTicks: 3000, // ✅10 战斗行动时间上限
MaxActions: 50, // ✅10 单方最多50次行动
}
// 2. 初始化行动条(先手/伏击判定)
// GDD-03 §3.1:普通遭遇双方初始 0;先手方初始 50
attacker.ATBGauge = context.AttackerInitGauge // 0 或 50
defender.ATBGauge = context.DefenderInitGauge // 0 或 50
// 3. 加载技能候选池
attacker.Skills = loadActiveSkills(attacker.CharacterID)
defender.Skills = loadActiveSkills(defender.CharacterID)
// 4. ATB 主循环
report := &BattleReport{Rounds: []RoundLog{}}
for battle.Tick < battle.MaxTicks {
battle.Tick++
// 4a. 行动条填充GDD-03 §3.1
for _, unit := range battle.Units {
if unit.HP > 0 {
increment := unit.Speed * 0.1 // ATB_BASE_COEFFICIENT = 0.1
unit.ATBGauge += increment
}
}
// 4b. 检查可行动单位
readyUnits := []*CombatUnit{}
for _, unit := range battle.Units {
if unit.ATBGauge >= 100 && unit.HP > 0 {
readyUnits = append(readyUnits, unit)
}
}
// 4c. 同 tick 满条冲突判定GDD-03 §3.1
if len(readyUnits) > 1 {
sort.Slice(readyUnits, func(i, j int) bool {
vi := readyUnits[i].Speed * (1 + randomFloat(-0.1, 0.1))
vj := readyUnits[j].Speed * (1 + randomFloat(-0.1, 0.1))
return vi > vj
})
}
// 4d. 逐个执行行动
for _, unit := range readyUnits {
target := getOpponent(battle, unit)
// 控制状态检查
if unit.HasStatus("stun") {
report.Rounds = append(report.Rounds, RoundLog{
Tick: battle.Tick, Actor: unit.ID,
Action: "stunned", Message: "眩晕中无法行动",
})
unit.ATBGauge = 0
continue
}
// 逃跑判定GDD-03 §3.8 ✅32-✅34
// 玩家预设血量/能量/SAN阈值,达到阈值后每次ATB满自动尝试逃跑
if unit.IsPlayer && shouldAttemptEscape(unit, context.EscapeConfig) {
escapeRate := calculateEscapeRate(unit, target, context)
if random(100) < escapeRate {
// 逃跑成功:本场战利品清零,无死亡惩罚
report.Rounds = append(report.Rounds, RoundLog{
Tick: battle.Tick, Actor: unit.ID,
Action: "escape_success", Message: "成功逃离战斗",
})
result := &BattleResult{
Winner: "escape",
EndCondition: "escape_success",
LootCleared: true, // ✅33 逃跑成功战利品清零
DeathPenalty: false, // ✅33 无死亡惩罚
}
report.Result = result
return result
} else {
// 逃跑失败:无惩罚,继续战斗
report.Rounds = append(report.Rounds, RoundLog{
Tick: battle.Tick, Actor: unit.ID,
Action: "escape_fail", Message: "逃跑失败",
})
unit.ATBGauge = 0
continue
}
}
// 技能选择GDD-03 §3.3 ✅24 触发率机制)
skill := selectSkill(unit, target, battle)
// 命中判定
hitChance := 100 - target.DodgeRate + unit.HitBonus
if random(100) > hitChance {
report.Rounds = append(report.Rounds, RoundLog{
Tick: battle.Tick, Actor: unit.ID, Skill: skill.ID,
Action: "miss", Message: "身形一晃,闪过了攻击",
})
unit.ATBGauge = 0
continue
}
// 伤害计算GDD-03 §2.2
damage := calculateDamage(unit, target, skill, context)
// 暴击判定GDD-03 §2.2 ✅18
critRate := unit.Luck * 0.15 + skill.CritBonus
if critRate > 60 { critRate = 60 } // 上限60%
isCrit := random(100) < critRate
if isCrit {
critMultiplier := 1.5 // 基础×1.5
if critMultiplier > 2.0 { critMultiplier = 2.0 } // 上限×2.0
damage = damage * critMultiplier
}
// 应用伤害
target.HP -= damage
if target.HP < 0 { target.HP = 0 }
// 记录战报
report.Rounds = append(report.Rounds, RoundLog{
Tick: battle.Tick,
Actor: unit.ID,
Skill: skill.ID,
Damage: damage,
IsCrit: isCrit,
HPAfter: map[string]int{attacker.ID: attacker.HP, defender.ID: defender.HP},
})
// 行动条归零
unit.ATBGauge = 0
// 行动次数计数
unit.ActionCount++
if unit.ActionCount >= battle.MaxActions {
goto endBattle
}
// 死亡判定
if target.HP <= 0 {
goto endBattle
}
// 天赋触发判定GDD-03 §4.2
checkAndTriggerTalents(unit, target, battle, report)
}
// 4e. DOT/状态效果结算
processStatusEffects(battle, report)
// 4f. 战斗结束条件检查GDD-03 §3.7
if attacker.HP <= 0 || defender.HP <= 0 {
break
}
}
endBattle:
// 5. 判定胜负GDD-03 §3.7 超时处理)
result := determineWinner(battle, context)
report.Result = result
return result
}
// 逃跑判定辅助函数GDD-03 §3.8 ✅32-✅34
// shouldAttemptEscape 判断是否应该尝试逃跑
func shouldAttemptEscape(unit *CombatUnit, config EscapeConfig) bool {
// 检查血量阈值
if config.HPThreshold > 0 {
hpPercent := float64(unit.HP) / float64(unit.MaxHP) * 100
if hpPercent <= config.HPThreshold {
return true
}
}
// 检查能量阈值
if config.EPThreshold > 0 {
epPercent := float64(unit.EP) / float64(unit.MaxEP) * 100
if epPercent <= config.EPThreshold {
return true
}
}
// 检查SAN阈值
if config.SANThreshold > 0 && unit.SAN > 0 {
if unit.SAN <= config.SANThreshold {
return true
}
}
return false
}
// calculateEscapeRate 计算逃跑成功率GDD-03 §3.8.3 ✅34
func calculateEscapeRate(escaper, chaser *CombatUnit, context BattleContext) float64 {
// 基础率35%
baseRate := 35.0
// 速度差系数:己方速度/对方速度上限2.0,下限0.5
speedRatio := float64(escaper.Speed) / float64(chaser.Speed)
if speedRatio > 2.0 { speedRatio = 2.0 }
if speedRatio < 0.5 { speedRatio = 0.5 }
// 状态修正
statusMod := 1.0
if escaper.HasStatus("stun") || escaper.HasStatus("root") {
return 0 // 眩晕/定身无法逃跑
}
if escaper.HasStatus("slow") {
statusMod = 0.7
}
if escaper.HasStatus("haste") {
statusMod = 1.3
}
// 道具修正
itemMod := 1.0
if context.HasItem("escape_talisman") {
itemMod = 1.5
}
if context.HasItem("teleport_talisman") {
return 100 // 瞬移符直接成功
}
finalRate := baseRate * speedRatio * statusMod * itemMod
if finalRate > 100 { finalRate = 100 }
return finalRate
}
```
### 5.3 战斗伤害计算公式(接入 GDD-03 §2.2
```go
// 物理伤害GDD-03 §2.2
func calculatePhysicalDamage(attacker, defender *CombatUnit, skill *Skill) float64 {
baseDamage := float64(attacker.Str)*1.0 + attacker.WeaponAtk // 攻击系数=1.0
skillDamage := baseDamage * skill.DamageCoeff
// 亲和度+共鸣修正GDD-03 §4.4 ✅20
affinityMod := 1.0 + skill.AffinityMod
resonanceMod := 1.0 + skill.ResonanceMod
skillDamage = skillDamage * affinityMod * resonanceMod
// 物理减伤率GDD-03 §2.2
defCoeff := 0.5
defConst := getDefenseConstant(defender.WorldTier) // Tier1:100 / Tier2:150 / ... / Tier6:640
phyReduce := float64(defender.Vit)*defCoeff / (float64(defender.Vit)*defCoeff + float64(defConst))
finalDamage := skillDamage * (1 - phyReduce)
// 阵营伤害修正GDD-03 §2.3 ✅12
alignmentMod := getAlignmentModifier(skill.Alignment, defender.Alignment)
finalDamage = finalDamage * alignmentMod
// 状态修正
finalDamage = finalDamage * attacker.StatusAtkMod
return math.Max(finalDamage, 1) // 最低1点伤害
}
// 法术伤害GDD-03 §2.2
func calculateMagicalDamage(attacker, defender *CombatUnit, skill *Skill) float64 {
baseDamage := float64(attacker.Spi)*1.0 + attacker.ArtifactAtk // 法攻系数=1.0
skillDamage := baseDamage * skill.DamageCoeff
// 亲和度+共鸣修正
affinityMod := 1.0 + skill.AffinityMod
resonanceMod := 1.0 + skill.ResonanceMod
skillDamage = skillDamage * affinityMod * resonanceMod
// 法术减伤率
magCoeff := 0.5
magConst := getDefenseConstant(defender.WorldTier) // 与防御常数一致
magReduce := float64(defender.Spi)*magCoeff / (float64(defender.Spi)*magCoeff + float64(magConst))
finalDamage := skillDamage * (1 - magReduce)
// 元素克制修正GDD-03 §2.4 ✅19
elementMod := getElementModifier(skill.Element, defender.Element)
finalDamage = finalDamage * elementMod
// 阵营伤害修正
alignmentMod := getAlignmentModifier(skill.Alignment, defender.Alignment)
finalDamage = finalDamage * alignmentMod
return math.Max(finalDamage, 1)
}
```
### 5.4 离线战斗性能优化
离线战斗与在线战斗共用同一套 ATB 引擎,但有以下优化:
| 优化项 | 说明 |
|--------|------|
| **跳过战报文案渲染** | 离线计算只生成结构化数据,不生成文字战报文案;文案在客户端展示时即时生成 |
| **批量预加载** | 一次性加载角色战斗快照 + 技能列表 + 天赋列表,减少 DB 查询 |
| **计算结果缓存** | 战斗结果缓存到 Valkey,上线时直接读取 |
| **异步结算** | 玩家上线时先返回"结算中"状态,后台 Goroutine 异步完成计算 |
### 5.5 离线战斗场景与惩罚对照(接入 GDD-03 §6.2
| 战斗场景 | 失败惩罚 | 离线处理 |
|----------|---------|---------|
| 普通野怪战败 | 不触发死亡惩罚;产出 -60% | 事件标记为 `combat_loss_normal` |
| 精英怪战败 | 轻度死亡惩罚(进度 -10% | 事件标记为 `combat_loss_elite`,应用轻度惩罚 |
| Boss 战败 | 完整死亡惩罚(进度 -20%~30% + 纯度 + 道伤) | 事件标记为 `combat_loss_boss`,应用完整惩罚 |
| PVP 失败 | 完整死亡惩罚 | 事件标记为 `pvp_loss`,应用完整惩罚 |
---
## 6. 离线上限与溢出处理
### 6.1 背包满处理
```go
// 离线产出溢出处理
func handleInventoryOverflow(characterID string, items []ItemDrop) {
inventory := loadInventory(characterID)
maxSlots := inventory.MaxSlots // 由境界/装备决定
for _, item := range items {
if inventory.UsedSlots >= maxSlots {
// 背包满:转入邮件系统
sendToMail(characterID, item, MailType_OfflineOverflow, "离线产出溢出")
continue
}
// 可堆叠物品尝试合并
if existing := inventory.FindStackable(item.ItemID); existing != nil {
remaining := existing.MaxStack - existing.Quantity
if remaining >= item.Quantity {
existing.Quantity += item.Quantity
} else {
existing.Quantity = existing.MaxStack
overflow := item.Quantity - remaining
sendToMail(characterID, Item{ItemID: item.ItemID, Quantity: overflow},
MailType_OfflineOverflow, "离线产出溢出")
}
} else {
inventory.AddItem(item)
}
}
}
```
### 6.2 货币上限处理
```go
// 离线货币产出上限处理(接入 GDD-06 经济参数)
func handleCurrencyOverflow(characterID string, currencyCode string, amount decimal.Decimal) {
balance := loadCurrencyBalance(characterID, currencyCode)
maxBalance := getCurrencyMaxBalance(currencyCode) // 由 Nacos 配置
if balance.Amount + amount > maxBalance {
overflow := (balance.Amount + amount) - maxBalance
balance.Amount = maxBalance
// 溢出部分按比例折算为低一档货币或转入邮件
converted := convertOverflowCurrency(currencyCode, overflow)
if converted != nil {
addCurrency(characterID, converted.Code, converted.Amount)
}
} else {
balance.Amount += amount
}
// 写入经济审计日志TDD-04 §5.4
writeEconomyAudit(characterID, currencyCode, "faucet", "offline_settle", amount)
}
```
### 6.3 弟子状态异常处理
| 异常状态 | 处理方式 |
|----------|---------|
| 弟子死亡 | 标记 `status = "dead"`,记录 `died_at`,生成墓碑数据GDD-07,不产出后续收益 |
| 弟子叛逃 | 标记 `status = "deserted"`,已产出保留,后续产出取消 |
| 弟子受伤 | 标记临时 debuff,产出效率 -30%~50%,持续 N 游戏小时 |
| 弟子顿悟 | 标记 `status = "insight"`,产出效率 +50%,持续 N 游戏小时,可能解锁新技能 |
---
## 7. 上线结算面板数据结构
### 7.1 结算报告 JSON Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OfflineSettlementReport",
"type": "object",
"required": ["report_id", "character_id", "offline_duration", "settlement_time", "summary"],
"properties": {
"report_id": {
"type": "string",
"description": "结算报告唯一IDUUID"
},
"character_id": {
"type": "string",
"description": "角色ID"
},
"settlement_version": {
"type": "integer",
"description": "结算版本号,用于幂等校验"
},
"offline_duration": {
"type": "object",
"properties": {
"real_seconds": { "type": "integer", "description": "离线现实秒数" },
"game_hours": { "type": "number", "description": "离线游戏时长(小时)" },
"capped": { "type": "boolean", "description": "是否触及上限截断" }
}
},
"settlement_time": {
"type": "string",
"format": "date-time",
"description": "结算执行时间"
},
"summary": {
"type": "object",
"description": "收益汇总",
"properties": {
"total_currency": {
"type": "array",
"items": {
"type": "object",
"properties": {
"currency_code": { "type": "string" },
"amount": { "type": "string", "description": "Decimal字符串" },
"source": { "type": "string", "enum": ["gather", "disciple", "combat_drop", "encounter"] }
}
}
},
"total_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"item_id": { "type": "string" },
"item_name": { "type": "string" },
"quantity": { "type": "integer" },
"rarity": { "type": "string" }
}
}
},
"overflow_items_count": { "type": "integer", "description": "溢出到邮件的物品数" },
"exp_gained": { "type": "string", "description": "修为增长" },
"energy_change": { "type": "string", "description": "能量变化" }
}
},
"battles": {
"type": "array",
"description": "战斗摘要列表",
"items": {
"type": "object",
"properties": {
"battle_id": { "type": "string" },
"battle_type": { "type": "string", "enum": ["expedition_pve", "pvp", "disciple_combat"] },
"trigger_time": { "type": "string", "format": "date-time" },
"opponent_name": { "type": "string" },
"opponent_race": { "type": "string" },
"opponent_realm": { "type": "string" },
"result": { "type": "string", "enum": ["win", "lose", "draw"] },
"end_condition": { "type": "string", "enum": ["hp_zero", "timeout", "surrender"] },
"rounds_count": { "type": "integer", "description": "行动次数" },
"damage_dealt": { "type": "string" },
"damage_taken": { "type": "string" },
"death_penalty_applied": { "type": "boolean" },
"drops_summary": {
"type": "object",
"properties": {
"currency": { "type": "array" },
"items": { "type": "array" }
}
}
}
}
},
"disciple_events": {
"type": "array",
"description": "弟子代挂事件列表",
"items": {
"type": "object",
"properties": {
"disciple_id": { "type": "string" },
"disciple_name": { "type": "string" },
"disciple_quality": { "type": "string" },
"event_type": { "type": "string", "enum": ["gather", "combat", "insight", "death", "desertion", "treasure"] },
"trigger_time": { "type": "string", "format": "date-time" },
"result": { "type": "string", "enum": ["success", "fail", "death", "insight"] },
"rewards": { "type": "object" },
"message": { "type": "string", "description": "叙事化描述" }
}
}
},
"encounters": {
"type": "array",
"description": "游历事件列表",
"items": {
"type": "object",
"properties": {
"encounter_id": { "type": "string" },
"encounter_type": { "type": "string" },
"trigger_time": { "type": "string", "format": "date-time" },
"zone_name": { "type": "string" },
"title": { "type": "string" },
"description": { "type": "string" },
"auto_choice": { "type": "string", "description": "离线自动选择的分支" },
"choice_reason": { "type": "string", "description": "选择策略说明" },
"result": { "type": "object" }
}
}
},
"state_changes": {
"type": "object",
"description": "角色状态变更",
"properties": {
"hp_change": { "type": "string" },
"energy_change": { "type": "string" },
"san_change": { "type": "string" },
"crime_score_change": { "type": "string" },
"realm_exp_change": { "type": "string" },
"death_occurred": { "type": "boolean" },
"tribulation_triggered": { "type": "boolean" }
}
}
}
}
```
### 7.2 结算面板 API
```protobuf
// gRPC 接口定义
service OfflineSettlementService {
// 拉取离线结算报告(玩家上线时调用)
rpc GetSettlementReport (GetSettlementReq) returns (SettlementReportResp);
// 确认结算(客户端展示完面板后调用,应用状态变更)
rpc ConfirmSettlement (ConfirmSettlementReq) returns (ConfirmSettlementResp);
// 查询历史结算报告
rpc ListSettlementHistory (ListSettlementReq) returns (ListSettlementResp);
}
message GetSettlementReq {
string character_id = 1;
}
message SettlementReportResp {
int32 code = 1;
string message = 2;
SettlementReport data = 3;
}
message SettlementReport {
string report_id = 1;
int64 settlement_version = 2;
OfflineDuration offline_duration = 3;
SettlementSummary summary = 4;
repeated BattleSummary battles = 5;
repeated DiscipleEventSummary disciple_events = 6;
repeated EncounterSummary encounters = 7;
StateChanges state_changes = 8;
string compressed_full_report = 9; // gzip 压缩的完整战报 JSON
}
```
---
## 8. 弟子代挂结算(接入 GDD-07 弟子系统)
### 8.1 弟子代挂结算流程
```
弟子代挂结算流程:
1. 加载弟子列表status = "dispatched"
2. 计算离线游戏时长
3. 逐弟子结算:
a. 检查派遣目标(自建门派资源点 / 帮派领地 / 历练点)
b. 计算基础产出§4.2 公式)
c. 随机事件判定(弟子遭遇/顿悟/死亡)
d. 累计产出到结算报告
4. 批量更新弟子状态
```
### 8.2 弟子品质对产出的影响
```go
// 弟子代挂产出计算
func calculateDiscipleOutput(disciple *Disciple, zone *Zone, gameHours float64) *OutputResult {
// 基础产出
baseRate := zone.BaseOutputRate
// 品质系数GDD-07 §2.3.2
qualityCoeff := map[string]float64{
"common": 0.8,
"fine": 1.0,
"excellent": 1.3,
"perfect": 1.8,
"immortal": 2.5,
}[disciple.Quality]
// 种族匹配系数
raceCoeff := calculateRaceMatch(disciple.RaceID, zone.ZoneType)
// 生活技能匹配
skillCoeff := 1.0
if hasMatchingSkill(disciple, zone) {
skillCoeff = 1.5
}
// 门派设施加成
facilityCoeff := getGuildFacilityBonus(disciple.GuildID, zone)
// 极品/仙品双倍产出概率GDD-07 §2.3.2
doubleChance := 0.0
if disciple.Quality == "perfect" {
doubleChance = 0.15 // 15% 概率双倍
} else if disciple.Quality == "immortal" {
doubleChance = 0.25 // 25% 概率双倍
}
// 计算总产出
totalOutput := baseRate * qualityCoeff * raceCoeff * skillCoeff * facilityCoeff * gameHours
// 随机波动 ±20%
totalOutput = totalOutput * randomFloat(0.8, 1.2)
// 双倍产出判定
if randomFloat(0, 1) < doubleChance {
totalOutput *= 2
}
return &OutputResult{Amount: totalOutput}
}
```
### 8.3 弟子死亡判定(接入 GDD-07 ✅T4 / GDD-13
```go
// 弟子离线死亡判定
func checkDiscipleDeath(disciple *Disciple, missionType string, difficulty int) bool {
// 基础死亡率(随难度递增)
baseDeathRate := map[string]float64{
"gathering": 0.001, // 采集0.1%
"training": 0.005, // 历练0.5%
"mercenary": 0.02, // 佣兵委托2%
"sect_proxy": 0.01, // 门派代挂1%
}[missionType]
// 难度修正
difficultyMod := 1.0 + float64(difficulty-1)*0.5 // 每星 +50%
// 品质修正(高品质弟子生存率更高)
qualityMod := map[string]float64{
"common": 1.2, // 凡品 +20% 死亡率
"fine": 1.0,
"excellent": 0.8, // 优品 -20%
"perfect": 0.6, // 极品 -40%
"immortal": 0.4, // 仙品 -60%
}[disciple.Quality]
// 保险道具修正
insuranceMod := 1.0
if disciple.InsuranceItemID != "" {
insuranceMod = 0.3 // 有保险道具,死亡率 -70%
}
// 弟子死亡率修正字段(培养/装备可降低)
deathRateMod := 1.0 - disciple.DeathRateModifier
finalDeathRate := baseDeathRate * difficultyMod * qualityMod * insuranceMod * deathRateMod
// 每游戏小时判定一次
return randomFloat(0, 1) < finalDeathRate
}
```
### 8.4 弟子事件池
| 事件类型 | 触发概率 | 效果 | 叙事示例 |
|----------|---------|------|---------|
| 正常产出 | 85% | 获得资源 | "弟子在矿脉中采集到灵石×N" |
| 发现宝藏 | 3% | 额外稀有材料 | "弟子在洞窟深处发现一株千年灵草" |
| 顿悟 | 5% | 弟子属性/技能提升 | "弟子在采集时顿悟,棍法熟练度+8%" |
| 遭遇战斗 | 4% | 胜利获额外奖励,失败受伤 | "弟子遭遇妖兽袭击,奋力击退" |
| 受伤 | 2% | 产出效率临时下降 | "弟子不慎中毒,需要休养" |
| 死亡 | 0.1%~2% | 弟子永久损失 | "弟子在禁地中殒落..." |
| 叛逃 | 0.05% | 弟子永久损失(仅低忠诚度) | "弟子心生异志,携物资潜逃" |
---
## 9. 游历事件离线触发(接入 GDD-22
### 9.1 随机事件池抽取
```go
// 离线游历事件抽取
func pickOfflineEncounter(character *Character, zone *Zone, gameTime int64) *Encounter {
// 加载区域事件池GDD-22 §三)
eventPool := loadZoneEventPool(zone.ID)
// 过滤不适用离线的事件
validEvents := filterEvents(eventPool, func(e Event) bool {
return e.OfflineEligible && character.RealmTier >= e.MinRealmTier
})
// 计算每个事件的权重
weightedEvents := []WeightedEvent{}
for _, event := range validEvents {
weight := event.BaseWeight
// 时段权重GDD-03 ✅5昼夜增益按游戏时间判定
timeMod := getTimeModifier(gameTime, event.TimePreference)
weight *= timeMod
// 天气/环境权重
envMod := getEnvironmentModifier(zone.Weather, event.ElementPreference)
weight *= envMod
// 疲劳衰减(同一区域连续探索)
fatigueMod := getFatigueDecay(character.ID, event.Category)
weight *= fatigueMod
// 卜算/感知类天赋加成
if character.HasTalent("divination") {
weight *= 1.3
}
weightedEvents = append(weightedEvents, WeightedEvent{Event: event, Weight: weight})
}
// 加权随机抽取
selected := weightedRandomSelect(weightedEvents)
return selected
}
```
### 9.2 分支选择策略
离线状态下玩家无法实时决策,系统按预设策略自动选择:
```go
// 离线自动分支选择策略
func autoSelectBranch(encounter *Encounter, character *Character) string {
// 策略优先级:
// 1. 安全优先(默认策略):选择风险最低的选项
// 2. 收益优先:选择预期收益最高的选项
// 3. 玩家预设:玩家上线时可设置离线偏好
strategy := character.OfflineStrategy // "safe" / "greedy" / "balanced"
switch strategy {
case "safe":
return selectSafestOption(encounter.Options, character)
case "greedy":
return selectHighestRewardOption(encounter.Options, character)
case "balanced":
return selectBalancedOption(encounter.Options, character)
default:
return selectSafestOption(encounter.Options, character)
}
}
// 安全优先选择
func selectSafestOption(options []EncounterOption, character *Character) string {
safest := options[0]
for _, opt := range options[1:] {
if opt.RiskLevel < safest.RiskLevel {
safest = opt
}
}
return safest.ID
}
// 收益优先选择
func selectHighestRewardOption(options []EncounterOption, character *Character) string {
best := options[0]
bestEV := calculateExpectedValue(best, character)
for _, opt := range options[1:] {
ev := calculateExpectedValue(opt, character)
if ev > bestEV {
best = opt
bestEV = ev
}
}
return best.ID
}
// 期望值计算
func calculateExpectedValue(option EncounterOption, character *Character) float64 {
rewardEV := option.RewardValue * option.SuccessRate
riskEV := option.RiskValue * (1 - option.SuccessRate) * option.DeathPenaltyWeight
return rewardEV - riskEV
}
```
### 9.3 离线事件类型与处理
| 事件类型 | 离线处理方式 | 接入文档 |
|----------|-------------|---------|
| 灵气异动 | 自动吸纳(安全策略)或标记(收益策略) | GDD-22 §三 |
| 古修残魂 | 默认超度(最安全),不选择夺舍 | GDD-22 §三 |
| 行脚商人 | 默认无视(避免被抢) | GDD-22 §三 |
| 心魔低语 | 默认压制(最安全) | GDD-22 §三 |
| 同族求救 | 默认无视(避免风险) | GDD-22 §三 |
| 天降宝箱 | 默认开启 | GDD-22 §三 |
| 遭遇战斗 | 自动按 ATB 引擎结算 | GDD-03 §六 |
| 采集点 | 自动采集(消耗能量) | GDD-06 §三 |
---
## 10. 性能优化
### 10.1 批量结算的并发模型
```
玩家上线触发结算
Goroutine PoolN=CPU核心数
├── Goroutine 1: 挂机资源结算
├── Goroutine 2: 弟子代挂结算
├── Goroutine 3: 游历事件结算
├── Goroutine 4: 战斗结算(可能多个)
└── Goroutine 5: 状态变更合并
Channel 汇总结果
批量写入 DB单事务
```
```go
// 并发结算模型
func SettleOfflineConcurrent(characterID string) (*SettlementReport, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 并发执行各模块结算
var (
gatherResult *GatherResult
discipleResult *DiscipleResult
encounterResult *EncounterResult
battleResults []*BattleResult
wg sync.WaitGroup
mu sync.Mutex
errs []error
)
wg.Add(4)
// 挂机资源结算
go func() {
defer wg.Done()
r, err := settleOfflineGather(ctx, characterID)
mu.Lock()
if err != nil { errs = append(errs, err) } else { gatherResult = r }
mu.Unlock()
}()
// 弟子代挂结算
go func() {
defer wg.Done()
r, err := settleOfflineDisciples(ctx, characterID)
mu.Lock()
if err != nil { errs = append(errs, err) } else { discipleResult = r }
mu.Unlock()
}()
// 游历事件结算
go func() {
defer wg.Done()
r, err := settleOfflineEncounters(ctx, characterID)
mu.Lock()
if err != nil { errs = append(errs, err) } else { encounterResult = r }
mu.Unlock()
}()
// 战斗结算(可能包含多场战斗)
go func() {
defer wg.Done()
results, err := settleOfflineBattles(ctx, characterID)
mu.Lock()
if err != nil { errs = append(errs, err) } else { battleResults = results }
mu.Unlock()
}()
wg.Wait()
if len(errs) > 0 {
return nil, fmt.Errorf("settlement errors: %v", errs)
}
// 合并结果
report := mergeResults(gatherResult, discipleResult, encounterResult, battleResults)
// 批量写入 DB单事务
err := applySettlementInTransaction(ctx, characterID, report)
if err != nil {
return nil, err
}
return report, nil
}
```
### 10.2 数据库写入优化
| 优化策略 | 说明 |
|----------|------|
| **单事务批量写入** | 所有结算结果在一个事务中写入,减少事务开销 |
| **UPSERT 代替 INSERT+UPDATE** | 货币余额、背包物品使用 `INSERT ... ON CONFLICT UPDATE` |
| **延迟写入经济审计** | 经济审计日志先缓存到 Valkey,定时批量刷入 DB每 5 分钟) |
| **分区表利用** | `battle_logs` 按周分区,`economy_audit_logs` 按月分区,写入只命中当前分区 |
| **连接池调优** | 结算高峰期使用独立连接池,避免影响在线请求 |
```go
// 单事务批量写入
func applySettlementInTransaction(ctx context.Context, characterID string, report *SettlementReport) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer tx.Rollback()
// 1. 更新角色状态
_, err = tx.ExecContext(ctx, `
UPDATE characters SET
exp = exp + $1,
san_current = GREATEST(0, LEAST(san_max, san_current + $2)),
crime_score = GREATEST(0, crime_score + $3),
last_online_at = NOW(),
updated_at = NOW()
WHERE id = $4
`, report.ExpGained, report.SanChange, report.CrimeChange, characterID)
// 2. 批量更新货币余额UPSERT
for _, curr := range report.TotalCurrency {
_, err = tx.ExecContext(ctx, `
INSERT INTO currency_balances (character_id, currency_code, amount, total_earned, updated_at)
VALUES ($1, $2, $3, $3, NOW())
ON CONFLICT (character_id, currency_code) DO UPDATE SET
amount = currency_balances.amount + EXCLUDED.amount,
total_earned = currency_balances.total_earned + EXCLUDED.total_earned,
updated_at = NOW()
`, characterID, curr.CurrencyCode, curr.Amount)
}
// 3. 批量插入背包物品
for _, item := range report.TotalItems {
_, err = tx.ExecContext(ctx, `
INSERT INTO inventories (id, character_id, item_id, slot_type, quantity, instance_data, acquired_at, created_at)
VALUES (gen_random_uuid(), $1, $2, 'bag', $3, $4, NOW(), NOW())
`, characterID, item.ItemID, item.Quantity, item.InstanceData)
}
// 4. 更新弟子状态
for _, de := range report.DiscipleEvents {
if de.EventType == "death" {
_, err = tx.ExecContext(ctx, `
UPDATE disciples SET status = 'dead', died_at = NOW(), tombstone_data = $1
WHERE id = $2
`, de.TombstoneData, de.DiscipleID)
}
}
// 5. 写入战斗记录
for _, battle := range report.Battles {
_, err = tx.ExecContext(ctx, `
INSERT INTO battles (id, battle_type, world_tier, realm_tier, attacker_id, defender_id, status, result_summary, created_at)
VALUES ($1, $2, $3, $4, $5, $6, 'completed', $7, $8)
`, battle.ID, battle.Type, battle.WorldTier, battle.RealmTier,
battle.AttackerID, battle.DefenderID, battle.ResultSummary, battle.CreatedAt)
}
// 6. 写入经济审计日志
for _, audit := range report.EconomyAudits {
_, err = tx.ExecContext(ctx, `
INSERT INTO economy_audit_logs (character_id, entity_type, entity_id, currency_code, flow_type, reason_code, amount, balance_after, world_tier, created_at)
VALUES ($1, 'character', $1, $2, 'faucet', 'offline_settle', $3, $4, $5, NOW())
`, characterID, audit.CurrencyCode, audit.Amount, audit.BalanceAfter, audit.WorldTier)
}
// 7. 标记结算完成
_, err = tx.ExecContext(ctx, `
UPDATE characters SET offline_settle_version = offline_settle_version + 1 WHERE id = $1
`, characterID)
return tx.Commit()
}
```
### 10.3 结算结果压缩存储
```go
// 结算结果压缩存储到 Valkey
func saveCompressedReport(characterID string, report *SettlementReport) error {
// JSON 序列化
jsonBytes, err := json.Marshal(report)
if err != nil {
return err
}
// gzip 压缩
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write(jsonBytes); err != nil {
return err
}
gz.Close()
// 存储到 Valkey,TTL = 7 天
key := fmt.Sprintf("offline_settle:%s:%d", characterID, report.SettlementVersion)
return valkey.Set(ctx, key, buf.Bytes(), 7*24*time.Hour)
}
// 从 Valkey 读取并解压
func loadCompressedReport(characterID string, version int64) (*SettlementReport, error) {
key := fmt.Sprintf("offline_settle:%s:%d", characterID, version)
data, err := valkey.Get(ctx, key).Bytes()
if err != nil {
return nil, err
}
// gzip 解压
gz, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer gz.Close()
var report SettlementReport
if err := json.NewDecoder(gz).Decode(&report); err != nil {
return nil, err
}
return &report, nil
}
```
### 10.4 性能指标与监控
| 指标 | 目标值 | 监控方式 |
|------|--------|---------|
| 单次结算耗时24h 离线) | < 500ms | Prometheus histogram |
| 单次结算耗时120h 离线 | < 3s | Prometheus histogram |
| 结算并发数 | 100/s峰值 | Grafana dashboard |
| DB 写入延迟 | < 100ms | PostgreSQL slow query log |
| Valkey 缓存命中率 | > 95% | Valkey INFO |
| 结算失败率 | < 0.1% | AlertManager |
---
## 11. 已确认决策记录
| # | 决策 | 来源 |
|---|------|------|
| O01 | 离线结算采用"延迟结算"模式离线期间只记录事件队列上线时批量结算 | 本文确认 |
| O02 | 最大离线结算时长按境界递增炼气72h筑基96h金丹120h元婴+无上限默认上限120h通过 Nacos 可配 | 本文确认 |
| O03 | 离线战斗使用与在线完全相同的 ATB 引擎服务端权威计算 | GDD-03 1 |
| O04 | 离线游历事件自动选择分支默认"安全优先"策略玩家可设置偏好 | GDD-22 |
| O05 | 弟子离线死亡判定每游戏小时执行一次死亡率与难度/品质/保险相关 | GDD-07 T4 |
| O06 | 结算结果压缩存储到 ValkeyTTL 7 支持幂等重复拉取 | 本文确认 |
| O07 | 背包满时溢出物品转入邮件系统货币溢出按比例折算低档货币 | TDD-04 |
| O08 | 结算面板展示战报摘要不含完整行动序列),完整战报按需加载 | TDD-05 |
| O09 | 离线产出遵循 GDD-06 经济参数受疲劳衰减/区域警觉值约束 | GDD-06 §5.3 |
| O10 | 弟子代挂产出按品质/种族/技能/设施四维系数计算 | GDD-07 §2.3.2 |
| O11 | 离线 PVP 战书默认接受GDD-03 14),被挑战方上线后查看战报 | GDD-03 §8.2 |
| O12 | 结算版本号机制保证幂等性重复登录不会重复结算 | 本文确认 |
---
## 12. 验收标准
| # | 验收条目 | 测试方法 |
|---|----------|----------|
| 1 | 玩家离线 24 现实小时后上线结算面板正确显示挂机资源/弟子产出/事件列表 | 离线 24h mock + 上线验证 |
| 2 | 离线战斗结果与在线相同条件下手动触发的战斗结果一致相同随机种子 | 相同参数对比测试 |
| 3 | 背包满时溢出物品正确转入邮件货币溢出正确折算 | 背包填满后离线验证 |
| 4 | 弟子离线死亡概率符合 GDD-07 设定品质/难度/保险修正 | 10000 次蒙特卡洛模拟 |
| 5 | 离线游历事件按"安全优先"策略自动选择不选择高风险选项 | 事件池 mock 验证 |
| 6 | 结算结果幂等同一结算版本重复调用不产生额外收益 | 重复调用验证 |
| 7 | 72h 离线结算耗时 < 2s单角色 | 性能基准测试 |
| 8 | 100 并发结算请求下系统稳定 DB 死锁 | 压力测试 |
| 9 | 结算报告 JSON Schema 验证通过 | JSON Schema 校验工具 |
| 10 | 离线期间的经济审计日志完整记录每笔产出/消耗 | 查询 economy_audit_logs |
| 11 | 弟子品质/数量上限符合 GDD-07 T4炼气2合体10弟子居+2 | 边界值测试 |
| 12 | 离线 PVP 被挑战默认接受上线后战报正确展示 | PVP mock 测试 |
| 13 | Valkey 缓存 TTL 过期后结算报告正确降级到 DB 查询 | TTL 过期测试 |
| 14 | 结算版本号递增正确旧版本报告不再重复应用 | 版本号递增测试 |
---
## 13. 数据库新增/变更(补充 TDD-04
### 13.1 离线结算相关新增字段
#### characters 表新增字段
| 字段 | 类型 | 说明 |
|------|------|------|
| offline_settle_version | bigint | 离线结算版本号每次结算 +1 |
| offline_strategy | varchar(16) | 离线事件选择策略safe / greedy / balanced默认 safe |
| last_settle_at | timestamptz | 上次结算时间 |
#### 新增表offline_event_queue
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | uuid | PK | |
| character_id | uuid | FK, IX | |
| event_type | varchar(32) | IX | combat / gather / disciple / encounter |
| trigger_time_game | bigint | IX | 触发时间游戏时间戳 |
| payload | jsonb | | 事件数据 |
| resolved | boolean | IX | 是否已结算 |
| result | jsonb | | 结算结果 |
| settle_version | bigint | | 关联的结算版本 |
| created_at | timestamptz | IX | |
**分区** `created_at` 日分区保留 7
#### 新增表offline_settlement_reports
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | uuid | PK | |
| character_id | uuid | FK, IX | |
| settle_version | bigint | UQ(character_id, settle_version) | 结算版本 |
| offline_duration_real_sec | int | | 离线现实秒数 |
| offline_duration_game_hours | numeric(10,2) | | 离线游戏时长 |
| summary | jsonb | | 收益汇总压缩 |
| compressed_report | bytea | | gzip 压缩的完整报告 |
| created_at | timestamptz | IX | |
---
## 14. Nacos 动态配置项
| 配置键 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `offline.max_settle_hours.default` | int | 120 | 默认最大离线结算时长现实小时金丹期 |
| `offline.max_settle_hours.qi_refining` | int | 72 | 炼气期离线上限现实小时 |
| `offline.max_settle_hours.foundation` | int | 96 | 筑基期离线上限现实小时 |
| `offline.max_settle_hours.golden_core` | int | 120 | 金丹期离线上限现实小时 |
| `offline.max_settle_hours.nascent_soul` | int | 0 | 元婴期+离线上限0=无上限) |
| `offline.tick_interval_game_hours` | float | 1.0 | 时间快进步长游戏小时 |
| `offline.event_check_interval` | int | 300 | 定时扫描离线玩家间隔 |
| `offline.gather.fatigue_decay_per_hour` | float | 0.05 | 挂机疲劳衰减率每游戏小时 |
| `offline.gather.fatigue_min` | float | 0.3 | 疲劳衰减最低系数 |
| `offline.disciple.death_rate_base.gathering` | float | 0.001 | 弟子采集基础死亡率 |
| `offline.disciple.death_rate_base.mercenary` | float | 0.02 | 弟子佣兵基础死亡率 |
| `offline.encounter.safe_strategy_weight` | float | 0.8 | 安全策略权重 |
| `offline.encounter.greedy_strategy_weight` | float | 0.3 | 收益策略权重 |
| `offline.battle.max_ticks` | int | 3000 | 战斗最大行动时间 |
| `offline.battle.max_actions` | int | 50 | 单方最大行动次数 |
| `offline.report.cache_ttl_hours` | int | 168 | 结算报告缓存 TTL小时 |
---
## 15. 版本记录
| 版本 | 日期 | 修订内容 | 作者 |
|------|------|----------|------|
| 1.1 | 2026-07-02 | 离线结算上限按境界递增炼气72h筑基96h金丹120h元婴+无上限默认上限从72h调整为120hNacos配置拆分为按境界独立配置 | Claude Code |
| 1.0 | 2026-07-02 | 初始版本时间快进算法离线产出公式ATB战斗结算弟子代挂游历事件上线结算面板性能优化数据库变更Nacos配置 | Claude Code |
---
*TDD-06 v1.1 | 2026-07-02 | 离线结算上限按境界递增调整炼气72h→筑基96h→金丹120h→元婴+无上限 | 前序v1.0*