用.NET Core實現一個類似於餓了嗎的簡易拆紅包功能
需求說明
以前很討厭點外賣的我,最近中午經常點外賣,因為確實很方便,提前點好餐,算準時間,就可以在下班的時候吃上飯,然後省下的那些時間就可以在中午的時候多休息一下了。
點餐結束後,會有一個好友分享紅包功能,雖說這個紅包不能提現,但卻可以抵扣點餐費用,對於經常點餐的人來說,直接用於抵扣現金確實是很大的誘惑,在點餐之後所獲得的那個紅包,必須要分享出去才能拆。
那麼如果自己也想實現以下搶紅包功能,需要說明的是,本文所描述的紅包功能更多的關注與隨機紅包的生成,至於高併發、資料一致性等問題,本文暫未涉及,以下是本文所討論的兩個技術點:
- 不同的消費金額獲取的紅包總額不同,消費金額越大,紅包總額就越大,紅包總數也就越多;
- 假設有一天,有一種需求是,需要保證參與搶紅包的人獲得的紅包金額在平均數附近波動,也就是儘量的服從正態分佈;
功能實現
本文描述的場景,所涉及到的金額以分為單位,目的是為了更好的處理隨機數。總體的示意圖如下:
消費後紅包的初始化
需求重點,使用者分享出去的紅包總額跟消費總額成正比,可以分拆的子紅包個數也與消費總額成正比。
比如:
- 10-20元的消費金額,可以分享的單個紅包金額為10元,可以供5個人搶
- 20-40元的消費金額,可以分享的單個紅包金額為20元,可以供8個人搶
- 40-60元的消費金額,可以分享的單個紅包金額為30元,可以供10個人搶
- 60-100元的消費金額,可以分享的單個紅包金額為40元,可以供10個人搶
- 100元以上的消費金額,可以分享的單個紅包金額為50元,可以供10個人搶
那麼我們設計出來一個實體,用於表示紅包資訊,以方便的配置及調整紅包規則
1: public class RedPacketsInfo
2: {
3: /// <summary>
4: /// 最大消費金額
5: /// </summary>
6: public int MaxAmount { get; set; }
7:
8: /// <summary>
9: /// 最小消費金額
10: /// </summary>
11: public int MinAmount { get; set; }
12:
13: /// <summary>
14: /// 紅包金額
15: /// </summary>
16: public int TotalAmount { get; set; }
17:
18: /// <summary>
19: /// 紅包可被分割的數量
20: /// </summary>
21: public int RedPacketQuantity { get; set; }
22: }
紅包初始化資訊
1: private static List<RedPacketsInfo> GetRedPackets()
2: {
3: return new List<RedPacketsInfo>()
4: {
5: new RedPacketsInfo
6: {
7: MinAmount = 1000,
8: MaxAmount = 2000,
9: RedPacketQuantity = 5,
10: TotalAmount=1000
11: },
12: new RedPacketsInfo
13: {
14: MinAmount = 2000,
15: MaxAmount = 3000,
16: RedPacketQuantity = 5,
17: TotalAmount=1000
18: },
19: new RedPacketsInfo
20: {
21: MinAmount = 4000,
22: MaxAmount = 6000,
23: RedPacketQuantity = 5,
24: TotalAmount=1000
25: },
26: new RedPacketsInfo
27: {
28: MinAmount = 6000,
29: MaxAmount = 8000,
30: RedPacketQuantity = 5,
31: TotalAmount=1000
32: },
33: new RedPacketsInfo
34: {
35: MinAmount = 10000,
36: MaxAmount = int.MaxValue,
37: RedPacketQuantity = 5,
38: TotalAmount=1000
39: }
40: };
41: }
接下來我們就可以通過消費金額獲取相應的紅包資訊了。
隨機紅包的生成時機及處理
隨機紅包的生成可以在搶之前生成也可以在搶的過程中確定,一般而言,很多時候紅包會在搶的過程中動態的實際分配,不過在本文中,紅包在使用者分享成功後會預先生成,主要原因是為了更好地處理處理資料,以使得資料能夠服從正態分佈。
以下是其流程圖,其中有一段邏輯是回撥功能,可能會有圈友會問,如何保證有回撥以及回撥是成功的,這個地方有很多種處理,比如MQ、任務排程等,此處也不做討論
那麼我們需要設計一個新的實體,以表示分享出去的紅包及其生成的隨機紅包:
1: public class SharedRedPacket
2: {
3: /// <summary>
4: /// 分享人UserId
5: /// </summary>
6: public int SenderUserId { get; set; }
7:
8: /// <summary>
9: /// 分享時間
10: /// </summary>
11: public DateTime SendTime { get; set; }
12:
13: public List<RobbedRedPacket> RobbedRedPackets { get; set; }
14: }
15:
16: public class RobbedRedPacket
17: {
18: /// <summary>
19: /// 搶到紅包的人的UserId
20: /// </summary>
21: public int UserId { get; set; }
22:
23: /// <summary>
24: /// 搶到的紅包金額
25: /// </summary>
26: public int Amount { get; set; }
27:
28: /// <summary>
29: /// 搶到時間
30: /// </summary>
31: public DateTime RobbedTime { get; set; }
32: }
在實現過程中,根據使用者消費金額獲取相應紅包,然後通過隨機數,生成n-1個原始的隨機資料,最後一個數據用總和減去n-1個數據的和獲取到。
1: //紅包隨機拆分
2: Random ran = new Random();
3: List<double> randoms = new List<double>(redPacketsList.Count);
4: for (int i = 0; i < redPacketsInfo.RedPacketQuantity - 1; i++)
5: {
6: int max = (totalAmount - (redPacketsInfo.RedPacketQuantity - i)) * 1;
7: int result = ran.Next(1, max);
8: randoms.Add(result);
9: totalAmount -= result;
10: }
11: randoms.Add(totalAmount);
然後通過設定好係數,以處理資料達到服從正太分佈的目的:
1: //正太分佈處理
2: for (int i = 0; i < redPacketsInfo.RedPacketQuantity; i++)
3: {
4: double a = Math.Sqrt(Math.Abs(2 * Math.Log(randoms[i], Math.E)));
5: double b = Math.Cos(2 * Math.PI * randoms[i]);
6: randoms[i] = a * b * 0.3 + 1;
7: }
經過第二次處理後,得到的資料與原始資料有偏差,那麼我們通過等比例方式再次處理,以確保拆分後的紅包總額等於紅包原始總額:
1: //生成最終的紅包資料
2: double d = originalTotal / randoms.Sum();
3: SharedRedPacket sharedRedPacket = new SharedRedPacket();
4: sharedRedPacket.RobbedRedPackets = new List<RobbedRedPacket>(redPacketsList.Count);
5: for (int i = 0; i < redPacketsInfo.RedPacketQuantity - 1; i++)
6: {
7: sharedRedPacket.RobbedRedPackets.Add(new RobbedRedPacket
8: {
9: Amount = (int)Math.Round(randoms[i] * d, 0)
10: });
11: }
12: sharedRedPacket.RobbedRedPackets.Add(new RobbedRedPacket
13: {
14: Amount = originalTotal - sharedRedPacket.RobbedRedPackets.Sum(p => p.Amount)
15: });
測試
測試效果圖如下:
部分程式碼如下,
1: Console.WriteLine("是否分享輸入Y分享成功,輸入N退出");
2: string result = Console.ReadLine();
3: if (result == "Y")
4: {
5: var leftRedPacket = sharedRedPacket.RobbedRedPackets.Where(p => p.UserId <= 0).ToList();
6: var robbedRedPacket = leftRedPacket[new Random().Next(1, leftRedPacket.Count + 1)];
7: Console.WriteLine("搶到的到紅包金額是:" + robbedRedPacket.Amount);
8: Console.WriteLine("-------------------------------------------------------");
9: }