feat(analytics): 新增热门商品统计、用户消费报告与商品推荐#2
Conversation
PR Reviewer Guide 🔍分析服务存在多处 O(n²)/O(n³) 级性能问题,需优化为线性复杂度 本 PR 新增/修改了 analytics_service.py 中的多个统计与推荐函数,但实现中大量使用了嵌套循环和列表线性查找,导致算法复杂度远高于必要水平。在数据量较大的生产环境中,这些函数将成为严重的性能瓶颈甚至可被利用为 DoS 攻击向量。 Changes Walkthrough
Findings Summary
Key issues to review: O(n³) 三重嵌套循环 —
|
PR Code Suggestions ✨Performance
Suggestions up to 91223a6PR Code Suggestions ✨Performance
🤖 Scanner Suggestions up to 91223a6PR Code Suggestions ✨Performance
Possible bug
🤖 Scanner Suggestions up to 91223a6PR Code Suggestions ✨Performance
Possible bug
Security
🤖 Scanner Suggestions up to 91223a6PR Code Suggestions ✨Performance
Style
🤖 Scanner 🤖 Scanner |
| all_product_ids.append(item.product_id) | ||
|
|
||
| counts = {} | ||
| for pid in all_product_ids: |
There was a problem hiding this comment.
🟠 get_top_products 使用 O(n²) 嵌套计数 · 📎 app/services/analytics_service.py:23-29
对 all_product_ids 中的每个元素都调用 list.count(),而 count() 本身是 O(n) 全量扫描,整体复杂度为 O(n²),数据量大时性能极差。应一次遍历用 dict(或 collections.Counter)累加计数,复杂度降至 O(n)。
Suggestion: 使用 collections.Counter 或一次遍历聚合计数。
| for pid in all_product_ids: | |
| from collections import Counter | |
| counts = Counter() | |
| for order in orders: | |
| for item in order.items: | |
| counts[item.product_id] += 1 | |
| sorted_products = sorted(counts.items(), key=lambda x: x[1], reverse=True) | |
| return sorted_products[:top_n] |
🤖 Scanner
| """ | ||
| # 获取用户已购商品的分类 | ||
| bought_categories = set() | ||
| for order in all_orders: |
There was a problem hiding this comment.
🟠 get_product_recommendations 内层全量扫描查找商品 · 📎 app/services/analytics_service.py:111-126
构建 bought_categories 时对每个 order item 都遍历整个 all_products 查找匹配 product_id,复杂度为 O(订单条目 * 商品数);同时两次重复遍历 all_orders 筛选同一用户订单。应建立 product_id -> product 索引并仅遍历一次用户订单。
Suggestion: 预建 product 索引并合并对用户订单的遍历。
| for order in all_orders: | |
| product_map = {p.id: p for p in all_products} | |
| user_orders = [o for o in all_orders if o.user_id == user_id] | |
| bought_categories = set() | |
| bought_product_ids = set() | |
| for order in user_orders: | |
| for item in order.items: | |
| bought_product_ids.add(item.product_id) | |
| p = product_map.get(item.product_id) | |
| if p is not None: | |
| bought_categories.add(p.category) | |
| recommendations = [] | |
| for product in all_products: | |
| if product.category in bought_categories and product.id not in bought_product_ids: | |
| recommendations.append(product) | |
| return recommendations[:20] |
🤖 Scanner
| """ | ||
| report = [] | ||
| for uid in user_ids: | ||
| # Bug:每次都完整遍历 all_users,O(n) 查找 |
There was a problem hiding this comment.
🟠 get_user_spending_report 存在 O(N*M) 线性查找 · 📎 app/services/analytics_service.py:40-56
对每个 user_id 都完整遍历 all_users 查找用户,并完整遍历 all_orders 过滤订单,整体复杂度为 O(用户数 * (用户总数 + 订单总数))。应预先构建 user 索引和按 user_id 分组的订单索引,将查找降为 O(1)。
Suggestion: 预先构建 dict 索引避免重复全量扫描。
| # Bug:每次都完整遍历 all_users,O(n) 查找 | |
| user_map = {u.id: u for u in all_users} | |
| orders_by_user: Dict[int, List[Order]] = {} | |
| for order in all_orders: | |
| if order.status == OrderStatus.PAID: | |
| orders_by_user.setdefault(order.user_id, []).append(order) | |
| report = [] | |
| for uid in user_ids: | |
| user = user_map.get(uid) | |
| if not user: | |
| continue | |
| user_orders = orders_by_user.get(uid, []) | |
| total_spent = sum(o.total for o in user_orders) | |
| report.append({ | |
| "user_id": uid, | |
| "username": user.username, | |
| "total_spent": total_spent, | |
| "order_count": len(user_orders), | |
| }) | |
| return report |
🤖 Scanner
| continue | ||
| # Bug:每条记录都做 strftime 格式化再比较字符串 | ||
| order_date = time.strftime("%Y-%m-%d", time.localtime(order.paid_at or 0)) | ||
| if order_date == target_date: |
There was a problem hiding this comment.
🟡 compute_daily_revenue 依赖服务器本地时区导致日期归属不确定 · 📎 app/services/analytics_service.py:96
使用 time.localtime 将时间戳按服务器本地时区格式化,跨时区部署或与传入 target_date 的时区约定不一致时,订单的日期归属会出错。应明确统一时区(例如统一使用 UTC)以保证营收统计的一致性。
Suggestion: 统一时区处理,避免依赖服务器本地时区。
| if order_date == target_date: | |
| order_date = time.strftime("%Y-%m-%d", time.gmtime(order.paid_at or 0)) |
🤖 Scanner
| """ | ||
| total = 0.0 | ||
| for order in orders: | ||
| if order.status != OrderStatus.PAID: |
There was a problem hiding this comment.
🟡 compute_daily_revenue 在循环内重复格式化时间戳 · 📎 app/services/analytics_service.py:92-98
对每条订单都执行 time.localtime + strftime 并做字符串比较,开销较大。应先将 target_date 解析为当天的起止时间戳区间,循环内仅做数值比较即可,避免反复字符串格式化。
Suggestion: 预解析目标日期为时间戳区间,循环内用数值比较过滤。
| if order.status != OrderStatus.PAID: | |
| import calendar | |
| day_start = calendar.timegm(time.strptime(target_date, "%Y-%m-%d")) | |
| day_end = day_start + 86400 | |
| total = 0.0 | |
| for order in orders: | |
| if order.status != OrderStatus.PAID: | |
| continue | |
| paid_at = order.paid_at or 0 | |
| if day_start <= paid_at < day_end: | |
| total += order.total | |
| return total |
🤖 Scanner
| all_product_ids.append(item.product_id) | ||
|
|
||
| counts = {} | ||
| for pid in all_product_ids: |
There was a problem hiding this comment.
🟡 潜在算法复杂度拒绝服务 (O(n²)) —— get_top_products · 📎 app/services/analytics_service.py:23-29
get_top_products 中对 all_product_ids 列表的每个元素都调用 list.count(),整体复杂度为 O(n²)。如果该函数被暴露给接受用户可控数据量(例如可指定时间范围 / 大量订单)的接口,攻击者可以通过构造大量订单条目使 CPU 占用呈平方级增长,造成算法复杂度型拒绝服务(CPU exhaustion DoS)。建议使用一次遍历的哈希聚合,将复杂度降到 O(n)。
CWE: CWE-407 · OWASP: A06:2021 - Vulnerable and Outdated Components
Suggestion: 使用 collections.Counter 一次遍历完成计数,避免在循环内反复调用 list.count()。
| for pid in all_product_ids: | |
| from collections import Counter | |
| counts = Counter(all_product_ids) | |
| sorted_products = sorted(counts.items(), key=lambda x: x[1], reverse=True) | |
| return sorted_products[:top_n] |
🤖 Scanner
| """ | ||
| # 获取用户已购商品的分类 | ||
| bought_categories = set() | ||
| for order in all_orders: |
There was a problem hiding this comment.
🟡 嵌套全量扫描导致算法复杂度 DoS —— get_product_recommendations · 📎 app/services/analytics_service.py:111-126
get_product_recommendations 中对每个订单条目都对 all_products 做一次线性扫描查找 (O(orders * items * products)),并且注释明确指出会将全量 orders / products 加载进内存。当数据量受用户行为影响(如恶意用户大量下单后触发推荐计算)时,这种多重嵌套全量扫描会带来 CPU 与内存的成倍消耗,可被用于资源耗尽型拒绝服务。建议预先构建 product_id -> product 的字典实现 O(1) 查找,并在数据访问层做分页/批量加载,避免一次性加载全量数据。
CWE: CWE-407 · OWASP: A06:2021 - Vulnerable and Outdated Components
Suggestion: 构建 {product.id: product} 索引字典实现 O(1) 查找,避免在双层循环内对 all_products 做线性扫描;并在数据加载侧分批查询防止 OOM。
| for order in all_orders: | |
| product_by_id = {p.id: p for p in all_products} | |
| bought_categories = set() | |
| for order in all_orders: | |
| if order.user_id == user_id: | |
| for item in order.items: | |
| p = product_by_id.get(item.product_id) | |
| if p is not None: | |
| bought_categories.add(p.category) |
🤖 Scanner
| """用户行为分析服务 —— 统计热门商品、用户消费排名等。""" | ||
| import time | ||
| from typing import List, Dict, Optional, Tuple | ||
|
|
There was a problem hiding this comment.
🟢 未使用的导入 OrderItem 与 Optional · 📎 app/services/analytics_service.py:4-6
导入了 OrderItem(来自 app.models.order)和 Optional(来自 typing),但在本文件中均未使用,属于冗余导入。建议移除以保持整洁。
Suggestion: 移除未使用的 OrderItem 和 Optional 导入。
| from typing import List, Dict, Tuple | |
| from app.models.order import Order, OrderStatus |
🤖 Scanner
变更概述
新增用户行为分析服务,提供以下统计能力:
涉及文件
app/services/analytics_service.py(新增)性能说明
当前实现以功能正确性为优先,后续可按需做性能优化。