# -*- encoding: utf-8 -*- """ @File : AgentOrderController.py @Time : 2024/3/14 10:53 @Author : stephen @Email : zhangdongming@asj6.wecom.work @Software: PyCharm """ import threading import time from datetime import datetime, timedelta from decimal import Decimal, ROUND_DOWN from django.http import QueryDict from django.views import View from AgentModel.models import AgentDevice, AgentCloudServicePackage, AgentCustomerPackage, AgentDeviceOrder, \ AgentDeviceOrderInstallment, AgentAccount from Ansjer.config import LOGGER from Model.models import Order_Model, Store_Meal, UnicomCombo, TimeZoneInfo from Object.CeleryBeatObject import CeleryBeatObj from Object.ResponseObject import ResponseObject from Object.TokenObject import TokenObject from Service.CommonService import CommonService class AgentOrderView(View): def get(self, request, *args, **kwargs): request.encoding = 'utf-8' operation = kwargs.get('operation') return self.validation(request.GET, request, operation) def post(self, request, *args, **kwargs): request.encoding = 'utf-8' operation = kwargs.get('operation') return self.validation(request.POST, request, operation) def delete(self, request, *args, **kwargs): request.encoding = 'utf-8' operation = kwargs.get('operation') delete = QueryDict(request.body) if not delete: delete = request.GET return self.validation(delete, request, operation) def put(self, request, *args, **kwargs): request.encoding = 'utf-8' operation = kwargs.get('operation') put = QueryDict(request.body) return self.validation(put, request, operation) def validation(self, request_dict, request, operation): response = ResponseObject() tko = TokenObject( request.META.get('HTTP_AUTHORIZATION'), returntpye='pc') if operation == 'addOrder': # 添加代理商订单 order_id = request_dict.get('order_id', None) uid = request_dict.get('uid', None) order_type = request_dict.get('order_type', None) package_id = request_dict.get('package_id', None) self.check_agent_service_package(order_id, uid, int(package_id)) return response.json(0) elif operation == 'addSettlementJob': self.add_settlement_job() return response.json(0) elif operation == 'delSettlementJob': self.del_settlement_job() return response.json(0) elif operation == 'updateSettlementJob': self.update_settlement_job() return response.json(0) @classmethod def update_settlement_job(cls): celery_beat_obj = CeleryBeatObj() job_name = 'Agent-updateSettlement' time_zone_info_qs = TimeZoneInfo.objects.filter(tz=8).values('zone_info') if time_zone_info_qs.exists(): time_zone = time_zone_info_qs[0]['zone_info'] cron_tuple = ('*/3', '*', '*', '*', '*', time_zone) celery_beat_obj.update_task(name=job_name, crontab=cron_tuple) @classmethod def add_settlement_job(cls): celery_beat_obj = CeleryBeatObj() job_name = 'Agent-updateSettlement' SMART_SCENE_TASK = 'Controller.CeleryTasks.tasks.update_installment_settlement_order' celery_beat_obj.creat_crontab_task( timezone_offset=8, name=job_name, task=SMART_SCENE_TASK, minute='*/2') @classmethod def del_settlement_job(cls): celery_beat_obj = CeleryBeatObj() job_name = 'Agent-updateSettlement' celery_beat_obj.del_task(job_name) @classmethod def check_agent_service_package(cls, order_id, uid, package_id): """ 检查是否代理服务套餐 @param package_id: 套餐id @param order_id: 订单ID @param uid: UID @return: True | False """ try: serial_number = CommonService.get_serial_number_by_uid(uid) a_device_qs = AgentDevice.objects.filter(serial_number=serial_number) \ .values('ac_id', 'type', 'status') LOGGER.info(f'******check_agent_service_package检查是否代理*****orderId:{order_id}') if not a_device_qs.exists(): return False LOGGER.info(f'******check_agent_service_package当前设备属于代理商*****serial_number:{serial_number}') asy = threading.Thread(target=cls.save_agent_package, args=(order_id, serial_number, a_device_qs[0]['ac_id'], package_id)) asy.start() return True except Exception as e: LOGGER.info('*****AgentOrderView.check_agent_service_package:errLine:{}, errMsg:{}' .format(e.__traceback__.tb_lineno, repr(e))) return False @classmethod def save_agent_package(cls, order_id, serial_number, ac_id, package_id): """ 保存代理套餐 """ try: order_qs = Order_Model.objects.filter(orderID=order_id, status=1).values('price', 'payTime', 'order_type') if not order_qs.exists(): LOGGER.info(f'******save_agent_package当前代理客户未添加此套餐******ac_id:{ac_id},package_id:{package_id}') return order_type = order_qs[0]['order_type'] package_type = 2 if order_type in [2, 3, 5] else 1 # 判断订单信息是云存还是4G package_id = int(package_id) agent_package_qs = AgentCloudServicePackage.objects.filter(type=package_type, package_id=package_id, status=1) if not agent_package_qs.exists(): LOGGER.info(f'******save_agent_package当前套餐未设置代理******order_id:{order_id},serial_number:{serial_number}') return agent_package = agent_package_qs.first() # 代理云服务套餐 LOGGER.info(f'******save_agent_package代理套餐******service_name:{agent_package_qs.first().service_name}') acp_qs = AgentCustomerPackage.objects.filter(ac_id=ac_id, cs_id=agent_package.id).values('id') if not acp_qs.exists(): LOGGER.info(f'******save_agent_package当前代理客户未添加此套餐******ac_id:{ac_id},package_id:{package_id}') return # 组装数据 now_time = int(time.time()) pay_price = Decimal(order_qs[0]['price']).quantize(Decimal('0.00')) profit = cls.calculate_order_profit(agent_package, pay_price) dict_data = {'ac_id': ac_id, 'serial_number': serial_number, 'csp_id': agent_package.id, 'order_id': order_id, 'status': 1, 'profit_amount': pay_price, 'profit': profit, 'pay_time': order_qs[0]['payTime'], 'created_time': now_time, 'updated_time': now_time} agent_order_obj = AgentDeviceOrder.objects.create(**dict_data) # 保存分期结算记录 cls.save_order_installment(agent_order_obj.id, package_type, package_id, profit, ac_id) LOGGER.info(f'******save_agent_package代理订单存表结束:{dict_data}') except Exception as e: LOGGER.info('*****AgentOrderView.save_agent_package:errLine:{}, errMsg:{}' .format(e.__traceback__.tb_lineno, repr(e))) @classmethod def calculate_order_profit(cls, agent_package, price): """ 计算利润 @param agent_package: 套餐配置 @param price: 支付价格 @return: 利润 """ profit = 0 price = Decimal(price).quantize(Decimal('0.00')) if agent_package.profit_type == 1: profit = agent_package.profit elif agent_package.profit_type == 2: profit_value = Decimal(agent_package.profit).quantize(Decimal('0.00')) cost = Decimal(agent_package.cost).quantize(Decimal('0.00')) profit = (price - cost) * (profit_value / 100) profit = profit.quantize(Decimal('0.00')) return profit @classmethod def get_settlement_interval(cls, package_type, package_id): try: if package_type == 1: # 云存 store_qs = Store_Meal.objects.filter(id=package_id).values('day', 'bucket_id', 'expire', 'icloud_store_meal_id') if not store_qs.exists(): return [] # 根据套餐周期计算往后每个月26号作为结算时间 return cls.get_future_timestamps(store_qs[0]['expire']) elif package_type == 2: # 4G combo4g_qs = UnicomCombo.objects.filter(id=package_id).values('expiration_days', 'expiration_type') if not combo4g_qs.exists(): return [] # 目前4G套餐都是基于按天类型创建 if combo4g_qs[0]['expiration_type'] == 0 and combo4g_qs[0]['expiration_days'] > 0: months = int(combo4g_qs[0]['expiration_days'] / 30) # 根据套餐周期计算往后每个月26号作为结算时间 return cls.get_future_timestamps(months) except Exception as e: LOGGER.info('*****AgentOrderView.get_settlement_interval:errLine:{}, errMsg:{}' .format(e.__traceback__.tb_lineno, repr(e))) return [] @staticmethod def get_future_timestamps(months): """ 生成未来若干个月的第一个月的26号11点的timestamp列表。 参数: months -- 未来需要生成timestamp的月份数量 返回值: timestamps -- 包含未来months个月第一个月的26号11点的timestamp的列表 """ current_time = datetime.now() # 获取当前时间,注意这会是系统当前时区的时间 current_month = current_time.month current_year = current_time.year timestamps = [] for _ in range(months): # 如果当前月已经是需要生成的月份,则年份和月份不变 if current_month == 1 and _ == 0: next_year = current_year next_month = current_month + 1 else: # 计算下一个月的年和月 if current_month == 12: next_month = 1 next_year = current_year + 1 else: next_month = current_month + 1 next_year = current_year # 生成下个月的26号11点的时间点 next_date = datetime(next_year, next_month, 26, 11, 0, 0) # 如果生成的日期超过了当月的实际天数(比如2月没有26号),则需要回退到当月的最后一天 last_day_of_month = (datetime(next_year, next_month, 1) + timedelta(days=31)).replace(day=1) - timedelta( days=1) if next_date > last_day_of_month: next_date = last_day_of_month.replace(hour=11, minute=0, second=0, microsecond=0) timestamps.append(int(next_date.timestamp())) # 更新当前月份和年份为下一次循环使用 current_month = next_month current_year = next_year return timestamps @staticmethod def distribute_commission(commission, periods): # 转换佣金和期数为Decimal类型,并设置精度 commission = Decimal(str(commission)).quantize(Decimal('0.01')) periods = Decimal(periods) # 每期基础金额(向下取整到最接近的0.01) base_amount = (commission / periods).quantize(Decimal('0.01'), rounding=ROUND_DOWN) # 初始化每期分配的金额列表 distributed_amounts = [base_amount] * int(periods) # 计算按照基础金额分配后的总和 total_distributed = sum(distributed_amounts) # 计算剩余需要分配的金额 remainder = commission - total_distributed # 分配剩余金额 if remainder > Decimal('0'): # 从第一期开始分配剩余金额 for i in range(len(distributed_amounts)): if remainder >= Decimal('0.01'): distributed_amounts[i] += Decimal('0.01') remainder -= Decimal('0.01') else: # 如果剩余金额不足0.01,则将其全部加到当前期 distributed_amounts[i] += remainder break return distributed_amounts @classmethod def save_order_installment(cls, agent_order_id, package_type, package_id, profit, ac_id=None): """ 保存代理订单分期信息 :param cls: 类方法的约定参数 :param agent_order_id: 代理订单ID :param package_type: 套餐类型 :param package_id: 套餐ID :param profit: 利润总额 :return: 无返回值 :param ac_id: 代理客户ID """ try: # 根据包裹类型和ID获取结算时间间隔列表 time_list = cls.get_settlement_interval(package_type, package_id) period_number = len(time_list) # 计算分期总数 # 输入合理性检查 if period_number == 0 or profit <= 0: LOGGER.info(f'Invalid input parameters: period_number={period_number}, profit={profit}') return n_time = int(datetime.now().timestamp()) # 获取当前时间戳 # 利润总额按分期数平均分配 amount_list = cls.distribute_commission(profit, period_number) installment_list = [] # 遍历分期数,生成分期记录列表 for time_point in range(period_number): installment_list.append(AgentDeviceOrderInstallment(ado_id=agent_order_id, period_number=period_number, ac_id=ac_id, amount=amount_list[time_point], due_date=time_list[time_point], status=1, created_time=n_time, updated_time=n_time)) # 分批处理大量数据,避免数据库压力过大 batch_size = 100 for i in range(0, len(installment_list), batch_size): AgentDeviceOrderInstallment.objects.bulk_create( installment_list[i:i + batch_size] ) # 记录创建完成的日志 LOGGER.info(f'*****AgentOrderView.save_OrderInstallment分期结算记录创建完成:{len(installment_list)} records') except Exception as e: # 记录异常信息 LOGGER.error('*****AgentOrderView.save_OrderInstallment:errLine:{}, errMsg:{}' .format(e.__traceback__.tb_lineno, repr(e))) @staticmethod def update_periodic_settlement(): """ 更新周期结算信息 返回值: - 无返回值 """ try: # 根据条件查询需要更新结算信息的订单分期记录 adoi_qs = AgentDeviceOrderInstallment.objects.filter(status=1, due_date__lte=int(time.time())) if not adoi_qs: # 如果没有找到符合条件的记录,直接返回 return ids = [] a_account_list = [] adoi_set = set() n_time = int(time.time()) for item in adoi_qs: # 准备分期记录的id列表和账户记录列表 ids.append(item.id) adoi_set.add(item.ado_id) a_account_list.append(AgentAccount(ac_id=item.ac_id, amount=item.amount, remark=f'周期结算', status=1, created_time=n_time, updated_time=n_time)) batch_size = 100 # 分批更新分期记录状态 for i in range(0, len(ids), batch_size): AgentDeviceOrderInstallment.objects.filter(id__in=ids[i:i + batch_size]) \ .update(status=2, settlement_time=n_time, updated_time=n_time) # 分批创建账户记录 for i in range(0, len(a_account_list), batch_size): AgentAccount.objects.bulk_create(a_account_list[i:i + batch_size]) # 检查是否所有分期都已结算,如果是,则更新订单状态为已结算 for ado in adoi_set: adoi_qs = AgentDeviceOrderInstallment.objects.filter(ado_id=ado, status=1) if not adoi_qs.exists(): AgentDeviceOrder.objects.filter(id=ado, status=1) \ .update(status=2, settlement_time=n_time, updated_time=n_time) except Exception as e: # 记录异常信息 LOGGER.error( f'*****AgentOrderView.update_periodic_settlement:errLine:{e.__traceback__.tb_lineno}, errMsg:{str(e)}')