|
|
@@ -0,0 +1,404 @@
|
|
|
+# 代币系统技术设计文档
|
|
|
+
|
|
|
+## 1. 需求概述
|
|
|
+
|
|
|
+新增两种道具:**代币** 和 **绑定代币**,支持以下核心功能:
|
|
|
+
|
|
|
+1. 代币/绑定代币可消耗用于购买部分付费商品(配置项控制哪些商品支持)
|
|
|
+2. 代币购买商品时触发累充VIP、首充、每日累充等功能
|
|
|
+3. 玩家可通过代币交易功能与其他玩家交换获得泪滴石
|
|
|
+4. 通过交易获得的代币为绑定代币,绑定代币不可再次交易
|
|
|
+5. 出售代币也触发累充VIP、首充、每日累充等功能
|
|
|
+6. 消耗比例:100个代币/绑定代币 = 1元付费货币等值商品
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 道具定义
|
|
|
+
|
|
|
+### 2.1 道具类型
|
|
|
+
|
|
|
+两个道具均属于**代币类(Token,ItemToolType=3)**,进入背包,通过 `RoleItemData.Items` 存储,不加入 `ItemsNotInPackage`。
|
|
|
+
|
|
|
+| 道具名称 | 建议ItemId | ItemInstanceId | 是否可交易 | 备注 |
|
|
|
+|---------|-----------|----------------|-----------|------|
|
|
|
+| 代币 | 36 | 3600 | ✅ 可交易 | 正常获得 |
|
|
|
+| 绑定代币 | 37 | 3700 | ❌ 不可交易 | 仅通过交易获得 |
|
|
|
+
|
|
|
+> **注意**:实际 ItemId 需对照现有 `item.xlsx` 最大值后续分配,不得与已有道具冲突。
|
|
|
+
|
|
|
+### 2.2 配置表改动(item.xlsx)
|
|
|
+
|
|
|
+新增两行:
|
|
|
+
|
|
|
+| 字段 | 代币 | 绑定代币 |
|
|
|
+|-----------|-----------|-----------|
|
|
|
+| Id | 36 | 37 |
|
|
|
+| ItemType | 3(Token) | 3(Token) |
|
|
|
+| ItemName | 代币 | 绑定代币 |
|
|
|
+| Stack | 99999999 | 99999999 |
|
|
|
+| AutoUse | 0 | 0 |
|
|
|
+| HideInBag | 0 | 0 |
|
|
|
+| DataItemName | Token | BoundToken |
|
|
|
+| ItemTypeDataName | Token | BoundToken |
|
|
|
+
|
|
|
+### 2.3 配置表改动(iteminstance.xlsx)
|
|
|
+
|
|
|
+新增两行:
|
|
|
+
|
|
|
+| 字段 | 代币 | 绑定代币 |
|
|
|
+|----------|------|---------|
|
|
|
+| InstanceId | 3600 | 3700 |
|
|
|
+| ItemId | 36 | 37 |
|
|
|
+| ItemType | 1 | 1 |
|
|
|
+
|
|
|
+### 2.4 代码常量(ItemDefines.cs)
|
|
|
+
|
|
|
+文件:`server/src/server/OpenCards.Server.Logic/Module/Item/ItemDefines.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+public static int TokenItemId = 36; // 代币
|
|
|
+public static int BoundTokenItemId = 37; // 绑定代币
|
|
|
+public static int TokenItemInstanceId = 3600; // 代币捆绑实例ID
|
|
|
+public static int BoundTokenItemInstanceId = 3700; // 绑定代币捆绑实例ID
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 商品代币购买功能
|
|
|
+
|
|
|
+### 3.1 配置表改动(Market.xlsx - market_gift 表)
|
|
|
+
|
|
|
+在 `Table_MarketGift` 对应的 `Market.xlsx` 的 `market_gift` sheet 新增字段:
|
|
|
+
|
|
|
+| 字段名 | 类型 | 说明 |
|
|
|
+|------------|-----|-----|
|
|
|
+| TokenEnable | int | 是否支持代币购买:0=不支持,1=支持 |
|
|
|
+
|
|
|
+对应代码改动,文件 `server/src/server/OpenCards.Server.Core/Table/Table_Market.cs`:
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <summary>
|
|
|
+/// 是否支持代币购买 0=不支持 1=支持
|
|
|
+/// </summary>
|
|
|
+public int TokenEnable;
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 消耗比例
|
|
|
+
|
|
|
+- 1元付费货币(CNY)= 100 代币/绑定代币
|
|
|
+- 下单时根据商品的 `Table_IAP.CNY` 字段计算所需代币数:`tokenCost = (int)(iap.CNY * 100)`
|
|
|
+
|
|
|
+### 3.3 代币购买流程(服务端)
|
|
|
+
|
|
|
+新增 `ChargeType.Token` 枚举值,文件 `MarketModule.cs`:
|
|
|
+
|
|
|
+```csharp
|
|
|
+private enum ChargeType
|
|
|
+{
|
|
|
+ Charge,
|
|
|
+ Ticket,
|
|
|
+ GMSend,
|
|
|
+ Token, // 代币购买
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在下单处理 `ClientGetRechargeOrderRequest` 的处理函数中,在现有 `Ticket` 判断之后追加代币购买逻辑:
|
|
|
+
|
|
|
+```
|
|
|
+if (giftConfig.TokenEnable == 1)
|
|
|
+{
|
|
|
+ // 1. 计算所需代币数量
|
|
|
+ tokenCost = (int)(iap.CNY * 100)
|
|
|
+
|
|
|
+ // 2. 优先尝试扣代币,失败则扣绑定代币,两者都不足则返回代币不足错误码
|
|
|
+ // 3. 扣款成功后调用 OnPaySuccess(productId, ext1, ..., ChargeType.Token)
|
|
|
+ // 4. 同时调用 RecordChargeToken(iap.CNY) 记录代币消费金额
|
|
|
+ // 5. 返回 CODE_CHARGE_BY_TOKEN 告知客户端
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**扣款优先级**:优先扣代币,代币不足时扣绑定代币,两者数量之和不足时返回失败。
|
|
|
+
|
|
|
+在 `ClientGetRechargeOrderResponse` 中新增响应码:
|
|
|
+
|
|
|
+```csharp
|
|
|
+public const int CODE_CHARGE_BY_TOKEN = 4; // 代币购买成功
|
|
|
+public const int CODE_TOKEN_NOT_ENOUGH = 505; // 代币不足
|
|
|
+public const int CODE_TOKEN_NOT_SUPPORT = 506; // 该商品不支持代币购买
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 代币购买对累充系统的触发
|
|
|
+
|
|
|
+在 `OnPaySuccess(ChargeType.Token)` 分支中,与 `ChargeType.Ticket` 处理方式相同:
|
|
|
+
|
|
|
+- `mRoleFlag.AddFlag(FlagDefines.ChargeFlag, convertPrice)` → 触发累计充值
|
|
|
+- `mRoleFlag.AddFlag(FlagDefines.UnrealRMBChargeFlag, rmbConvertPrice)` → 非真实RMB累计(不影响真实充值统计)
|
|
|
+- `mRoleFlag.AddFlag(FlagDefines.BuyCountFlag, 1)` → 购买次数累计
|
|
|
+- `CheckFosterRecharge(convertPrice)` → 触发每日累充(养成豪礼)
|
|
|
+- `CheckRechargeInfo()` → 触发动态难度检查
|
|
|
+- `GetModule<HeroModule>().AddRechargBUFData(convertPrice)` → 充值BUFF
|
|
|
+- `DispatchEvent(EventDefines.EventAfterPayDelivery, productId)` → 发货后事件(触发首充、VIP等检测)
|
|
|
+- `DispatchEvent(EventDefines.EventAfterPayDeliveryWithPrice, productId, price)` → 带价格的发货后事件
|
|
|
+
|
|
|
+> **关键**:代币购买同样走完整的 `OnPaySuccess` 流程,确保所有充值触发链均正常响应。VipExp 通过 `GiftDropID` 掉落表配置自动发放,无需单独处理。
|
|
|
+
|
|
|
+### 3.5 客户端二次确认弹窗
|
|
|
+
|
|
|
+当商品 `TokenEnable == 1` 时,客户端在点击购买按钮后弹出二次确认弹窗。
|
|
|
+
|
|
|
+**弹窗内容**(服务端下发商品信息中含 `TokenEnable` 字段,客户端据此决定是否展示):
|
|
|
+
|
|
|
+```
|
|
|
+标题:购买确认
|
|
|
+正文:该商品支持代币购买
|
|
|
+ · 直接充值:使用真实货币支付,立即购买
|
|
|
+ · 代币购买:消耗 {tokenCost} 个代币(当前拥有: {ownToken})
|
|
|
+[直接充值] [代币购买]
|
|
|
+```
|
|
|
+
|
|
|
+**服务端协议新增**:在商品列表同步协议中,将 `TokenEnable` 和 `TokenCost` 下发给客户端。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 代币交易系统
|
|
|
+
|
|
|
+### 4.1 功能概述
|
|
|
+
|
|
|
+玩家A(卖方)挂出代币,玩家B(买方)支付泪滴石,完成交易后:
|
|
|
+- 玩家B消耗泪滴石,获得**绑定代币**(非代币)
|
|
|
+- 玩家A获得泪滴石,此次出售触发累充系统
|
|
|
+
|
|
|
+绑定代币不可再次挂出交易。
|
|
|
+
|
|
|
+### 4.2 新增数据表(TokenTradeData)
|
|
|
+
|
|
|
+新增 `TokenTradeData.cs`(`OpenCards.Core/ORM/` 目录):
|
|
|
+
|
|
|
+```csharp
|
|
|
+[PersistType]
|
|
|
+public class TokenTradeOrder : ISerializable, IObjectMapping
|
|
|
+{
|
|
|
+ [PersistField(PersistStrategy.Primary)]
|
|
|
+ public string OrderId; // 订单唯一ID(UUID)
|
|
|
+
|
|
|
+ [PersistField]
|
|
|
+ public string SellerRoleId; // 卖方角色ID
|
|
|
+
|
|
|
+ [PersistField]
|
|
|
+ public int TokenAmount; // 挂出的代币数量
|
|
|
+
|
|
|
+ [PersistField]
|
|
|
+ public int TearStonePrice; // 要求的泪滴石数量
|
|
|
+
|
|
|
+ [PersistField]
|
|
|
+ public long CreateTime; // 创建时间(ms时间戳)
|
|
|
+
|
|
|
+ [PersistField]
|
|
|
+ public long ExpireTime; // 过期时间(ms时间戳)
|
|
|
+
|
|
|
+ [PersistField]
|
|
|
+ public int Status; // 0=挂单中,1=已成交,2=已取消,3=已过期
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+持久化类型常量在 `PersistenceConstants` 中新增:
|
|
|
+
|
|
|
+```csharp
|
|
|
+public const string TYPE_TOKEN_TRADE_ORDER = "token_trade_order";
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 新增协议(TokenTrade 协议文件)
|
|
|
+
|
|
|
+新建 `0x23A00.TokenTrade.cs`(`OpenCards.Core/Protocol/Client/` 目录):
|
|
|
+
|
|
|
+```
|
|
|
+消息码段:0x23A00 起
|
|
|
+```
|
|
|
+
|
|
|
+| 协议名 | 方向 | 说明 |
|
|
|
+|-------|-----|-----|
|
|
|
+| `ClientTokenTradeListRequest` | C→S | 获取交易列表 |
|
|
|
+| `ClientTokenTradeListResponse` | S→C | 返回交易列表 |
|
|
|
+| `ClientTokenTradePutRequest` | C→S | 挂出代币 |
|
|
|
+| `ClientTokenTradePutResponse` | S→C | 挂出结果 |
|
|
|
+| `ClientTokenTradeCancelRequest` | C→S | 取消挂单 |
|
|
|
+| `ClientTokenTradeCancelResponse` | S→C | 取消结果 |
|
|
|
+| `ClientTokenTradeBuyRequest` | C→S | 购买(用泪滴石换绑定代币) |
|
|
|
+| `ClientTokenTradeBuyResponse` | S→C | 购买结果 |
|
|
|
+| `ClientTokenTradeMyOrderRequest` | C→S | 查询自己的挂单 |
|
|
|
+| `ClientTokenTradeMyOrderResponse` | S→C | 返回自己的挂单 |
|
|
|
+
|
|
|
+**关键字段说明**:
|
|
|
+
|
|
|
+`ClientTokenTradePutRequest`:
|
|
|
+- `c2s_tokenAmount`:挂出代币数量
|
|
|
+- `c2s_tearStonePrice`:期望收取的泪滴石数量
|
|
|
+
|
|
|
+`ClientTokenTradeBuyRequest`:
|
|
|
+- `c2s_orderId`:目标订单ID
|
|
|
+
|
|
|
+### 4.4 新增模块(TokenTradeModule)
|
|
|
+
|
|
|
+新建 `server/src/server/OpenCards.Server.Logic/Module/TokenTradeModule.cs`。
|
|
|
+
|
|
|
+**挂单逻辑(DoTokenTradePut)**:
|
|
|
+
|
|
|
+```
|
|
|
+1. 校验:挂出的必须是代币(ItemId=36),不得挂绑定代币(ItemId=37)
|
|
|
+2. 校验代币余量是否足够
|
|
|
+3. 校验玩家当前挂单数量是否超上限(配置表控制,默认5单)
|
|
|
+4. 扣除玩家代币(EventRemoveItem,RemoveItemReason.TokenTrade)
|
|
|
+5. 生成 TokenTradeOrder,写入全服 Redis/DB(中心服维护订单列表)
|
|
|
+6. 返回成功
|
|
|
+```
|
|
|
+
|
|
|
+**购买逻辑(DoTokenTradeBuy)**:
|
|
|
+
|
|
|
+```
|
|
|
+1. 从中心服获取订单,校验订单状态=挂单中
|
|
|
+2. 校验买方泪滴石余量 >= 订单价格
|
|
|
+3. 原子操作:
|
|
|
+ a. 扣除买方泪滴石(EventRemoveItem,RemoveItemReason.TokenTradeBuy)
|
|
|
+ b. 发放买方 绑定代币(EventAddItem,ItemInstanceId=3700,AddItemReason.TokenTrade)
|
|
|
+ c. 向卖方发邮件,附带泪滴石奖励(EventSendMailWithItems)
|
|
|
+ d. 订单状态改为已成交
|
|
|
+4. 触发卖方出售代币的累充事件(见 4.5)
|
|
|
+```
|
|
|
+
|
|
|
+**取消挂单逻辑(DoTokenTradeCancel)**:
|
|
|
+
|
|
|
+```
|
|
|
+1. 校验订单属于本玩家且状态=挂单中
|
|
|
+2. 退还代币给玩家(EventAddItem,ItemInstanceId=3600,AddItemReason.TokenTradeCancel)
|
|
|
+3. 订单状态改为已取消
|
|
|
+```
|
|
|
+
|
|
|
+**定时过期**:订单过期时间由配置表控制(建议7天),服务端定时扫描过期订单,自动取消并退还代币。
|
|
|
+
|
|
|
+### 4.5 出售代币触发累充
|
|
|
+
|
|
|
+出售成交后,对**卖方**触发累充系统,等效金额按 `泪滴石数量 / 100` CNY 计算:
|
|
|
+
|
|
|
+```csharp
|
|
|
+float equivalentCNY = tearStoneAmount / 100f;
|
|
|
+long convertPrice = Convert.ToInt64(equivalentCNY * 100);
|
|
|
+
|
|
|
+// 触发累充系列
|
|
|
+GetModule<MarketModule>().CheckFosterRecharge(convertPrice);
|
|
|
+GetModule<MarketModule>().CheckRechargeInfo();
|
|
|
+mRoleFlag.AddFlag(FlagDefines.ChargeFlag, convertPrice, Flag.MapType.EPersist);
|
|
|
+mRoleFlag.AddFlag(FlagDefines.UnrealRMBChargeFlag, convertPrice, Flag.MapType.EPersist);
|
|
|
+mRoleFlag.AddFlag(FlagDefines.BuyCountFlag, 1, Flag.MapType.EPersist);
|
|
|
+DispatchEvent(EventDefines.EventAfterPayDeliveryWithPrice, 0, equivalentCNY);
|
|
|
+```
|
|
|
+
|
|
|
+> 出售代币不触发首充(`EventAfterPayDelivery` 不传 productId,或传 0 使相关模块跳过首充发货)。首充只在商品购买时触发,按现有逻辑执行。
|
|
|
+
|
|
|
+### 4.6 新增 AddItemReason / RemoveItemReason
|
|
|
+
|
|
|
+在 `ItemDefines.cs` 中追加:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// AddItemReason
|
|
|
+public static int TokenTrade = 129; // 代币交易获得绑定代币
|
|
|
+public static int TokenTradeCancel = 130; // 代币交易取消退还代币
|
|
|
+
|
|
|
+// RemoveItemReason
|
|
|
+public static int TokenTrade = 60; // 挂出代币
|
|
|
+public static int TokenTradeBuy = 61; // 购买时消耗泪滴石
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 泪滴石道具确认
|
|
|
+
|
|
|
+泪滴石为现有道具,已在英雄重置、斗技场等功能中使用(见客户端语言文件)。实现前需确认服务端 ItemId,通过查询 `item.xlsx` 获取,代币交易模块中引用该 ItemId 作为常量。
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 待确认实际ID,填入后在 ItemDefines 中注册
|
|
|
+public static int TearStoneItemId = ???;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 涉及文件清单
|
|
|
+
|
|
|
+### 6.1 配置表
|
|
|
+
|
|
|
+| 文件 | Sheet | 操作 |
|
|
|
+|-----|------|-----|
|
|
|
+| `item.xlsx` | item | 新增代币(36)、绑定代币(37)两行 |
|
|
|
+| `iteminstance.xlsx` | item_instance | 新增 3600、3700 两行 |
|
|
|
+| `Market.xlsx` | market_gift | 新增 `TokenEnable` 字段列 |
|
|
|
+
|
|
|
+### 6.2 服务端代码
|
|
|
+
|
|
|
+| 文件 | 操作 |
|
|
|
+|-----|-----|
|
|
|
+| `OpenCards.Core/ORM/TokenTradeData.cs` | **新建**:订单数据结构 |
|
|
|
+| `OpenCards.Core/Protocol/Client/0x23A00.TokenTrade.cs` | **新建**:交易协议定义 |
|
|
|
+| `OpenCards.Core/Data/Constants.cs` | 新增消息码段常量 `TOKEN_TRADE_START` |
|
|
|
+| `OpenCards.Server.Logic/Module/Item/ItemDefines.cs` | 新增道具ID常量、新增 AddItemReason/RemoveItemReason |
|
|
|
+| `OpenCards.Server.Logic/Module/TokenTradeModule.cs` | **新建**:代币交易业务模块 |
|
|
|
+| `OpenCards.Server.Logic/Module/MarketModule.cs` | 新增 `ChargeType.Token`;在下单处理中追加代币购买分支;新增 `RecordChargeToken()` |
|
|
|
+| `OpenCards.Server.Core/Table/Table_Market.cs` | `Table_MarketGift` 新增 `TokenEnable` 字段 |
|
|
|
+| `OpenCards.Server.Core/Table/Table_IAP.cs` | 无需改动(复用 `CNY` 字段计算代币数) |
|
|
|
+| `OpenCards.GenCodec/` | 重新生成序列化代码(自动生成,运行代码生成工具) |
|
|
|
+| `OpenCards.Server.GenORM/` | 重新生成 ORM 代码(自动生成) |
|
|
|
+
|
|
|
+### 6.3 客户端(服务端接口侧)
|
|
|
+
|
|
|
+| 文件 | 操作 |
|
|
|
+|-----|-----|
|
|
|
+| 商品列表协议 | 在现有商品信息中新增 `TokenEnable`、`TokenCost` 字段下发 |
|
|
|
+| 下单协议响应 | 新增 `CODE_CHARGE_BY_TOKEN`、`CODE_TOKEN_NOT_ENOUGH` 等响应码 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 关键设计决策
|
|
|
+
|
|
|
+### 7.1 代币/绑定代币均进背包,不走 CurrencyModule
|
|
|
+
|
|
|
+代币属于代币类(`ItemToolType.Token=3`),存储在 `RoleItemData.Items`,无需改动 `CurrencyModule.cs` 和 `RoleData.cs`。查询数量、增删均通过事件 `EventGetItemCount` / `EventAddItemImp` / `EventRemoveItem` 走背包通用逻辑。
|
|
|
+
|
|
|
+### 7.2 绑定代币以独立 ItemId 区分
|
|
|
+
|
|
|
+绑定代币(ItemId=37)与代币(ItemId=36)是两个独立道具,通过 ItemId 区分是否可交易,无需额外绑定标记字段,逻辑简单清晰。
|
|
|
+
|
|
|
+### 7.3 代币购买触发累充等同于 Ticket 机制
|
|
|
+
|
|
|
+现有 `ChargeType.Ticket` 已实现"非真实充值走完整充值流程"的模式,`ChargeType.Token` 与其保持一致,复用 `OnPaySuccess` 完整逻辑,确保首充、VIP、每日累充等所有依赖充值事件的功能均正常触发。
|
|
|
+
|
|
|
+### 7.4 代币交易为全服功能
|
|
|
+
|
|
|
+代币交易订单是全服共享的(非单角色),需中心服(或独立的交易服务)维护订单列表。订单数据存储在全服 Redis/DB,而非角色本地数据。
|
|
|
+
|
|
|
+### 7.5 购买代币时扣款优先级
|
|
|
+
|
|
|
+优先消耗代币,代币不足时消耗绑定代币。两者合计不足才返回错误。逻辑在服务端实现,客户端展示合计数量。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 错误码汇总
|
|
|
+
|
|
|
+| 错误码常量 | 值 | 说明 |
|
|
|
+|----------|---|-----|
|
|
|
+| `CODE_CHARGE_BY_TOKEN` | 4 | 代币购买成功(下单响应) |
|
|
|
+| `CODE_TOKEN_NOT_ENOUGH` | 505 | 代币不足 |
|
|
|
+| `CODE_TOKEN_NOT_SUPPORT` | 506 | 该商品不支持代币购买 |
|
|
|
+| `CODE_TRADE_ORDER_NOT_FOUND` | 507 | 交易订单不存在 |
|
|
|
+| `CODE_TRADE_ORDER_SOLD` | 508 | 订单已被购买 |
|
|
|
+| `CODE_TRADE_TOKEN_NOT_TRADEABLE` | 509 | 绑定代币不可交易 |
|
|
|
+| `CODE_TRADE_EXCEED_MAX_ORDER` | 510 | 超出最大挂单数量 |
|
|
|
+| `CODE_TRADE_TEARSTONE_NOT_ENOUGH` | 511 | 泪滴石不足 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 开发顺序建议
|
|
|
+
|
|
|
+1. **配置表**:`item.xlsx`、`iteminstance.xlsx`、`Market.xlsx` 新增字段,更新到 ServerData
|
|
|
+2. **基础道具**:`ItemDefines.cs` 新增常量,导表,验证背包可正常收发
|
|
|
+3. **商品购买**:`Table_Market.cs` 加字段 → `MarketModule.cs` 加代币购买分支 → 联调充值触发链
|
|
|
+4. **交易系统**:新建协议 → 新建 `TokenTradeModule` → 接入中心服订单存储 → 联调全流程
|
|
|
+5. **代码生成**:重新生成 GenCodec / GenORM
|
|
|
+6. **GM指令**:新增 GM 指令发放/扣除代币以方便测试
|