很多人做商城类系统,习惯一上来就写 Spring Boot CRUD,把商品表、用户表、订单表建好,再套一个协同过滤算法,最后发现推荐接口返回的结果全是“猜你喜欢同款”,因为根本没什么候选商品。我做这套化妆品推荐商城时,把顺序倒过来了:先用爬虫把化妆品数据灌进库,再围绕商家后台设计运营规则,最后用可视化看板验证推荐效果。整套系统跑下来,最大的感受是——真正花时间的不是 UserController,而是数据链路的脏数据治理。
这篇内容会完整拆解这个项目的四条链路:爬虫并发采集、推荐系统落地、商家后台权限与闭环、可视化大屏的实现。每个部分都带实际踩坑记录和最终方案。如果你正打算做一个能演示、能讲清楚、能不断迭代的电商或推荐类项目,或者你正在写毕业设计、准备整理项目经验,这篇应该能给你省下不少弯路。
1. 业务拆解与整体架构:爬虫、商城、推荐、可视化四条链路怎么各司其职
1.1 业务闭环的起点不是商城,而是数据
通常商城系统的起点是商品录入。化妆品品类有个特点:SKU 极多,一个“精华液”可以按品牌、系列、规格、肤质分成几十个变体。如果纯靠后台人工录入,数据质量和运营效率都跟不上。所以我把爬虫当成“数据供给系统”设计:爬虫模块从公开数据源采集品牌、类目、成分、价格、销量、用户评价等原始信息,写入 raw_data 表;经过清洗和标准化之后,再同步到商品主数据表。推荐系统、商城商品列表、可视化统计全部读主数据表,不直接碰爬虫层的原始数据。
这个设计解决了一个很经典的问题:爬虫是易变的,业务系统是稳定的。目标数据源的结构变了、接口加了验证、某个反爬策略升级,最多只会影响 raw_data 层,不会导致商城下单报错。这是我把爬虫独立成一个采集进程,而不是写成 Spring Boot 里的一个 @Service 的直接原因。如果你把爬虫代码和业务代码写在一起,将来任何一个站点规则变化,你都得重新打包部署整个 Spring Boot 应用,这个成本在项目初期不明显,一旦数据源多了之后会非常痛苦。
1.2 技术选型:为什么爬虫用 Python,业务用 Spring Boot
与其纠结“Spring Boot 能不能写爬虫”,不如想清楚每个任务的生态。Spring Boot 负责商品管理、订单、权限、推荐算法调度、可视化数据接口,优势是稳定、社区资料多、和 MySQL/Redis 的生态配合成熟。Python requests + BeautifulSoup 负责化妆品数据采集,原因很实际:处理 HTML/JSON 的时候 Python 的库更顺手,requests 的会话管理和重试机制也简单,写一个小型任务脚本比用 Java HttpClient 快很多。
有同学问为什么不直接上 Scrapy 或分布式爬虫。我的判断是:当前这个项目的数据量级在万级 SKU,单机并发 8~16 个线程就已经能跑完,Scrapy 的中间件体系对我不产生额外收益;分布式爬虫更是性能过剩,还要引入消息队列、任务调度器,复杂度明显增加。但我在采集脚本里预留了良好的接口抽象:采集器只负责产出“已解析的字典对象”,下一步如果真的要上分布式,可以直接把同一个 parse() 逻辑丢到 Scrapy 的 item pipeline 或者 Kafka 里,不需要改任何业务代码。选型不是越重越好,而是够用和可演进之间的平衡。
1.3 四层架构与数据库建模的核心表
Spring Boot 部分我按经典的四层架构拆:controller -> service -> mapper -> model,每个业务域内部再独立分包。这个分层看似老生常谈,但在这个项目里有实际意义:爬虫采集、推荐计算、商城订单、商家权限分散在不同 domain 包下,改动某个模块时不会牵连其他模块的 controller 和 mapper。推荐算法是离线计算的,它不该依赖订单接口;商家后台权限是独立的,也不该和商城前台用户混在一个 service 里。
数据库建模是整个项目里返工次数最多的部分,我最后确定的表结构大致如下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| t_product | 商品主数据 | id, brand_id, category_id, product_name, sku_id, price, stock, status, source_key |
| t_user_behavior | 用户行为流水 | id, user_id, product_id, behavior_type(click/cart/order/favorite), score, created_at |
| t_recommendation_rule | 商家推荐规则 | id, merchant_id, product_id, rule_type, weight, start_time, end_time |
| t_merchant | 商家账号 | id, merchant_name, login_name, contact, status |
| t_order | 订单表 | id, order_no, user_id, merchant_id, total_amount, status |
source_key 字段存的是品牌名加原始商品编号,这是整个爬虫幂等去重的命根子。t_user_behavior 的 score 按行为类型赋值:浏览等于 1,收藏等于 3,加购等于 4,下单等于 10。这样后面做协同过滤时,不需要单独维护一张评分表,直接从这个字段取值。推荐系统的输入不一定是用户主动打出的星星评分,很多网站的评分功能形同虚设,但行为日志一定有。把行为转成分数值,是低成本高收益的做法。
2. 爬虫模块从零实现:requests 并发采集和数据清洗的每一步
2.1 数据源怎么选:优先公开数据,合规才是长期主义
爬虫模块的定位是“为项目提供足够的化妆品数据”,不是专门去对抗某个大型电商平台。我的建议是优先选择三类数据源:公开的化妆品成分评测数据集,整理成 JSON 或 CSV 后直接入库;品牌官网公开的产品规格、价格、成分页;少量可以在商家授权范围内补充的结构化测试数据。
提醒一句:如果目标站点的 robots.txt 明确不允许采集,或者接口做了复杂签名,就不要硬碰。一套系统的价值在于稳定可复现,而不是数据量越大越好。我实际能稳定采集的量已经足够支撑商城展示和推荐算法演示,重要的是把“采集 -> 清洗 -> 入库”的工程链路跑通。合规、稳、可复现,比爬得多重要得多。
2.2 采集脚本的字段设计和主流程
采集脚本用 Python 实现,核心流程分四步。第一步,构造种子列表:从起始页拿到品牌列表,生成待采集 URL 队列。第二步,请求页面:用 requests.Session 配置公共 header 访问详情页。第三步,解析内容:用 BeautifulSoup 按页面 DOM 结构抽取价格、规格、成分、销量。第四步,结构化输出:每条记录写成一个 dict,再批量写入 MySQL,或者先写 JSON 行文件,由 Spring Boot 的后台功能读入到主库。
字段设计上,我不建议直接把页面看到的文本全部塞进表里。比如页面上“现价 299,原价 329”这种字符串,要拆成 price_now 和 price_origin 两个数值字段;销量“10万+”要换算成 estimated_sales_min 和 estimated_sales_max。推荐算法和可视化统计都依赖数值字段,这一步如果不做,后期清洗成本会成倍上涨。而且当你给商家展示“价格分布区间”这类图表时,字段类型是字符串的表会让聚合查询非常难写。
2.3 并发设计:为什么我选 ThreadPoolExecutor 而不是 asyncio
“爬虫并发设计到底哪个好”是每个写爬虫的人都会纠结的问题。我的结论是:如果你的瓶颈在 I/O 等待,也就是 HTTP 请求返回和数据库批量插入这些场景,并发粒度控制在 8 到 16,ThreadPoolExecutor 和 asyncio 的最终收益差别不大。但 ThreadPoolExecutor 的调试和异常处理更直观,断点进去能看到每个线程的执行情况,不怕某个协程忘记 await 导致整个事件循环挂住。
我当时写的核心并发逻辑大概是这样的:
from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_one(url): # 带 retry 和超时控制的单个页面抓取逻辑 pass with ThreadPoolExecutor(max_workers=8) as executor: futures = [executor.submit(crawl_one, url) for url in urls] for future in as_completed(futures): result = future.result() if result: all_results.append(result)两个关键细节要注意:第一,requests.Session 不能在线程间共享,它不是线程安全的。我会给每个线程创建独立 Session,或者用 threading.local 保存,避免出现连接串用错、cookie 乱掉这种诡异问题。第二,失败重试必须做在单个任务内部,不要依赖外层统一重试。如果一个坏 URL 抛异常了,它应该内部重试两次后记录失败日志,而不是让整个线程池停下来。
2.4 去重、幂等和增量更新的工程细节
爬虫最容易埋的雷就是重复数据。我遇到过一个典型场景:品牌详情页里有“相关推荐”区块,如果不做过滤,会把推荐位里出现但当前列表并不存在的商品也当详情抓下来,导致同一商品出现多个不同的价格记录。
去重策略我用了三层。第一层是 URL 去重:用布隆过滤器记录已经处理过的详情页 URL,避免同一个详情页被抓两次。第二层是业务主键去重:入库时对 source_key 做唯一索引,防止同一条商品记录在并发写库的竞争条件下重复插入。第三层是快照对比:更新前先查旧记录,如果价格、销量字段的差异超过预设阈值,就插入一条变更历史,否则只更新时间戳。增量更新的核心是把“删除-重插”改成“比对-更新”。我最后实现的是一个 upsert 语句:source_key 冲突时更新指定字段。这样即使每天跑一次定时任务,也不会把商城正在展示的商品记录搞丢。
2.5 爬虫和 Spring Boot 模块的松耦合方式
爬虫脚本和 Spring Boot 应用之间,我只留了一个约定:数据库表结构和状态字段。Python 脚本在采集完成后,通过命令行参数指定要写哪张 raw 表;Spring Boot 这边提供一个 AdminController 接口,可以触发“把 raw_data 清洗成商品主数据”的同步任务。这样做的最大好处是,爬虫挂掉了不会影响业务系统,业务系统挂掉了也不会阻塞爬虫采集,两边的开发节奏完全独立。
实际运行中最省心的方案是在部署机配 crontab 每天凌晨跑脚本,白天商城访问高峰期不受影响。这样既保证了数据每天更新,又不会让爬虫任务占用白天的数据库连接和网络带宽。如果你希望更实时,可以改成消息队列触发,但项目初期真没必要。
3. 推荐系统实现的关键决策:两套召回加商家权重,比堆复杂模型更有用
3.1 用户行为收集:从埋点到行为打分
推荐系统的地基是行为数据。我在商品详情页和加购、下单接口里埋了行为上报点,但要注意:不是所有请求都值得进推荐计算。比如买家反复刷新页面、爬虫测试请求、用户连续点击同一商品超过五次这类噪声样本,都需要在清洗阶段过滤掉。我给行为流水表增加了一个 batch_id 字段,运行定时清洗任务时只处理最近 30 分钟的新增数据,避免每次都对全表做扫描。
行为打分公式我采用简单加权:点击 1 分、收藏 3 分、加购 4 分、下单 10 分。具体权重可以根据业务调整,但方向不能反——下单行为一定比浏览行为更能代表用户偏好。打分后数据写回 t_user_behavior 表,供协同过滤计算使用。这套思路的关键不是说分数多精确,而是把不同行为的价值差异体现出来,让推荐算法有个可比较的排序信号。
3.2 Item-CF 的实现步骤:相似度计算和 Top-N 推荐
推荐部分我选了基于物品的协同过滤,也就是 Item-CF。原因很实际:用户规模还没到千万级,不需要上大规模向量召回;商品数量在几千到几万的量级,比用户数小得多,相似度矩阵可以离线算好;冷启动容易兜底,新用户没有行为时,直接推荐热门榜和商家加权榜。
Item-CF 核心分三步。第一步,构建“用户-商品”倒排索引,从行为表中取出打分数据。第二步,计算商品之间的余弦相似度,把相似度矩阵写入 MySQL 表或 Redis,定时离线更新。第三步,推荐时找出用户最近交互过的商品集合,按相似度加权求和,生成 Top-N 候选列表。这里有个新手容易踩的坑:不要试图把全量用户-商品矩阵读进内存计算相似度。正确做法是分页取最近 90 天的行为数据,只对“出现过交互的商品”计算两两相似度;对长期没有新行为的商品,直接走离线任务兜底,不要在用户访问推荐接口时实时计算。
3.3 基于内容的推荐:成分标签和类目标签的补充
Item-CF 有一个天然问题:新商品没有足够共现行为时,很难被推荐出来。所以我同时实现了基于内容的召回,用商品本身的标签做相似度计算。化妆品领域的标签非常典型:类目有精华、面霜、防晒、洁面;肤质有干皮、油皮、敏感肌;成分有烟酰胺、玻尿酸、水杨酸、A醇。
每个商品维护一个标签向量,比如“类目=精华,肤质=敏感肌,成分=烟酰胺”。对用户行为记录中的商品标签做统计,得到用户的“标签偏好向量”,再和候选商品的标签向量做余弦相似度。实现不复杂,但新商品冷启动的表现比纯 Item-CF 好很多。当你给商家后台展示“推荐位为什么出现这个商品”的说明时,内容标签也能作为解释依据:因为用户偏好专注敏感肌修复,而这个商品主打敏感肌可用。
3.4 商家权重干预:推荐系统不是黑盒,商家要有控制权
这是做商家后台时我最坚持的设计。推荐系统如果完全不透明,商家无法理解为什么自己的商品排序靠后。于是我在推荐候选集合并入排名阶段加了一个商家可控权重,最终排序分可以近似理解为:协同过滤得分乘以 0.6,加上内容匹配得分乘以 0.3,再加上商家运营权重乘以 0.1。商家可以在商家后台设置“品牌曝光加成”和“指定商品置顶时段”,在合理范围内影响排序结果。这样既保留算法的个性化能力,又让运营有可操作空间。
商家权重不能无限大。我在 rule_type 字段里规定了几种受限操作:置顶最多 3 个商品,一天最多申请 2 次,每次不超过 4 小时。目的是防止商家恶意刷曝光导致用户体验崩坏。推荐系统不可能永远准确,它能做的是在算法结果和商业运营之间找一个可配置的平衡点,而不是让某一方彻底压制另一方。
3.5 推荐结果缓存:把 Redis 放到推荐接口前面
推荐接口如果每次请求都实时算一遍,QPS 一上来基本必挂。我的方案是离线任务每天凌晨把相似度矩阵和 Top-K 推荐结果写入 Redis,接口只读取缓存结果;只有缓存过期或用户首次访问时才触发一次同步计算。缓存结构用了两种:用户维度是 recommend:user:{userId} 保存 Top-N 商品列表,TTL 设为 2 小时;商品相似度是 sim:product:{productId} 保存相似商品 JSON,TTL 设为 24 小时,因为商品相似度矩阵变化频率比用户推荐结果低得多。
缓存击穿是容易踩的坑。如果推荐任务是每小时刷新,正好在刷新瞬间有很多用户同时请求失效的数据,就可能击穿。我的做法是加一个单飞线程:当缓存为空时,只允许一个线程回源计算,其他线程阻塞等待,回源成功后统一返回。配合 Redis 可视化客户端观察 key 的 TTL 和命中率,基本能第一时间发现缓存策略失衡的问题。
4. 商城与商家后台:权限、库存、推荐位的业务闭环
4.1 商品和商家权限模型的设计
商城侧的常规功能是商品展示、搜索、详情、加入购物车、下单。需要重点说明的是商家权限模型,因为多人共用一套后台最容易出权限事故。我设计了三类角色:商城管理员管理所有品类、审核商家、配置全局推荐位;商家只能管理自己的商品、库存、订单和推荐位;运营角色只读统计报表,允许导出数据。
Spring Boot 侧用 Spring Security 加 JWT 做认证,接口上通过 @PreAuthorize("hasRole('MERCHANT')") 控制商家访问范围。我在这里踩了一个坑:JWT 里只放 userId,结果发现一个用户可能对应多个商家账号,或者一个商家下有多个操作员。后来我把 user_id 和 merchant_id 都放进 Token 的 Claims 里,扩展了商家管理员子账号表,才算理顺。如果你做类似的商家系统,一开始就规划好用户和商家是多对多还是多对一,后面会少改很多代码。
4.2 商家后台的关键操作与推荐位申请流程
商家的核心诉求其实很朴素:上架商品、改价格、盯库存、看销量、争取推荐位。推荐位申请流程是我觉得比较顺的一个闭环。商家先选择一个商品,再选择推荐类型,包括置顶、分类加权、品牌曝光三选一。系统校验商家的积分和价格限制,比如商品价格不能高于同类目平均价的 30%,防止价格虚高商品霸占曝光。管理员审核通过后,数据写入 t_recommendation_rule 表。推荐系统在下一轮离线计算中读取新规则,调整候选排序。
这个流程和推荐算法接得比较自然。因为规则表是独立字段,商家提交申请后不需要改任何推荐算法代码,只是在最终排序分里增加一个权重来源。如果你把推荐规则硬编码在算法里,商家每提一次申请都要发版,运营效率和系统稳定性都会出问题。
4.3 订单-库存-推荐榜的一致性设计
可视化大屏上有一个“今日热卖 Top10”,很多人以为榜单是从缓存里读的。我设计时明确要求:这个榜单必须是订单库的实时聚合结果,不能依赖推荐缓存。因为推荐缓存是基于行为的预估值,而运营看板需要的是准确的成交事实。实现上我建了一张定时聚合表 t_stat_hourly,每小时从订单表汇总一次销量、销售额和热卖品类,大屏接口只查这张聚合表,不直接扫订单表。否则高峰期查询量大,订单表会被统计 SQL 拖垮。
还有一个容易忽略的问题:库存不足的商品必须从推荐榜剔除,否则系统会把没货的商品反复推给用户。我把库存状态耦合进推荐候选过滤条件,每次生成推荐列表时都检查库存字段。这个逻辑虽然简单,但它的意义很大——推荐系统必备的准确性和可用性,往往就是被库存、下架状态这些小细节决定的。
5. 可视化大屏:把系统数据变成商家能看懂的决策信息
5.1 大屏技术选型:ECharts 加独立前端工程
可视化大屏我单独建了一个前端工程,用 Vue3 加 ECharts,数据接口由 Spring Boot 的 /api/stats 提供。之所以单独建工程而不是塞进商城管理后台,是因为大屏页面的布局比例、刷新频率和普通后台差异太大,混在一起会导致路由和样式互相干扰。ECharts 的文档资料最全,社区方案也多,对地图、折线图、柱状图、词云这些大屏常见图表的支持都很成熟。如果你自己搭数据可视化,不建议一开始就上重量级 BI 平台,ECharts 直接写配置项就可以满足绝大部分展示需求。
5.2 商家端核心指标与接口聚合
大屏上我展示的核心指标分成四块。第一块是实时成交:今日订单量、销售额、客单价。第二块是商品分析:热卖 Top10、滞销 Top10、库存预警。第三块是用户分析:新增用户数、复购率、用户画像标签分布。第四块是推荐效果:推荐位点击率、推荐转化率、各渠道曝光占比。
推荐效果这一块最能体现系统价值。因为商城内有“自然列表”和“推荐位”两种曝光方式,我在商品卡片点击上报时带上 position 字段,标记来源是 natural 还是 recommend。可视化接口按来源分组对比点击率、加购率和成交率。这样运营就能直观回答一个问题:推荐系统到底是不是在帮商城提升转化,还是只是把流量从自然位挪到了推荐位,并没有增量。这个对比口径,比单纯展示推荐点击率更有说服力。
5.3 大屏适配踩坑记录:设计稿到 1080P 的缩放问题
大屏设备分辨率常见的是 1080P 或 4K,但设计稿有时候会按 3000 乘 2000 来做。直接写像素单位会导致在小屏上要么溢出、要么留白严重。我最后的适配方案是根元素字体大小不写死,用动态 rem 以 1920 乘 1080 为基准缩放;图表容器不设固定高度,用 flex 布局;每个图表的尺寸在 resize 事件里重新计算。对于极端尺寸比例的设计稿,用 scale 方案把整体内容等比缩放,保证不溢出。
还有一个隐藏坑:大屏页面上的轮播和闪烁效果如果用了 setInterval,但组件卸载时没有清理定时器,切换路由后会内存泄漏,甚至出现叠影。这个坑在后台管理页面里不常见,但在大屏项目里几乎必踩。我最后统一封装了一个 useInterval 工具,让定时器生命周期和组件绑定,页面销毁自动清理。
5.4 数据刷新与任务调度的配合
可视化数据接口每 5 分钟刷新一次,刷新的节奏不是前端频繁请求数据库,而是后端先把数据聚合到 t_stat_hourly 表,再通过 /api/stats 接口提供结果。前端用 setInterval 每隔 5 分钟轮询一次接口。这个间隔足够展示趋势变化,也不会对数据库造成压力。
我还给大屏接口加了一层 Redis 缓存,聚合任务结束后主动刷新缓存,大屏请求永远读缓存数据。这样即使有几张大屏同时挂在会议室,后端也能毫无压力地扛住。如果你想做更实时的推送,可以在本地上用 WebSocket 把聚合结果主动推给前端,但对一个商家展示场景来说,5 分钟轮询已经足够。
6. 上线前必须处理的稳定性与安全细节
6.1 定时任务与数据一致性的取舍
现在系统里至少有三类定时任务:每天凌晨的爬虫采集、商品数据清洗、推荐矩阵重算、缓存预热;每小时一次的订单聚合统计;每 5 分钟一次的大屏数据刷新。这些任务我用 Spring Task 调度,最轻量也够用。唯一的注意点是任务之间有先后依赖,比如商品清洗必须等爬虫采集完成,推荐矩阵重算又必须等商品主数据稳定。我把整个任务链拆成不同 job,按固定时间错峰执行,而不是写一个巨型方法把每件事全部塞进去。
数据一致性上,我的原则是最终一致,而不是强一致。爬虫凌晨写 raw_data 时,商城用户正在看的商品主数据不受影响;等到清洗任务跑完,主数据才整体更新。就算某个任务执行失败,上一版数据仍然可以正常支撑商城展示和推荐,不会出现线上商城拿不到数据的情况。
6.2 Spring Boot Actuator 未授权访问:差点把系统资料暴露了
这是一个非常值得写出来的坑。Spring Boot 2.x 引入 actuator 后,只要依赖存在而且没有显式配置,很多端点默认暴露,比如 /actuator/env、/actuator/heapdump、/actuator/threaddump。我前期在本机开发没注意,结果部署到服务器后用端口扫描工具检查,发现外部可以直接访问 /actuator/env,里面带数据库连接串、Redis 密码等敏感配置。这个问题如果上线才发现,等于把整个系统底裤亮给所有人看。
处理方式其实很简单:关闭非必要端点,只保留 health 和 info;设置独立管理端口,并且让管理端口只允许内网访问;如果不对公网提供监控能力,就不要把 actuator 放出去。这个坑在上线前一定要排查一遍,它的优先级高于推荐算法调参。项目能跑通只是第一步,安全稳不稳才是能不能拿来展示和交付的分水岭。
6.3 Redis 和 MySQL 的数据一致性策略
商城用 Redis 缓存了商品详情和推荐列表,MySQL 是最终数据源。更新商品价格后,缓存如果不删,用户看到的还是旧价格。我的策略是先更新数据库,再删缓存,而不是先删缓存再写库。因为并发请求下,删缓存后写库完成前,可能有其他线程把旧数据读回缓存,最终导致脏读。
后来我用了 Canal 监听 MySQL binlog 变动的方案,把商品表变更自动同步成 Redis 删除动作,这样只要 MySQL 里发生变化,缓存也会跟着更新,一致性保障更稳。如果你觉得 Canal 部署太重,先在代码里做双删也可以:更新数据库后删除缓存,延迟几百毫秒再删一次。但项目演示和真实生产环境我推荐 Canal,因为代码侵入更小,依赖关系更清晰。
6.4 部署流程与演示环境的注意事项
整个项目部署结构比较标准:后端 Spring Boot 打包成 jar,用 systemd 守护进程运行;前端大屏和管理后台打包成静态文件,用 Nginx 托管,动态请求反向代理到后端 8080;MySQL 8.0 和 Redis 6 跑在同一台服务器。这套部署方案足够支撑一个演示级别甚至小范围商用的系统。
演示环境有一个经验可以分享:比如明天要给客户或者老师演示,不要等到现场才去跑爬虫,现场网络很可能访问不了目标站点,或者目标站点的页面结构已经变了。我会在演示前把商品数据快照导入演示库,把爬虫脚本的开关停在只展示历史采集数据的状态,保证演示时商城、推荐、大屏都是正常可用的。这个细节不复杂,但能让演示效果稳定很多。
这个项目做完,我最大的体会是:一套系统里最复杂的往往不是某个独立技术点,而是数据如何在模块之间顺畅流动。爬虫负责把外部的商品信息接进来,推荐系统负责把行为转成偏好,商家后台负责让人能干预排序,可视化大屏负责让所有数据变得可读。每一环的单点实现都不算高深,但串起来后,它就是一个能演示、能运营、能持续迭代的完整产品。
如果你打算照着这个思路做一个类似的项目,我建议你先花一半时间把数据链路跑通,包括爬虫采集、清洗入库、行为埋点、统计分析。推荐算法再花哨,也是建立在数据质量之上的。先把最朴素的两套推荐候选跑稳,把商家可干预的权重设计好,把可视化指标口径定义清楚,比什么都重要。等这套闭环真正运转起来,你也就理解了为什么说一个毕业设计级别的系统,同样能体现架构能力和工程素养。