Jelajahi Sumber

运营商代理订单优化

zhangdongming 1 bulan lalu
induk
melakukan
c727931456

+ 1 - 1
AdminController/CloudServiceManage/AgentCustomerController.py

@@ -630,7 +630,7 @@ class AgentCustomerView(View):
                 end_month = start_month + 2
 
                 start_date = datetime(year, start_month, 1)
-                end_date = datetime(year + 1, 1, 1) if end_month == 12 else datetime(year, end_month + 1, 1)
+                end_date = datetime(year + 1, 1, 2) if end_month == 12 else datetime(year, end_month + 1, 1)
 
                 start_time = int(start_date.timestamp())
                 end_time = int(end_date.timestamp()) - 1

+ 1 - 1
AdminController/CloudServiceManage/AgentDeviceController.py

@@ -286,7 +286,7 @@ class AgentDeviceView(View):
                 ).values_list('serial_number', flat=True))
 
                 for row in unique_serial_numbers:
-                    serial_number = row.get('serial_number')
+                    serial_number = row
                     if serial_number not in existing:
                         device = AgentDevice(
                             ac_id=ac_id,

+ 260 - 129
AdminController/CloudServiceManage/AgentOrderController.py

@@ -87,7 +87,7 @@ class AgentOrderView(View):
             return True
         except Exception as e:
             LOGGER.error('*****支付成功保存云服务代理订单异常orderID:{},errLine:{}, errMsg:{}'
-                        .format(order_id, e.__traceback__.tb_lineno, repr(e)))
+                         .format(order_id, e.__traceback__.tb_lineno, repr(e)))
         return False
 
     @classmethod
@@ -98,7 +98,8 @@ class AgentOrderView(View):
         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}')
+                LOGGER.info(
+                    f'******save_agent_package当前代理客户未添加此套餐******ac_id:{ac_id},package_id:{package_id}')
                 return
 
             order_type = order_qs[0]['order_type']
@@ -109,14 +110,16 @@ class AgentOrderView(View):
             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}')
+                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}')
+                LOGGER.info(
+                    f'******save_agent_package当前代理客户未添加此套餐******ac_id:{ac_id},package_id:{package_id}')
                 return
 
             # 组装数据
@@ -131,7 +134,8 @@ class AgentOrderView(View):
             agent_order_obj = AgentDeviceOrder.objects.create(**dict_data)
 
             # 保存分期结算记录
-            cls.save_order_installment(agent_order_obj.id, package_type, package_id, profit, ac_id)
+            cls.save_order_installment(agent_order_obj.id, package_type, package_id, profit, ac_id,
+                                       order_qs[0]['payTime'])
 
             LOGGER.info(f'******save_agent_package代理订单存表结束:{dict_data}')
         except Exception as e:
@@ -158,165 +162,292 @@ class AgentOrderView(View):
         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 []
+    def get_quarterly_settlement_dates(cls, start_date, months):
+        """
+        获取季度结算日期列表,按照以下规则:
+        1. 固定在四个季度结算日期(1月1日、4月1日、7月1日、10月1日)进行结算
+        2. 从购买时间到结算日不满1个月的不在当前季度结算,累积到下一个季度
+        3. 中间季度每季度计算3个月
+        4. 最后一个季度计算剩余的时间
+
+        :param start_date: 套餐开始日期(datetime)
+        :param months: 套餐总月数
+        :return: 包含(结算日期timestamp, 该季度使用月数)的元组列表
+        """
+        # 固定的季度结算日期
+        QUARTER_DATES = [(1, 1), (4, 1), (7, 1), (10, 1)]  # (月, 日)
+
+        # 计算套餐结束日期
+        end_date = start_date + timedelta(days=int(months * 30.5))
+
+        # 初始化结果列表
+        result = []
+
+        # 找到开始日期后的第一个季度结算日
+        current_year = start_date.year
+        current_quarter_idx = 0
+
+        # 找到开始日期之后的第一个季度结算日
+        for i, (month, day) in enumerate(QUARTER_DATES):
+            quarter_date = datetime(current_year, month, day)
+            if quarter_date > start_date:
+                current_quarter_idx = i
+                break
+        else:
+            # 如果当年没有更多季度结算日,则移到下一年的第一个季度结算日
+            current_year += 1
+            current_quarter_idx = 0
+
+        # 第一个季度的结算日期
+        month, day = QUARTER_DATES[current_quarter_idx]
+        first_settlement_date = datetime(current_year, month, day)
+
+        # 计算第一个季度的整月数
+        days_in_first_quarter = (first_settlement_date - start_date).days
+        whole_months_first_quarter = int(days_in_first_quarter / 30.5)
+
+        # 计算剩余的月数(总月数减去第一个季度的整月数,如果第一个季度有整月数)
+        remaining_months = months
+
+        # 如果第一个季度有整月数,则添加第一个季度的结算记录并减去已结算的月数
+        if whole_months_first_quarter >= 1:
+            result.append((int(first_settlement_date.timestamp()), whole_months_first_quarter))
+            remaining_months -= whole_months_first_quarter
+        else:
+            # 即使不足1个月,也添加第一个季度的结算记录,但月数为0
+            # 这样可以确保在7月1日进行第一次结算
+            result.append((int(first_settlement_date.timestamp()), 0))
+
+        # 如果没有剩余月数,直接返回结果
+        if remaining_months <= 0:
+            return result
+
+        # 特殊处理年套餐(12个月)的情况
+        if months == 12 and whole_months_first_quarter == 0:
+            # 确保总共有5次季度结算,最后一次是剩余的月数
+            # 第一次结算已经添加(7月1日,整月数为0)
+
+            # 添加第二次结算(10月1日,整月数为3)
+            current_quarter_idx = (current_quarter_idx + 1) % len(QUARTER_DATES)
+            if current_quarter_idx == 0:
+                current_year += 1
+            month, day = QUARTER_DATES[current_quarter_idx]
+            settlement_date = datetime(current_year, month, day)
+            result.append((int(settlement_date.timestamp()), 3))
+
+            # 添加第三次结算(1月1日,整月数为3)
+            current_quarter_idx = (current_quarter_idx + 1) % len(QUARTER_DATES)
+            if current_quarter_idx == 0:
+                current_year += 1
+            month, day = QUARTER_DATES[current_quarter_idx]
+            settlement_date = datetime(current_year, month, day)
+            result.append((int(settlement_date.timestamp()), 3))
+
+            # 添加第四次结算(4月1日,整月数为3)
+            current_quarter_idx = (current_quarter_idx + 1) % len(QUARTER_DATES)
+            if current_quarter_idx == 0:
+                current_year += 1
+            month, day = QUARTER_DATES[current_quarter_idx]
+            settlement_date = datetime(current_year, month, day)
+            result.append((int(settlement_date.timestamp()), 3))
+
+            # 添加第五次结算(7月1日,整月数为剩余的月数)
+            current_quarter_idx = (current_quarter_idx + 1) % len(QUARTER_DATES)
+            if current_quarter_idx == 0:
+                current_year += 1
+            month, day = QUARTER_DATES[current_quarter_idx]
+            settlement_date = datetime(current_year, month, day)
+            # 剩余的月数为12减去前面已经结算的月数
+            remaining = 12 - (0 + 3 + 3 + 3)
+            result.append((int(settlement_date.timestamp()), remaining))
+
+            return result
+
+        # 非年套餐的处理逻辑
+        # 计算完整季度的数量(每季度3个月)
+        full_quarters = int(remaining_months / 3)
+
+        # 计算最后一个季度的剩余月数
+        last_quarter_months = remaining_months % 3
+
+        # 当前日期设置为第一个结算日期
+        current_date = first_settlement_date
+
+        # 添加完整季度的结算记录
+        for _ in range(full_quarters):
+            # 移到下一个季度结算日
+            current_quarter_idx = (current_quarter_idx + 1) % len(QUARTER_DATES)
+            if current_quarter_idx == 0:
+                current_year += 1
+
+            month, day = QUARTER_DATES[current_quarter_idx]
+            settlement_date = datetime(current_year, month, day)
+
+            # 添加完整季度的结算记录(3个月)
+            result.append((int(settlement_date.timestamp()), 3))
+
+            # 更新当前日期
+            current_date = settlement_date
+
+        # 如果有剩余月数,添加最后一个季度的结算记录
+        if last_quarter_months > 0:
+            # 移到下一个季度结算日
+            current_quarter_idx = (current_quarter_idx + 1) % len(QUARTER_DATES)
+            if current_quarter_idx == 0:
+                current_year += 1
+
+            month, day = QUARTER_DATES[current_quarter_idx]
+            settlement_date = datetime(current_year, month, day)
+
+            # 添加最后一个季度的结算记录(剩余月数)
+            result.append((int(settlement_date.timestamp()), last_quarter_months))
+
+        return result
 
     @staticmethod
-    def get_future_timestamps(months):
+    def calculate_months_in_period(start_date, end_date):
         """
-        生成未来若干个月的第一个月的26号11点的timestamp列表。
-
-        参数:
-        months -- 未来需要生成timestamp的月份数量
+        计算两个日期之间的整月数,不足一个月不计入
 
-        返回值:
-        timestamps -- 包含未来months个月第一个月的26号11点的timestamp的列表
+        :param start_date: 开始日期
+        :param end_date: 结束日期
+        :return: 整月数
         """
-        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)
+        # 计算天数差
+        days_diff = (end_date - start_date).days
 
-            # 如果生成的日期超过了当月的实际天数(比如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)
+        # 转换为整月数(按平均每月30.5天计算)
+        whole_months = int(days_diff / 30.5)
 
-            timestamps.append(int(next_date.timestamp()))
+        return whole_months
 
-            # 更新当前月份和年份为下一次循环使用
-            current_month = next_month
-            current_year = next_year
+    @staticmethod
+    def calculate_quarterly_profit(profit, settlement_dates_with_months):
+        """
+        计算季度利润分配,基于整月数
 
-        return timestamps
+        :param profit: 总利润
+        :param settlement_dates_with_months: 包含(结算日期, 整月数)的元组列表
+        :return: 每个季度的利润列表
+        """
+        profit = Decimal(str(profit)).quantize(Decimal('0.01'))
 
-    @staticmethod
-    def distribute_commission(commission, periods):
-        # 转换佣金和期数为Decimal类型,并设置精度
-        commission = Decimal(str(commission)).quantize(Decimal('0.01'))
-        periods = Decimal(periods)
+        # 计算总整月数
+        total_months = sum(months for _, months in settlement_dates_with_months)
 
-        # 每期基础金额(向下取整到最接近的0.01)
-        base_amount = (commission / periods).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
+        # 如果总月数为0,返回空列表
+        if total_months == 0:
+            return []
 
-        # 初始化每期分配的金额列表
-        distributed_amounts = [base_amount] * int(periods)
+        # 计算每月利润
+        monthly_profit = profit / Decimal(total_months)
 
-        # 计算按照基础金额分配后的总和
-        total_distributed = sum(distributed_amounts)
+        # 计算每个季度的利润
+        quarterly_amounts = []
+        for _, months in settlement_dates_with_months:
+            # 计算当前季度的利润(基于整月数)
+            amount = (monthly_profit * Decimal(months)).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
+            quarterly_amounts.append(amount)
 
-        # 计算剩余需要分配的金额
-        remainder = commission - total_distributed
+        # 处理舍入误差,确保总和等于总利润
+        total_allocated = sum(quarterly_amounts)
+        remainder = profit - total_allocated
 
-        # 分配剩余金额
-        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
+        # 将剩余的分配到第一个季度
+        if remainder > Decimal('0') and quarterly_amounts:
+            quarterly_amounts[0] += remainder
 
-        return distributed_amounts
+        return quarterly_amounts
 
     @classmethod
-    def save_order_installment(cls, agent_order_id, package_type, package_id, profit, ac_id=None):
+    def save_order_installment(cls, agent_order_id, package_type, package_id, profit, ac_id=None, pay_time=0):
         """
-        保存代理订单分期信息
+        保存代理订单分期信息(季度结算逻辑),不满一个月的时间累积到下一个季度
 
         :param cls: 类方法的约定参数
         :param agent_order_id: 代理订单ID
-        :param package_type: 套餐类型
+        :param package_type: 套餐类型(1:云存, 2:4G)
         :param package_id: 套餐ID
         :param profit: 利润总额
-        :return: 无返回值
         :param ac_id: 代理客户ID
+        :param pay_time: 订单支付时间
+        :return: 无返回值
         """
         try:
-            # 根据包裹类型和ID获取结算时间间隔列表
-            time_list = cls.get_settlement_interval(package_type, package_id)
-            period_number = len(time_list)  # 计算分期总数
+            # 转换支付时间为datetime对象
+            pay_time_dt = datetime.fromtimestamp(pay_time)
+
+            # 获取套餐月数
+            if package_type == 1:  # 云存
+                store = Store_Meal.objects.filter(id=package_id).first()
+                if not store:
+                    LOGGER.info(f'云存套餐不存在: {package_id}')
+                    return
+                months = store.expire
+            else:  # 4G
+                combo = UnicomCombo.objects.filter(id=package_id).first()
+                if not combo:
+                    LOGGER.info(f'4G套餐不存在: {package_id}')
+                    return
+                months = int(combo.expiration_days / 30)
+
+            if months <= 0 or profit <= 0:
+                LOGGER.info(f'无效参数: months={months}, profit={profit}')
+                return
 
-            # 输入合理性检查
-            if period_number == 0 or profit <= 0:
-                LOGGER.info(f'Invalid input parameters: period_number={period_number}, profit={profit}')
+            LOGGER.info(
+                f'开始计算季度结算: 订单ID={agent_order_id}, 开始日期={pay_time_dt}, 套餐月数={months}, 总利润={profit}')
+
+            # 获取季度结算日期和每个季度的整月数
+            settlement_dates_with_months = cls.get_quarterly_settlement_dates(pay_time_dt, months)
+
+            # 记录季度结算日期和月数
+            for i, (date, months_used) in enumerate(settlement_dates_with_months):
+                date_str = datetime.fromtimestamp(date).strftime('%Y-%m-%d')
+                LOGGER.info(f'季度{i + 1}结算日期: {date_str}, 整月数: {months_used}')
+
+            # 如果没有有效的结算日期,则退出
+            if not settlement_dates_with_months:
+                LOGGER.info(f'没有有效的季度结算日期: start_date={pay_time_dt}, months={months}')
                 return
 
-            n_time = int(datetime.now().timestamp())  # 获取当前时间戳
-            # 利润总额按分期数平均分配
-            amount_list = cls.distribute_commission(profit, period_number)
+            # 计算每个季度的利润分配
+            amounts = cls.calculate_quarterly_profit(profit, settlement_dates_with_months)
+
+            # 记录每个季度的利润分配
+            for i, amount in enumerate(amounts):
+                LOGGER.info(f'季度{i + 1}利润分配: {amount}')
 
+            # 创建分期记录
+            n_time = int(time.time())
             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')
+            for i, ((settlement_date, months_used), amount) in enumerate(zip(settlement_dates_with_months, amounts)):
+                # 只有使用满一个月才创建结算记录
+                if months_used >= 1:
+                    installment_list.append(AgentDeviceOrderInstallment(
+                        ado_id=agent_order_id,
+                        period_number=len(settlement_dates_with_months),
+                        ac_id=ac_id,
+                        amount=amount,
+                        due_date=settlement_date,
+                        status=1,
+                        created_time=n_time,
+                        updated_time=n_time
+                    ))
+
+            # 批量创建
+            if installment_list:
+                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'季度分期结算记录创建完成: {len(installment_list)}条, 订单ID: {agent_order_id}')
+            else:
+                LOGGER.info(f'没有创建季度分期结算记录: 订单ID: {agent_order_id}')
 
         except Exception as e:
-            # 记录异常信息
-            LOGGER.error('*****AgentOrderView.save_OrderInstallment:errLine:{}, errMsg:{}'
-                         .format(e.__traceback__.tb_lineno, repr(e)))
+            LOGGER.error(f'保存季度分期结算记录异常: 行号:{e.__traceback__.tb_lineno}, 错误:{repr(e)}')
 
     @staticmethod
     def update_periodic_settlement():