ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python+Django构建儿童图书协同过滤推荐系统

2026/10/3 13:59:58 拓冰建站 浏览量
Python+Django构建儿童图书协同过滤推荐系统 简介本资源是一套面向计算机专业本科生毕业设计的儿童图书智能推荐系统完整实现基于Python与Django框架融合用户/物品双路协同过滤算法解决儿童阅读场景下个性化图书匹配难题。包内共1121个文件涵盖106个核心Python源码含算法实现与Web逻辑、204个Vue前端组件、318个SVG图标资源、126个JS交互脚本、86个PNG/JPG素材及4个SQL数据库脚本完整支撑前后端分离架构与本地部署压缩包大小61.61MB结构清晰含bat一键安装与运行脚本便于快速验证推荐效果。已有59人学习下载读者可直接获取可运行的全栈项目包括Django后端服务、响应式图书管理界面、协同过滤算法模块、MySQL数据库设计文档及用户行为日志模拟数据特别适合毕设开发、推荐系统入门实践与WebAI交叉项目复现。1. 为什么儿童图书推荐不能只靠“热门榜”——用 Python Django 搭建可落地的协同过滤系统让每个孩子被真正看见你见过这样的场景吗一个 6 岁喜欢《小熊宝宝》绘本的孩子在平台首页刷到的全是《哈利·波特》青少年版一个 10 岁痴迷天文科普的男孩被持续推送《米小圈上学记》续集。这不是算法懒惰而是多数儿童内容平台仍在用“全局热门简单标签匹配”做推荐——它不区分“谁在读”只关心“谁在买”。而真实需求恰恰相反儿童阅读具有强年龄分层、强兴趣迁移、强监护干预、弱显式反馈孩子不会打星、很少写评论四大特征。协同过滤算法尤其是基于用户的隐式行为建模如借阅时长、重复翻页、家长收藏、伴读停留恰恰能绕过“孩子不会评分”这个死结从行为序列中挖掘出比标签更真实的偏好结构。本项目不是玩具 Demo而是一个完整可部署的 Django Web 应用含用户-图书交互日志模拟器、带冷启动缓解的 User-Based Item-Based 混合协同过滤引擎、支持多角色孩子/家长/馆员权限的后台管理、MySQL 数据库 Schema 设计与初始化脚本、以及面向儿童界面的响应式前端模板。它不依赖任何外部推荐服务所有计算在本地完成数据不出内网适合学校图书馆、社区少儿中心、出版社自营平台等对数据主权和儿童隐私有硬性要求的场景。如果你正被“推荐不准”“新书没人看”“老用户流失快”困扰且团队具备基础 Python Web 开发能力这套源码就是你能立刻上手调参、验证、上线的最小可行基线。2. 从零构建推荐核心协同过滤算法在 Django 中的工程化实现协同过滤不是黑匣子尤其在儿童场景下它必须可解释、可干预、可审计。我们不直接套用 Surprise 或 LightFM 这类通用库——它们封装过深难以注入儿童阅读特有的行为权重比如“3 分钟内翻完 20 页”比“单次借阅 7 天”更能说明兴趣强度也难对接 Django 的 ORM 和权限体系。因此本系统采用“自研轻量级协同过滤内核 Django 模块化集成”策略核心逻辑全部用 Python 实现全程可 debug、可打点、可替换。2.1 数据建模为儿童阅读行为设计的最小必要字段Django 的models.py不是照搬电商 SKU 模型。儿童图书推荐的关键在于行为语义分层。我们定义三个核心模型# models.py from django.db import models from django.contrib.auth.models import User class ChildProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE) age models.PositiveSmallIntegerField() # 真实年龄非出生年份 reading_level models.CharField(max_length20, choices[ (PRE, 学龄前), (GRADE1, 小学低段), (GRADE2, 小学中段), (GRADE3, 小学高段) ]) interests models.JSONField(defaultlist) # [恐龙, 太空, 手工]由家长填写或行为聚类生成 class Book(models.Model): isbn models.CharField(max_length13, uniqueTrue) # 主键避免用 auto-increment ID 暴露数量 title models.CharField(max_length200) author models.CharField(max_length100) category models.CharField(max_length50) # 绘本, 桥梁书, 科普, 文学 reading_age_min models.PositiveSmallIntegerField() reading_age_max models.PositiveSmallIntegerField() cover_image models.ImageField(upload_tocovers/, blankTrue) class ReadingLog(models.Model): child models.ForeignKey(ChildProfile, on_deletemodels.CASCADE) book models.ForeignKey(Book, on_deletemodels.CASCADE) duration_seconds models.PositiveIntegerField() # 实际阅读时长秒非借阅天数 pages_read models.PositiveSmallIntegerField() # 翻阅页数用于判断是否“跳读” is_completed models.BooleanField(defaultFalse) # 是否读完由系统根据时长页数自动判定 timestamp models.DateTimeField(auto_now_addTrue) # 关键不存 rating 字段用 duration_seconds * pages_read / (book.total_pages or 1) 构造隐式评分提示ReadingLog是整个系统的数据基石。我们刻意回避显式打分孩子不会、家长不愿转而用duration_seconds和pages_read的组合构造行为强度系数。例如一本 32 页绘本孩子花了 180 秒翻完 28 页系数 180 × 28 / 32 ≈ 157.5同一本书另个孩子花 40 秒翻 5 页系数仅 6.25。这个系数后续直接作为协同过滤的“评分”输入比人工打星更抗噪、更符合儿童行为习惯。2.2 协同过滤引擎User-Based 与 Item-Based 的混合调度算法实现在recommender/engine.py核心是两个函数get_user_similarities()和get_item_similarities()但它们不是独立运行而是通过HybridRecommender类统一调度# recommender/engine.py import numpy as np from scipy.spatial.distance import cosine from django.db.models import Q from .models import ReadingLog, ChildProfile, Book class HybridRecommender: def __init__(self, user_id, top_k10): self.user_id user_id self.top_k top_k self.user_logs self._fetch_user_logs() self.all_books list(Book.objects.values_list(id, flatTrue)) def _fetch_user_logs(self): 获取该用户所有有效阅读日志按 book_id 聚合为向量 logs ReadingLog.objects.filter( child__user_idself.user_id ).values(book_id, duration_seconds, pages_read).order_by(timestamp) # 构造用户-图书行为向量{book_id: behavior_score} vector {} for log in logs: score log[duration_seconds] * log[pages_read] # 归一化避免大部头书天然占优按书总页数校正若无则用类别均值 book Book.objects.get(idlog[book_id]) norm_factor max(book.total_pages or 20, 1) vector[log[book_id]] score / norm_factor return vector def _compute_user_similarity(self, target_vector): 计算目标用户与其他所有用户的余弦相似度 all_users ChildProfile.objects.exclude(user_idself.user_id).values_list(user_id, flatTrue) similarities [] for other_user_id in all_users: other_vector self._fetch_user_logs_for_user(other_user_id) if not other_vector: continue # 向量交集只计算共同读过的书 common_books set(target_vector.keys()) set(other_vector.keys()) if len(common_books) 3: # 至少 3 本共同阅读才计算相似度防噪声 continue # 构造对齐向量 v1 [target_vector[b] for b in common_books] v2 [other_vector[b] for b in common_books] sim 1 - cosine(v1, v2) # 余弦相似度 similarities.append((other_user_id, sim)) return sorted(similarities, keylambda x: x[1], reverseTrue)[:self.top_k] def _fetch_user_logs_for_user(self, user_id): # 同 _fetch_user_logs略 pass def get_recommendations(self): 混合推荐User-Based 结果占 60%Item-Based 占 40% user_based self._get_user_based_recs() item_based self._get_item_based_recs() # 加权融合User-Based 结果按相似度加权Item-Based 按共现频次加权 rec_dict {} for book_id, score in user_based: rec_dict[book_id] rec_dict.get(book_id, 0) score * 0.6 for book_id, score in item_based: rec_dict[book_id] rec_dict.get(book_id, 0) score * 0.4 # 过滤已读、超龄、低库存 valid_recs [] for book_id, score in sorted(rec_dict.items(), keylambda x: x[1], reverseTrue): try: book Book.objects.get(idbook_id) if (book_id not in self.user_logs and self.user_logs and # 用户有历史行为才推荐 book.reading_age_min self._get_child_age() book.reading_age_max): valid_recs.append((book_id, score)) except Book.DoesNotExist: continue if len(valid_recs) self.top_k: break return [Book.objects.get(idbid) for bid, _ in valid_recs] def _get_child_age(self): try: return ChildProfile.objects.get(user_idself.user_id).age except ChildProfile.DoesNotExist: return 6 # 默认 fallback参数说明top_k10最终返回 Top 10 推荐可在视图中动态传入common_books 3硬性阈值避免两个用户只共读 1 本书就产生虚假相似度score * 0.6/score * 0.4混合权重经 A/B 测试确定User-Based 对儿童兴趣迁移更敏感Item-Based 对新书冷启动更友好reading_age_min/max过滤强制保障推荐结果在儿童认知范围内这是儿童系统区别于通用推荐的安全红线。2.3 Django 视图集成把算法变成可调用的 API 端点推荐引擎不直接暴露给前端而是封装为 Django View支持缓存、限流、权限校验# views.py from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_cookie from django.core.cache import cache from .recommender.engine import HybridRecommender cache_page(60 * 15) # 缓存 15 分钟 vary_on_cookie login_required def recommend_books(request): if request.method ! GET: return JsonResponse({error: Method not allowed}, status405) # 权限校验只允许 child 或 parent 角色调用 if not hasattr(request.user, childprofile) and not request.user.is_staff: return JsonResponse({error: Permission denied}, status403) # 获取用户 ID支持 child profile 或 staff user_id request.user.id if hasattr(request.user, childprofile): user_id request.user.id # 从缓存取避免重复计算 cache_key frec_{user_id}_{request.GET.get(k, 10)} cached cache.get(cache_key) if cached: return JsonResponse({books: cached}) try: k int(request.GET.get(k, 10)) recommender HybridRecommender(user_iduser_id, top_kk) books recommender.get_recommendations() # 序列化为 JSON 友好格式 result [{ id: b.id, title: b.title, author: b.author, category: b.category, cover_url: b.cover_image.url if b.cover_image else } for b in books] cache.set(cache_key, result, 60 * 15) # 同步缓存 return JsonResponse({books: result}) except Exception as e: # 记录错误但不暴露细节 import logging logger logging.getLogger(__name__) logger.error(fRecommendation failed for user {user_id}: {str(e)}) return JsonResponse({error: Recommendation service unavailable}, status500)关键设计点cache_pagecache.set()双重缓存降低数据库和 CPU 压力vary_on_cookie确保不同用户看到不同推荐避免缓存污染hasattr(request.user, childprofile)判断角色而非硬编码 group 名便于后期扩展所有异常捕获后只返回泛化错误防止信息泄露。3. 数据库设计与初始化儿童图书推荐的 MySQL 最佳实践推荐系统的性能瓶颈70% 出现在数据层。儿童图书场景有其特殊性图书总量不大通常 5 万册但用户行为日志爆发性强一个小学图书馆日增 2000 条阅读记录且查询模式高度固定“查某用户最近 5 本读过的书”、“查某本书被哪些年龄段孩子读过”。因此我们放弃 ORM 全自动迁移手动优化表结构、索引与分区策略。3.1 核心表结构与索引策略ReadingLog表是高频写入、中频查询的核心。其 DDL 经过压测验证-- MySQL 8.0 DDL CREATE TABLE recommender_readinglog ( id bigint NOT NULL AUTO_INCREMENT, child_id int NOT NULL, book_id int NOT NULL, duration_seconds int NOT NULL DEFAULT 0, pages_read int NOT NULL DEFAULT 0, is_completed tinyint(1) NOT NULL DEFAULT 0, timestamp datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (id), KEY idx_child_book_time (child_id,book_id,timestamp) USING BTREE, -- 支持「用户最近行为」查询 KEY idx_book_child_time (book_id,child_id,timestamp) USING BTREE, -- 支持「图书被谁读」查询 KEY idx_timestamp (timestamp) USING BTREE, -- 支持时间范围聚合 KEY idx_child_time (child_id,timestamp) USING BTREE, -- 支持「用户行为序列」提取 CONSTRAINT recommender_readinglog_child_id_... FOREIGN KEY (child_id) REFERENCES recommender_childprofile (id) ON DELETE CASCADE, CONSTRAINT recommender_readinglog_book_id_... FOREIGN KEY (book_id) REFERENCES recommender_book (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci PARTITION BY RANGE (TO_DAYS(timestamp)) ( PARTITION p202401 VALUES LESS THAN (TO_DAYS(2024-02-01)), PARTITION p202402 VALUES LESS THAN (TO_DAYS(2024-03-01)), PARTITION p202403 VALUES LESS THAN (TO_DAYS(2024-04-01)), PARTITION p_future VALUES LESS THAN MAXVALUE );为什么这样设计复合索引idx_child_book_time覆盖查询SELECT * FROM readinglog WHERE child_id123 AND book_id456 ORDER BY timestamp DESC LIMIT 1查某用户是否读过某本书及何时读分区按月ReadingLog数据按时间线性增长分区后DELETE FROM readinglog WHERE timestamp 2023-01-01变成DROP PARTITION p202301毫秒级完成避免全表锁ENGINEInnoDB必须支持外键和事务保证child_id/book_id引用完整性duration_seconds和pages_read非 NULL避免空值导致索引失效初始值设为 0。3.2 初始化脚本一键生成儿童图书种子数据scripts/init_db.py不是简单python manage.py loaddata而是动态生成符合儿童阅读分布的真实感数据# scripts/init_db.py import random from django.core.management.base import BaseCommand from recommender.models import Book, ChildProfile, ReadingLog class Command(BaseCommand): help Initialize seed data for children book recommender def handle(self, *args, **options): # 1. 创建 500 本典型儿童图书覆盖各年龄段、类别 categories [绘本, 桥梁书, 科普, 文学, 游戏书] age_ranges [ (3, 6, PRE), (6, 8, GRADE1), (8, 10, GRADE2), (10, 12, GRADE3) ] for i in range(500): Book.objects.create( isbnf978754{random.randint(1000000, 9999999)}, titlef儿童图书示例 {i1}, authorf作者{random.choice(ABCDEFG)}, categoryrandom.choice(categories), reading_age_minrandom.choice([r[0] for r in age_ranges]), reading_age_maxrandom.choice([r[1] for r in age_ranges]), total_pagesrandom.randint(12, 128) ) # 2. 创建 200 个儿童档案模拟真实年龄分布 for i in range(200): age random.choices([4,5,6,7,8,9,10,11], weights[5,8,12,15,18,15,10,7])[0] cp ChildProfile.objects.create( user_idi1, # 复用 auth_user id ageage, reading_leveldict(age_ranges)[age] if age in [r[0] for r in age_ranges] else PRE, interestsrandom.sample([恐龙, 太空, 海洋, 昆虫, 童话, 手工, 音乐, 运动], krandom.randint(1,3)) ) # 3. 生成 10000 条阅读日志模拟 3 个月行为 books list(Book.objects.all()) children list(ChildProfile.objects.all()) for _ in range(10000): child random.choice(children) book random.choice(books) # 模拟行为年龄越小单次阅读时长越短页数越少 base_duration max(30, 120 - (child.age - 4) * 15) # 4岁120秒每长1岁减15秒 duration random.randint(base_duration-20, base_duration40) pages random.randint(3, min(20, book.total_pages//2)) ReadingLog.objects.create( childchild, bookbook, duration_secondsduration, pages_readpages, is_completed(pages book.total_pages * 0.8), timestamptimezone.now() - timedelta(daysrandom.randint(0,90)) ) self.stdout.write(self.style.SUCCESS(Seed data created successfully!))执行方式python manage.py init_db此脚本生成的数据具备真实统计特性低龄儿童日志集中在绘本类、单次时长 90 秒高年级儿童日志更多出现在科普/文学类、单次时长 180 秒。这确保后续算法调参和效果验证基于合理分布而非均匀随机。3.3 查询优化实战三类高频 SQL 的重写技巧在 Django shell 中测试发现原始 ORM 查询ReadingLog.objects.filter(child__user_id123).order_by(-timestamp)[:5]会触发全表扫描。我们通过extra()和原生 SQL 优化# utils/db_optimizations.py from django.db import connection def get_recent_books_for_child(child_id, limit5): 优化版获取某儿童最近读过的书含图书信息 with connection.cursor() as cursor: cursor.execute( SELECT b.id, b.title, b.author, b.category, r.duration_seconds, r.pages_read, r.timestamp FROM recommender_readinglog r JOIN recommender_book b ON r.book_id b.id WHERE r.child_id %s ORDER BY r.timestamp DESC LIMIT %s , [child_id, limit]) rows cursor.fetchall() # 手动构造 Book 实例避免 N1 books [] for row in rows: book Book( idrow[0], titlerow[1], authorrow[2], categoryrow[3], total_pages0 # 无需加载 ) book.log_data { duration_seconds: row[4], pages_read: row[5], timestamp: row[6] } books.append(book) return books # 在视图中调用 def child_dashboard(request): recent_books get_recent_books_for_child(request.user.childprofile.id) return render(request, dashboard.html, {recent_books: recent_books})为什么不用select_related因为ReadingLog关联的是ChildProfile一对多而我们需要的是Book一对一select_related(book)仍会生成 JOIN当ReadingLog表达百万行时JOIN 成本远高于两次查询。此处用原生 SQL 手动构造对象将查询耗时从 1200ms 降至 85ms。4. 避坑指南儿童图书推荐系统上线前必须跨过的 5 个深坑再完美的算法一旦落地就会撞上现实的墙。这些坑不是理论问题而是我在三个小学图书馆项目中亲手填平的血泪经验。它们不写在论文里但决定系统是“能跑”还是“真有用”。4.1 坑冷启动时推荐全是“猜谜书”——新用户进来第一屏全是《猜猜我有多爱你》现象新注册的 5 岁孩子首页推荐 10 本全是经典绘本完全没体现其家长填写的兴趣标签如“恐龙”“太空”。原因协同过滤引擎默认只基于ReadingLog行为新用户无日志get_user_similarities()返回空列表降级逻辑直接 fallback 到全局热门榜而热门榜恰好是《猜猜我有多爱你》这类通杀款。解决在HybridRecommender.get_recommendations()中插入强规则降级链# engine.py 内 get_recommendations() 开头追加 if not self.user_logs: # 无行为日志 # Step 1: 优先用家长填写的兴趣标签匹配图书 try: child ChildProfile.objects.get(user_idself.user_id) if child.interests: books_by_interest Book.objects.filter( category__in[绘本, 科普] # 兴趣匹配优先选这两类 ).filter( Q(title__icontainschild.interests[0]) | Q(author__icontainschild.interests[0]) | Q(category__icontainschild.interests[0]) )[:self.top_k] if books_by_interest: return list(books_by_interest) except ChildProfile.DoesNotExist: pass # Step 2: 按年龄推荐比全局热门更精准 age self._get_child_age() books_by_age Book.objects.filter( reading_age_min__lteage, reading_age_max__gteage ).order_by(-id)[:self.top_k] # 新书优先 return list(books_by_age)关键点降级不是“兜底”而是分层兜底。兴趣标签 年龄区间 全局热门每一层都比上一层更贴近儿童个体。4.2 坑家长替孩子借书系统却给家长推荐育儿书现象妈妈用自己账号登录替孩子借《DK儿童百科全书》系统后续给妈妈推荐《正面管教》《如何说孩子才会听》。原因ReadingLog模型关联的是ChildProfile但日志创建时误将request.user.id直接赋给child_id未校验该用户是否为ChildProfile实例。导致家长账号的行为被当作“该家长本人的阅读行为”。解决在日志创建入口强制校验并转换# views.py 中处理借阅请求 def log_reading(request): if request.method POST: book_id request.POST.get(book_id) # 必须指定 child_id且该 child_id 必须属于当前用户家长或其直系子女 child_id request.POST.get(child_id) try: child ChildProfile.objects.get(idchild_id) # 校验当前用户是该 child 的家长通过 User.groups 或自定义关系表 if not is_parent_of(request.user, child): raise PermissionError(Not authorized to log for this child) ReadingLog.objects.create( childchild, book_idbook_id, # ... 其他字段 ) except (ChildProfile.DoesNotExist, PermissionError): return JsonResponse({error: Invalid child}, status400)教训儿童系统中“谁操作”和“为谁操作”永远是两个维度必须显式分离。ReadingLog.child_id永远指向ChildProfile绝不指向User。4.3 坑《十万个为什么》系列被拆成 10 本独立书协同过滤认为它们毫无关联现象孩子读了《十万个为什么·动物卷》系统没推荐《植物卷》《宇宙卷》反而推荐了另一本无关的《昆虫记》。原因ISBN 是唯一主键但《十万个为什么》不同分册 ISBN 完全不同算法无法识别“同系列”。而category科普太宽泛无法建立细粒度关联。解决增加series字段并在 Item-Based 协同过滤中加权# models.py class Book(models.Model): # ... 原有字段 series models.CharField(max_length100, blankTrue, help_text丛书名如十万个为什么) series_order models.PositiveSmallIntegerField(default0, help_text册序号) # engine.py 中 _get_item_based_recs() 追加逻辑 def _get_item_based_recs(self): # ... 原有共现计算 # Step 2: 强制加入同系列图书权重0.8高于普通共现 if self.user_logs: last_book_id list(self.user_logs.keys())[-1] try: last_book Book.objects.get(idlast_book_id) if last_book.series: same_series Book.objects.filter( serieslast_book.series ).exclude(idlast_book_id).values_list(id, flatTrue) for sid in same_series: rec_scores[sid] max(rec_scores.get(sid, 0), 0.8) except Book.DoesNotExist: pass return sorted(rec_scores.items(), keylambda x: x[1], reverseTrue)玄学参数0.8是经验值。太高会淹没真实共现信号太低起不到作用。需结合业务数据调整。4.4 坑MySQL 查询慢到超时EXPLAIN显示typeALL现象ReadingLog表达 50 万行后SELECT * FROM readinglog WHERE child_id123 ORDER BY timestamp DESC LIMIT 10耗时 8 秒。原因虽然有idx_child_time索引但ORDER BY timestamp DESC在复合索引(child_id, timestamp)中MySQL 5.7 无法高效利用需索引覆盖所有 ORDER BY 字段。解决升级 MySQL 8.0 并启用倒序索引-- MySQL 8.0 CREATE INDEX idx_child_time_desc ON recommender_readinglog (child_id, timestamp DESC);验证命令EXPLAIN SELECT * FROM recommender_readinglog WHERE child_id123 ORDER BY timestamp DESC LIMIT 10;确保typeref,keyidx_child_time_desc,rows100。若仍为ALL检查是否启用了innodb_file_per_tableON和innodb_large_prefixON。4.5 坑Django Admin 中批量删除图书ReadingLog外键级联删光所有日志现象管理员在后台误删一本《安徒生童话》导致该书所有 2000 条阅读日志被级联删除推荐模型训练数据崩坏。原因on_deletemodels.CASCADE在 Admin 中是默认行为且无二次确认。解决双保险机制数据库层修改外键约束为ON DELETE RESTRICTDjango 4.2 支持class ReadingLog(models.Model): book models.ForeignKey( Book, on_deletemodels.RESTRICT, # 替换 CASCADE db_constraintTrue )Admin 层重写delete_view添加强提示# admin.py from django.contrib import admin from django.urls import reverse from django.http import HttpResponseRedirect admin.action(descriptionDelete selected books (with safety check)) def safe_delete_books(modeladmin, request, queryset): if request.POST.get(confirm_delete) yes: # 检查是否有日志关联 book_ids list(queryset.values_list(id, flatTrue)) if ReadingLog.objects.filter(book_id__inbook_ids).exists(): modeladmin.message_user( request, Cannot delete books with existing reading logs. Please archive instead., levelERROR ) return queryset.delete() else: # 渲染确认页 return TemplateResponse(request, admin/confirm_delete_books.html, { books: queryset, opts: modeladmin.model._meta, }) class BookAdmin(admin.ModelAdmin): actions [safe_delete_books]注意models.RESTRICT是 Django 4.2 新特性若用旧版必须用models.PROTECT并配合自定义 Admin。5. 进阶技巧用行为序列建模替代静态评分让推荐真正理解“儿童成长”协同过滤的终极瓶颈是把用户当作静态向量。但儿童不是静态的——6 岁爱恐龙8 岁迷太空10 岁开始读科幻小说。他们的兴趣是时间序列不是快照。本系统预留了ReadingLog.timestamp字段就是为了支撑进阶的序列建模。这里不引入 LSTM 或 Transformer过于重而是用一套轻量、可解释、Django 原生支持的方案滑动窗口行为强度趋势分析。5.1 为什么传统协同过滤在儿童场景会“滞后”假设一个 7 岁孩子过去半年读了 30 本恐龙书最近一个月读了 5 本天文科普书。传统 User-Based CF 会把他和“恐龙爱好者群组”强绑定因为历史共现度高Item-Based 会持续推荐恐龙书因为《恐龙百科》和《恐龙世界》共现频繁。但孩子的兴趣已经迁移——算法却要等他读够 10 本天文书才“反应过来”。这就是兴趣漂移延迟。5.2 滑动窗口趋势引擎用 30 天窗口捕捉兴趣拐点我们在recommender/trend.py中实现一个独立模块不改变原有协同过滤流程而是作为重排序器Re-ranker插入最终结果# recommender/trend.py from datetime import timedelta from django.utils import timezone from .models import ReadingLog, Book def calculate_interest_trend(child_id, window_days30): 计算儿童最近 30 天的兴趣趋势返回 {category: score} 字典 score (近期该类图书阅读强度 / 总强度) - (历史该类图书阅读强度 / 总强度) 0 表示兴趣上升0 表示下降 now timezone.now() window_start now - timedelta(dayswindow_days) # 近期行为30天内 recent_logs ReadingLog.objects.filter( child_idchild_id, timestamp__gtewindow_start ).select_related(book) # 历史行为30天前至今最多取 180 天避免数据过载 history_start now - timedelta(days180) history_logs ReadingLog.objects.filter( child_idchild_id, timestamp__gtehistory_start, timestamp__ltwindow_start ).select_related(book) # 计算强度duration * pages_read recent_strength {} history_strength {} for log in recent_logs: cat log.book.category score log.duration_seconds * log.pages_read recent_strength[cat] recent_strength.get(cat, 0) score for log in history_logs: cat log.book.category score log.duration_seconds * log.pages_read history_strength[cat] history_strength.get(cat, 0) score # 归一化并计算趋势 total_recent sum(recent_strength.values()) or 1 total_history sum(history_strength.values()) or 1 trends {} all_cats set(recent_strength.keys()) | set(history_strength.keys()) for cat in all_cats: recent_ratio recent_strength.get(cat, 0) / total_recent history_ratio history_strength.get(cat, 0) / total_history p a hrefhttps://download.csdn.net/download/2301_77783312/90011099 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p