Recommendtion System Introduction

本文介绍了推荐系统的两种主要算法:基于用户的协同过滤和基于内容的推荐。基于用户的协同过滤依赖于用户与物品之间的关系来生成推荐,而不需要了解物品本身的特性。基于内容的推荐则依据物品属性进行推荐,这种方法虽然有效但具有领域局限性。

collaborative filtering

producing recommendations based on, and only based on, knowledge of users’ rela-tionships to items. These techniques require no knowledge of the properties of the items themselves.

In fact, these are the two broadest categories of recommender engine algorithms: user-based and item-based recommenders.

 

 

content-based recommendation

based on the attributes of items. There’s nothing wrong with content-based techniques; on the contrary, they can work quite well. They’re necessarily domain-specific approaches, and they’d be hard to meaningfully codify into a framework. To build an effective content-based book rec-ommender, one would have to decide which attributes of a book—page count, author,publisher, color, font—are meaningful, and to what degree. None of this knowledgetranslates into any other domain; recommending books this way doesn’t help in the area of recommending pizza toppings.

 

 

 

 

 

 

 

Reference

<<Mahout In Action>>

<template> <div class="home-preview"> <!-- 新闻资讯 --> <div id="animate_newsnews" class="news animate__animated"> <div class="news_title_box"> <span class="news_title">公告资讯</span> <span class="news_subhead">{{ "news".toUpperCase() }}</span> </div> <div v-if="newsList.length" class="list list20 index-pv1"> <div v-for="(item, index) in newsList" :key="index" @click="toDetail('newsDetail', item)" class="list-item animation-box" > <div class="img-box"> <img :src="baseUrl + item.picture" class="image" /> </div> <div class="infoBox"> <div class="infoBox-left"> <div class="name">{{ item.title }}</div> <div class="time_item"> <span class="icon iconfont icon-shijian21"></span> <span class="label">发布时间:</span> <span class="text">{{ item.addtime.split(" ")[0] }}</span> </div> <div class="publisher_item"> <span class="icon iconfont icon-touxiang18"></span> <span class="label">发布人:</span> <span class="text">{{ item.name }}</span> </div> <div class="like_item"> <span class="icon iconfont icon-zan10"></span> <span class="label">点赞:</span> <span class="text">{{ item.thumbsupnum }}</span> </div> <div class="collect_item"> <span class="icon iconfont icon-shoucang10"></span> <span class="label">收藏:</span> <span class="text">{{ item.storeupnum }}</span> </div> <div class="view_item"> <span class="icon iconfont icon-liulan13"></span> <span class="label">浏览次数:</span> <span class="text">{{ item.clicknum }}</span> </div> </div> <div class="desc">{{ item.introduction }}</div> </div> </div> </div> <div class="moreBtn" @click="moreBtn('news')"> <span class="text">更多</span> <i class="icon iconfont icon-gengduo1"></i> </div> </div> <!-- 新闻资讯 --> <!-- 商品推荐 --> <div id="animate_recommendfeiyiwenzhang" class="recommend animate__animated" > <div class="recommend_title_box"> <span class="recommend_title">文章推荐</span> <span class="recommend_subhead" >{{ "feiyiwenzhang".toUpperCase() }} RECOMMEND</span > </div> <!-- 样式一 --> <div class="list list1 index-pv1"> <div v-for="(item, index) in feiyiwenzhangRecommend" :key="index" @click="toDetail('feiyiwenzhangDetail', item)" class="list-item animation-box" > <img v-if="preHttp(item.feiyitupian)" :src="item.feiyitupian.split(',')[0]" alt="" /> <img v-else :src=" baseUrl + (item.feiyitupian ? item.feiyitupian.split(',')[0] : '') " alt="" /> <div class="name line1">{{ item.feiyibiaoti }}</div> <div class="name line1">{{ item.feiyileixing }}</div> <div class="time_item"> <span class="icon iconfont icon-shijian21"></span> <span class="label">发布时间:</span> <span class="text">{{ item.addtime.split(" ")[0] }}</span> </div> <div class="like_item"> <span class="icon iconfont icon-zan10"></span> <span class="label">点赞:</span> <span class="text">{{ item.thumbsupnum }}</span> </div> <div class="collect_item"> <span class="icon iconfont icon-shoucang10"></span> <span class="label">收藏:</span> <span class="text">{{ item.storeupnum }}</span> </div> <div class="view_item"> <span class="icon iconfont icon-liulan04"></span> <span class="label">浏览次数:</span> <span class="text">{{ item.clicknum }}</span> </div> </div> </div> <div class="moreBtn" @click="moreBtn('feiyiwenzhang')"> <span class="text">更多</span> <i class="icon iconfont icon-gengduo1"></i> </div> </div> <!-- 商品推荐 --> <div id="animate_recommendfeiyiwenzhang" class="recommend animate__animated" > <div class="recommend_title_box"> <span class="recommend_title">商品推荐</span> <span class="recommend_subhead" >{{ "goods".toUpperCase() }} RECOMMEND</span > </div> <!-- 样式一 --> <div class="list list1 index-pv1"> <div v-for="(item, index) in goodsList" :key="index" @click="toDetail('goodsDetail', item)" class="list-item animation-box" > <img v-if="preHttp(item.pic)" :src="item.pic.split(',')[0]" alt="" /> <img v-else :src="baseUrl + (item.pic ? item.pic.split(',')[0] : '')" alt="" /> <div class="name line1">{{ item.goodsname }}</div> <div class="name line1">{{ item.yonghuming }}</div> <div class="name line1">{{ item.shouji }}</div> <div class="name line1">{{ item.goodsprice }}</div> <div class="time_item"> <span class="icon iconfont icon-shijian21"></span> <span class="label">发布时间:</span> <span class="text">{{ item.createtime.split(" ")[0] }}</span> </div> <div></div> </div> </div> <div class="moreBtn" @click="moreBtn('goodsList')"> <span class="text">更多</span> <i class="icon iconfont icon-gengduo1"></i> </div> </div> <!-- 商品推荐 --> <div id="animate_recommendfeiyihuodong" class="recommend animate__animated"> <div class="recommend_title_box"> <span class="recommend_title">活动推荐</span> <span class="recommend_subhead" >{{ "feiyihuodong".toUpperCase() }} RECOMMEND</span > </div> <!-- 样式一 --> <div class="list list1 index-pv1"> <div v-for="(item, index) in feiyihuodongRecommend" :key="index" @click="toDetail('feiyihuodongDetail', item)" class="list-item animation-box" > <img v-if="preHttp(item.huodongtupian)" :src="item.huodongtupian.split(',')[0]" alt="" /> <img v-else :src=" baseUrl + (item.huodongtupian ? item.huodongtupian.split(',')[0] : '') " alt="" /> <div class="name line1">{{ item.huodongmingcheng }}</div> <div class="name line1">{{ item.huodongleixing }}</div> <div class="time_item"> <span class="icon iconfont icon-shijian21"></span> <span class="label">发布时间:</span> <span class="text">{{ item.addtime.split(" ")[0] }}</span> </div> <div class="collect_item"> <span class="icon iconfont icon-shoucang10"></span> <span class="label">收藏:</span> <span class="text">{{ item.storeupnum }}</span> </div> <div class="view_item"> <span class="icon iconfont icon-liulan04"></span> <span class="label">浏览次数:</span> <span class="text">{{ item.clicknum }}</span> </div> </div> </div> <div class="moreBtn" @click="moreBtn('feiyihuodong')"> <span class="text">更多</span> <i class="icon iconfont icon-gengduo1"></i> </div> </div> </div> </template> <script> import "animate.css"; import Swiper from "swiper"; export default { //数据集合 data() { return { baseUrl: "", goodsList: [], newsList: [], feiyiwenzhangRecommend: [], feiyihuodongRecommend: [], }; }, created() { this.baseUrl = this.$config.baseUrl; this.getNewsList(); this.getList(); this.getGoodsList(); }, mounted() { window.addEventListener("scroll", this.handleScroll); setTimeout(() => { this.handleScroll(); }, 100); this.swiperChanges(); }, beforeDestroy() { window.removeEventListener("scroll", this.handleScroll); }, //方法集合 methods: { swiperChanges() { setTimeout(() => {}, 750); }, listIndexClick11(index, name) { this["listIndex11" + name] = index[this["listColumn11" + name]]; this.getList(); }, handleScroll() { let arr = [ { id: "about", css: "animate__" }, { id: "system", css: "animate__" }, { id: "animate_recommendfeiyiwenzhang", css: "animate__" }, { id: "animate_recommendfeiyihuodong", css: "animate__" }, { id: "animate_newsnews", css: "animate__" }, ]; for (let i in arr) { let doc = document.getElementById(arr[i].id); if (doc) { let top = doc.offsetTop; let win_top = window.innerHeight + window.pageYOffset; // console.log(top,win_top) if (win_top > top && doc.classList.value.indexOf(arr[i].css) < 0) { // console.log(doc) doc.classList.add(arr[i].css); } } } }, preHttp(str) { return str && str.substr(0, 4) == "http"; }, getNewsList() { let data = { page: 1, limit: 6, sort: "addtime", order: "desc", }; this.$http.get("news/list", { params: data }).then((res) => { if (res.data.code == 0) { this.newsList = res.data.data.list; } }); }, getGoodsList() { let data = { page: 1, limit: 6, sort: "createtime", order: "desc", }; this.$http.get("yonghuGoods/getGoodsAllList", { params: data }).then((res) => { if (res.data.code == 0) { this.goodsList = res.data.list.list; } }); }, getList() { let autoSortUrl = ""; let data = {}; autoSortUrl = "feiyiwenzhang/autoSort"; if (localStorage.getItem("frontToken")) { autoSortUrl = "feiyiwenzhang/autoSort2"; } data = { page: 1, limit: 6, }; this.$http.get(autoSortUrl, { params: data }).then((res) => { if (res.data.code == 0) { this.feiyiwenzhangRecommend = res.data.data.list; } }); autoSortUrl = "feiyihuodong/autoSort"; data = { page: 1, limit: 6, }; this.$http.get(autoSortUrl, { params: data }).then((res) => { if (res.data.code == 0) { this.feiyihuodongRecommend = res.data.data.list; } }); }, toDetail(path, item) { this.$router.push({ path: "/index/" + path, query: { id: item.id } }); }, moreBtn(path) { this.$router.push({ path: "/index/" + path }); }, }, }; </script> <style rel="stylesheet/scss" lang="scss" scoped> .home-preview { margin: 0px auto; flex-direction: column; background: #fff; display: flex; width: 100%; .news { padding: 0; margin: 15px 0; background: none; width: 100%; position: relative; order: 4; .news_title_box { padding: 0 0 5px; margin: 0 auto; background: none; width: 1200px; border-color: #3b5d9a; border-width: 0 0 2px; position: relative; border-style: solid; text-align: left; .news_title { margin: 0 10px 0 0; color: #3b5d9a; background: none; width: auto; font-size: 26px; line-height: 40px; } .news_subhead { margin: 0; color: #999; display: none; width: auto; font-size: 18px; line-height: 40px; text-align: center; } } .index-pv1 .animation-box:hover { transform: rotate(0deg) scale(1) skew(0deg, 0deg) translate3d(0px, 0px, 0px); -webkit-perspective: 1000px; perspective: 1000px; transition: 0.3s; z-index: 1; } .index-pv1 .animation-box img:hover { transform: rotate(0deg) scale(1) skew(0deg, 0deg) translate3d(0px, 0px, 0px); -webkit-perspective: 1000px; perspective: 1000px; transition: 0.3s; } .list20 { padding: 0; margin: 0 auto; background: #fff; display: flex; width: 1200px; border-color: #eaeaea; border-width: 0 0 0 1px; justify-content: space-between; border-style: solid; flex-wrap: wrap; height: auto; .list-item { cursor: pointer; padding: 10px; margin: 0; background: #fff; width: calc(33.33% - 0px); border-color: #eaeaea; border-width: 0 1px 1px 0; border-style: solid; height: auto; .img-box { border: 0px solid #eee; padding: 0; overflow: hidden; background: #fff; width: 160px; float: left; height: 120px; img { object-fit: cover; display: block; width: 100%; height: 100%; } } .infoBox { padding: 0; overflow: hidden; display: block; width: calc(100% - 180px); float: right; height: auto; .infoBox-left { padding: 0; margin: 0; color: #666; width: 100%; .name { padding: 0; overflow: hidden; color: #333; white-space: nowrap; font-weight: 600; width: 100%; font-size: 14px; line-height: 36px; text-overflow: ellipsis; } .time_item { padding: 0 10px; display: none; border-color: #ddd; border-width: 0 0 1px 0; border-style: dashed; .icon { margin: 0 2px 0 0; color: #666; font-size: 14px; line-height: 28px; } .label { color: #666; font-size: 14px; line-height: 1.5; } .text { color: #666; font-size: 14px; line-height: 1.5; } } .publisher_item { padding: 0 10px; display: none; border-color: #ddd; border-width: 0 0 1px 0; border-style: dashed; .icon { margin: 0 2px 0 0; color: inherit; font-size: 14px; line-height: 28px; } .label { color: #666; font-size: 14px; line-height: 1.5; } .text { color: #666; font-size: 14px; line-height: 28px; } } .like_item { padding: 0 10px; display: none; border-color: #ddd; border-width: 0 0 1px 0; border-style: dashed; .icon { margin: 0 2px 0 0; color: inherit; font-size: 14px; line-height: 28px; } .label { color: #666; font-size: 14px; line-height: 1.5; } .text { color: #666; font-size: 14px; line-height: 28px; } } .collect_item { padding: 0 10px; display: none; border-color: #ddd; border-width: 0 0 1px 0; border-style: dashed; .icon { margin: 0 2px 0 0; color: inherit; font-size: 14px; line-height: 28px; } .label { color: #666; font-size: 14px; line-height: 1.5; } .text { color: #666; font-size: 14px; line-height: 28px; } } .view_item { padding: 0 10px; display: none; .icon { margin: 0 2px 0 0; color: inherit; font-size: 14px; line-height: 28px; } .label { color: #666; font-size: 14px; line-height: 1.5; } .text { color: #666; font-size: 14px; line-height: 28px; } } } .desc { margin: 0; overflow: hidden; color: #666; display: flex; width: 100%; font-size: 14px; line-height: 24px; align-items: center; flex-wrap: wrap; height: 72px; } } } .list-item:hover { .infoBox { .infoBox-left { .name { } .time_item { .icon { color: #000; } .label { color: #000; } .text { color: #000; } } .publisher_item { .icon { color: #000; } .label { color: #000; } .text { color: #000; } } .like_item { .icon { color: #000; } .label { color: #000; } .text { color: #000; } } .collect_item { .icon { color: #000; } .label { color: #000; } .text { color: #000; } } .view_item { .icon { color: #000; } .label { color: #000; } .text { color: #000; } } } .desc { } } } } .moreBtn { border: 0px solid #999; cursor: pointer; padding: 0; margin: 0; display: inline-block; line-height: 32px; right: calc((100% - 1200px) / 2); float: right; top: 5px; background: none; width: auto; position: absolute; text-align: right; .text { color: #999; font-size: 14px; } .icon { color: #999; font-size: 14px; } } } .recommend { padding: 0; margin: 15px 0; background: #fff; width: 100%; position: relative; .recommend_title_box { padding: 0px; margin: 0 auto 10px; background: none; width: 1200px; position: relative; text-align: left; .recommend_title { margin: 0; color: #000; background: none; width: auto; font-size: 24px; line-height: 36px; } .recommend_subhead { margin: 0; color: #999; display: none; width: auto; font-size: 18px; line-height: 40px; text-align: center; } } .index-pv1 .animation-box { transform: rotate(0deg) scale(1) skew(0deg, 0deg) translate3d(0px, 0px, 0px); z-index: initial; } .index-pv1 .animation-box:hover { transform: rotate(0deg) scale(1) skew(0deg, 0deg) translate3d(0px, 0px, 0px); -webkit-perspective: 1000px; perspective: 1000px; transition: 0s; z-index: 1; } .index-pv1 .animation-box img { transform: rotate(0deg) scale(1) skew(0deg, 0deg) translate3d(0px, 0px, 0px); } .index-pv1 .animation-box img:hover { transform: rotate(0deg) scale(1) skew(0deg, 0deg) translate3d(0px, 0px, 0px); -webkit-perspective: 1000px; perspective: 1000px; transition: 0s; } .list1 { padding: 0; margin: 0 auto; background: #fff; width: 1200px; border-color: #eaeaea; border-width: 0 0 0 1px; border-style: solid; height: auto; .list-item { cursor: pointer; padding: 10px; margin: 0; color: #888; display: inline-block; font-size: 14px; border-color: #eaeaea; background: #fff; width: calc(16.66% - 0px); border-width: 1px 1px 1px 0; position: relative; border-style: solid; height: auto; img { margin: 0 0 5px; object-fit: cover; display: block; width: 100%; height: 180px; } .name { padding: 0 10px; overflow: hidden; color: #333; white-space: nowrap; font-weight: 600; width: 100%; font-size: 14px; line-height: 30px; text-overflow: ellipsis; } .price { padding: 0 10px; color: #f00; font-size: 14px; line-height: 1.5; } .time_item { padding: 0 10px; display: none; .icon { margin: 0 2px 0 0; color: inherit; display: none; font-size: inherit; line-height: 1.5; } .label { color: inherit; font-size: inherit; line-height: 1.5; } .text { color: inherit; font-size: inherit; line-height: 1.5; } } .publisher_item { padding: 0 10px; display: none; .icon { margin: 0 2px 0 0; color: inherit; display: none; font-size: inherit; line-height: 1.5; } .label { color: inherit; font-size: inherit; line-height: 1.5; } .text { color: inherit; font-size: inherit; line-height: 1.5; } } .like_item { padding: 0 10px; display: none; .icon { margin: 0 2px 0 0; color: inherit; display: none; font-size: inherit; line-height: 1.5; } .label { color: inherit; font-size: inherit; line-height: 1.5; } .text { color: inherit; font-size: inherit; line-height: 1.5; } } .collect_item { padding: 0 10px; display: none; .icon { margin: 0 2px 0 0; color: inherit; display: none; font-size: inherit; line-height: 1.5; } .label { color: inherit; font-size: inherit; line-height: 1.5; } .text { color: inherit; font-size: inherit; line-height: 1.5; } } .view_item { padding: 0 10px; display: none; .icon { margin: 0 2px 0 0; color: inherit; display: none; font-size: inherit; line-height: 1.5; } .label { color: inherit; font-size: inherit; line-height: 1.5; } .text { color: inherit; font-size: inherit; line-height: 1.5; } } } } .moreBtn { border: 0px solid #999; cursor: pointer; padding: 0; margin: 0; display: inline-block; line-height: 32px; right: calc((100% - 1200px) / 2); float: right; top: 5px; background: none; width: auto; position: absolute; text-align: right; .text { color: #999; font-size: 14px; } .icon { color: #999; font-size: 14px; } } } } </style> Cannot read properties of null (reading 'list') TypeError: Cannot read properties of null (reading 'list') at VueComponent.eval (webpack-internal:///./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/pages/home/home.vue?vue&type=script&lang=js:99:42)
12-24
``` TypeError: Cannot read properties of null (reading 'list') at VueComponent.eval (... home.vue:99:42) ``` 这个错误表明:**你的代码尝试访问一个 `null` 或 `undefined` 对象的 `.list` 属性**,具体发生在 `home.vue` 文件中请求商品列表后的处理逻辑。 --- ### ✅ 错误定位与原因分析 你提供的 Vue 组件中,有如下关键代码: ```js this.$http.get("yonghuGoods/getGoodsAllList", { params: data }).then((res) => { if (res.data.code == 0) { this.goodsList = res.data.list.list; } }); ``` #### ❌ 问题就出在这里: ```js this.goodsList = res.data.list.list; ``` 你试图从 `res.data.list.list` 中取数据,但正确的结构应该是 `res.data.data.list` 或 `res.data.list`,而 **`res.data.list` 是 `null`**,所以再读 `.list` 就报错: > `Cannot read properties of null (reading 'list')` 这说明你对后端返回的数据结构理解错误。 --- ### ✅ 正确解决方法 #### 1. **修正前端数据提取路径** 假设后端返回的是标准格式: ```json { "code": 0, "msg": "success", "data": { "total": 100, "list": [ ... ] } } ``` 那么你应该修改为: ```js getGoodsList() { let data = { page: 1, limit: 6, sort: "createtime", order: "desc" }; this.$http.get("yonghuGoods/getGoodsAllList", { params: data }) .then(res => { if (res.data.code === 0 && res.data.data && res.data.data.list) { this.goodsList = res.data.data.list; // ✅ 正确路径 } else { console.error("商品列表数据异常:", res.data); this.goodsList = []; } }) .catch(err => { console.error("请求商品列表失败:", err); this.goodsList = []; }); } ``` > 🔥 关键修复点:将 `res.data.list.list` 改为 `res.data.data.list` --- #### 2. **为什么会出现 `null`?常见原因** | 原因 | 说明 | |------|------| | 后端接口返回了空数据 | 查询无结果时返回 `data: null` 而不是 `{ list: [] }` | | 接口地址 404 或权限不足 | 返回非 JSON 内容(如 HTML 登录页),解析成 `null` | | 字段名拼写错误 | 比如后端是 `items` 而你写成了 `list` | --- #### 3. **推荐增强健壮性的写法(防错)** 使用可选链操作符(?.)避免深层访问崩溃: ```js this.goodsList = res.data?.data?.list || []; ``` 或者加判断: ```js if (res.data.code === 0) { const list = res.data.data?.list; this.goodsList = Array.isArray(list) ? list : []; } ``` --- #### 4. **验证后端返回结构(Java 示例)** 确保你的 Spring Boot 控制器返回统一格式。以下是一个正确实现: ```java package com.example.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @RestController public class YonghuGoodsController { // 模拟数据库数据 private List<Map<String, Object>> allGoods = List.of( createGoods(1L, "华为手机", "张三", "13800138000", 5999.0), createGoods(2L, "小米电视", "李四", "13900139000", 3999.0) ); @GetMapping("/yonghuGoods/getGoodsAllList") public Map<String, Object> getGoodsAllList( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "6") Integer limit, @RequestParam(defaultValue = "createtime") String sort, @RequestParam(defaultValue = "desc") String order) { // 模拟分页 int start = (page - 1) * limit; int total = allGoods.size(); List<Map<String, Object>> paginated = allGoods.stream() .skip(start) .limit(limit) .collect(Collectors.toList()); Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("msg", "success"); Map<String, Object> data = new HashMap<>(); data.put("total", total); data.put("list", paginated); // ✅ 必须是 data.list result.put("data", data); return result; } private Map<String, Object> createGoods(Long id, String name, String user, String phone, Double price) { Map<String, Object> map = new HashMap<>(); map.put("id", id); map.put("goodsname", name); map.put("yonghuming", user); map.put("shouji", phone); map.put("goodsprice", price); map.put("pic", "/upload/goods.jpg"); map.put("createtime", "2024-01-01 10:00:00"); return map; } } ``` --- ### ✅ 总结修复步骤 1. ✅ 修改前端:`res.data.list.list` → `res.data.data.list` 2. ✅ 加空值判断或默认值:`|| []` 3. ✅ 打印 `res.data` 查看真实结构:`console.log(res.data)` 4. ✅ 确保后端返回 `data: { list: [...] }` 结构 5. ✅ 使用 Postman 测试接口是否正常返回 JSON ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值