Kaynağa Gözat

代币系统

jeson_fxd 3 gün önce
ebeveyn
işleme
34a7ef18be

+ 2 - 0
data/ClientScript/Protocol/generated/_errorCode.lua

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

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

@@ -7,12 +7,13 @@ namespace OpenCards.Core.Protocol.Client
 {
     //  1. 获取交易所信息 
     [MessageType(Constants.LOGIC_TOKEN_EXCHANGE_START + 1)]  // 0x57001
-    [RequestMsg(typeof(ClientTokenExchangeInfoResponse))]
-    public class ClientTokenExchangeInfoRequest : ClientRequest, ILogicProtocol 
+    [RequestMsg(typeof(ClientTokenExchangeInfoResponse), true, true, false)]
+    public class ClientTokenExchangeInfoRequest : ClientRequest, ILogicProtocol
     {
     }
 
     [MessageType(Constants.LOGIC_TOKEN_EXCHANGE_START + 2)]  // 
+    [ResponseMsg(true)]
     public class ClientTokenExchangeInfoResponse : ClientResponse, ILogicProtocol
     {
         public TokenExchangeSnapshot s2c_snapshot = new TokenExchangeSnapshot();
@@ -33,14 +34,16 @@ namespace OpenCards.Core.Protocol.Client
     [ResponseMsg(true)]
     public class ClientTokenExchangeBuyResponse : ClientResponse, ILogicProtocol
     {
-        [MessageCode("数量必须为100的整数倍")] 
+        [MessageCode("数量必须为100的整数倍")]
         public const int CODE_EXCHANGE_AMOUNT_INVALID = CODE_ERROR + 1;
-        [MessageCode("泪滴石不足")] 
+        [MessageCode("泪滴石不足")]
         public const int CODE_EXCHANGE_TEARSTONE_NOT_ENOUGH = CODE_ERROR + 2;
-        [MessageCode("超过可买上限")] 
+        [MessageCode("超过可买上限")]
         public const int CODE_EXCHANGE_AMOUNT_EXCEED = CODE_ERROR + 3;
-        [MessageCode("当前价格已经发生变动,请重新操作")] 
+        [MessageCode("当前价格已经发生变动,请重新操作")]
         public const int CODE_EXCHANGE_PRICE_CHANGED = CODE_ERROR + 4;
+        [MessageCode("从中心服获取数据失败")]
+        public const int CODE_EXCHANGE_FETCH_ERROR = CODE_ERROR + 7;
 
         public TokenExchangeSnapshot s2c_snapshot = new TokenExchangeSnapshot();
     }
@@ -58,16 +61,18 @@ namespace OpenCards.Core.Protocol.Client
     [ResponseMsg(true)]
     public class ClientTokenExchangeSellResponse : ClientResponse, ILogicProtocol
     {
-        [MessageCode("数量必须为100的整数倍")] 
+        [MessageCode("数量必须为100的整数倍")]
         public const int CODE_EXCHANGE_AMOUNT_INVALID = CODE_ERROR + 1;
-        [MessageCode("绑定代币不可卖出")] 
+        [MessageCode("绑定代币不可卖出")]
         public const int CODE_EXCHANGE_BOUND_NOT_SELLABLE = CODE_ERROR + 5;
-        [MessageCode("代币不足")] 
+        [MessageCode("代币不足")]
         public const int CODE_EXCHANGE_TOKEN_NOT_ENOUGH = CODE_ERROR + 6;
-        [MessageCode("超过可卖上限")] 
+        [MessageCode("超过可卖上限")]
         public const int CODE_EXCHANGE_AMOUNT_EXCEED = CODE_ERROR + 3;
-        [MessageCode("当前价格已经发生变动,请重新操作")] 
+        [MessageCode("当前价格已经发生变动,请重新操作")]
         public const int CODE_EXCHANGE_PRICE_CHANGED = CODE_ERROR + 4;
+        [MessageCode("从中心服获取数据失败")]
+        public const int CODE_EXCHANGE_FETCH_ERROR = CODE_ERROR + 7;
 
         public TokenExchangeSnapshot s2c_snapshot = new TokenExchangeSnapshot();
 

+ 2 - 0
server/src/data/ClientScript/Protocol/generated/_response-code.lua

@@ -5276,6 +5276,7 @@
     [502] = "泪滴石不足",
     [503] = "超过可买上限",
     [504] = "当前价格已经发生变动,请重新操作",
+    [507] = "从中心服获取数据失败",
   }
   MessageCodeAttribute["OpenCards.Core.Protocol.Client.ClientTokenExchangeInfoResponse"] = {
     [200] = "成功",
@@ -5293,6 +5294,7 @@
     [506] = "代币不足",
     [503] = "超过可卖上限",
     [504] = "当前价格已经发生变动,请重新操作",
+    [507] = "从中心服获取数据失败",
   }
   MessageCodeAttribute["OpenCards.Core.Protocol.Client.ClientTrialVoidGuestResponse"] = {
     [200] = "成功",

+ 58 - 0
server/src/server/OpenCards.Server.Core/RPC/Center.cs

@@ -598,4 +598,62 @@ namespace OpenCards.Server.Core.RPC
             s2c_code = code;
         }
     }
+
+    #region 代币交易所(Center 按服单实例,请求不含多服列表)
+    //Logic拉取本服全局交易所数据
+    [ProtocolRoute("LogicService", "CenterService")]
+    public class GetTokenExchangeGlobalRequest : Request
+    {
+        /// <summary>
+        /// Logic 带上serverID,Center校验req  == this.serverID
+        /// </summary>
+        public string l2c_serverID;
+    }
+    [ProtocolRoute("CenterService", "LogicService")]
+    public class GetTokenExchangeGlobalResponse: Response
+    {
+        public const int CODE_SERVER_MISMATCH = Response.CODE_ERROR + 1;
+
+        public float c2l_unitPrice;
+
+        public long c2l_nextAdjustTime;
+    }
+
+    // 买入成交后上报,Center累加本服总量
+    [ProtocolRoute("LogicService","CenterService")]
+    public class ReportTokenExchangeBuyRequest : Request
+    {
+        public string l2c_serverID;
+        public string l2c_roleID;
+        public int l2c_tokenAmount; //买入数量
+        public float l2c_unitPrice; //客户端成交价格,center校验
+    }
+    [ProtocolRoute("CenterService","LogicService")]
+    public class ReportTokenExchangeBuyResponse : Response
+    {
+        public const int CODE_EXCHANGE_PRICE_CHANGE = Response.CODE_ERROR + 2;
+
+        public float c2l_unitPrice;
+        public long c2l_nextAdjustTime;
+    }
+
+    // 卖出成交后上报,Center累加本服总量
+    [ProtocolRoute("LogicService","CenterService")]
+    public class ReportTokenExchangeSellRequest : Request
+    {
+        public string l2c_serverID;
+        public string l2c_roleID;
+        public int l2c_tokenAmount; //卖出数量
+        public float l2c_unitPrice; //客户端成交价格,center校验
+    }
+    [ProtocolRoute("CenterService","LogicService")]
+    public class ReportTokenExchangeSellResponse : Response
+    {
+        public const int CODE_EXCHANGE_PRICE_CHANGE = Response.CODE_ERROR + 2;
+
+        public float c2l_unitPrice;
+        public long c2l_nextAdjustTime;
+    }
+
+    #endregion
 }

+ 115 - 180
server/src/server/OpenCards.Server.Logic/Module/TokenExchange/TokenExchangeModule.cs

@@ -1,11 +1,14 @@
 using DeepCore.Protocol;
 using DeepCrystal.ORM;
+using DeepCrystal.RPC;
 using OpenCards.Core.Data;
 using OpenCards.Core.ORM;
 using OpenCards.Core.Protocol.Client;
 using OpenCards.Server.Common.Flag;
+using OpenCards.Server.Common.RPC;
 using OpenCards.Server.Core;
 using OpenCards.Server.Core.ORM;
+using OpenCards.Server.Core.RPC;
 using OpenCards.Server.Core.Utils;
 using System;
 using System.Collections.Generic;
@@ -16,10 +19,12 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
 {
     public class TokenExchangeModule : ILogicModule
     {
+        private IRemoteService mCenterService;
+
         // 角色账单(每个玩家)
         private TokenExchangeRoleDataMapping roleMapping;
         // 全服单价,按serverId分key
-        private MappingBase<TokenExchangeGlobalData> globalMapping;
+
         private TLRoleFlagModule mRoleFlag;
         private IDisposable mAdjustTimer;
 
@@ -51,19 +56,24 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
         {
             await this.LoadRoleMappingData(roleMapping, RoleMappingDataDefines.TokenExchangeRoleData);
 
-            //全服mapping (key 用 serverID,不用 roleID)
-            await EnsureGlobalDataAsync();
+            //await globalMapping.LoadDataAsync();
+            //await globalMapping.LoadOrCreateDataAsync(() => new TokenExchangeGlobalData
+            //{
+            //    UnitPrice = 2.00f,
+            //    LastAdjustTime = TimeUtils.CurrentTimeMs,
+            //    NextAdjustTime = CalcNextAdjustTime(TimeUtils.CurrentTimeMs),
+            //    PeriodBuyCount = 0,
+            //    PeriodSellCount = 0,
+            //});
 
             mRoleFlag = GetModule<TLRoleFlagModule>();
         }
 
-        public override Task OnStartedAsync()
+        public override async Task OnStartedAsync()
         {
-            mAdjustTimer = service.Provider.CreateTimer(OnAdjustTimerTick, this,
-                TimeSpan.FromSeconds(30 + new Random().Next(1, 30)),
-                TimeSpan.FromMinutes(1));
-
-            return base.OnStartedAsync();
+            mCenterService = await RpcUtils.GetCenterService(service.Provider, service.serverID);
+            //await base.OnStartedAsync();
+            //return;
         }
 
         public override void OnSaveData(IObjectTransaction trans, StringBuilder sb, bool fullData)
@@ -72,13 +82,20 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             if (trans != null)
             {
                 if (fullData)
+                {
                     roleMapping?.BatchSaveData(trans);
+                    //globalMapping?.BatchSaveData(trans);
+                }
                 else
+                {
                     roleMapping?.BatchFlush(trans);
+                    //globalMapping?.BatchFlush(trans);
+                }
             }
             else
             {
                 sb.AppendLine(roleMapping.Data.GetType().Name + ":" + LogicUtils.ToJson(roleMapping.Data));
+                //sb.AppendLine(globalMapping.Data.GetType().Name + ":" + LogicUtils.ToJson(globalMapping.Data));
             }
         }
 
@@ -86,9 +103,19 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
         {
             mAdjustTimer?.Dispose();
             roleMapping?.Dispose();
-            globalMapping?.Dispose();
+            //globalMapping?.Dispose();
         }
 
+        private async Task<GetTokenExchangeGlobalResponse> FetchGlobalAsync()
+        {
+            if (mCenterService == null)
+                mCenterService = await RpcUtils.GetCenterService(service.Provider, service.serverID);
+
+            return await mCenterService.CallAsync<GetTokenExchangeGlobalResponse>(new GetTokenExchangeGlobalRequest()
+            {
+                l2c_serverID = service.serverID,
+            });
+        }
 
         public TokenExchangeModule(LogicService service) : base(service)
         {
@@ -97,6 +124,7 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             RegisterMessageHandler<LogicService.MsgClientTokenExchangeSellRequest>(HandleTokenExchangeSellReq);
             RegisterMessageHandler<LogicService.MsgClientTokenExchangeLogRequest>(HandleTokenExchangeLogReq);
             roleMapping = new TokenExchangeRoleDataMapping(PersistenceConstants.TYPE_TOKEN_EXCHANGE_ROLE, service.roleID, service);
+            //globalMapping = new TokenExchangeGlobalDataMapping(PersistenceConstants.TYPE_TOKEN_EXCHANGE_GLOBAL, service.serverID, service);
         }
 
         private int HandleTokenExchangeLogReq(int c2s_tradeType, ref List<TokenExchangeLogEntry> s2c_buyLogs, ref List<TokenExchangeLogEntry> s2c_sellLogs)
@@ -139,18 +167,27 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
         private async Task<ClientTokenExchangeSellResponse> HandleTokenExchangeSellReq(ClientTokenExchangeSellRequest req)
         {
             var rsp = new ClientTokenExchangeSellResponse();
-            var globalData = globalMapping.Data;
+            //var globalData = globalMapping.Data;
             int amount = req.c2s_tokenAmount;
             long tokenCount = GetTokenCount();
 
+            var globalRsp = await FetchGlobalAsync();
+            if (!Response.CheckSuccess(globalRsp))
+            {
+                rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_FETCH_ERROR;
+                return rsp;
+            }
+
             if (amount <= 0 || amount % 100 != 0)
             {
                 rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_AMOUNT_INVALID;
                 return rsp;
             }
-            if (!IsUnitPriceMatch(req.c2s_unitPrice, globalData.UnitPrice))
+
+            float unitPrice = globalRsp.c2l_unitPrice;
+            if (!IsUnitPriceMatch(req.c2s_unitPrice, unitPrice))
             {
-                rsp.s2c_snapshot = BuildSnapshot(globalData);
+                rsp.s2c_snapshot = BuildSnapshot(unitPrice, globalRsp.c2l_nextAdjustTime);
                 rsp.s2c_code = ClientTokenExchangeSellResponse.CODE_EXCHANGE_PRICE_CHANGED;
                 return rsp;
             }
@@ -181,19 +218,25 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             DispatchEvent(EventDefines.EventAddItem, ItemDefines.DiamondItemId, netSearStone,
                 AddItemReason.TokenExchangeSell, 0, LogicUtils.FileLine);
 
-            //记录
-            await ModifyGlobalAsync(global =>
+            // 发给中心服记数据
+            var reportRsp = await mCenterService.CallAsync<ReportTokenExchangeSellResponse>(new ReportTokenExchangeSellRequest()
             {
-                global.PeriodSellCount += amount;
-                return true;
+                l2c_serverID = service.serverID,
+                l2c_roleID = service.roleID,
+                l2c_tokenAmount = amount,
+                l2c_unitPrice = req.c2s_unitPrice,
             });
+            if (!Response.CheckSuccess(reportRsp))
+            {
+                log.Error($"{service.ErrorBaseInfo} TokenExchange buy report failed. code={reportRsp.s2c_code}");
+            }
 
             AppendExchangeLog(roleMapping.Data.SellLogs, new TokenExchangeRecord()
             {
                 recordId = GenRecordId(),
                 TradeType = TradeTypeSell,
                 TradeTime = TimeUtils.CurrentTimeMs,
-                UnitPrice = globalData.UnitPrice,
+                UnitPrice = unitPrice,
                 TokenAmount = amount,
                 TearStoneAmount = netSearStone,
                 GrossTearStone = grossTearStone,
@@ -208,7 +251,7 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             rsp.s2c_grossTearStone = grossTearStone;
             rsp.s2c_freeAmount = freeAmount;
             rsp.s2c_netTearStone = netSearStone;
-            rsp.s2c_snapshot = BuildSnapshot(globalData);
+            rsp.s2c_snapshot = BuildSnapshot(unitPrice, globalRsp.c2l_nextAdjustTime);
             return rsp;
         }
 
@@ -216,26 +259,34 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
         {
             var rsp = new ClientTokenExchangeBuyResponse();
             int amount = req.c2s_tokenAmount;
-            var globalData = globalMapping.Data;
+
+            var globalRsp = await FetchGlobalAsync();
+            if (!Response.CheckSuccess(globalRsp))
+            {
+                rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_FETCH_ERROR;
+                return rsp;
+            }
 
             if (amount <= 0 || amount % 100 != 0)
             {
                 rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_AMOUNT_INVALID;
                 return rsp;
             }
-            if (!IsUnitPriceMatch(req.c2s_unitPrice, globalData.UnitPrice))
+
+            float unitPrice = globalRsp.c2l_unitPrice;
+            if (!IsUnitPriceMatch(req.c2s_unitPrice, unitPrice))
             {
-                rsp.s2c_snapshot = BuildSnapshot(globalData);
+                rsp.s2c_snapshot = BuildSnapshot(unitPrice, globalRsp.c2l_nextAdjustTime);
                 rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_PRICE_CHANGED;
                 return rsp;
             }
-            if (amount > CalcMaxBuyAmount(GetTearStoneCount(), globalData.UnitPrice))
+            if (amount > CalcMaxBuyAmount(GetTearStoneCount(), unitPrice))
             {
                 rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_AMOUNT_EXCEED;
                 return rsp;
             }
             // 数量判断
-            var tearStoneCost = (int)(amount * globalData.UnitPrice);
+            var tearStoneCost = (int)(amount * unitPrice);
             if (GetTearStoneCount() < tearStoneCost)
             {
                 rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_TEARSTONE_NOT_ENOUGH;
@@ -248,23 +299,30 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
                 rsp.s2c_code = ClientTokenExchangeBuyResponse.CODE_EXCHANGE_TEARSTONE_NOT_ENOUGH;
                 return rsp;
             }
-            // 发代币
-            DispatchEvent(EventDefines.EventAddItem, ItemDefines.BoundTokenItemId, amount,
+            // 发绑定代币(EventAddItem 参数为 itemInstanceId,非 itemId)
+            DispatchEvent(EventDefines.EventAddItem, ItemDefines.BoundTokenItemInstanceId, amount,
                 AddItemReason.TokenExchangeBuy, 0, LogicUtils.FileLine);
 
-            // 记
-            await ModifyGlobalAsync(global =>
+            // 记数据
+            var reportRsp = await mCenterService.CallAsync<ReportTokenExchangeBuyResponse>(new ReportTokenExchangeBuyRequest()
             {
-                global.PeriodBuyCount += amount;
-                return true;
+                l2c_serverID = service.serverID,
+                l2c_roleID = service.roleID,
+                l2c_tokenAmount = amount,
+                l2c_unitPrice = req.c2s_unitPrice,
             });
-            // 日志是每个服每个玩家单独保存, 不能存服务器级别上
+            if (!Response.CheckSuccess(reportRsp))
+            {
+                log.Error($"{service.ErrorBaseInfo} TokenExchange buy report failed. code={reportRsp.s2c_code}");
+            }
+
+            // 日志是每个服每个玩家单独保存
             AppendExchangeLog(roleMapping.Data.BuyLogs, new TokenExchangeRecord()
             {
                 recordId = GenRecordId(),
                 TradeType = TradeTypeBuy,
                 TradeTime = TimeUtils.CurrentTimeMs,
-                UnitPrice = globalData.UnitPrice,
+                UnitPrice = unitPrice,
                 TokenAmount = amount,
                 TearStoneAmount = tearStoneCost,
                 GrossTearStone = 0,
@@ -274,41 +332,36 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             await roleMapping.FlushAsync();
 
             rsp.s2c_code = Response.CODE_OK;
-            rsp.s2c_snapshot = BuildSnapshot(globalData);
+            rsp.s2c_snapshot = BuildSnapshot(
+                reportRsp.c2l_unitPrice > 0 ? reportRsp.c2l_unitPrice:unitPrice,
+                reportRsp.c2l_nextAdjustTime > 0 ? reportRsp.c2l_nextAdjustTime:globalRsp.c2l_nextAdjustTime
+                ) ;
             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)
+        private async Task<ClientTokenExchangeInfoResponse> HandleTokenExchangeInfoReq(ClientTokenExchangeInfoRequest msg)
         {
-            if (globalMapping?.Data == null)
-                return GetErrorCode("globalMapping is null");
+            var rsp = new ClientTokenExchangeInfoResponse();
+            if (roleMapping?.Data == null)
+            {
+                rsp.s2c_code = Response.CODE_ERROR;
+                return rsp;
+            }
 
-            s2c_snapshot = BuildSnapshot(globalMapping.Data);
-            return Response.CODE_OK;
+            var globalRsp = await FetchGlobalAsync();
+            if (!Response.CheckSuccess(globalRsp))
+
+            {
+                rsp.s2c_code = Response.CODE_ERROR;
+                return rsp;
+            }
+            rsp.s2c_snapshot = BuildSnapshot(globalRsp.c2l_unitPrice, globalRsp.c2l_nextAdjustTime);
+            return rsp;
         }
 
         /// <summary>
@@ -347,114 +400,6 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             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>
@@ -521,17 +466,6 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
             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>
@@ -580,15 +514,16 @@ namespace OpenCards.Server.Logic.Module.TokenExchange
         }
 
         //组建快照
-        public TokenExchangeSnapshot BuildSnapshot(TokenExchangeGlobalData global)
+        public TokenExchangeSnapshot BuildSnapshot(float unitPrice, long nextAdjustTime)
         {
             return new TokenExchangeSnapshot()
             {
-                unitPrice = global.UnitPrice,
-                nextAdjustTime = global.NextAdjustTime,
-                maxBuyAmount = CalcMaxBuyAmount(GetTearStoneCount(), global.UnitPrice),
+                unitPrice = unitPrice,
+                nextAdjustTime = nextAdjustTime,
+                maxBuyAmount = CalcMaxBuyAmount(GetTearStoneCount(), unitPrice),
                 maxSellAmount = CalcMaxSellAmount(GetTokenCount())
             };
         }
+
     }
 }

+ 4 - 0
server/src/server/OpenCards.Service.Center/CenterService.cs

@@ -31,6 +31,7 @@ using OpenCards.Server.Core.PublicSnap;
 using OpenCards.Server.Common.RankAchieve;
 using OpenCards.Server.Common.SSLogger;
 using OpenCards.Server.Common.Role;
+using System.Threading;
 
 namespace OpenCards.Service.Center
 {
@@ -107,6 +108,7 @@ namespace OpenCards.Service.Center
             InitGlobalCmd();
             InitRankData();
             await WishSummonBossChallengeRankStartAsync();
+            await OnTokenExchangeStartAsync();
         }
 
         protected override async Task OnStopAsync(ServiceStopInfo stop)
@@ -115,6 +117,7 @@ namespace OpenCards.Service.Center
 
             mSaveDataTimer?.Dispose();
             mWishSummonBossChallengeSettleTimer?.Dispose();
+            _tokenExchangeAdjustTime?.Dispose();
             try
             {
                 foreach (var item in mHandleEntity)
@@ -149,6 +152,7 @@ namespace OpenCards.Service.Center
                 RankAchieve.Instance.OnSaveDataBatch(trans);
                 RankSort.Instance.OnSaveDataBatch(trans);
                 OnBattleFinishHistoryDataBatch(trans);
+                OnTokenExchangeSaveDataBatch(trans);
                 foreach (var item in mHandleEntity)
                 {
                     item.OnSaveDataBatch(trans);

+ 252 - 0
server/src/server/OpenCards.Service.Center/Model/TokenExchangeHandler.cs

@@ -0,0 +1,252 @@
+using DeepCore.Protocol;
+using DeepCrystal.ORM;
+using DeepCrystal.RPC;
+using OpenCards.Core.Data;
+using OpenCards.Core.ORM;
+using OpenCards.Core.Protocol.Client;
+using OpenCards.Server.Common.RPC;
+using OpenCards.Server.Core.RPC;
+using OpenCards.Server.Core.Utils;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace OpenCards.Service.Center
+{
+    public partial class CenterService
+    {
+        private TokenExchangeGlobalDataMapping _tokenExchangeGlobalMapping;
+        private readonly SemaphoreSlim _tokenExchangeLock = new SemaphoreSlim(1, 1);
+        private IDisposable _tokenExchangeAdjustTime;
+
+        private async Task OnTokenExchangeStartAsync()
+        {
+            _tokenExchangeGlobalMapping = new TokenExchangeGlobalDataMapping(PersistenceConstants.TYPE_TOKEN_EXCHANGE_GLOBAL, serverID, this);
+            await _tokenExchangeGlobalMapping.LoadOrCreateDataAsync(() => new TokenExchangeGlobalData()
+            {
+                UnitPrice = 2.00f,
+                LastAdjustTime = TimeUtils.CurrentTimeMs,
+                NextAdjustTime = CalcNextAdjustTime(TimeUtils.CurrentTimeMs),
+                PeriodBuyCount = 0,
+                PeriodSellCount = 0,
+
+            });
+
+            _tokenExchangeAdjustTime = this.Provider.CreateTimer(
+                OnTokenExchangeAdjustTime, this,
+                TimeSpan.FromSeconds(30 + new Random().Next(1, 30)),
+                TimeSpan.FromMinutes(1));
+        }
+
+        private void OnTokenExchangeSaveDataBatch(IObjectTransaction trans)
+        {
+            _tokenExchangeGlobalMapping?.BatchFlush(trans);
+        }
+
+        [RpcHandler(typeof(GetTokenExchangeGlobalRequest), typeof(GetTokenExchangeGlobalResponse))]
+        public Task<GetTokenExchangeGlobalResponse> rpc_Handle(GetTokenExchangeGlobalRequest req)
+        {
+            var rsp = new GetTokenExchangeGlobalResponse();
+            if (!ValidateServerId(req?.l2c_serverID))
+            {
+                rsp.s2c_code = GetTokenExchangeGlobalResponse.CODE_SERVER_MISMATCH;
+                return Task.FromResult(rsp);
+            }
+
+            var data = _tokenExchangeGlobalMapping?.Data;
+            if (data == null)
+            {
+                rsp.s2c_code = Response.CODE_ERROR;
+                return Task.FromResult(rsp);
+            }
+
+            rsp.c2l_unitPrice = data.UnitPrice;
+            rsp.c2l_nextAdjustTime = data.NextAdjustTime;
+            rsp.s2c_code = Response.CODE_OK;
+            return Task.FromResult(rsp);
+        }
+
+        [RpcHandler(typeof(ReportTokenExchangeBuyRequest), typeof(ReportTokenExchangeBuyResponse))]
+        public async Task<ReportTokenExchangeBuyResponse> rpc_Handle(ReportTokenExchangeBuyRequest req)
+        {
+            var rsp = new ReportTokenExchangeBuyResponse();
+            if (req == null || !ValidateServerId(req.l2c_serverID) || req.l2c_tokenAmount <= 0)
+            {
+                rsp.s2c_code = Response.CODE_ERROR;
+                return rsp;
+            }
+            await _tokenExchangeLock.WaitAsync();
+
+            try
+            {
+                var global = _tokenExchangeGlobalMapping.Data;
+                if (global == null)
+                {
+                    rsp.s2c_code = Response.CODE_ERROR;
+                    return rsp;
+                }
+                if (!IsUnitPriceMatch(req.l2c_unitPrice, global.UnitPrice))
+                {
+                    rsp.s2c_code = ReportTokenExchangeBuyResponse.CODE_EXCHANGE_PRICE_CHANGE;
+                    rsp.c2l_unitPrice = global.UnitPrice;
+                    rsp.c2l_nextAdjustTime = global.NextAdjustTime;
+                    return rsp;
+                }
+                global.PeriodBuyCount += req.l2c_tokenAmount;
+                await _tokenExchangeGlobalMapping.SaveDataAsync(global);
+
+                rsp.s2c_code = Response.CODE_OK;
+                rsp.c2l_unitPrice = global.UnitPrice;
+                rsp.c2l_nextAdjustTime = global.NextAdjustTime;
+                return rsp;
+            }
+            finally
+            {
+                _tokenExchangeLock.Release();
+            }
+        }
+
+        [RpcHandler(typeof(ReportTokenExchangeSellRequest), typeof(ReportTokenExchangeSellResponse))]
+        public async Task<ReportTokenExchangeSellResponse> rpc_Handle(ReportTokenExchangeSellRequest req)
+        {
+            var rsp = new ReportTokenExchangeSellResponse();
+            if (req == null || !ValidateServerId(req.l2c_serverID) || req.l2c_tokenAmount <= 0)
+            {
+                rsp.s2c_code = Response.CODE_ERROR;
+                return rsp;
+            }
+            await _tokenExchangeLock.WaitAsync();
+            try
+            {
+                var global = _tokenExchangeGlobalMapping.Data;
+                if (global == null)
+                {
+                    rsp.s2c_code = Response.CODE_ERROR;
+                    return rsp;
+                }
+                if (!IsUnitPriceMatch(req.l2c_unitPrice, global.UnitPrice))
+                {
+                    rsp.s2c_code = ReportTokenExchangeSellResponse.CODE_EXCHANGE_PRICE_CHANGE;
+                    rsp.c2l_unitPrice = global.UnitPrice;
+                    rsp.c2l_nextAdjustTime = global.NextAdjustTime;
+                    return rsp;
+                }
+                // 代码同buyRequest ,仅次数加的数量不一样
+                global.PeriodSellCount += req.l2c_tokenAmount;
+                await _tokenExchangeGlobalMapping.SaveDataAsync(global);
+
+                rsp.s2c_code = Response.CODE_OK;
+                rsp.c2l_unitPrice = global.UnitPrice;
+                rsp.c2l_nextAdjustTime = global.NextAdjustTime;
+
+                return rsp;
+            }
+            finally
+            {
+                _tokenExchangeLock.Release();
+            }
+
+        }
+
+
+        private bool ValidateServerId(string reqServerId)
+        {
+            return string.IsNullOrEmpty(reqServerId) || reqServerId == this.serverID;
+        }
+
+        /// <summary>
+        /// 返回下一个调价时间点, 当日12点或者次日00:00 (当天24点)
+        /// </summary>
+        /// <param name="nowMs"></param>
+        /// <returns></returns>
+        private long CalcNextAdjustTime(long nowMs)
+        {
+            var now = TimeUtils.TimeStampToDateTime(nowMs);
+            var dayStart = now.GetDayStart();
+            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 OnTokenExchangeAdjustTime(object state)
+        {
+            await _tokenExchangeLock.WaitAsync();
+            try
+            {
+                var global = _tokenExchangeGlobalMapping?.Data;
+                if (global == null)
+                    return;
+
+                long now = TimeUtils.CurrentTimeMs;
+                if (now < global.NextAdjustTime)
+                    return;
+
+                float newPrice = CalcAdjustedUnitPrice(global.UnitPrice, global.PeriodBuyCount, global.PeriodSellCount);
+
+                global.UnitPrice = newPrice;
+                global.LastAdjustTime = now;
+                global.NextAdjustTime = CalcNextAdjustTime(now);
+                global.PeriodBuyCount = 0;
+                global.PeriodSellCount = 0;
+
+                await _tokenExchangeGlobalMapping.SaveDataAsync(global);
+                await BroadcastTokenExchangePriceAsync(global);
+            }
+            finally
+            {
+
+            }
+        }
+
+        private async Task BroadcastTokenExchangePriceAsync(TokenExchangeGlobalData global)
+        {
+            var connectorService = await RpcUtils.GetConnectorService(Provider, serverID);
+            if (connectorService == null)
+            {
+                log.Warn($"TokenExchange broadcast skipped, connector not found. serverId={serverID}");
+                return;
+            }
+            var notify = new ClientTokenExchangePriceNotify()
+            {
+                s2c_snapshot = new TokenExchangeSnapshot()
+                {
+                    unitPrice = global.UnitPrice,
+                    nextAdjustTime = global.NextAdjustTime,
+                }
+            };
+            connectorService.Invoke(new ConnectorBroadcastNotify(notify));
+
+        }
+
+        private float CalcAdjustedUnitPrice(float currentPrice, long buyVolume, long sellVolume)
+        {
+            float ratio = sellVolume <= 0 ? 1.00f : (float)buyVolume / sellVolume;
+            ratio = Math.Max(Math.Min(ratio, 1.10f), 0.90f);
+            float newPrice = Math.Min(Math.Max(currentPrice * ratio, 1.00f), 4.00f);
+            return (float)Math.Round(newPrice, 2, MidpointRounding.AwayFromZero);
+        }
+
+        private static bool IsUnitPriceMatch(float clientPrice, float serverPrice)
+        {
+            return Math.Round(clientPrice, 2, MidpointRounding.AwayFromZero)
+                == Math.Round(serverPrice, 2, MidpointRounding.AwayFromZero);
+        }
+
+        //[RpcHandler(typeof(GetTokenExchangeGlobalRequest), typeof(GetTokenExchangeGlobalResponse))]
+        //public Task<GetTokenExchangeGlobalResponse> rpc_Handle(GetTokenExchangeGlobalRequest req)
+        //{
+        //    return OnGetTokenExchangeGlobalRequest(req);
+        //}
+
+    }
+
+}