jeson_fxd 15 часов назад
Родитель
Сommit
ecf855a5a8

+ 5 - 5
data/ClientScript/Protocol/generated/_errorCode.lua

@@ -648,8 +648,8 @@ ClientTokenExchangeBuyResponse_501 数量必须为100的整数倍
 ClientTokenExchangeBuyResponse_502 泪滴石不足
 ClientTokenExchangeBuyResponse_503 超过可买上限
 ClientTokenExchangeBuyResponse_504 当前价格已经发生变动,请重新操作
-ClientTokenExchangeSellResponse_505 数量必须为100的整数倍
-ClientTokenExchangeSellResponse_506 绑定代币不可卖出
-ClientTokenExchangeSellResponse_507 代币不足
-ClientTokenExchangeSellResponse_508 超过可卖上限
-ClientTokenExchangeSellResponse_509 当前价格已经发生变动,请重新操作
+ClientTokenExchangeSellResponse_501 数量必须为100的整数倍
+ClientTokenExchangeSellResponse_505 绑定代币不可卖出
+ClientTokenExchangeSellResponse_506 代币不足
+ClientTokenExchangeSellResponse_503 超过可卖上限
+ClientTokenExchangeSellResponse_504 当前价格已经发生变动,请重新操作

+ 1 - 0
server/src/core/OpenCards.Core/Data/0x40700.TokenExchange.cs

@@ -6,6 +6,7 @@ namespace OpenCards.Core.Data
     [MessageType(Constants.TOKEN_EXCHANGE_START + 1)]  // 0x40701
     public class TokenExchangeLogEntry : ISerializable
     {
+        public int recordId;         //交易id
         public long tradeTime;       // 成交时间 ms
         public float unitPrice;      // 成交单价(两位小数)
         public int tokenAmount;      // 代币数量(100 整数倍)

+ 5 - 0
server/src/core/OpenCards.Core/ORM/Constants.cs

@@ -201,6 +201,8 @@
 
         public const string TYPE_CHARGE_BY_TICKET_RECORD = "ChargeByTicket:";
 
+        public const string TYPE_CHARGE_BY_TOKEN_RECORD = "ChargeByToken:";
+
         public const string TYPE_FORMATION_RECOMMEND_DATA = "RoleFormationRecommend:";
         
         public const string TYPE_FORMATION_RECOMMEND_RECORD = "FormationRecommendRecord:";
@@ -215,5 +217,8 @@
         //神兽
         public const string TYPE_HALLOWSDATA = "HallowsData:";
 
+        // Token 代币
+        public const string TYPE_TOKEN_EXCHANGE_GLOBAL = "TokenExchangeGlobal";
+        public const string TYPE_TOKEN_EXCHANGE_ROLE = "TokenExchangeRole";
     }
 }

+ 0 - 157
server/src/core/OpenCards.Core/ORM/NewServerActivityData.cs

@@ -1,157 +0,0 @@
-using DeepCore;
-using DeepCore.IO;
-using DeepCore.ORM;
-using System;
-
-namespace OpenCards.Core.ORM
-{
-    /// <summary>
-    ///  新服狂欢  活动数据
-    /// </summary>
-    [PersistType]
-    public class NewServerActivityData : IObjectMapping, ISerializable
-    {
-        /// <summary>
-        /// 活动集合 key:活动唯一id
-        /// </summary>
-        [PersistField]
-        public HashMap<int, NewServerActivityInfo> ActivityMap = new HashMap<int, NewServerActivityInfo>();
-    }
-
-    /// <summary>
-    /// 心愿召唤活动总数据
-    /// </summary>
-    [PersistType]
-    public class NewServerActivityInfo : IObjectMapping, ISerializable
-    {
-        [PersistField]
-        public NewServerActivityBaseData BaseData = new NewServerActivityBaseData();
-
-        [PersistField]
-        public NewServerActivitySummonData SummonData = new NewServerActivitySummonData();
-
-        [PersistField]
-        public NewServerActivityBossChallengeData BossChallengeData = new NewServerActivityBossChallengeData();
-
-        [PersistField]
-        public NewServerActivityHeroGiftData HeroGiftData = new NewServerActivityHeroGiftData();
-
-    }
-
-    /// <summary>
-    /// 心愿召唤活动基础数据
-    /// </summary>
-    [PersistType]
-    public class NewServerActivityBaseData : IObjectMapping, ISerializable
-    {
-        /// <summary>
-        /// 活动唯一id
-        /// </summary>
-        [PersistField]
-        public int ActivityUniqueId;
-
-        /// <summary>
-        /// 活动开始时间
-        /// </summary>
-        [PersistField]
-        public long ActivityStartTimeStamp;
-
-        /// <summary>
-        /// 活动结束时间
-        /// </summary>
-        [PersistField]
-        public long ActivityEndTimeStamp;
-
-        /// <summary>
-        /// 活动类型
-        /// </summary>
-        [PersistField]
-        public int ActivityType;
-
-        /// <summary>
-        /// 活动开启状态
-        /// </summary>
-        [PersistField]
-        public bool ActivityOpenState;
-
-        /// <summary>
-        /// 活动组id
-        /// </summary>
-        [PersistField]
-        public int ActivityGroupId;
-    }
-
-    /// <summary>
-    /// 心愿召唤活动召唤数据
-    /// </summary>
-    [PersistType]
-    public class NewServerActivitySummonData : IObjectMapping, ISerializable
-    {
-        /// <summary>
-        /// 英雄捆Id
-        /// </summary>
-        [PersistField]
-        public int SelectHeroInstanceId;
-
-        /// <summary>
-        /// 计数
-        /// </summary>
-        [PersistField]
-        public int HeroCounter;
-
-        // 每日召唤次数
-        [PersistField]
-        public int DailyCount;
-
-        // 总召唤次数
-        [PersistField]
-        public int TotalCount;
-
-        // 召唤时间戳
-        [PersistField]
-        public long Timestamp;
-
-        // 掉落信息,用于动态权重计算,key:掉落库id,value:<捆id, 掉落次数>
-        [PersistField]
-        public HashMap<int, HashMap<int, int>> DropedInfo = new HashMap<int, HashMap<int, int>>();
-
-        /// <summary>
-        /// 任务埋点记录
-        /// </summary>
-        [PersistField]
-        public HashMap<int, bool> TaskDigRecordMap = new HashMap<int, bool>();
-    }
-
-    /// <summary>
-    /// 心愿召唤Boss挑战活动数据
-    /// </summary>
-    [PersistType]
-    public class NewServerActivityBossChallengeData : IObjectMapping, ISerializable
-    {
-        /// <summary>
-        /// 挑战次数
-        /// </summary>
-        [PersistField]
-        public int FightTimes;
-
-        /// <summary>
-        /// 历史最高伤害
-        /// </summary>
-        [PersistField]
-        public double MaxDamage;
-    }
-
-    /// <summary>
-    /// 心愿召唤活动英雄礼包数据
-    /// </summary>
-    [PersistType]
-    public class NewServerActivityHeroGiftData : IObjectMapping, ISerializable
-    {
-        /// <summary>
-        /// 锁定的英雄id
-        /// </summary>
-        [PersistField]
-        public int LockHeroId;
-    }
-
-}

+ 92 - 0
server/src/core/OpenCards.Core/ORM/TokenExchangeGlobalData.cs

@@ -0,0 +1,92 @@
+using DeepCore.ORM;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using DeepCore.IO;
+using DeepCore;
+using System.Collections;
+using OpenCards.Core.Data;
+
+namespace OpenCards.Core.ORM
+{
+    /// <summary>代币交易所数据</summary>
+    [PersistType]
+    public class TokenExchangeGlobalData : ISerializable, IObjectMapping
+    {
+        /// <summary> 当前交易价,保留2位小数 </summary>
+        [PersistField]
+        public float UnitPrice;
+
+        /// <summary> 上次调价时间戳 (ms) </summary>
+        [PersistField]
+        public long LastAdjustTime;
+
+        /// <summary> 下一档调价时间戳 </summary>
+        [PersistField]
+        public long NextAdjustTime;
+
+        /// <summary>
+        /// 本轮 本服 买入代币总量
+        /// </summary>
+        [PersistField]
+        public long PeriodBuyCount;
+
+        /// <summary>
+        /// 本轮 本服 卖出代币总量
+        /// </summary>
+        [PersistField]
+        public long PeriodSellCount;
+    }
+
+    [PersistType]
+    public class TokenExchangeRoleData : ISerializable, IObjectMapping
+    {
+        [PersistField]
+        public long nextRecordId = 1;   // 记录id,自增
+
+        [PersistField]
+        public List<TokenExchangeRecord> BuyLogs = new List<TokenExchangeRecord>();
+
+        [PersistField]
+        public List<TokenExchangeRecord> SellLogs = new List<TokenExchangeRecord>();
+    }
+
+    [PersistType]
+    public class TokenExchangeRecord : ISerializable, IObjectMapping
+    {
+        [PersistField(PersistStrategy.Primary)]
+        public long recordId; // 记录id,自增
+
+        [PersistField]
+        public int TradeType; //  1=买入,2=卖出
+
+        [PersistField]
+        public long TradeTime; // 交易时间
+
+        [PersistField]
+        public float UnitPrice; // 成交单价,保留2位小数
+
+        [PersistField]
+        public int TokenAmount; // 交易代币数量,100整数倍
+
+        /// <summary>
+        /// 买入时:消耗的泪滴石总数
+        /// 卖出时:实际到账泪滴石,扣除手续费后
+        /// </summary>
+        [PersistField]
+        public int TearStoneAmount;
+
+        /// <summary>
+        /// 仅卖出有效,卖出毛收入泪滴石
+        /// </summary>
+        [PersistField]
+        public int GrossTearStone;
+
+        /// <summary>
+        /// 仅卖出有效,手续费泪滴石 (gross * 5%, 向下取整)
+        /// </summary>
+        [PersistField]
+        public int FreeAmount;
+    }
+
+}

+ 10 - 10
server/src/core/OpenCards.Core/Protocol/Client/0x57000.Logic.TokenExchange.cs

@@ -50,8 +50,8 @@ namespace OpenCards.Core.Protocol.Client
     [RequestMsg(typeof(ClientTokenExchangeSellResponse), true, true, false)]
     public class ClientTokenExchangeSellRequest : ClientRequest, ILogicProtocol
     {
-        public int c2s_tokenAmount;   // 100 整数倍
-        public float c2s_unitPrice;
+        public int c2s_tokenAmount;   // 卖出代币数量,100 整数倍
+        public float c2s_unitPrice;   // 客户端界面展示的单价,服务端校验
     }
 
     [MessageType(Constants.LOGIC_TOKEN_EXCHANGE_START + 6)]  // 
@@ -59,22 +59,22 @@ namespace OpenCards.Core.Protocol.Client
     public class ClientTokenExchangeSellResponse : ClientResponse, ILogicProtocol
     {
         [MessageCode("数量必须为100的整数倍")] 
-        public const int CODE_EXCHANGE_AMOUNT_INVALID = CODE_ERROR + 5;
+        public const int CODE_EXCHANGE_AMOUNT_INVALID = CODE_ERROR + 1;
         [MessageCode("绑定代币不可卖出")] 
-        public const int CODE_EXCHANGE_BOUND_NOT_SELLABLE = CODE_ERROR + 6;
+        public const int CODE_EXCHANGE_BOUND_NOT_SELLABLE = CODE_ERROR + 5;
         [MessageCode("代币不足")] 
-        public const int CODE_EXCHANGE_TOKEN_NOT_ENOUGH = CODE_ERROR + 7;
+        public const int CODE_EXCHANGE_TOKEN_NOT_ENOUGH = CODE_ERROR + 6;
         [MessageCode("超过可卖上限")] 
-        public const int CODE_EXCHANGE_AMOUNT_EXCEED = CODE_ERROR + 8;
+        public const int CODE_EXCHANGE_AMOUNT_EXCEED = CODE_ERROR + 3;
         [MessageCode("当前价格已经发生变动,请重新操作")] 
-        public const int CODE_EXCHANGE_PRICE_CHANGED = CODE_ERROR + 9;
+        public const int CODE_EXCHANGE_PRICE_CHANGED = CODE_ERROR + 4;
 
         public TokenExchangeSnapshot s2c_snapshot = new TokenExchangeSnapshot();
 
         /// <summary>仅卖出成功时有效</summary>
-        public int s2c_grossTearStone;
-        public int s2c_feeAmount;
-        public int s2c_netTearStone;
+        public int s2c_grossTearStone; // 毛收入泪滴石
+        public int s2c_freeAmount;   // 手续费(向下取整)
+        public int s2c_netTearStone;  // 净收入泪滴石
     }
 
     //  4. 查询账单 

+ 1 - 0
server/src/server/OpenCards.Server.Logic/LogicDefines.cs

@@ -47,6 +47,7 @@ namespace OpenCards.Server.Logic
         RoleFormationRecommendData,
         NewGuildBoss,
         TOTAL_NUMBER,
+        TokenExchangeRoleData,
     }
     public static class LogicDefines
     {

+ 12 - 2
server/src/server/OpenCards.Server.Logic/Module/Item/ItemDefines.cs

@@ -134,8 +134,6 @@ namespace OpenCards.Server.Logic.Module
         public static int TokenItemInstanceId = 30003001;
         /// <summary>绑定代币捆实例 ID</summary>
         public static int BoundTokenItemInstanceId = 30003101;
-        /// <summary>泪滴石(交易所消耗/获得),对应 item.xlsx Id=1</summary>
-        //public static int TearStoneItemId = 1;
 
         /// <summary>
         /// 不进背包的物品id列表
@@ -288,6 +286,11 @@ namespace OpenCards.Server.Logic.Module
         public static int DungeonSweep = 126;
         public static int DungeonBoxReward = 127;
         public static int HallowsReset = 128;
+        /// <summary> 交易所买入 获得绑定代币 </summary>
+        public static int TokenExchangeBuy = 129;         
+        /// <summary> 交易所卖出 获得泪滴石 </summary>
+        public static int TokenExchangeSell = 130; 
+
     }
 
     public class RemoveItemReason
@@ -351,6 +354,13 @@ namespace OpenCards.Server.Logic.Module
         public static int HallowsLeveUp = 57;
         public static int HallowsCombi = 58;
         public static int HallowsReset = 59;
+        /// <summary> 商品代币购买扣款 </summary>
+        public static int TokenPurchase = 60;
+        /// <summary> 交易所买入获得绑定代币 </summary>
+        public static int TokenExchangeBuy = 61;
+        /// <summary> 交易卖出消耗代币 </summary>
+        public static int TokenExchangeSell = 62;
+
     }
 
     public class RemoveItemSecondaryReason

+ 207 - 95
server/src/server/OpenCards.Server.Logic/Module/MarketModule.cs

@@ -26,7 +26,7 @@ namespace OpenCards.Server.Logic.Module
     public class MarketModule : ILogicModule
     {
         public MarketDataMapping mMarketDataMapping;
-        
+
         private TLRoleFlagModule mRoleFlag;
 
         private RequireModule mRequire;
@@ -69,7 +69,7 @@ namespace OpenCards.Server.Logic.Module
             RegisterMessageHandler<LogicService.MsgClientGetFirstRechargeInfoRequest>(OnMsgClientGetFirstRechargeInfoRequest);
             //首充礼包显示次数
             RegisterMessageHandler<LogicService.MsgClientSetFirstRechargeShowRequest>(OnMsgClientSetFirstRechargeShowRequest);
-            
+
             //累充礼包
             RegisterMessageHandler<LogicService.MsgClientGetAccumulateRechargeInfoRequest>(OnMsgClientGetAccumulateRechargeInfoRequest);
             RegisterMessageHandler<LogicService.MsgClientGetRecommendGiftInfoRequest>(OnMsgClientGetRecommendGiftInfoRequest);
@@ -566,7 +566,7 @@ namespace OpenCards.Server.Logic.Module
                     if (drop != null)
                     {
                         drops.AddRange(drop);
-                    }                    
+                    }
                     //DoGiftDrop(drop, AddItemReason.Market, gift.GiftGroup);
                 }
                 if (kv.Value.seniorDropID > 0)
@@ -603,7 +603,7 @@ namespace OpenCards.Server.Logic.Module
             if (!mMarketDataMapping.MarketProgressRewardInfos.TryGetValue(c2s_marketPkgID, out var mappingInfo))
             {
                 return GetErrorCode();
-            }            
+            }
             if (!IsBuyGift(c2s_marketPkgID))
             {
                 return ClientBuyMarketProgressRewardScoreResponse.ERROR_SENIOR_NOT_OPEN;
@@ -765,7 +765,7 @@ namespace OpenCards.Server.Logic.Module
             if (subTabCfg != null)
             {
                 CheckReddotProgressReward(subTabCfg.ID, marketPkgID);
-            }            
+            }
         }
 
         //是否领取完所有奖励
@@ -842,15 +842,15 @@ namespace OpenCards.Server.Logic.Module
         }
 
         private int GetFundPeriod(long periodTime, long serverOpenTime, ref int index)
-        {           
+        {
             index = 0;
-            var nowDate = TimeUtils.Now;            
+            var nowDate = TimeUtils.Now;
             var now = nowDate.GetTimeStamp();
-         
+
             var passTime = now - serverOpenTime;
             int periodCnt = (int)(passTime / periodTime);
             //log.Error(passTime- periodTime);
-            var curPeriodStartTime = serverOpenTime + periodCnt * periodTime;                        
+            var curPeriodStartTime = serverOpenTime + periodCnt * periodTime;
             var curPeridStartDate = TimeUtils.TimeStampToDateTime(curPeriodStartTime);
             //index = nowDate.DayOfYear - curPeridStartDate.DayOfYear;
             index = (nowDate - curPeridStartDate).Days + 1;
@@ -938,7 +938,7 @@ namespace OpenCards.Server.Logic.Module
         }
 
         public List<ItemInstanceData> GetProgressRewardUnGotReward(MarketProgressRewardInfoMapping mappingInfo,
-            int marketPkgID, int subType, int payTimes,int progress)
+            int marketPkgID, int subType, int payTimes, int progress)
         {
 
             Action<int, HashMap<int, int>> dropReward = (giftDropID, rewards) =>
@@ -1061,7 +1061,7 @@ namespace OpenCards.Server.Logic.Module
             if (cfgRewardModule.PrivilegeID > 0)
             {
                 ClearFlagByType(subTabCfg.ChildFlagType, cfgRewardModule.PrivilegeID, FlagDefines.PurchaseFlag);
-            }            
+            }
         }
 
         public void ClearBuyGift(int marketPkgID)
@@ -1139,7 +1139,7 @@ namespace OpenCards.Server.Logic.Module
             if (price > 0)
             {
                 float rate = GetRateByCurrencyCode(mMarketDataMapping.CurrencyCode);
-                money = price* rate;
+                money = price * rate;
             }
 
             foreach (var kv in rechargeCount)
@@ -1323,7 +1323,7 @@ namespace OpenCards.Server.Logic.Module
                 {
                     return GetErrorCode();
                 }
-                info.Value.BuyCnt =  GetFlagByType(subTabCfg.ChildFlagType, info.Value.ID, FlagDefines.PurchaseFlag);
+                info.Value.BuyCnt = GetFlagByType(subTabCfg.ChildFlagType, info.Value.ID, FlagDefines.PurchaseFlag);
                 c2s_DayList.Add(info.Value.Data);
             }
 
@@ -1388,7 +1388,7 @@ namespace OpenCards.Server.Logic.Module
                 FosterPayBonusSdkDot(cfg.GiftId, s2c_id);
 
                 //检测 是否都已经领取 
-                if (!CheckFosterRedInfo_1()) 
+                if (!CheckFosterRedInfo_1())
                 {
                     DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate), false, true);
                 }
@@ -1401,11 +1401,11 @@ namespace OpenCards.Server.Logic.Module
         public bool CheckFosterRedInfo()
         {
             bool red = false;
-            foreach (var info in mMarketDataMapping.ActivityAcumulate.Infos) 
+            foreach (var info in mMarketDataMapping.ActivityAcumulate.Infos)
             {
                 if (info.Value.State == 0)
                 {
-                    if (mMarketDataMapping.ActivityAcumulate.Day >= info.Value.Day) 
+                    if (mMarketDataMapping.ActivityAcumulate.Day >= info.Value.Day)
                     {
                         red = true;
                         return red;
@@ -1429,7 +1429,7 @@ namespace OpenCards.Server.Logic.Module
                 {
                     var BuyCnt = GetFlagByType(subTabCfg.ChildFlagType, info.Value.ID, FlagDefines.PurchaseFlag);
 
-                    if (BuyCnt > 0) 
+                    if (BuyCnt > 0)
                     {
                         Buy = true;
                         break;
@@ -1437,10 +1437,10 @@ namespace OpenCards.Server.Logic.Module
                 }
 
             }
-            if (!Buy ) 
+            if (!Buy)
             {
                 red = true;
-              
+
             }
             return red;
         }
@@ -1462,7 +1462,7 @@ namespace OpenCards.Server.Logic.Module
             return red;
         }
 
-        public bool CheckFosterRedInfo_Daily ()
+        public bool CheckFosterRedInfo_Daily()
         {
             bool red = false;
             bool Buy = false;
@@ -1637,7 +1637,7 @@ namespace OpenCards.Server.Logic.Module
                 DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate), false, true);
 
             }
-            else 
+            else
             {
                 DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate), true, true);
             }
@@ -1664,7 +1664,7 @@ namespace OpenCards.Server.Logic.Module
             {
                 foreach (var type in Table_PayDynamicDifficultyManager._typeMap)
                 {
-                    PayDynamicDifficultyInfo info =  new PayDynamicDifficultyInfo();
+                    PayDynamicDifficultyInfo info = new PayDynamicDifficultyInfo();
 
                     if (mMarketDataMapping.PayDynamicDifficulty.ContainsKey(type.Key))
                     {
@@ -1673,23 +1673,23 @@ namespace OpenCards.Server.Logic.Module
                     }
 
                     var temp = info.CalculatePrice;
-                    while (value >  (temp + (float)denominator)) 
+                    while (value > (temp + (float)denominator))
                     {
                         temp += denominator;
                         var cfg = Table_PayDynamicDifficultyManager.GetPayDynamicDifficultyCfg(type.Key, temp);
-                        if (cfg != null) 
+                        if (cfg != null)
                         {
-                            info.CalculatePrice  = temp;
-                            info.Cnt += cfg.StageNumber ;
-               
+                            info.CalculatePrice = temp;
+                            info.Cnt += cfg.StageNumber;
+
                         }
-                       
+
                     }
-                    mMarketDataMapping.PayDynamicDifficulty.AddOrUpdate(type.Key,info); 
+                    mMarketDataMapping.PayDynamicDifficulty.AddOrUpdate(type.Key, info);
                 }
 
             }
-          
+
         }
 
         // 1:不是用支付动态难度 0:是用动态难度
@@ -1698,7 +1698,7 @@ namespace OpenCards.Server.Logic.Module
             int Ret = 0;
             if (mMarketDataMapping.PayDynamicDifficulty.ContainsKey(FightMod))
             {
-                if (mMarketDataMapping.PayDynamicDifficulty[FightMod].Cnt> 0) 
+                if (mMarketDataMapping.PayDynamicDifficulty[FightMod].Cnt > 0)
                 {
                     Ret = 1;
                 }
@@ -1767,7 +1767,7 @@ namespace OpenCards.Server.Logic.Module
             CheckReddotMonthGift();
             CheckReddotTop();
             CheckReddotSummonGift();
-            CheckReddotProgressReward((int)MarketSubId.GrowUp,0);            
+            CheckReddotProgressReward((int)MarketSubId.GrowUp, 0);
             //犒赏令,基金
             foreach (var kv in Table_ProgressRewardModuleManager.IDMap)
             {
@@ -2303,7 +2303,7 @@ namespace OpenCards.Server.Logic.Module
         /// <summary>
         /// 检查个人成长礼包红点
         /// </summary>        
-        private void CheckReddotProgressReward(int subId,int marketPkgID)
+        private void CheckReddotProgressReward(int subId, int marketPkgID)
         {
             string key = string.Empty;
             bool isReddot = false;
@@ -2313,13 +2313,13 @@ namespace OpenCards.Server.Logic.Module
             {
                 key = string.Format(RedDotConstantConfig.MARKET_MAIN_BUTTON, (int)MarketTabType.TypeLimit, subTabCfg.ID);
                 foreach (var giftID in Table_MarketGiftGrowthManager.GetGiftIds())
-                {                    
+                {
                     if (!IsBuyGift(giftID))
                     {
                         continue;
                     }
                     if (MarketProgressRewardGetAll(giftID))
-                    {                        
+                    {
                         continue;
                     }
                     var gift = Table_MarketGiftManager.GetByID(giftID);
@@ -2367,15 +2367,15 @@ namespace OpenCards.Server.Logic.Module
                     {
                         isReddot = true;
                         break;
-                    }                    
+                    }
                 }
                 DispatchEvent(EventDefines.EventChangeReddotStatusItem, key, isReddot, true);
                 return;
             }
-            else if (subId == (int)MarketSubId.RoyalReward || 
-                subId == (int)MarketSubId.DreamReward || 
+            else if (subId == (int)MarketSubId.RoyalReward ||
+                subId == (int)MarketSubId.DreamReward ||
                 subId == (int)MarketSubId.MazeReward ||
-                subId == (int)MarketSubId.SuperFund || 
+                subId == (int)MarketSubId.SuperFund ||
                 subId == (int)MarketSubId.LuxuryFund)
             {
                 if (subId == (int)MarketSubId.SuperFund || subId == (int)MarketSubId.LuxuryFund)
@@ -2386,11 +2386,11 @@ namespace OpenCards.Server.Logic.Module
                 {
                     key = string.Format(RedDotConstantConfig.MARKET_MAIN_BUTTON, (int)MarketTabType.TypeReward, subId);
                 }
-                
+
                 if (!mMarketDataMapping.MarketProgressRewardInfos.TryGetValue(marketPkgID, out var mappingInfo))
                 {
                     return;
-                }                
+                }
                 var cfgs = Table_ProgressRewardManager.GetProgressRewards(marketPkgID, mappingInfo.PlanID);
                 if (cfgs == null)
                 {
@@ -2529,10 +2529,10 @@ namespace OpenCards.Server.Logic.Module
         public long GetRoyalOpenTime()
         {
             var createTime = GetModule<RoleModule>().roleDataMapping.CreateTime;
-            return createTime.AddDays(ConstantConfig.RoyalRewardPanelOpenRequire).GetTimeStamp();            
+            return createTime.AddDays(ConstantConfig.RoyalRewardPanelOpenRequire).GetTimeStamp();
         }
 
-        public bool IsMazeRewardOpen(int giftId=0)
+        public bool IsMazeRewardOpen(int giftId = 0)
         {
             int isOpen = Convert.ToInt32(mRoleFlag.GetFlag(FlagDefines.MazeRewardOpenStatus, Common.Flag.Flag.MapType.EPersist));
             if (isOpen == 1)
@@ -2543,7 +2543,7 @@ namespace OpenCards.Server.Logic.Module
             if (giftId > 0)
             {
                 return CheckGiftOpenRequire(giftId);
-            }            
+            }
             foreach (var kv in Table_ProgressRewardModuleManager.IDMap)
             {
                 if (kv.Value.Type != (int)MarketProgressType.ProgressModuleType_Normal)
@@ -2589,13 +2589,13 @@ namespace OpenCards.Server.Logic.Module
         }
 
         public bool GetSubscribePrivilege(SubscribePrivilege t, out int v)
-        {            
+        {
             v = 0;
             long endtime = 0;
             if (!GetAndCheckSubscibeStatus(ref endtime))
             {
                 return false;
-            }                 
+            }
             var cfg = Table_SubscribePrivilegeManager.GetPrivilegeCfg((int)t);
             if (cfg == null)
             {
@@ -2610,7 +2610,7 @@ namespace OpenCards.Server.Logic.Module
             long nowTs = TimeUtils.CurrentTimeMs;
             foreach (var kv in mMarketDataMapping.SubscribeInfos)
             {
-                var infoMapping = kv.Value;                
+                var infoMapping = kv.Value;
                 if (nowTs >= infoMapping.EndTime)
                 {
                     if (infoMapping.EndTime > 0)
@@ -2711,8 +2711,8 @@ namespace OpenCards.Server.Logic.Module
         /// 获取累充界面
         /// </summary>
         private int OnMsgClientGetAccumulateRechargeInfoRequest(ref float s2c_recharge_value, ref HashMap<int, int> s2c_gift_awards, ref int s2c_round)
-        {                    
-            if (TimeUtils.TryParse(ConstantConfig.AccumulateRechargeNewCreaterTime,out var r))
+        {
+            if (TimeUtils.TryParse(ConstantConfig.AccumulateRechargeNewCreaterTime, out var r))
             {
                 var createTime = TimeUtils.GetDayStart(GetModule<RoleModule>().RoleData.CreateTime);
                 if (createTime >= r)
@@ -2720,7 +2720,7 @@ namespace OpenCards.Server.Logic.Module
                     s2c_round = 1;
                 }
             }
-            
+
             s2c_recharge_value = CalCurrencyCodeValue();
             var subTable = Table_MarketSubTabManager.GetByID((int)MarketSubId.Accumulate);
             if (subTable != null)
@@ -2839,7 +2839,7 @@ namespace OpenCards.Server.Logic.Module
                     s2c_stock = CheckGiftPurchase(subTable.ChildFlagType, giftTable);
                 }
             }
-            
+
             return Response.CODE_OK;
         }
 
@@ -3021,7 +3021,7 @@ namespace OpenCards.Server.Logic.Module
                 }
             }
 
-  
+
 
             //直接掉落奖励
             var drop = GetGiftDrop(data.ID, data.GiftDropID);
@@ -3186,7 +3186,7 @@ namespace OpenCards.Server.Logic.Module
                 int stock = CheckGiftPurchase(package.ChildFlagType, giftConfig);
                 if (stock <= 0 && stock != -1)
                 {
-                    log.Info("检查礼包限购:  ChildFlagType=" + package.ChildFlagType+"    stock="+stock);
+                    log.Info("检查礼包限购:  ChildFlagType=" + package.ChildFlagType + "    stock=" + stock);
                     rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_RECHARGE_PURCHASE_ERROR;
                     return rsp;
                 }
@@ -3200,6 +3200,51 @@ namespace OpenCards.Server.Logic.Module
                 return rsp;
             }
 
+
+            // TODO:  临时参数、代码测试
+            req.c2s_useToken = 1;
+            giftConfig.TokenEnable = 1;
+
+            // 代币购买商品
+            /// 扣款优先级(服务端最终裁决)
+            ///1. 计算 tokenCost = CNY * 100
+            ///2.若 boundCount + tokenCount < tokenCost → 失败
+            ///3.若 boundCount >= tokenCost → 仅扣 boundCount 中 tokenCost
+            ///4.否则 → 扣光全部 boundCount,再扣(tokenCost - boundCount) 的代币
+            if (req.c2s_useToken == 1)
+            {
+                if (giftConfig.TokenEnable != 1)
+                {
+                    rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_TOKEN_NOT_SUPPORT;
+                    return rsp;
+                }
+                int tokenCost = (int)(iap.CNY * 100);
+
+                long boundCount = GetItemCount(ItemDefines.BoundTokenItemId);
+                long tokenCount = GetItemCount(ItemDefines.TokenItemId);
+                
+                //检查代币是否足够
+                if (boundCount + tokenCount < tokenCount)
+                {
+                    rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_TOKEN_NOT_ENOUGH;
+                    return rsp;
+                }
+
+                if (!TryDeductTokenForPurchase(tokenCost, req.c2s_giftId, out _))
+                {
+                    rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_TOKEN_NOT_ENOUGH;
+                    return rsp;
+                }
+
+                await OnPaySuccess(req.c2s_giftId, req.c2s_pushId, TimeUtils.Now.ToString(), TimeUtils.Now.ToString(),
+                    req.c2s_uiType,ChargeType.Token /*, triggerVip: giftConfig.VipChargeEnable == 1*/ );
+
+                await RecordChargeToken(iap.CNY);
+
+                rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_CHARGE_BY_TOKEN;
+                return rsp;
+            }
+
             if (ConstantConfig.UseChargeTicket && iap.TicketItemId > 0)
             {
                 Ref<bool> success = new Ref<bool>();
@@ -3213,18 +3258,18 @@ namespace OpenCards.Server.Logic.Module
 
                     rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_CHARGE_BY_TICKET;
                     return rsp;
-                }   
+                }
                 DispatchEvent(EventDefines.EventRemoveItem, ItemDefines.Ticket1ItemId, (int)iap.CNY, RemoveItemReason.Charge, req.c2s_giftId, LogicUtils.FileLine, success);
                 if (success.value)
                 {
                     await OnPaySuccess(req.c2s_giftId, req.c2s_pushId, TimeUtils.Now.ToString(), TimeUtils.Now.ToString(),
                         req.c2s_uiType, ChargeType.Ticket);
-                    
+
                     await RecordChargeTicket(iap.CNY);
 
                     rsp.s2c_code = ClientGetRechargeOrderResponse.CODE_CHARGE_BY_TICKET;
                     return rsp;
-                }   
+                }
             }
 
             //创建订单
@@ -3264,6 +3309,61 @@ namespace OpenCards.Server.Logic.Module
             return rsp;
         }
 
+        private async Task RecordChargeToken(float price)
+        {
+            MappingHash record = new MappingHash(PersistenceConstants.TYPE_CHARGE_BY_TOKEN_RECORD, service);
+            if (!await record.ExistsAsync(service.roleID))
+            {
+                await record.SetAsync(service.roleID, price);
+            }
+            else
+            {
+                var total = await record.GetAsync<float>(service.roleID);
+                await record.SetAsync(service.roleID, total + price);
+            }
+        }
+
+        /// <summary>
+        /// 代币购买扣款,优先扣除绑定代币,不足再扣除代币
+        /// </summary>
+        /// <param name="tokenCost"></param>
+        /// <param name="c2s_giftId"></param>
+        /// <param name="useToken"></param>
+        /// <returns></returns>
+        private bool TryDeductTokenForPurchase(int tokenCost, int c2s_giftId, out int useToken)
+        {
+            useToken = 0;
+
+            long boundCount = GetModule<MarketModule>().GetItemCount(ItemDefines.BoundTokenItemId);
+            long tokenCount = GetModule<MarketModule>().GetItemCount(ItemDefines.TokenItemId);
+
+            if (boundCount + tokenCount < tokenCost)
+                return false;
+
+            Ref<bool> success = new Ref<bool>();
+            int boundDeduct = (int)Math.Min(boundCount, tokenCost);
+            int tokenDeduct = tokenCost - boundDeduct;
+            
+            if (boundDeduct > 0)
+            {
+                DispatchEvent(EventDefines.EventRemoveItem,ItemDefines.BoundTokenItemId, boundDeduct,
+                    RemoveItemReason.TokenPurchase,c2s_giftId, LogicUtils.FileLine, success);
+                if (!success.value)
+                    return false;
+            }
+
+            if (tokenDeduct > 0)
+            {
+                DispatchEvent(EventDefines.EventRemoveItem,ItemDefines.TokenItemId, tokenDeduct,
+                    RemoveItemReason.TokenPurchase, c2s_giftId, LogicUtils.FileLine, success);
+                if (!success.value)
+                    return false;
+            }
+
+            useToken = tokenDeduct;
+            return true;
+        }
+
         private async Task RecordChargeTicket(float price)
         {
             MappingHash record = new MappingHash(PersistenceConstants.TYPE_CHARGE_BY_TICKET_RECORD, service);
@@ -3278,6 +3378,18 @@ namespace OpenCards.Server.Logic.Module
             }
         }
 
+        /// <summary>
+        /// 获取道具数目
+        /// </summary>
+        /// <param name="itemId"></param>
+        /// <returns></returns>
+        public long GetItemCount(int itemId)
+        {
+            var ownCount = new Ref<long>();
+            DispatchEvent(EventDefines.EventGetItemCount, itemId, ownCount);
+            return ownCount.value;
+        }
+
         /// <summary>
         /// 检测订单
         /// </summary>
@@ -3313,7 +3425,7 @@ namespace OpenCards.Server.Logic.Module
                 orderId = req.c2s_orderID,
             };
             var payRsp = await payServer.CallAsync<ChangeOrderStateRpcResponse>(payReq);
-			for (int i = 0; i < ConstantConfig.PayRetryTimes; i++)
+            for (int i = 0; i < ConstantConfig.PayRetryTimes; i++)
             {
                 if (payRsp.s2c_code == PayResponse.CODE_ORDER_NOT_PURCHASE)
                 {
@@ -3464,7 +3576,7 @@ namespace OpenCards.Server.Logic.Module
             FosterDailyBonusSdkDot(productId, price);
 
             await PaySdkDot(orderId, transNo, productId, true, uiType, payType);
-            
+
             if (productId == ConstantConfig.PrivilegeGiftId)
             {
                 mRoleFlag.SetFlag(FlagDefines.PrivilegeGiftMailReward, 1, Flag.MapType.E1Day);
@@ -3509,7 +3621,7 @@ namespace OpenCards.Server.Logic.Module
                 var ext1 = payRsp.ext1s[i];
                 var orderId = payRsp.orderIds[i];
                 var transNo = payRsp.transNos[i];
-                
+
                 rsp.s2c_orderIds.Add(orderId);
                 rsp.s2c_productIds.Add(productId.ToString());
 
@@ -3828,7 +3940,7 @@ namespace OpenCards.Server.Logic.Module
 
                         //不需要检查限购
                         //if (!mRequire.DoRequire(gift.PurchaseTimes, out _))
-                            //continue;
+                        //continue;
 
                         //限购条件去重
                         if (!filter.TryAdd(gift.No, gift.ID))
@@ -4102,7 +4214,7 @@ namespace OpenCards.Server.Logic.Module
                 {
                     foreach (var kv in marketSubTab.MarketPackageMap)
                     {
-                        if(mMarketDataMapping.SubscribeInfos.TryGetValue(kv.Key, out var infoMapping))
+                        if (mMarketDataMapping.SubscribeInfos.TryGetValue(kv.Key, out var infoMapping))
                         {
                             if (TimeUtils.CurrentTimeMs >= infoMapping.EndTime)
                             {
@@ -4208,7 +4320,7 @@ namespace OpenCards.Server.Logic.Module
                             0);
                             mappingInfo.RewardRecord.AddRange(missDays);
                         }
-                        
+
                         //marketPackageData.Ext.TryAddOrUpdate("payMoney", payMoney);
                         //marketPackageData.Ext.TryAddOrUpdate("leftSeconds", 0);
                         //marketPackageData.Ext.TryAddOrUpdate("canReward", 0);
@@ -4218,7 +4330,7 @@ namespace OpenCards.Server.Logic.Module
                             //月卡过期
                             if (mappingInfo.OpenTime > 0)
                             {
-                                ExpireMonthCard(giftId, mappingInfo);                                
+                                ExpireMonthCard(giftId, mappingInfo);
                             }
                         }
                         int payMoney = GetMonthCardPayMoney(mappingInfo.PayMoney, mappingInfo.RechargeCount);
@@ -4241,7 +4353,7 @@ namespace OpenCards.Server.Logic.Module
                 {
                     continue;
                 }
-                if(!mMarketDataMapping.MarketMonthCardRewardInfos.TryGetValue(giftId, out var mappingInfo))
+                if (!mMarketDataMapping.MarketMonthCardRewardInfos.TryGetValue(giftId, out var mappingInfo))
                 {
                     continue;
                 }
@@ -4279,15 +4391,15 @@ namespace OpenCards.Server.Logic.Module
                 if (subTabCfg == null)
                 {
                     continue;
-                }                
+                }
 
                 if (!mMarketDataMapping.MarketProgressRewardInfos.TryGetValue(giftID, out var mappingInfo))
                 {
                     continue;
-                }                
+                }
 
-                var payTimes = GetFlagByType(subTabCfg.ChildFlagType, giftID, FlagDefines.PurchaseFlag);    
-                if(TryResetMarketProgress(mappingInfo, cfg, subTabCfg.SubType,payTimes))
+                var payTimes = GetFlagByType(subTabCfg.ChildFlagType, giftID, FlagDefines.PurchaseFlag);
+                if (TryResetMarketProgress(mappingInfo, cfg, subTabCfg.SubType, payTimes))
                 {
                     ClearFlagByType(subTabCfg.ChildFlagType, giftID, FlagDefines.PurchaseFlag);
                     if (cfg.PrivilegeID > 0)
@@ -4514,7 +4626,7 @@ namespace OpenCards.Server.Logic.Module
                     //{
                     //    continue;
                     //}
-                    if(!IsMazeRewardOpen(giftID))
+                    if (!IsMazeRewardOpen(giftID))
                     {
                         continue;
                     }
@@ -4737,7 +4849,7 @@ namespace OpenCards.Server.Logic.Module
                 return true;
             }
 
-            
+
             int stageId = Convert.ToInt32(GetModule<TLRoleFlagModule>().GetFlag(FlagDefines.StageClearFlag, Flag.MapType.EPersist));
             var stageTable = Table_ChapterManager.GetById(stageId);
             if (stageTable == null)
@@ -4772,7 +4884,7 @@ namespace OpenCards.Server.Logic.Module
                 }
             }
             isReplacedGiftId = tempGiftList.Count > 0;
-            
+
 
             if (isReplacedGiftId == false)
             {
@@ -4812,7 +4924,7 @@ namespace OpenCards.Server.Logic.Module
                             tempGiftList.Add(id);
                         }
                     }
-                } 
+                }
                 if (tempGiftList.Count == 0)
                 {
                     log.InfoFormat("GetPushGiftByDepotId count is 0 depotId={0}, partId={1}", depotId, partId);
@@ -4835,10 +4947,10 @@ namespace OpenCards.Server.Logic.Module
                 if (tempGiftList.Count == 0)
                 {
                     log.InfoFormat("GetPushGiftByDepotId count is 0 depotId={0}, partId={1}", depotId, depotId);
-                    return false;   
+                    return false;
                 }
             }
-            
+
             var depotLimitTable = Table_PushGiftDepotLimitManager.GetByDepotID(depotId);
             if (depotLimitTable != null)
             {
@@ -4875,7 +4987,7 @@ namespace OpenCards.Server.Logic.Module
 
                         showCount++;
                     }
-                    
+
                     // 不同弹脸点的弹脸礼包共享一个弹脸礼包同时存在上限,当游戏中的弹脸礼包数量(倒计时未结束)已达该上限时,不会再弹出新的弹脸礼包
                     if (sameGigftCount >= ConstantConfig.TotalGiftLimit)
                     {
@@ -4891,7 +5003,7 @@ namespace OpenCards.Server.Logic.Module
                     }
 
                     int hasCountLimit = 0;
-    
+
                     foreach (var kv in mMarketDataMapping.PushDepotCount)
                     {
                         var table = Table_PushGiftDepotLimitManager.GetByDepotID(kv.Key);
@@ -4916,8 +5028,8 @@ namespace OpenCards.Server.Logic.Module
                         log.InfoFormat("GetPushGiftByDepotId TotalGiftLimit count={0}", hasCountLimit);
                         return false;
                     }
-                    
-                    
+
+
                     // a.单库同时存在礼包上限:当游戏中存在该礼包库的礼包(倒计时未结束),且数量已达上限时,不会再新增该礼包库的礼包
                     if (depotLimitTable.SingleDepotLimit > 0)
                     {
@@ -4925,12 +5037,12 @@ namespace OpenCards.Server.Logic.Module
                         {
                             if (kv.Key == depotId && kv.Value >= depotLimitTable.SingleDepotLimit)
                             {
-                                log.InfoFormat("zhangs3: GetPushGiftByDepotId DepotCountLimit key={0} count={1}",depotId, kv.Value);
+                                log.InfoFormat("zhangs3: GetPushGiftByDepotId DepotCountLimit key={0} count={1}", depotId, kv.Value);
                                 return false;
                             }
                         }
                     }
-                    
+
                 }
 
                 //拥有个数
@@ -4942,7 +5054,7 @@ namespace OpenCards.Server.Logic.Module
                 {
                     mMarketDataMapping.PushDepotNum.Add(depotId, 1);
                 }
-                
+
                 //触发次数
                 if (mMarketDataMapping.PushDepotCount.ContainsKey(depotId))
                 {
@@ -4953,7 +5065,7 @@ namespace OpenCards.Server.Logic.Module
                     mMarketDataMapping.PushDepotCount.Add(depotId, 1);
                 }
             }
-            
+
 
             Random rnd = new Random();
             int index = rnd.Next(0, tempGiftList.Count);
@@ -4973,7 +5085,7 @@ namespace OpenCards.Server.Logic.Module
                 {
                     continue;
                 }
-                
+
                 bool isKey = false;
                 var tmpKeyList = new List<string>();
                 foreach (string requireKey in gift.OpenRequire.key)
@@ -5301,7 +5413,7 @@ namespace OpenCards.Server.Logic.Module
                         {
                             return oldValue;
                         }
-                        
+
                         return Convert.ToInt32(mRoleFlag.GetFlagByDate(prefix + id, Common.Flag.Flag.MapType.E30Day, 0, 1));
                     }
                 default:
@@ -5501,7 +5613,7 @@ namespace OpenCards.Server.Logic.Module
             // 重置弹出条件数据
             mMarketDataMapping.PopupKeyCount.Clear();
 
-            
+
             // 重置弹出库数量数据
             mMarketDataMapping.PushDepotNum.Clear();
             // 重置弹出库触发数据
@@ -5995,7 +6107,7 @@ namespace OpenCards.Server.Logic.Module
 
         private void OnEventMainLevelChange(int stageID)
         {
-            CheckReddotProgressReward((int)MarketSubId.GrowUp,0);
+            CheckReddotProgressReward((int)MarketSubId.GrowUp, 0);
         }
 
         private void OnEventAfterPayDelivery(int productId)
@@ -6012,14 +6124,14 @@ namespace OpenCards.Server.Logic.Module
                 }
                 if (kv.Value.PrivilegeID == productId)
                 {
-                    GetPrivilegeReward(kv.Value.ID, kv.Value);                    
+                    GetPrivilegeReward(kv.Value.ID, kv.Value);
                 }
             }
 
             var gift = Table_MarketGiftManager.GetByID(productId);
             if (gift != null && gift.GiftGroup == (int)MarketSubId.Subscribe)
             {
-                mMarketDataMapping.SubscribeInfos.TryGetOrCreate(gift.ID, out var infoMapping, (t) => new SubscribeInfo());    
+                mMarketDataMapping.SubscribeInfos.TryGetOrCreate(gift.ID, out var infoMapping, (t) => new SubscribeInfo());
                 long nowTs = TimeUtils.CurrentTimeMs;
                 infoMapping.BuyTime = nowTs;
                 long effectTime = (long)30 * (long)86400000;
@@ -6030,7 +6142,7 @@ namespace OpenCards.Server.Logic.Module
                     effectTime += infoMapping.EndTime - nowTs;
                     newOpen = false;
                 }
-                infoMapping.EndTime = nowTs + effectTime;                
+                infoMapping.EndTime = nowTs + effectTime;
                 HashMap<int, MarketTab> markets = new HashMap<int, MarketTab>();
                 MarketTab nTab = new MarketTab()
                 {
@@ -6163,7 +6275,7 @@ namespace OpenCards.Server.Logic.Module
                     {
                         DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate), true, true);
                     }
-                    else 
+                    else
                     {
                         DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate), false, true);
                     }
@@ -6171,7 +6283,7 @@ namespace OpenCards.Server.Logic.Module
                     {
                         DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate_Daily), true, true);
                     }
-                    else 
+                    else
                     {
                         DispatchEvent(EventDefines.EventChangeReddotStatusItem, string.Format(RedDotConstantConfig.CULTIVATE_GIFT_BUTTON, (int)FosterType.FosterType_Acumulate_Daily), false, true);
                     }
@@ -6622,16 +6734,16 @@ namespace OpenCards.Server.Logic.Module
         private async Task HeroSummonPassportSdkDot(int giftId, float price)
         {
             bool hasGift = false;
-            foreach(var table in Table_ActivityHeroShowManager.ActivityGroupIdMap)
+            foreach (var table in Table_ActivityHeroShowManager.ActivityGroupIdMap)
             {
-                if(giftId == table.Value.ProgressRewardID)
+                if (giftId == table.Value.ProgressRewardID)
                 {
                     hasGift = true;
                     break;
                 }
             }
 
-            if(!hasGift)
+            if (!hasGift)
             {
                 return;
             }
@@ -6686,7 +6798,7 @@ namespace OpenCards.Server.Logic.Module
                 return;
             }
 
-            if(table.GiftGroup != ConstantConfig.FosterGiftGroupID)
+            if (table.GiftGroup != ConstantConfig.FosterGiftGroupID)
             {
                 return;
             }

+ 606 - 0
server/src/server/OpenCards.Server.Logic/Module/TokenExchange/TokenExchangeModule.cs

@@ -0,0 +1,606 @@
+using DeepCore.Protocol;
+using DeepCrystal.ORM;
+using OpenCards.Core.Data;
+using OpenCards.Core.ORM;
+using OpenCards.Core.Protocol.Client;
+using OpenCards.Server.Common.Flag;
+using OpenCards.Server.Core;
+using OpenCards.Server.Core.ORM;
+using OpenCards.Server.Core.Utils;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OpenCards.Server.Logic.Module.TokenExchange
+{
+    public class TokenExchangeModule : ILogicModule
+    {
+        // 角色账单(每个玩家)
+        private TokenExchangeRoleDataMapping roleMapping;
+        // 全服单价,按serverId分key
+        private MappingBase<TokenExchangeGlobalData> globalMapping;
+        private TLRoleFlagModule mRoleFlag;
+        private IDisposable mAdjustTimer;
+
+        private readonly int TradeTypeBuy = 1;
+        private readonly int TradeTypeSell = 2;
+        private readonly int LogMaxCount = 20;
+
+        //private long mNextRecordId = 1;
+
+        [InitOnLoad]
+        private static void Init()
+        {
+            ModuleManager.RegistryModule<TokenExchangeModule>();
+            GetAllRoleDatas += (roleId, list) =>
+            {
+                list.Add(new TokenExchangeRoleDataMapping(PersistenceConstants.TYPE_TOKEN_EXCHANGE_ROLE, roleId));
+            };
+        }
+
+        public override Task OnCreateRoleAsync()
+        {
+            roleMapping.Data = this.CreateRoleMappingData<TokenExchangeRoleData>(RoleMappingDataDefines.TokenExchangeRoleData);
+            // 初始化
+            var data = roleMapping.Data;
+            data.nextRecordId = 1;
+            data.SellLogs = new List<TokenExchangeRecord>();
+            data.BuyLogs = new List<TokenExchangeRecord>();
+            return Task.CompletedTask;
+        }
+
+        public override async Task OnStartAsync()
+        {
+            // 玩家Mapping (与 MarketModule 类似,构造时传入 service)
+            roleMapping = new TokenExchangeRoleDataMapping(PersistenceConstants.TYPE_TOKEN_EXCHANGE_ROLE, service.roleID, service);
+            await this.LoadRoleMappingData(roleMapping, RoleMappingDataDefines.TokenExchangeRoleData);
+
+            if (roleMapping.Data.BuyLogs == null)
+                roleMapping.Data.BuyLogs = new List<TokenExchangeRecord>();
+            if (roleMapping.Data.SellLogs == null)
+                roleMapping.Data.SellLogs = new List<TokenExchangeRecord>();
+            if (roleMapping.Data.nextRecordId <= 0)
+                roleMapping.Data.nextRecordId = 1;
+
+            //全服mapping (key 用 serverID,不用 roleID)
+            await EnsureGlobalDataAsync();
+
+            mRoleFlag = GetModule<TLRoleFlagModule>();
+        }
+
+        public override Task OnStartedAsync()
+        {
+            mAdjustTimer = service.Provider.CreateTimer(OnAdjustTimerTick, this,
+                TimeSpan.FromSeconds(30 + new Random().Next(1, 30)),
+                TimeSpan.FromMinutes(1));
+
+            return base.OnStartedAsync();
+        }
+
+        public override void OnSaveData(IObjectTransaction trans, StringBuilder sb, bool fullData)
+        {
+            // 只保存玩家数据,global在买卖/调价时flush
+            if (trans != null)
+            {
+                if (fullData)
+                    roleMapping?.BatchSaveData(trans);
+                else
+                    roleMapping?.BatchFlush(trans);
+            }
+            else
+            {
+                sb.AppendLine(roleMapping.Data.GetType().Name + ":" + LogicUtils.ToJson(roleMapping.Data));
+            }
+        }
+
+        protected override void Disposing()
+        {
+            mAdjustTimer?.Dispose();
+            roleMapping?.Dispose();
+            globalMapping?.Dispose();
+        }
+
+
+        public TokenExchangeModule(LogicService service) : base(service)
+        {
+            RegisterMessageHandler<LogicService.MsgClientTokenExchangeInfoRequest>(HandleTokenExchangeInfoReq);
+            RegisterMessageHandler<LogicService.MsgClientTokenExchangeBuyRequest>(HandleTokenExchangeBuyReq);
+            RegisterMessageHandler<LogicService.MsgClientTokenExchangeSellRequest>(HandleTokenExchangeSellReq);
+            RegisterMessageHandler<LogicService.MsgClientTokenExchangeLogRequest>(HandleTokenExchangeLogReq);
+        }
+
+        private int HandleTokenExchangeLogReq(int c2s_tradeType, ref List<TokenExchangeLogEntry> s2c_buyLogs, ref List<TokenExchangeLogEntry> s2c_sellLogs)
+        {
+            var roleData = roleMapping.Data;
+            if (roleData == null)
+                return GetErrorCode("role data null");
+
+            switch (c2s_tradeType)
+            {
+                case 0:
+                    s2c_sellLogs = BuildLogEntries(roleData.SellLogs);
+                    break;
+                case 1:
+                    s2c_buyLogs = BuildLogEntries(roleData.BuyLogs);
+                    break;
+                default:
+                    return GetErrorCode($"c2s_tradeType error: {c2s_tradeType}");
+            }
+
+            return Response.CODE_OK;
+        }
+
+        private List<TokenExchangeLogEntry> BuildLogEntries(List<TokenExchangeRecord> logs)
+        {
+            if (logs.Count == 0 || logs == null)
+                return new List<TokenExchangeLogEntry>();
+
+            int start = Math.Max(0, logs.Count - LogMaxCount);
+            var result = new List<TokenExchangeLogEntry>(Math.Min(logs.Count, LogMaxCount));
+
+            for (int i = logs.Count - 1; i >= start; i--)
+            {
+                result.Add(ToLogEntry(logs[i]));
+            }
+
+            return result;
+        }
+
+        private async Task<ClientTokenExchangeSellResponse> HandleTokenExchangeSellReq(ClientTokenExchangeSellRequest req)
+        {
+            var rsp = new ClientTokenExchangeSellResponse();
+            var globalData = globalMapping.Data;
+            int amount = req.c2s_tokenAmount;
+            long tokenCount = GetTokenCount();
+
+            if (amount <= 0 || amount % 100 != 0)
+            {
+                rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_AMOUNT_INVALID;
+                return rsp;
+            }
+            if (!IsUnitPriceMatch(req.c2s_unitPrice, globalData.UnitPrice))
+            {
+                rsp.s2c_snapshot = BuildSnapshot(globalData);
+                rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_PRICE_CHANGED;
+                return rsp;
+            }
+            if (amount > CalcMaxSellAmount(tokenCount))
+            {
+                rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_AMOUNT_EXCEED;
+                return rsp;
+            }
+            if (tokenCount < amount)
+            {
+                rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_TOKEN_NOT_ENOUGH;
+                return rsp;
+            }
+
+            // 计算数量
+            CalcSellTearStone(amount, req.c2s_unitPrice, out int grossTearStone, out int freeAmount, out int netSearStone);
+
+            // 扣代币
+            Ref<bool> success = new Ref<bool>();
+            DispatchEvent(EventDefines.EventRemoveItem, ItemDefines.TokenItemId, amount,
+                RemoveItemReason.TokenExchangeSell, 0, LogicUtils.FileLine, success);
+            if (!success.value)
+            {
+                rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_TOKEN_NOT_ENOUGH;
+                return rsp;
+            }
+            // 加泪滴石
+            DispatchEvent(EventDefines.EventAddItem, ItemDefines.DiamondItemId, netSearStone,
+                AddItemReason.TokenExchangeSell, 0, LogicUtils.FileLine);
+
+            //记录
+            await ModifyGlobalAsync(global =>
+            {
+                global.PeriodSellCount += amount;
+                return true;
+            });
+
+            AppendExchangeLog(roleMapping.Data.SellLogs, new TokenExchangeRecord()
+            {
+                recordId = GenRecordId(),
+                TradeType = TradeTypeSell,
+                TradeTime = TimeUtils.CurrentTimeMs,
+                UnitPrice = globalData.UnitPrice,
+                TokenAmount = amount,
+                TearStoneAmount = netSearStone,
+                GrossTearStone = grossTearStone,
+                FreeAmount = freeAmount
+            });
+
+            await roleMapping.FlushAsync();
+
+            TriggerRechargeOnTokenSell(netSearStone);
+
+            rsp.s2c_code = Response.CODE_OK;
+            rsp.s2c_grossTearStone = grossTearStone;
+            rsp.s2c_freeAmount = freeAmount;
+            rsp.s2c_netTearStone = netSearStone;
+            rsp.s2c_snapshot = BuildSnapshot(globalData);
+            return rsp;
+        }
+
+        private async Task<ClientTokenExchangeBuyResponse> HandleTokenExchangeBuyReq(ClientTokenExchangeBuyRequest req)
+        {
+            var rsp = new ClientTokenExchangeBuyResponse();
+            int amount = req.c2s_tokenAmount;
+            var globalData = globalMapping.Data;
+
+            if (amount <= 0 || amount % 100 != 0)
+            {
+                rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_AMOUNT_INVALID;
+                return rsp;
+            }
+            if (!IsUnitPriceMatch(req.c2s_unitPrice, globalData.UnitPrice))
+            {
+                rsp.s2c_snapshot = BuildSnapshot(globalData);
+                rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_PRICE_CHANGED;
+                return rsp;
+            }
+            if (amount > CalcMaxBuyAmount(GetTearStoneCount(), globalData.UnitPrice))
+            {
+                rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_AMOUNT_EXCEED;
+                return rsp;
+            }
+            // 数量判断
+            var tearStoneCost = (int)(amount * globalData.UnitPrice);
+            if (GetTearStoneCount() < tearStoneCost)
+            {
+                rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_TEARSTONE_NOT_ENOUGH;
+                return rsp;
+            }
+            // 扣泪滴石
+            var success = new Ref<bool>();
+            if (!DeductTearStoneForPurchase(tearStoneCost))
+            {
+                rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_TEARSTONE_NOT_ENOUGH;
+                return rsp;
+            }
+            // 发代币
+            DispatchEvent(EventDefines.EventAddItem, ItemDefines.BoundTokenItemId, amount,
+                AddItemReason.TokenExchangeBuy, 0, LogicUtils.FileLine);
+
+            // 记录
+            await ModifyGlobalAsync(global =>
+            {
+                global.PeriodBuyCount += amount;
+                return true;
+            });
+            // 日志是每个服每个玩家单独保存, 不能存服务器级别上
+            AppendExchangeLog(roleMapping.Data.BuyLogs, new TokenExchangeRecord()
+            {
+                recordId = GenRecordId(),
+                TradeType = TradeTypeBuy,
+                TradeTime = TimeUtils.CurrentTimeMs,
+                UnitPrice = globalData.UnitPrice,
+                TokenAmount = amount,
+                TearStoneAmount = tearStoneCost,
+                GrossTearStone = 0,
+                FreeAmount = 0,
+            });
+
+            await roleMapping.FlushAsync();
+
+            rsp.s2c_code = Response.CODE_OK;
+            rsp.s2c_snapshot = BuildSnapshot(globalData);
+            return rsp;
+        }
+
+        /// <summary>
+        /// 修改全服数据前重新 Load,改完后 Flush,降低覆盖其它玩家写入的风险。
+        /// </summary>
+        /// <param name="modifier"></param>
+        /// <returns></returns>
+        private async Task<bool> ModifyGlobalAsync(Func<TokenExchangeGlobalData, bool> modifier)
+        {
+            await globalMapping.LoadDataAsync();
+
+            if (globalMapping.Data == null)
+                return false;
+
+            if (!modifier(globalMapping.Data))
+                return false;
+
+            await globalMapping.FlushAsync();
+            return true;
+        }
+
+        private long GenRecordId()
+        {
+            return roleMapping.Data.nextRecordId++;
+        }
+
+        private int HandleTokenExchangeInfoReq(ref TokenExchangeSnapshot s2c_snapshot)
+        {
+            if (globalMapping?.Data == null)
+                return GetErrorCode("globalMapping is null");
+
+            s2c_snapshot = BuildSnapshot(globalMapping.Data);
+            return Response.CODE_OK;
+        }
+
+        /// <summary>
+        /// 扣泪滴石,优先扣免费
+        /// </summary>
+        /// <param name="cost"></param>
+        /// <returns></returns>
+        private bool DeductTearStoneForPurchase(int cost)
+        {
+            var tearStone = GetModule<MarketModule>().GetItemCount(ItemDefines.DiamondItemId);
+            var freeTearStone = GetModule<MarketModule>().GetItemCount(ItemDefines.FreeDiamondItemId);
+
+            if (tearStone + freeTearStone < cost)
+                return false;
+
+            Ref<bool> success = new Ref<bool>();
+            int freeTearDeduct = (int)Math.Min(freeTearStone, cost);
+            int tearDeduct = cost - freeTearDeduct;
+
+            if (freeTearDeduct > 0)
+            {
+                DispatchEvent(EventDefines.EventRemoveItem, ItemDefines.FreeDiamondItemId, freeTearDeduct,
+                    RemoveItemReason.TokenPurchase, 0, LogicUtils.FileLine, success);
+                if (!success.value)
+                    return false;
+            }
+
+            if (tearDeduct > 0)
+            {
+                DispatchEvent(EventDefines.EventRemoveItem, ItemDefines.DiamondItemId, tearDeduct,
+                    RemoveItemReason.TokenPurchase, 0, LogicUtils.FileLine, success);
+                if (!success.value)
+                    return false;
+            }
+
+            return true;
+        }
+
+        // 全局加载
+        private async Task EnsureGlobalDataAsync()
+        {
+            if (globalMapping != null && globalMapping.Data != null)
+                return;
+
+            globalMapping = new MappingBase<TokenExchangeGlobalData>(PersistenceConstants.TYPE_TOKEN_EXCHANGE_GLOBAL, service.serverID);
+
+            await globalMapping.LoadOrCreateDataAsync(() => new TokenExchangeGlobalData
+            {
+                UnitPrice = 2.00f,
+                LastAdjustTime = TimeUtils.CurrentTimeMs,
+                NextAdjustTime = CalcNextAdjustTime(TimeUtils.CurrentTimeMs),
+                PeriodBuyCount = 0,
+                PeriodSellCount = 0,
+            });
+        }
+
+        /// <summary>
+        /// 返回下一个调价时间点, 当日12点或者次日00:00 (当天24点)
+        /// </summary>
+        /// <param name="nowMs"></param>
+        /// <returns></returns>
+        private long CalcNextAdjustTime(long nowMs)
+        {
+            var now = TimeUtils.TimeStampToDateTime(nowMs);
+            var dayStart = TimeUtils.GetDayStart(now);
+            var noon = dayStart.AddHours(12);
+            var midnight = dayStart.AddDays(1);
+
+            long noonMs = (long)(noon - TimeUtils.GetUTCStartTime()).TotalMilliseconds;
+            long midnightMs = (long)(midnight - TimeUtils.GetUTCStartTime()).TotalMilliseconds;
+
+            if (nowMs < noonMs)
+                return noonMs;
+            if (nowMs < midnightMs)
+                return midnightMs;
+
+            return noonMs + TimeUtils.OndDayTimeMs;
+        }
+
+        private async Task OnAdjustTimerTick(object state)
+        {
+            if (globalMapping?.Data == null)
+                return;
+            if (TimeUtils.CurrentTimeMs < globalMapping.Data.LastAdjustTime)
+                return;
+            await DoAdjustUnitPrice();
+        }
+
+        /// <summary>
+        /// 定时调价
+        /// </summary>
+        public async Task DoAdjustUnitPrice()
+        {
+            await globalMapping.LoadDataAsync();
+
+            var globalData = globalMapping.Data;
+            if (globalData == null)
+                return;
+
+            long now = TimeUtils.CurrentTimeMs;
+
+            if (now < globalData.NextAdjustTime) 
+                return; // 其他玩家实例已调价            
+
+            float newPrice = CalcAdjustedUnitPrice(globalData.UnitPrice, globalData.PeriodBuyCount, globalData.PeriodSellCount);
+
+            globalData.UnitPrice = newPrice;
+            globalData.LastAdjustTime = now;
+            globalData.NextAdjustTime = CalcNextAdjustTime(now);
+            globalData.PeriodBuyCount = 0;
+            globalData.PeriodSellCount = 0;
+
+            await globalMapping.FlushAsync();
+
+            // 改价后通知在线玩家
+            NotifyClient(new ClientTokenExchangePriceNotify()
+            {
+                s2c_snapshot = BuildSnapshot(globalData)
+            });
+        }
+
+        /// <summary>
+        /// 按公式计算调整后单价
+        /// buyVolume / sellVolume:上一调价周期内的累计量。
+        /// 调整后交易单价 = Min(Max(当前交易单价 * Max(Min(买入总量 / 卖出总量, 1.10),0.90)1.00),4.00)
+        /// </summary> 
+        /// <returns></returns>
+        private float CalcAdjustedUnitPrice(float currentPrice, long buyVolume, long sellVolume)
+        {
+            float ratio;
+
+            // 本轮没有卖出
+            if (sellVolume <= 0)
+            {
+                ratio = 1.00f;
+            }
+            else
+            {
+                ratio = (float)buyVolume / sellVolume;
+                ratio = Math.Max(Math.Min(ratio, 1.10f), 0.90f);
+            }
+            float newPrice = currentPrice * ratio;
+            newPrice = Math.Min(Math.Max(newPrice, 1.00f), 4.00f);
+            return (float)Math.Round(newPrice, 2, MidpointRounding.AwayFromZero);
+        }
+
+        /// <summary>
+        ///  判断客户端与服务器单价是否一致,保留2位小数
+        /// </summary>
+        /// <param name="clientPrice"></param>
+        /// <param name="serverPrice"></param>
+        /// <returns></returns>
+        private bool IsUnitPriceMatch(float clientPrice, float serverPrice)
+        {
+            return Math.Round(clientPrice, 2, MidpointRounding.AwayFromZero)
+                == Math.Round(serverPrice, 2, MidpointRounding.AwayFromZero);
+        }
+
+        /// <summary>
+        /// 拥有的泪滴石 
+        /// 免费和付费在此地可共用
+        /// </summary>
+        /// <returns></returns>
+        private long GetTearStoneCount()
+        {
+            var tearStone = GetModule<MarketModule>().GetItemCount(ItemDefines.DiamondItemId);
+            var freeTearStone = GetModule<MarketModule>().GetItemCount(ItemDefines.FreeDiamondItemId);
+            return tearStone + freeTearStone;
+        }
+
+        private long GetTokenCount()
+            => GetModule<MarketModule>().GetItemCount(ItemDefines.TokenItemId);
+
+        /// <summary>
+        /// 可卖出上限: floor ( 代币 / 100 ) * 100
+        /// </summary>
+        /// <param name="tokenCount"></param>
+        /// <returns></returns>
+        public int CalcMaxSellAmount(long tokenCount)
+        {
+            return (int)(tokenCount / 100) * 100;
+        }
+
+        /// <summary>
+        /// 可买入上限 : floor ( 泪滴石 / (单价 * 100) ) * 100
+        /// </summary>
+        /// <param name="tearStoneCount"></param>
+        /// <param name="unitPrice"></param>
+        /// <returns></returns>
+        public int CalcMaxBuyAmount(long tearStoneCount, float unitPrice)
+        {
+            if (unitPrice <= 0)
+                return 0;
+            int units = (int)(tearStoneCount / (unitPrice * 100)); // 取整
+            return units * 100;
+        }
+
+        /// <summary>
+        /// 计算卖出手续费、到账泪滴石,
+        /// </summary>
+        /// <param name="tokenAmount"></param>
+        /// <param name="unitPrice"></param>
+        /// <param name="grossTearStone"></param>
+        /// <param name="freeAmount"> 5%手续费 </param> 
+        /// <param name="netTearStone"></param>
+        private void CalcSellTearStone(int tokenAmount, float unitPrice,
+            out int grossTearStone, out int freeAmount, out int netTearStone)
+        {
+            grossTearStone = (int)(tokenAmount * unitPrice);
+            freeAmount = grossTearStone * 5 / 100; // 向下取整
+            netTearStone = grossTearStone - freeAmount;
+        }
+
+        /// 买卖数量校验
+        //public int CheckBuyAndSell(int amount, bool isBuy, bool isSell, int tearStone, float unitPrice, int tokenCount)
+        //{
+        //    if (amount <= 0 || amount % 100 != 0)
+        //        return CODE_EXCHANGE_AMOUNT_INVALID;
+        //    if (isBuy && amount > CalcMaxBuyAmount(tearStone, unitPrice))
+        //        return CODE_EXCHANGE_AMOUNT_EXCEED;
+        //    if (isSell && amount > CalcMaxSellAmount(tokenCount))
+        //        return CODE_EXCHANGE_AMOUNT_EXCEED;
+        //}
+
+        /// <summary>
+        /// 买卖记录,保留20条
+        /// </summary>
+        private void AppendExchangeLog(List<TokenExchangeRecord> logs, TokenExchangeRecord record, int maxCount = 20)
+        {
+            logs.Add(record);
+            while (logs.Count > maxCount)
+            {
+                logs.RemoveAt(0);
+            }
+        }
+
+        /// <summary>
+        /// 出售代币后 触发充值链
+        /// netTearStone: 扣除手续费后 实际获得泪滴石数量
+        /// </summary>
+        private void TriggerRechargeOnTokenSell(int netTearStone)
+        {
+            float equivalentCNY = netTearStone / 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.EventAfterPayDelivery, 0);
+            DispatchEvent(EventDefines.EventAfterPayDeliveryWithPrice, 0, equivalentCNY);
+        }
+
+        private static TokenExchangeLogEntry ToLogEntry(TokenExchangeRecord record)
+        {
+            return new TokenExchangeLogEntry()
+            {
+                recordId = (int)record.recordId,
+                unitPrice = record.UnitPrice,
+                tradeTime = record.TradeTime,
+                tokenAmount = record.TokenAmount,
+                tearStoneAmount = record.TearStoneAmount,
+                grossTearStone = record.GrossTearStone,
+                feeAmount = record.FreeAmount
+            };
+        }
+
+        //组建快照
+        public TokenExchangeSnapshot BuildSnapshot(TokenExchangeGlobalData global)
+        {
+            return new TokenExchangeSnapshot()
+            {
+                unitPrice = global.UnitPrice,
+                nextAdjustTime = global.NextAdjustTime,
+                maxBuyAmount = CalcMaxBuyAmount(GetTearStoneCount(), global.UnitPrice),
+                maxSellAmount = CalcMaxSellAmount(GetTokenCount())
+            };
+        }
+    }
+}

+ 1 - 1
server/src/server/OpenCards.Server.Main/_launch_server_local.xml

@@ -19,7 +19,7 @@
             <StopServerDataURL>
 				http://127.0.0.1/cjw_60000_stop_server.json
 			</StopServerDataURL>
-            <ServerListUrl>http://192.168.0.85/serverlist.json</ServerListUrl>
+            <ServerListUrl>http://127.0.0.1/serverlist.json</ServerListUrl>
             <YmnCheckUrl>https://heimdall.vgplay.vn/check</YmnCheckUrl>
             <UpdateServerConfig>
 				http://127.0.0.1/update_server_config.json