服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
137 lines
4.9 KiB
Go
137 lines
4.9 KiB
Go
package cerashop
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
)
|
|
|
|
// 商城的钱包扣费策略。
|
|
//
|
|
// 现场背景:客户端**只上报"支付方式 0 = 点券"**,没有任何"用代币券买"的选项
|
|
// (见 dnfbridge 的 currentCeraShopPaymentMode*)。也就是说代币券 / 欢乐代币券
|
|
// 能不能花掉,完全由服务端决定 —— 服务端不主动从这两个钱包扣,它们就是一组
|
|
// 只能看不能用的数字(这正是现场 2026-09-19 抱怨的另一半)。
|
|
//
|
|
// 所以这里实现官方的"瀑布"语义:欢乐代币券 → 代币券 → 点券,依次扣到够为止。
|
|
type PayPolicy int
|
|
|
|
const (
|
|
// PayPolicyCeraOnly 只扣点券,**这是零值**。
|
|
//
|
|
// 零值必须等于改动前的行为:既有调用方一行都不用改,也不会因为漏传字段而
|
|
// 悄悄换一种扣费方式(Go 里"新增 bool 字段默认 false"就是这种陷阱)。
|
|
PayPolicyCeraOnly PayPolicy = iota
|
|
// PayPolicyWaterfall 欢乐代币券 → 代币券 → 点券,依次扣。
|
|
PayPolicyWaterfall
|
|
// PayPolicyTokenCeraOnly 只扣欢乐代币券 + 代币券,不动点券。
|
|
// 对应客户端支持、而本服 PVF 尚未配置的 `[buy only cera point]` 商品;
|
|
// 余额不够就整笔失败。
|
|
PayPolicyTokenCeraOnly
|
|
)
|
|
|
|
// ErrWalletInsufficient 表示按给定策略扣不动这笔钱。
|
|
//
|
|
// 与 ErrCeraInsufficient(只针对点券)分开:调用方需要区分"点券不够"和
|
|
// "代币券+点券都不够",日志里才看得清玩家到底缺哪种钱。
|
|
var ErrWalletInsufficient = errors.New("cera shop: wallet balance is insufficient for the payment policy")
|
|
|
|
// WalletPayment 是一次结账从各钱包实际扣掉的数量。
|
|
type WalletPayment struct {
|
|
FromHappyTokenCera int64
|
|
FromTokenCera int64
|
|
FromCera int64
|
|
}
|
|
|
|
// Total 是这次结账各钱包扣费之和。
|
|
func (p WalletPayment) Total() int64 {
|
|
return p.FromHappyTokenCera + p.FromTokenCera + p.FromCera
|
|
}
|
|
|
|
// IsZero 表示没有从任何钱包扣钱。
|
|
func (p WalletPayment) IsZero() bool {
|
|
return p.FromHappyTokenCera == 0 && p.FromTokenCera == 0 && p.FromCera == 0
|
|
}
|
|
|
|
// String 便于日志与断言(例如 "happy=100 token=50 cera=0")。
|
|
func (p WalletPayment) String() string {
|
|
return fmt.Sprintf("happy=%d token=%d cera=%d", p.FromHappyTokenCera, p.FromTokenCera, p.FromCera)
|
|
}
|
|
|
|
// PlanWalletPayment 按策略算出各钱包要扣多少,**不改动传入的钱包**。
|
|
//
|
|
// 返回的 WalletPayment 保证 Total() == cost(成功时);失败时返回零值与错误。
|
|
// 这是一个纯函数,方便把"扣费顺序"这类最容易出错的逻辑单独钉死。
|
|
func PlanWalletPayment(wallet Wallet, cost int64, policy PayPolicy) (WalletPayment, error) {
|
|
if cost < 0 {
|
|
return WalletPayment{}, fmt.Errorf("%w: negative cost %d", ErrWalletInsufficient, cost)
|
|
}
|
|
if cost == 0 {
|
|
return WalletPayment{}, nil
|
|
}
|
|
// 负数余额只可能来自脏数据;当成 0,绝不让它把扣费算成"多扣"。
|
|
happy := wallet.HappyTokenCera
|
|
if happy < 0 {
|
|
happy = 0
|
|
}
|
|
token := wallet.TokenCera
|
|
if token < 0 {
|
|
token = 0
|
|
}
|
|
cera := wallet.Cera
|
|
if cera < 0 {
|
|
cera = 0
|
|
}
|
|
|
|
var payment WalletPayment
|
|
remaining := cost
|
|
|
|
// 两种非"纯点券"的策略都先花欢乐代币券:它是活动货币,优先级最高。
|
|
if policy == PayPolicyWaterfall || policy == PayPolicyTokenCeraOnly {
|
|
payment.FromHappyTokenCera = minInt64(remaining, happy)
|
|
remaining -= payment.FromHappyTokenCera
|
|
if remaining > 0 {
|
|
payment.FromTokenCera = minInt64(remaining, token)
|
|
remaining -= payment.FromTokenCera
|
|
}
|
|
}
|
|
if policy == PayPolicyWaterfall && remaining > 0 {
|
|
payment.FromCera = minInt64(remaining, cera)
|
|
remaining -= payment.FromCera
|
|
}
|
|
// 未知策略一律按"只扣点券"处理,绝不猜一种更宽松的扣法。
|
|
if policy != PayPolicyWaterfall && policy != PayPolicyTokenCeraOnly {
|
|
payment = WalletPayment{}
|
|
remaining = cost
|
|
payment.FromCera = minInt64(remaining, cera)
|
|
remaining -= payment.FromCera
|
|
}
|
|
if remaining > 0 {
|
|
return WalletPayment{}, fmt.Errorf("%w: policy=%d cost=%d wallet=%+v", ErrWalletInsufficient, policy, cost, wallet)
|
|
}
|
|
return payment, nil
|
|
}
|
|
|
|
// ApplyWalletPayment 把扣费方案写回账号钱包,并返回扣费后的钱包。
|
|
//
|
|
// 调用方负责保存账号记录(本函数只改内存里的记录,与仓库无关,便于在事务回调里用)。
|
|
// 每个字段都夹到 >= 0:方案本身已经保证不会扣超,这里只是最后一道防线。
|
|
func ApplyWalletPayment(account *dnfrepo.AccountRecord, payment WalletPayment) Wallet {
|
|
if account == nil {
|
|
return Wallet{}
|
|
}
|
|
wallet := WalletBalance(*account)
|
|
SetHappyTokenCeraBalance(account, wallet.HappyTokenCera-payment.FromHappyTokenCera)
|
|
SetTokenCeraBalance(account, wallet.TokenCera-payment.FromTokenCera)
|
|
SetBalance(account, wallet.Cera-payment.FromCera)
|
|
return WalletBalance(*account)
|
|
}
|
|
|
|
func minInt64(left, right int64) int64 {
|
|
if left < right {
|
|
return left
|
|
}
|
|
return right
|
|
}
|