import logging from datetime import timedelta from django.db import transaction, models from django.utils import timezone from pingcap_sso.models import ( UserPointStat, UserAwardedPoint, PointDailyCheckIn, PointProduct, PointProductRedemptionRecord, award_points_by_value ) from pingcap_sso.user_behavior_track.models import TrackEvent from pingcap_sso.utils.requests import get_client_agent, get_client_ip logger = logging.getLogger(__name__) AWARD_USER_COMPLETE_PERSONAL_INFO = 'user_complete_personal_info' AWARD_USER_COMPLETE_COMPANY_INFO = 'user_complete_company_info' AWARD_USER_CONNECT_GITHUB = 'user_connect_github' AWARD_USER_BIND_EMAIL = 'user_bind_email' AWARD_USER_BIND_PHONE = 'user_bind_phone' AWARD_JOIN_ORG = 'join_org' AWARD_ASKTUG_NEW_POST = 'asktug_new_post' AWARD_ASKTUG_NEW_TOPIC = 'asktug_new_topic' AWARD_ASKTUG_PINNED = 'asktug_pinned' AWARD_ASKTUG_LIKE = 'asktug_like' AWARD_ASKTUG_WAS_LIKED = 'asktug_was_liked' AWARD_ASKTUG_BOOKMARK = 'asktug_bookmark' AWARD_ASKTUG_WAS_BOOKMARKED = 'asktug_was_bookmarked' AWARD_ASKTUG_SOLVED = 'asktug_solved' AWARD_ASKTUG_MARK_SOLVED = 'asktug_mark_sovled' AWARD_PE_PASS_PCTA = 'pe_pass_pcta' AWARD_PE_PASS_PCTP = 'pe_pass_pctp' AWARD_PE_PASS_PCSD = 'pe_pass_pcsd' AWARD_PE_PASS_PRACZH = 'pe_pass_praczh' AWARD_PE_COMPLETE_101 = 'pe_complete_101' AWARD_PE_COMPLETE_201 = 'pe_complete_201' AWARD_PE_COMPLETE_301 = 'pe_complete_301' AWARD_PE_COMPLETE_302 = 'pe_complete_302' AWARD_PE_COMPLETE_304 = 'pe_complete_304' AWARD_PE_COMPLETE_305 = 'pe_complete_305' AWARD_PE_COMPLETE_307 = 'pe_complete_307' AWARD_PE_COMPLETE_308 = 'pe_complete_308' AWARD_PE_COMPLETE_V2_960001 = 'pe_complete_v2_960001' AWARD_PE_COMPLETE_V2_1050001 = 'pe_complete_v2_1050001' AWARD_PE_COMPLETE_V2_750002 = 'pe_complete_v2_750002' AWARD_PE_COMPLETE_V2_840002 = 'pe_complete_v2_840002' AWARD_PE_COMPLETE_V2_780002 = 'pe_complete_v2_780002' AWARD_PE_COMPLETE_V2_1110001 = 'pe_complete_v2_1110001' AWARD_RULES = { AWARD_USER_COMPLETE_PERSONAL_INFO: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 20, 'exps': 20, 'reason': '完成个人信息填写' }, AWARD_USER_COMPLETE_COMPANY_INFO: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成公司信息填写' }, AWARD_USER_CONNECT_GITHUB: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 20, 'exps': 10, 'reason': '绑定 GitHub 账号' }, AWARD_USER_BIND_EMAIL: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 20, 'exps': 10, 'reason': '绑定邮箱' }, AWARD_USER_BIND_PHONE: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 20, 'exps': 10, 'reason': '绑定手机号' }, AWARD_JOIN_ORG: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 200, 'exps': 200, 'reason': '完成认证' }, AWARD_ASKTUG_NEW_POST: { 'allow_multiple_award': True, 'daily_limits': 10, 'points': 8, 'exps': 8, 'reason': '回复问题' }, AWARD_ASKTUG_NEW_TOPIC: { 'allow_multiple_award': True, 'daily_limits': 10, 'points': 5, 'exps': 5, 'reason': '发布问题帖子' }, AWARD_ASKTUG_PINNED: { 'allow_multiple_award': True, 'daily_limits': 3, 'points': 20, 'exps': 10, 'reason': '帖子被置顶' }, # deprecated AWARD_ASKTUG_LIKE: { 'allow_multiple_award': True, 'daily_limits': 0, 'points': 0, 'exps': 0, 'reason': '点赞了回复' }, AWARD_ASKTUG_WAS_LIKED: { 'allow_multiple_award': True, 'daily_limits': 10, 'points': 2, 'exps': 2, 'reason': '回复被点赞' }, # deprecated AWARD_ASKTUG_BOOKMARK: { 'allow_multiple_award': True, 'daily_limits': 0, 'points': 0, 'exps': 0, 'reason': '收藏了回复' }, # deprecated AWARD_ASKTUG_WAS_BOOKMARKED: { 'allow_multiple_award': True, 'daily_limits': 10, 'points': 5, 'exps': 5, 'reason': '回复被收藏' }, AWARD_ASKTUG_SOLVED: { 'allow_multiple_award': True, 'daily_limits': 10, 'points': 15, 'exps': 15, 'reason': '回答问题被选为『最佳答案』' }, AWARD_ASKTUG_MARK_SOLVED: { 'allow_multiple_award': True, 'daily_limits': 10, 'points': 5, 'exps': 5, 'reason': '主动标记『最佳答案』' }, AWARD_PE_PASS_PCTA: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 200, 'exps': 200, 'reason': '通过 PCTA 认证' }, AWARD_PE_PASS_PCTP: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 500, 'exps': 500, 'reason': '通过 PCTP 认证' }, AWARD_PE_PASS_PCSD: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 350, 'exps': 350, 'reason': '通过 PCSD 认证' }, AWARD_PE_PASS_PRACZH: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '通过从业者认证' }, AWARD_PE_COMPLETE_101: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 101 - TiDB 快速起步 课程学习' }, AWARD_PE_COMPLETE_201: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 201 - TiDB 集群的架构与特点 课程学习' }, AWARD_PE_COMPLETE_301: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 301 - TiDB 系统管理基础 课程学习' }, AWARD_PE_COMPLETE_302: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 302 - TiDB 高级系统管理 课程学习' }, AWARD_PE_COMPLETE_304: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 304 - TiDB 性能调优案例 课程学习' }, AWARD_PE_COMPLETE_305: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 305 - TiDB 故障排除案例 课程学习' }, AWARD_PE_COMPLETE_307: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 307 - TiDB 性能调优 课程学习' }, AWARD_PE_COMPLETE_308: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 308 - TiDB 故障排除 课程学习' }, AWARD_PE_COMPLETE_V2_960001: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 TiDB 数据库核心原理与架构(101) 课程学习' }, AWARD_PE_COMPLETE_V2_1050001: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 在 TiDB 上学习 SQL 语句(201.2) 课程学习' }, AWARD_PE_COMPLETE_V2_750002: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 TiDB 特有功能与事务控制(201.3) 课程学习' }, AWARD_PE_COMPLETE_V2_840002: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 使用 Connector/J(201.4) 课程学习' }, AWARD_PE_COMPLETE_V2_780002: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 100, 'exps': 100, 'reason': '完成 在 TiDB 上开发应用的最佳实践(201.5) 课程学习' }, AWARD_PE_COMPLETE_V2_1110001: { 'allow_multiple_award': False, 'daily_limits': None, 'points': 50, 'exps': 50, 'reason': '完成 TiDB 数据库管理(303) 课程学习' }, } def award_points_by_type( target_user, award_type, reason=None, digest=None, comment='', acting_user=None, discourse_topic_id=None, discourse_post_id=None, discourse_user_action_id=None): try: rule = AWARD_RULES[award_type] allow_multiple_award = rule['allow_multiple_award'] daily_limits = rule['daily_limits'] award_digest = digest or rule['reason'] award_reason = reason or '系统自动授予: {}'.format(rule['reason']) can_award = False except KeyError: logger.error('`award_points_by_type` receive invalid award_type: {}'.format(award_type)) return with transaction.atomic(): try: user_stat = UserPointStat.objects.select_for_update().get(user=target_user) except UserPointStat.DoesNotExist: UserPointStat.objects.create(user=target_user) user_stat = UserPointStat.objects.select_for_update().get(user=target_user) if allow_multiple_award: if daily_limits: can_award = UserAwardedPoint.objects.extra( where=[ 'DATE(CONVERT_TZ(created_at, "+00:00","+08:00")) = %s' ], params=[timezone.localdate()] ).filter( award_type=award_type, target_user=target_user ).count() < daily_limits else: can_award = True else: can_award = UserAwardedPoint.objects.filter( award_type=award_type, target_user=target_user).count() == 0 if can_award: UserAwardedPoint.objects.create( target_user=target_user, acting_user=acting_user, points=rule['points'], experience=rule['exps'], reason=award_reason, digest=award_digest, comment=comment, award_type=award_type, discourse_topic_id=discourse_topic_id, discourse_post_id=discourse_post_id, discourse_user_action_id=discourse_user_action_id ) user_stat.points = models.F('points') + rule['points'] user_stat.experience = models.F('experience') + rule['exps'] user_stat.save() def revoke_award_by_type_without_limits( target_user, award_type, reason=None, digest=None, comment='', acting_user=None, discourse_topic_id=None, discourse_post_id=None, discourse_user_action_id=None): try: rule = AWARD_RULES[award_type] award_digest = digest or '撤销奖励: {}'.format(rule['reason']) award_reason = reason or '系统自动撤销奖励: {}'.format(rule['reason']) award_type = 'revoke_{}'.format(award_type) points = -rule['points'] exps = -rule['exps'] except KeyError: logger.error('`award_points_by_type` receive invalid award_type: {}'.format(award_type)) return with transaction.atomic(): try: user_stat = UserPointStat.objects.select_for_update().get(user=target_user) except UserPointStat.DoesNotExist: UserPointStat.objects.create(user=target_user) user_stat = UserPointStat.objects.select_for_update().get(user=target_user) award_record = UserAwardedPoint.objects.create( target_user=target_user, acting_user=acting_user, points=points, experience=exps, reason=award_reason, digest=award_digest, award_type=award_type, discourse_topic_id=discourse_topic_id, discourse_post_id=discourse_post_id, discourse_user_action_id=discourse_user_action_id ) user_stat.points = models.F('points') + points user_stat.experience = models.F('experience') + exps user_stat.save() return award_record def daily_checkin(user, request): today = timezone.localdate() three_days_ago = today - timedelta(days=3) checkin, created = PointDailyCheckIn.objects.get_or_create( user=user, date=today ) if created: histories = PointDailyCheckIn.objects.filter( user=user, date__gte=three_days_ago).values_list('date', flat=True) continues_checkin_count = 1 for i in range(1, 3): if (today - timedelta(days=i)) in histories: continues_checkin_count += 1 else: break points, exps = _daily_checkin_award_by_continues_count(continues_checkin_count) tomorrow_points, _ = _daily_checkin_award_by_continues_count(continues_checkin_count + 1) if continues_checkin_count < 3: digest = '连续签到 {} 天'.format(continues_checkin_count) else: digest = '连续签到 3 天及以上' award_points_by_value(user, points, exps, digest, digest) TrackEvent.objects.create( openid=user.openid, event_type='sso.daily_checkin', event_body={ 'continues_checkin_count': continues_checkin_count, 'points': points, 'exps': exps }, user_agent=get_client_agent(request), ip=get_client_ip(request), description='签到', source=TrackEvent.SOURCE_SERVER ) else: points = 0 tomorrow_points = 0 continues_checkin_count = 1 return created, continues_checkin_count, points, tomorrow_points def _daily_checkin_award_by_continues_count(continues_checkin_count): points = exps = 0 if continues_checkin_count == 1: points = exps = 2 elif continues_checkin_count == 2: points = exps = 3 elif continues_checkin_count > 2: points = exps = 5 else: raise Exception('Unexpected continues_checkin_count: {}'.format( continues_checkin_count)) return points, exps def redeem_product( request, user, product, consignee_name, consignee_phone, consignee_addrss, comment ): user_stat = UserPointStat.get_stat(user) try: with transaction.atomic(): user_stat = UserPointStat.objects.select_for_update().get(id=user_stat.id) product = PointProduct.objects.select_for_update().get( deleted_by_user__isnull=True, id=product.id) if user_stat.points < product.points: return None, '积分不足' if product.remains < 1: return None, '该物品暂时无货' digest = '兑换商品「{}」'.format(product.name) award_points_by_value(user, -product.points, 0, digest, digest, user) product.remains = models.F('remains') - 1 product.save() record = PointProductRedemptionRecord.objects.create( user=user, product=product, points=product.points, consignee_name=consignee_name, consignee_phone=consignee_phone, consignee_addrss=consignee_addrss, comment=comment ) except PointProduct.DoesNotExist: return None, '兑换失败,商品不存在或已下架,请刷新重试' except Exception as e: logger.error(str(e)) return None, '系统错误,兑换失败,请刷新重试' TrackEvent.objects.create( openid=user.openid, event_type='sso.redeem_product', event_body={ 'product_id': product.id, 'product_name': product.name, 'points': product.points }, user_agent=get_client_agent(request), ip=get_client_ip(request), description=digest, source=TrackEvent.SOURCE_SERVER ) return record, '兑换成功'