Files
90CNServer/server/internal/modules/dnf/cerashop/payment.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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
}