本文为博主原创,未经授权,严禁转载及使用。
本文链接:https://blog.csdn.net/zyooooxie/article/details/108440488
之前分享过2期 抽奖1:多用户抽奖、抽奖结果、积分的变动 、
抽奖2:中奖概率(单一奖品)、奖品库存 ;
最近我遇到一个需求:实际某抽奖活动 发的奖品是多件;
接到需求后,想着要去校验发的奖品,和之前类似 我的想法还是大数据量地抽 。
此外,遇到一些抽奖失败的情况,有些新的想法,就来再做一次分享。
【实际这篇博客推迟发布N个月】
个人博客:https://blog.csdn.net/zyooooxie
【以下所有内容仅为个人项目经历,如有不同,纯属正常】
需求3
某抽奖活动,奖品分4类,总共40种;每次开奖,随机中奖;抽奖机会可以通过做任务获得;每天最多可参与抽奖99次;
隐藏规则:
- 固定第X次、第XX次、第XXX次、第XXXX次 … 会抽中 某类奖品(此类奖品共4种,此规则会随机发3种);发放以三次为一个循环;
- 固定第a次、第b次、第c次、第d次 … 抽中 某奖品;
对比 前2期分享中的活动,不同点在于:
- 当天抽奖次数多;
- 奖品种类多;
- 某奖品库存不足时,会发兜底奖品;
- 发奖有隐藏规则;
测试点:兜底奖品(兜底礼包)
兜底礼包:其他礼包库存不足后,会发放的礼包;一般都会把库存很足很足的作为兜底礼包;【不会出现兜底礼包也没库存的情况】
此外,兜底礼包也会作为普通奖品发放,有自己本身的中奖概率;
我的用例:
- 所有礼包 库存都足够,看兜底礼包本身的概率;
- 某礼包(概率很高) 库存即将没有,多位用户同时来抢,看实际在其 库存=0前后 发放的礼包;
- 某礼包 库存已经为0了,后续再发放此礼包时 会走兜底礼包,看此时兜底礼包的概率;
测试点:抽奖失败
前端用例 看展示
- 无抽奖次数,Fiddler改 查询抽奖次数的接口返回值:有M次抽奖次数;再去抽奖,看前端;
- 有抽奖次数,正常抽奖,Fiddler改 抽奖的接口返回值:抽奖失败;看前端;
后台用例 看数据
- 抽奖次数不足;
- 并发抽奖、重复请求;
- 超过当天参与次数;
- 发奖失败1:奖品配置得 有问题;
- 发奖失败2:调用发奖系统 超时;
- 特殊情况:这个需求,后台做了防并发,大批量抽奖时 接口有时返回的是 “errorMessage”:“系统异常”
后三条用例 必须留意的是 抽奖次数是否扣除+是否有奖品发放。
测试点:中奖概率-多个奖品
前面说了本期活动的不同点,细说下 我想要 脚本实现的:要做抽奖后 剩余次数的校验;要做抽奖后 发放奖品的校验;要做某奖品中奖概率的校验;隐藏规则的校验;
【脚本思路:准备用户、清理相关数据、增加抽奖次数、抽奖、获取抽奖结果、检查抽奖结果、统计抽奖结果、得出概率】
实际执行:
- 设置此次抽奖的用户数;
- 设置抽奖的次数;
- 【for循环】每位用户 清数据,加抽奖次数,登录,抽奖、检查抽奖结果、统计当前中奖信息;
- 统计所有用户的数据,和实际配置做对比,得出结论;
four_card_times, ty_times 是隐藏规则1 和 隐藏规则2;
(代码有删改)
"""
@blog: https://blog.csdn.net/zyooooxie
"""
class NewYear(object):
def __init__(self, txt_read_rows: int, txt_skip_row: int):
"""
TXT文件读取的数量、跳过的数量
:param txt_read_rows:
:param txt_skip_row:
"""
self.__txt_read = txt_read_rows
self.__txt_skip = txt_skip_row
Log.info('txt_read:{};txt_skip:{}'.format(self.__txt_read, self.__txt_skip))
@property
def use_rows(self):
"""
返回__txt_read;设置成属性,方便子类继承
:return:
"""
return self.__txt_read
def get_user_phone_nickname_list(self):
"""
获取TXT文件的数据,返回list:userId、phone
:return:
"""
user_id_phone = read_txt_user(nrows=self.__txt_read, skiprows=self.__txt_skip)
userId_mobile_nickName = list(map(lambda x: (x[0], x[1], '****'.join([str(x[1])[:3], str(x[1])[-4:]])), user_id_phone))
return userId_mobile_nickName
@staticmethod
def get_session_id(user_id: str, user_phone: str):
"""
获取session
:param user_id:
:param user_phone:
:return:
"""
session, user = get_session(user_phone)
assert user_id == user # 部分手机号注销过
Log.info('user_id: {} ,user_session: {}'.format(user, session))
return session
@staticmethod
def send_request(url: str, session: str, params_dict: dict, add_header: dict = None):
"""
发请求
:param url:
:param session:
:param params_dict:
:param add_header:
:return:
"""
header = {'Cookie': 'sessionId={}'.format(session), 'Content-Type': 'application/json;charset=utf-8'}
if add_header is not None:
header.update(add_header)
res = requests.post(url, json=params_dict, headers=header, verify=False)
res_dict = res.json()
Log.info(res_dict)
return res_dict
@staticmethod
def join_url(url: str):
"""
URL拼接
:param url:
:return:
"""
if url.find('zyooooxie') != -1:
return url
base_url = 'https://blog.csdn.net/zyooooxie/commonPost/'
url = urljoin(base_url, url)
return url
def check_draw_results(self, response_dict: dict, draw_successfully_time: str, statistics_list_dict: list or dict,
sum_dict: dict):
"""
检查抽奖结果
:param response_dict:res.json()
:param draw_successfully_time:成功请求的次数
:param statistics_list_dict:推荐使用dict;统计结果dict-不包含 隐藏规则的那些结果
:param sum_dict:统计结果dict-包含全部结果
:return:
"""
# 读取抽奖请求的响应值;对隐藏规则的2种 做校验;
# 其他普通抽奖的 计入statistics_list_dict;
# 所有抽奖的 计入sum_dict;
d1 = response_dict.get('obj')[0]
# 隐藏规则-直接写死
four_card_times, ty_times = ["1","14","18","38"], ["11","28","42"]
if draw_successfully_time in four_card_times:
self.update_result_dict(sum_dict, d1['productName'])
elif draw_successfully_time in ty_times:
assert d1['productName'] == 'TY'
self.update_result_dict(sum_dict, d1['productName'])
else:
key_ = d1['productName']
self.update_result_dict(sum_dict, key_)
if isinstance(statistics_list_dict, list):
# 每个类型为一个dict
C_dict, P_dict, W_dict, XN_dict = statistics_list_dict
if d1['productType'] == 'P': # P
self.update_result_dict(P_dict, d1['productName'])
elif d1['productType'] == 'XN': # XN
self.update_result_dict(XN_dict, d1['productName'])
elif d1['productType'] == 'W': # W
self.update_result_dict(W_dict, d1['productName'])
elif d1['productType'] == 'C': # C
self.update_result_dict(C_dict, d1['productName'])
else:
raise Exception('传参有误:{}'.format(d1['productType']))
elif isinstance(statistics_list_dict, dict):
# 不区分类型,直接写入一起
self.update_result_dict(statistics_list_dict, key_)
else:
raise Exception('statistics_list_dict传参有误:{}'.format(statistics_list_dict))
@staticmethod
def update_result_dict(test_dict: dict, test_key: str):
"""
更新 统计结果dict
:param test_dict:
:param test_key:
:return:
"""
# key存在,value的值+1;key不存在,value设置为1;
if test_dict.get(test_key) is None:
test_dict.update({test_key: 1})
else:
test_dict.update({test_key: 1 + test_dict[test_key]})
def draw(self, draw_num: int, phone: str, user_id: str = 'zyooooxie',
session: str = None, db: Connection = None, cur: Cursor = None, every_inspect: str = None):
"""
抽奖
:param draw_num:【特殊设计】为1时,不去请求抽奖次数接口,抽完直接 返回session;
:param phone:
:param user_id:
:param session:
:param db:
:param cur:
:param every_inspect:是否每次抽奖都做 抽奖次数+发放的奖品 校验;非None 校验;默认 不校验;
:return:
"""
# 清除参与的数据;删除、设置缓存:抽奖次数 random.randint(95, 98);
# 获取用户的总抽奖次数;
# 【for循环】抽奖:发请求、检查结果、统计中奖信息;
# 响应异常情形:抽奖失败、异常返回值;
draw_url = 'zyooooxie/draw'
if db is None:
ld_db, ld_cur = connect_db(zy_db='yes')
else:
ld_db, ld_cur = db, cur
self.delete_data(user_id, dd_db_info=ld_db, dd_cur_info=ld_cur)
test_kn_redis1(user_id)
if session is None:
session = self.get_session_id(user_id, phone)
# 特殊设计:=1时 不获取抽奖总次数
if draw_num != 1:
remain_times = self.get_user_draw_times(session=session)
Log.debug('可抽次数:{}'.format(remain_times))
Log.info('准备抽{}次'.format(draw_num))
else:
remain_times = None
four_card_data = list()
# 隐藏规则-直接写死
four_card_times = ["1","14","18","28"]
# statistics_list_dict = [dict() for _ in range(4)] # 4种 奖品,分4个dict 来统计
statistics_list_dict = dict() # 统计非隐藏规则的奖品
sum_dict = dict() # 统计所有奖品
error_time = draw_num
success_time = 0
for n in range(draw_num):
try:
res_dict = self.send_request(self.join_url(draw_url), session=session, params_dict={})
if res_dict.get('success') is False:
continue
success_time = success_time + 1
# Log.debug('{}:Response success'.format(success_time))
self.check_draw_results(res_dict, str(success_time), statistics_list_dict, sum_dict=sum_dict)
# if n >= 3 + error_time: # 响应异常的请求后 打印后3次的抽奖结果
# Log.debug(statistics_list_dict)
if str(success_time) in four_card_times:
Log.info('four_card_times:当前次数为 {}'.format(success_time))
four_card_data.append(res_dict.get('obj')[0]['productName'])
Log.info(four_card_data)
if len(four_card_data) <= 3: # 前3次 不可重复
assert len(set(four_card_data)) == len(four_card_data)
else:
assert len(set(four_card_data)) == 3 # 只3种
self.check_four_card(four_card_data)
if every_inspect is not None:
Log.info('请求成功,校验:{}'.format(n))
self.result_check(success_time=success_time, user_id=user_id, statistics_dict=sum_dict,
session=session, error_time=error_time, draw_num=draw_num,
initial_times=remain_times, db=ld_db, cur=ld_cur)
except (JSONDecodeError, TypeError) as e:
Log.info('响应异常:{}'.format(e.args))
Log.info(traceback.format_exc())
error_time = n
time.sleep(12) # 异常响应后,强制等待12s
break # break是 舍弃掉此次的抽奖结果【无法确定‘失败’这次是否发奖品】 + 此账号停止抽奖
# time.sleep(0.1)
# 特殊设计:=1时 返回session
if draw_num == 1:
return session
# 2种情形:无异常到抽完draw_num次,结束时 总校验 + 异常抽奖,break后 校验
Log.info('用户:{} 抽奖结束,开始校验'.format(user_id))
self.result_check(success_time=success_time, user_id=user_id, statistics_dict=sum_dict,
session=session, error_time=error_time, draw_num=draw_num, initial_times=remain_times,
db=ld_db, cur=ld_cur)
# statistics_list_dict 若是list,改为dict
if isinstance(statistics_list_dict, list):
statistics_dict = reduce(lambda x, y: {**x, **y}, statistics_list_dict)
else:
statistics_dict = statistics_list_dict
phone_user_id = phone if phone is not None else user_id
Log.info('{} 统计的奖品结果:{}'.format(phone_user_id, statistics_dict))
all_times = sum(statistics_dict.values())
# 隐藏规则 不计入
Log.info('{} 统计的抽奖次数:{}'.format(phone_user_id, all_times))
res_dict = {k: round(v / all_times, 3) for k, v in statistics_dict.items()}
Log.info('{} 统计的奖品概率:{}'.format(phone_user_id, res_dict))
if db is None:
ld_cur.close()
ld_db.close()
return statistics_dict
def result_check(self, success_time: int, user_id: str, statistics_dict: dict, session: str, error_time: int,
draw_num: int, initial_times: int, db: Connection, cur: Cursor):
"""
检查中奖结果
:param success_time:抽奖成功次数
:param user_id:
:param statistics_dict:统计dict-包含全部结果
:param session:
:param error_time:错误次数
:param draw_num:
:param initial_times:初始抽奖总次数
:param db:
:param cur:
:return:
"""
sql = """
SELECT C_ID, D_VALUE, PRODUCT_TYPE FROM table_award WHERE USER_ID = '{}' ORDER BY ID ASC ;
""".format(user_id)
# 获取表里、此活动的记录
data_tuple = fetchall_data_no_close(sql, db, cur)
Log.debug(data_tuple)
Log.info(statistics_dict)
remain_times2 = self.get_user_draw_times(session=session)
Log.info('剩余抽奖次数:{}'.format(remain_times2))
if error_time == draw_num: # 未出现 异常返回值
assert initial_times == remain_times2 + success_time
assert len(data_tuple) == success_time
award_data = data_tuple
# 后台做了些防并发的处理,所以某次请求 响应体异常,
# 实际在后面补发了礼包+扣次数 or 实际没有处理此次失败。
else:
if len(data_tuple) == success_time: # 表里的记录数 = 成功发奖的次数
Log.info('没有补发')
assert initial_times == remain_times2 + success_time
award_data = data_tuple
else:
assert len(data_tuple) == success_time + 1 # 表里的记录数 = 成功发奖的次数 + 1
Log.info('补发了')
assert initial_times == remain_times2 + success_time + 1
Log.info('比statistics_dict多的:{}'.format(data_tuple[-1]))
award_data = data_tuple[:-1]
table_result = self.award_packet_change(award_data)
eq_res = operator.eq(table_result, statistics_dict)
if eq_res is False:
Log.error('对比失败:{} 和 {}'.format(table_result, statistics_dict))
raise
Log.info('抽奖次数、发奖奖品 校验通过')
def award_packet_change(self, award_packet_data: tuple):
"""
表的数据 转换成 统计dict
:param award_packet_data:
:return:
"""
# COMMODITY_ID、DETAIL_VALUE、PRODUCT_TYPE
table_dict = {('XN1111', '11', 'XN'): '奖品名a',
('C2222', '', 'C'): '奖品名b',
('P3333',33, 'P'): '奖品名c',
('W4444', '', 'W'): '奖品名d'
} # 40种奖品
XN_P = list()
C_W = list()
for d in award_packet_data:
if d[-1] in ('XN', 'P'):
XN_P.append(d)
else:
C_W.append(d)
# 统计 奖品的出现次数
apd_counter1 = Counter(XN_P)
apd_counter2 = Counter([(apd[0], '', apd[-1]) for apd in C_W])
# COMMODITY_ID、PRODUCT_TYPE保留;DETAIL_VALUE 改为''
apd_list1 = list(apd_counter1.items())
apd_list2 = list(apd_counter2.items())
award_packet_result = dict()
for al in apd_list1 + apd_list2:
if al[0] in table_dict:
award_packet_result.update({table_dict.get(al[0]): al[1]})
else:
Log.error('发的奖品没找到:{}'.format(al))
raise
Log.debug('表里的数据统计后:{}'.format(award_packet_result))
return award_packet_result
def get_user_draw_times(self, session: str):
"""
获取抽奖次数
:param session:
:return:
"""
url = 'zyooooxie/drawInfo'
res_dict = self.send_request(self.join_url(url), session=session, params_dict={})
if res_dict.get('obj') is not None:
rt = res_dict.get('obj').get('Times')
return rt
else:
Log.error('返回值有异常')
raise
@staticmethod
def check_four_card(test_list):
"""
drawFourCardTimes:3种类型 固定顺序循环
:param test_list:
:return:
"""
assert test_list[-1] == test_list[len(test_list) - 1 - 3]
def draw_many_users(self, draw_num: int, every_inspect: str = None):
"""
许多用户抽奖
:param draw_num:
:param every_inspect:
:return:
"""
all_statistics_list = list()
db, cur = connect_db(zy_db='yes')
for d in self.get_user_phone_nickname_list():
sd = self.draw(draw_num, user_id=d[0], phone=str(d[1]), db=db, cur=cur, every_inspect=every_inspect)
all_statistics_list.append(sd)
else:
Log.debug('总体结果:{}'.format(all_statistics_list))
cur.close()
db.close()
if draw_num == 1:
# all_statistics_list 统计的是 session
return
# all_statistics_list是 每个用户抽奖结果dict 的 list
all_statistics_dict = reduce(self.update_value, all_statistics_list)
all_times = sum(all_statistics_dict.values())
all_result = {k: round(v / all_times, 3) * 100 for k, v in all_statistics_dict.items()}
Log.info('最后的统计结果:{}'.format(all_statistics_dict))
Log.info('统计的次数:{}'.format(all_times))
Log.info('计算的概率:{}'.format(all_result))
@staticmethod
def update_value(test_dict1: dict, test_dict2: dict):
"""
dict1、dict2的key相同、value相加
:param test_dict1:
:param test_dict2:
:return:
"""
temp = dict()
for k in test_dict1.keys() | test_dict2.keys():
temp[k] = sum(d.get(k, 0) for d in (test_dict1, test_dict2))
return temp
if __name__ == '__main__':
pass
ny = NewYear(10, random.randint(5, 88888))
# test_kn_redis1() 设置 抽奖次数为 random.randint(95, 99)
ny.draw_many_users(draw_num=random.randint(91, 97))
看下执行结果:
上图的马赛克 没办法,不想违规。
3张截图 出现的情形是:
- 某用户正常请求N次抽奖接口,无异常返回值;统计其的数据;统计本轮执行的所有用户的数据;
- 某用户请求抽奖接口遇到异常返回值,强制等待12s后,去表里查询此用户的发奖数据,对比发现-没有补发;break;统计其的数据;统计本轮执行的所有用户的数据;
- 某用户请求抽奖接口遇到异常返回值,强制等待12s后,去表里查询此用户的发奖数据,对比发现-补发了;break;统计其的数据【补发的不计入】;继续下一个用户;
本文链接:https://blog.csdn.net/zyooooxie/article/details/108440488
交流技术 欢迎+QQ 153132336 zy
个人博客 https://blog.csdn.net/zyooooxie