04-砍价系统核心流程设计

image-20201129160344316

参考资料:-

1. 砍价核心数据流程

image-20201129155739877

2. 砍价规则生成流程

image-20201129155914282

image-20201129155946630

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
//算法工具类
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

@SuppressWarnings("all")
public class BargainUtil {
/**
* 随机金额倍数指数范围
*/
private static final BigDecimal HIGH_INDEX = new BigDecimal("1.5");
private static final BigDecimal LOW_INDEX = new BigDecimal("0.5");

public BargainUtil() {

}

/**
* 返回砍价列表
* @param role 砍价规则
* @return
*/
public static List<BigInteger> getbargainList(BargainRole role) throws Exception {
if (role.getLowPrice() == null) {
role.setLowPrice(BigInteger.ZERO);
}
return calculation(role);
}

/**
* 计算砍价明细,并模拟砍价
* @param role 砍价规则
*/
private static List<BigInteger> calculation(BargainRole role) throws Exception {
/*
* 砍价例子:
* 规则:前10%的人可帮砍掉帮砍金额的80%
* 商品:随机模式,默认10人帮砍,则发起人砍价成功
*/

if (role.getPercentagePerson() == null || role.getPercentagePrice() == null) {
throw new Exception("砍价策略必须设置");
}
/* 不能设为0|单方面设置100 */
boolean b1 = role.getPercentagePrice().compareTo(0) <= 0 || role.getPercentagePerson().compareTo(0) <= 0;
boolean b2 = role.getPercentagePrice().equals(100) && !role.getPercentagePerson().equals(100);
boolean b3 = !role.getPercentagePrice().equals(100) && role.getPercentagePerson().equals(100);
if (b1 || b2 || b3) {
throw new Exception("砍价策略设置有误");
}
if (role.getTotalPrice() == null || role.getTotalPrice().compareTo(BigInteger.ZERO) <= 0) {
throw new Exception("商品价格数据异常");
}
if (role.getTotalTimes() == null || role.getTotalTimes().compareTo(0) <= 0) {
throw new Exception("砍价次数数据异常");
}
/* 砍价人次不能高于商品价格 */
//砍价人次(默认N人帮砍发起人砍价成功) 10 <= 19.9
if (new BigInteger(role.getTotalTimes().toString()).compareTo(role.getTotalPrice().subtract(role.getLowPrice())) > 0) {
throw new Exception("砍价人次不能高于商品价格");
}
//待砍金额(即帮砍金额) = 原价 - 底价(活动价)
//如商品原价 200 底价 19.9,待砍金额 = 200 - 19.9 = 180.1 (四舍五入) = 180
BigInteger difference = role.getTotalPrice().subtract(role.getLowPrice());
BigInteger index = BigInteger.ONE;
//在待砍金额 >100w 时,将待砍金额缩小 100 倍,即相当于按单位 元 来处理,其他不变
if (difference.compareTo(new BigInteger("100000000")) > 0) {
index = new BigInteger("10000");
difference = difference.divide(index);
}

//第1批人数:默认进行帮砍发起人可砍价成功的人数 × 前 N% 的人砍掉帮砍金额一定比例
//如前10%的人可帮砍掉帮砍金额80%,默认10人帮砍,发起人砍价成功
//此第1批人数 = 10 × 10% = 1人
Integer part1 = BigDecimal.valueOf(role.getTotalTimes() * role.getPercentagePerson())
.divide(BigDecimal.valueOf(100), BigDecimal.ROUND_DOWN).intValue();
if (part1 == 0) {
part1 = 1;
}
//第1批人可砍金额:待砍金额 × 前一定比例的人砍掉帮砍金额的 N%
//如前10%的人可帮砍掉帮砍金额80%,默认10人帮砍,发起人砍价成功
//此第1批人可砍金额 = 18010(180.1) × 80(80%) / 100 = 144.08 = 144 元(舍弃小数点)
BigInteger price1 = new BigDecimal(difference.multiply(BigInteger.valueOf(role.getPercentagePrice())))
.divide(BigDecimal.valueOf(100), BigDecimal.ROUND_DOWN).toBigInteger();

//第2批人数:默认进行帮砍发起人可砍价成功的人数 - 第1批人
//10 - 1 = 9 人
Integer part2 = role.getTotalTimes() - part1;
//第2批人可砍金额:帮砍价格 - 第1批砍掉的价格
//180 - 144 = 36 元
BigInteger price2 = difference.subtract(price1);

List<BigInteger> firstlist;
//当默认帮砍可成功的人数=1 或 第1批人数>第1批可砍价格 或 第二批人数>第2批可砍价格
//(10 == 1) || (1 > 144) || (9 > 36)
if (role.getTotalTimes() == 1 || part1 > price1.intValue() || part2 > price2.intValue()) {
firstlist = random(difference, role.getTotalTimes());
} else {
//使用第1批人和第1批可砍价格进行随机分配金额
//part1: 传参(144, 1)
firstlist = random(price1, part1);
if (part2.compareTo(0) > 0) {
//part2: 传参(36, 9)
List<BigInteger> secondlist = random(price2, part2);
firstlist.addAll(secondlist);
}
}
//firstlist = {109, 46, 30, 50, 46, 30, 50, 51, 19, 51}

//处理 index > 1 的情况,即 帮砍金额 > 100w 时
if (index.compareTo(BigInteger.ONE) > 0) {
for (int i = 0; i < firstlist.size(); i++) {
firstlist.set(i, firstlist.get(i).multiply(index));
role.setTotalPrice(role.getTotalPrice().subtract(firstlist.get(i)));
}
if (role.getTotalPrice().subtract(role.getLowPrice()).compareTo(BigInteger.ZERO) > 0) {
firstlist.set(firstlist.size() - 1, firstlist.get(firstlist.size() - 1).add(role.getTotalPrice().subtract(role.getLowPrice())));
}
}

return firstlist;
}

/**
* 随机分配金额
* @param price 阶段可砍金额
* @param persons 阶段助力次数
* @return
*/
private static List<BigInteger> random(BigInteger price, Integer persons) throws Exception {
//阶段平均可砍金额
//144 / 1 = 144, 36 / 9 = 4
BigInteger average = price.divide(BigInteger.valueOf(persons));

//随机数上限
//144 * 1.5 = 216, 36 * 1.5 = 54
BigInteger high = new BigDecimal(average).multiply(HIGH_INDEX).toBigInteger();
if (high.compareTo(price) > 0) {
high = price;
}
//随机数下限
//144 * 0.5 = 72, 36 * 0.5 = 18
BigInteger low = new BigDecimal(average).multiply(LOW_INDEX).toBigInteger();
if (low.compareTo(BigInteger.ONE) < 0) {
low = BigInteger.ONE;
}

int[] ints;
try {
//random.ints(a, b, c) 第1个参数为生成随机数数组的元素个数,第2和第3个参数为随机数的范围
//part1: 1人,72~256之间的随机数数组, 如 [109]
//part2: 9人,18~54之间的随机数数组,如 [46, 30, 50, 46, 30, 50, 51, 19, 51]
ints = new Random().ints(persons, low.intValue(), high.intValue() + 1).toArray();
} catch (Exception e) {
throw new Exception("砍价规则不合规,请调整");
}

//将生成的随机数放入 list 中,sum 为随机金额数的累加值
List<BigInteger> list = new ArrayList<>();
BigInteger sum = BigInteger.ZERO;
for (int x : ints) {
sum = sum.add(BigInteger.valueOf(x));
list.add(BigInteger.valueOf(x));
}

//模拟砍价:生成的随机砍价金额数组的累积和 sum 默认砍1次,如果没砍到 0 及 <0 则继续使用 list 中的金额挨个往下砍
//part1: sum = 随机出的 [109] 的和为 109 < price(144), 进 else if
//part2: sum = 随机出的 [46, 30, 50, 46, 30, 50, 51, 19, 51] 的和为 373 < price(36),进 if
if (sum.compareTo(price) > 0) {
//part2: 373-36=337
int val = sum.subtract(price).intValue();
//val=337, 遍历 list(长度9), 每遍历一个 list 的元素,判断该元素 > 1,将该元素-1,将 val-1
//直到 val = 0,list 中每个元素逐个-1,总共减掉了 val 的大小
while (val > 0) {
for (int i = 0; i < list.size(); i++) {
if (val > 0) {
if (list.get(i).compareTo(BigInteger.ONE) > 0) {
list.set(i, list.get(i).subtract(BigInteger.ONE));
val--;
}
} else {
break;
}
}
}
} else if (sum.compareTo(price) < 0) {
//part1: 144-109=35
int val = price.subtract(sum).intValue();
//val=35, 遍历 list(长度1), 每遍历一个 list 的元素,将该元素+1,将 val-1
//直到 val = 0,list 中每个元素逐个+1,总共加上了 val 大小的值
while (val > 0) {
for (int i = 0; i < list.size(); i++) {
if (val > 0) {
list.set(i, list.get(i).add(BigInteger.ONE));
val--;
} else {
break;
}
}
}
}

return list;
}

/**
* 生成不重复随机数
*/
public static List<Integer> getRandomnum(Integer count, Integer min, Integer max) {
if (count == null || min == null || max == null) {
return null;
}
if (max - min < count) {
throw new RuntimeException("illegal parameter");
}
HashSet<Integer> set = new HashSet<>();
while (set.size() < count) {
set.add(new Random().ints(1, min, max).findFirst().getAsInt());
}
return new ArrayList<>(set);
}

/**
* 生成砍价附加规则随机数
*/
public static Map getExtraRoleDetails(Integer looptimes, Integer roleSectiontimes, List<Map> roleExtra) {
int[] sections = new int[looptimes + 1];
sections[0] = 1;
//sections = {1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50}
for (int i = 1; i < looptimes + 1; i++) {
sections[i] = roleSectiontimes * i;
}
//list : {3, 3}
List<Integer> list = new ArrayList<>();
roleExtra.forEach((map) -> {
Map<String, Integer> extra = (Map<String, Integer>) map;
int tag = extra.get("num") == null ? 0 : Integer.parseInt(extra.get("num").toString());
while (tag > 0) {
list.add(extra.get("person"));
tag--;
}
});
//砍价金额明细,长度是 sections长度 * 随机出现的次数
//details(size=10*2=20): {"22":"3","44":"3","34":"3","13":"3","46":"3","14":"3","36":"3","37":"3","16":"3","49":"3","28":"3","18":"3","29":"3","1":"3","3":"3","7":"3","8":"3","41":"3","31":"3","21":"3"}
Map<String, Integer> details = new HashMap<>();
for (int i = 0; i < sections.length - 1; i++) {
//randomnums: 在 sections 中相邻两个数之间生成 list.size() 个随机数
List<Integer> randomnums = getRandomnum(list.size(), sections[i], sections[i + 1]);
for (int j = 0; j < list.size(); j++) {
details.put(String.valueOf(randomnums.get(j)), list.get(j));
}
}
return details;
}

public static void main(String[] args) throws Exception {
int[] ints = new Random().ints(1, 72, 256 + 1).toArray();
System.out.println("Arrays.toString(ints) = " + Arrays.toString(ints));

int[] ints2 = new Random().ints(9, 18, 54 + 1).toArray();
System.out.println("Arrays.toString(ints2) = " + Arrays.toString(ints2));

BargainRole role = new BargainRole();
role.setTotalPrice(new BigInteger("100000000010"));
role.setLowPrice(new BigInteger("1000001"));
role.setTotalTimes(100);
role.setPercentagePerson(99);
role.setPercentagePrice(99);

List<BigInteger> list = BargainUtil.getbargainList(role);
System.out.println("随机金额列表(单位:分):" + list);
BigInteger sum = BigInteger.ZERO;
for (int i = 0; i < list.size(); i++) {
sum = sum.add(list.get(i));
}
System.out.println("确认总额:" + sum);
System.out.println("列表size:" + list.size());
}
}


//Bean类
class BargainRole {
/**
* 商品金额
*/
private BigInteger totalPrice;
/**
* 最低砍至金额
*/
private BigInteger lowPrice;
/**
* 砍价总数量
*/
private Integer totalTimes;
/**
* 前 N %位帮砍者(可砍掉一定的百分比金额)
*/
private Integer percentagePerson;
/**
* 可砍掉商品帮砍金额的 N %
*/
private Integer percentagePrice;


public BigInteger getTotalPrice() {
return totalPrice;
}

public void setTotalPrice(BigInteger totalPrice) {
this.totalPrice = totalPrice;
}

public Integer getTotalTimes() {
return totalTimes;
}

public void setTotalTimes(Integer totalTimes) {
this.totalTimes = totalTimes;
}

public Integer getPercentagePerson() {
return percentagePerson;
}

public void setPercentagePerson(Integer percentagePerson) {
this.percentagePerson = percentagePerson;
}

public Integer getPercentagePrice() {
return percentagePrice;
}

public void setPercentagePrice(Integer percentagePrice) {
this.percentagePrice = percentagePrice;
}

public BigInteger getLowPrice() {
return lowPrice;
}

public void setLowPrice(BigInteger lowPrice) {
this.lowPrice = lowPrice;
}

@Override
public String toString() {
return "BargainRole{" +
"totalPrice=" + totalPrice +
", lowPrice=" + lowPrice +
", totalTimes=" + totalTimes +
", percentagePerson=" + percentagePerson +
", percentagePrice=" + percentagePrice +
'}';
}
}

砍价活动示例

每人最多砍 1 个商品(发起人)

每人最多帮砍 1 个商品(帮砍人)

前 10%位帮砍者,砍掉商品帮砍金额的 80%(活动砍价规则)

砍价商品示例

商品原价 200、商品底价 19.9

固定模式(不校验附加规则):固定 10 人帮砍,发起人砍价成功

随机模式(需校验附加规则):

  1. 默认 10 人帮砍,发起人砍价成功

  2. 其中每有 5 人发起砍价:

2.1 随机出现 2 次,3 人进行帮砍,发起人砍价成功(用于生成砍价明细)

砍价规则 - 与商品1对1:

原价200、底价19.9、前10%砍掉帮砍金额的80%、10人帮砍可以砍价成功

附加规则 - 与商品1对1:

配置的循环次数10、每 5 人发起砍价、随机2次3人帮砍可以砍价成功(可多个)

砍价规则表:砍价金额明细 roleAmountdetails

-> 商品添加时使用随机模式,即附加规则,会生成该砍价金额明细

核心算法:

  1. 通过配置文件配置的循环次数默认x,每y人发起砍价,生成1个递增倍数的数组(长度x+1,元素值y值倍数递增) array

如循环默认10次,每5人发起砍价:[1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

  1. 通过附加规则(随机x次y人帮砍,发起人砍价成功),生成1个人次列表(长度x,元素值均为y) list

如随机2次,3人帮砍,发起人砍价成功:{3, 3}

  1. 通过双重循环+随机数组生成random.ints()方法,生成砍价规则金额明细(长度(array.length-1) * list.size()) map

如array.length-1=10, list.size()=2, 生成的砍价规则金额明细 map 为{“22”:”3”,”44”:”3”,”34”:”3”,”13”:”3”,”46”:”3”,”14”:”3”,”36”:”3”,”37”:”3”,”16”:”3”,”49”:”3”,”28”:”3”,”18”:”3”,”29”:”3”,”1”:”3”,”3”:”3”,”7”:”3”,”8”:”3”,”41”:”3”,”31”:”3”,”21”:”3”}

用户砍价表:砍价金额明细 ubAmountdetails

-> 在用户发起砍价时生成通过砍价规则表里的砍价金额明细,生成当前商品的砍价明细存入用户砍价表,帮砍时去查询取出对应值

核心算法:(模拟砍价)

  1. 计算金额 price

第1部分 list 为前面配置一定比例帮砍者可砍掉帮砍金额的百分比 * 帮砍金额

第2部分 list 剩余需要帮砍的金额

如帮砍金额=200-19.9=180.1,第1部分 (200-19.9)*80%=144, 第2部分 (200-19.9)-144=36.1, 即 144+36.1=180.1

  1. 生成随机数组 array

第1部分的帮砍金额,以1.5倍和0.5倍作为上下限通过 random.ints() 生成随机数组,长度为默认帮砍成功人数 * 该帮砍人次百分比(即这部分比例的人数)

第2部分的帮砍金额,同样方式生成数组,长度为默认帮砍成功人数 - 第1部分人数(即剩余人数)

如 第1部分 1人,72~256之间的随机数数组, 如 [109]

如 第2部分 9人,18~54之间的随机数数组,如 [46, 30, 50, 46, 30, 50, 51, 19, 51]

  1. 将数组转为 list,经过模拟砍价后,生成list,拼接第1部分和第2部分的总 list

如默认10人帮砍,发起人砍价成功时的 用户帮砍金额明细 list = {14408, 382, 358, 363, 350, 325, 421, 545, 527, 331}, 总=18010

如随机x次3人帮砍,发起人砍价成功时的 用户帮砍金额明细 list = {14408, 1984, 1618}, 总=18010

3. 砍价发起 & 帮砍

image-20201129160117038

4. 砍价成功下单

image-20201129160151594

5. 仿拼多多砍价算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ReducePriceUtils {
/**
* 1.总金额不超过总共可砍的价格*100 单位是分
* 2.每次砍价都能砍到金额,最低不能低于1分,最大金额不能超过(总共可砍的价)*100
*/
private static final int MINMONEY = 1;
private static final int MAXMONEY = 200*100;

/**
* 这里为了避免某一次砍价占用大量资金,我们需要设定非最后一次砍价的最大金额,
* 我们把他设置为砍价金额平均值的N倍
*/
private static final double TIMES = 3.1;

/**
* 砍价合法性校验
* @param money
* @param count
* @return
*/
private static boolean isRight(int money,int count){
double avg = money/count;
//小于最小金额
if (avg<MINMONEY) {
return false;
}else if (avg>MAXMONEY) {
return false;
}
return true;
}

/**
* 随机分配一个金额
* @param mnoney
* @param minS:最小金额
* @param maxS:最大金额
* @param count
* @return
*/
private static int randomReducePrice(int money,int minS,int maxS,int count){
//若只有一个,直接返回
if (count==1) {
return money;
}
//如果最大金额和最小金额相等,直接返回金额
if (minS==maxS) {
return minS;
}
int max=maxS>money?money:maxS;
//分配砍价正确情况,允许砍价的最大值
int maxY = money-(count-1)*minS;
//分配砍价正确情况,允许砍价最小值
int minY = money-(count-1)*maxS;
//随机产生砍价的最小值
int min = minS>minY?minS:minY;
//随机产生砍价的最大值
max = max>maxY?maxY:max;
//随机产生一个砍价
return (int)Math.rint(Math.random()*(max-min) +min);
}

/**
* 砍价
* @param money 可砍总价
* @param count 个数
* @return
*/
public static List<Double> splitReducePrice(int money,int count){
//红包合法性分析
if (!isRight(money, count)) {
return new ArrayList<>();
}
//红包列表
List<Double> list = new ArrayList<>();
//每个红包的最大的金额为平均金额的TIMES倍
int max = (int)(money*TIMES/count);
max = max>MAXMONEY?MAXMONEY:max;
//分配红包
int sum = 0;

for(int i=0;i<count;i++){
int one = randomReducePrice(money, MINMONEY, max, count-i);
list.add(one/100.0);
money-=one;
sum += one;
}
System.out.println("sum:"+sum);
return list;
}

public static void main(String[] args) {
List<Double> list = splitReducePrice(19799, 10);
System.out.println(list);
}

砍价分配结果:

1
2
sum:19799
[1.65, 3.94, 4.29, 3.44, 1.09, 33.65, 11.41, 43.78, 46.74, 48.0]

04-砍价系统核心流程设计
https://janycode.github.io/2020/11/29/17_项目设计/01_业务设计/04-砍价系统核心流程设计/
作者
Jerry(姜源)
发布于
2020年11月29日
许可协议