尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
基于书评文本的推荐系统:Java实现标签提取与相似度匹配
简介一套基于网络书评文本内容的个性化书籍推荐系统设计源码面向Java开发者、推荐系统研究者及自然语言处理入门者解决如何从大量书评中甄别有效评论、构建书籍标签集并计算标签关联度以实现内容精准匹配的问题。压缩包共40个文件以31个Java源文件为核心覆盖数据预处理、特征提取、标签构建与推荐计算等模块同时包含YAML配置文件、Maven构建文件、XML配置文件、Git忽略文件及说明文档整体仅65KB结构紧凑方便直接导入开发环境阅读。已有279人学习浏览。读者可从中看到书评文本清洗、分词、向量化到基于深度学习的标签集构建与关联度计算完整流程理解文本分类技术如何落地于推荐场景也能借鉴其模块划分和工程组织方式为独立设计个性化推荐模块提供可复用的代码与思路。1. 书评里挖出来的推荐系统基于文本内容的书籍匹配到底怎么落地如果你以为个性化推荐只能靠“用户评分 协同过滤”那这个项目会颠覆你的习惯。它把目光聚焦在书评文本本身——通过文本分类、清洗分词、向量化和深度学习特征提取从书评中构建出书籍标签集再计算标签集之间的关联度最终实现基于内容而非行为记录的精准匹配。这意味着即便是没有历史评分记录的冷门新书也能被合理推荐给偏好匹配的读者。这套系统用 Java 实现基于 Maven 构建入口清晰、结构紧凑适合想研究推荐算法工程化落地、文本处理链路、以及 Java 后端完整项目结构的开发者。它不只是一个算法 Demo而是一个能跑、能改、能扩展的完整系统源码。接下来我按“项目定位 → 目录拆解 → 核心链路 → 部署运行 → 常见问题 → 二次开发”这条路线把这份源码里里外外讲透。2. 系统架构与技术选型从书评文本到推荐结果的四层链路2.1 项目整体思路为什么选书评文本做推荐源传统的书籍推荐系统多数依赖用户对书籍的评分行为或者简单的浏览历史这类信号容易受主观情绪和刷分行为干扰。而书评文本是读者阅读后产出的客观内容描述信息密度远高于一个星级分数。这个项目选择从网络书评文本入手根本原因是文本里包含了读者对书籍主题、风格、情节、情感倾向的描述这些描述比分数更容易转化为标签也更能体现“客观内容特征”。系统设计上并不是直接用深度学习做端到端推荐而是把推荐问题拆成了三个子问题第一如何从杂乱的书评里筛选出有效评论第二如何从有效评论中提取标签第三如何利用标签集合计算书籍之间的关联度。这三点对应了文本分类、文本处理、深度学习特征提取三大模块三个模块串起来就是完整的推荐链路。2.2 技术栈画像Java Maven YAML 深度学习的工程组合从 upload.zip 的文件列表可以明确看出这不是一个纯算法脚本项目而是一个标准的 Java 工程核心语言是 Java。选择 Java 做推荐系统后端一个很现实的好处是跨平台和生态成熟尤其是 Spring Boot 这类框架与 YAML 配置结合后部署和调参都非常直接。项目里包含了 YAML 配置文件常见做法是用它定义数据源、模型参数、服务端口等运行时配置。构建工具用的是 Mavenpom.xml 里管理了所有依赖包括文本处理库、深度学习框架、JSON 解析工具等。项目提供了 mvnw 和 mvnw.cmd说明它带了 Maven Wrapper这样即使本地没有安装 Maven 也能直接构建。目录里的 readme.txt 会写明项目的启动方式.gitignore 则规范了版本控制时要忽略的文件类型比如 target 目录、IDE 配置文件等。2.3 文本处理链路清洗、分词、向量化的工程实现书评文本进入系统后首先要经过一段标准的文本预处理流程。这个过程在推荐系统里往往决定模型效果的上限因为输入文本的质量直接决定了标签提取的准确性。实际工程中常见的处理步骤如下先去 HTML 标签和特殊符号再统一大小写然后做中文分词或英文 tokenize之后去停用词、做词干化最后用 TF-IDF 或词向量把文本转成数字表示。具体到这份源码31 个 Java 源文件里必然包含了这一完整流程的实现类。分词之后文本会被转换成向量输入到深度学习模型中模型输出的特征再映射为书籍标签。整个链路是流水线式的每一级的输出都是下一级的输入理解这条链路是读懂源码的关键。2.4 深度学习特征提取与标签关联度计算标签集的构建是本系统的核心创新点。与传统的基于关键词的简单标签抽取不同这个项目利用深度学习模型从书评文本中提取高维特征通过学习文本特征与书籍语义之间的关联性生成更有区分度的书籍标签集。换句话说标签不再是单纯的词频统计结果而是模型根据大量书评内容学习出来的语义特征表达。有了书籍的标签集之后下一步是计算标签集之间的关联度。这一步通常使用余弦相似度或杰卡德相似系数来实现具体做法是把每本书的标签集表示成向量然后计算向量之间的相似度。关联度高的书籍会被相互推荐。这个项目里核心的推荐算法模块和数据处理模块就集中在这部分属于二次开发时最需要关注的重点区域。3. 源码结构与核心模块拆解40 个文件背后的工程组织3.1 文件清单与工程目录映射整个上传包总计 40 个文件其中 31 个 Java 源文件、3 个 YAML 配置文件、1 个 Maven 构建文件、1 个 Markdown 文档、1 个命令行脚本、1 个 XML 配置文件、1 个文本文件外加 mvnw、mvnw.cmd、.gitignore。这类结构对应的是一个典型的生产级 Java 后端工程标准目录布局如下src/main/java Java 源码主目录按包名组织 src/main/resources 配置文件存放位置YAML、XML pom.xml Maven 依赖与构建配置 mvnw / mvnw.cmd Maven Wrapper 脚本 .gitignore Git 忽略规则 readme.txt 项目说明文档从文件数量来看系统的功能划分是清晰的。31 个 Java 文件意味着包结构里至少包含 controller、service、repository/model、util、algorithm 等多个职责层。三层架构是这类 Java 工程的标配控制器层负责接口服务层承载业务逻辑算法层实现推荐核心。3.2 Java 包结构与职责划分虽然没有直接给出包名目录树但根据项目描述可以推断源码中必然包含了以下几个核心分包。推荐系统一般比普通 CRUD 项目多一个隐式分层即数据访问、业务处理、算法执行三层界限更明显。com.example.recommend ├── controller 接收请求返回推荐结果 ├── service 推荐业务编排调用算法、组装结果 ├── model 数据实体Book、Review、Tag ├── util 文本清洗、分词、相似度计算工具 ├── algorithm 深度学习特征提取、标签关联度计算 └── config 配置读取与参数绑定对于想二次开发的人来说先确认包结构再动手会事半功倍。拿到源码后第一步不是看具体算法实现而是先用 IDE 打开工程依次展开 src/main/java 下的包路径把 controller 层和 config 层先看明白因为这两层决定了系统的入口和可配置项。3.3 pom.xml 与依赖管理的关键逻辑pom.xml 是 Maven 工程的心脏里面的依赖定义直接决定了项目的技术走向。基于项目的功能描述依赖库大概率包括深度学习框架比如 DJL、DeepLearning4j 或 TensorFlow Java API、文本处理库比如 Apache OpenNLP、Stanford CoreNLP 或 HanLP、数据序列化工具Jackson 用于 JSON 解析以及 Spring Boot 相关模块。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 文本处理相关依赖 -- dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependency !-- 深度学习框架依赖 -- dependency groupIdai.djl/groupId artifactIdapi/artifactId version0.20.0/version /dependency /dependencies以上依赖只是按场景推断的示例实际以 pom.xml 为准。需要说明的是Maven 工程最容易翻车的地方就是依赖冲突和版本不兼容尤其是深度学习库与 Spring Boot 在一起时传递依赖很容易拉进来一堆用不到的包。遇到启动报错时优先执行 mvn dependency:tree 查看依赖树。3.4 配置文件中的可调参数从端口到模型权重3 个 YAML 配置文件是项目的运行参数中枢。最外层通常是 application.yml 或 application.yaml里面定义了服务端口、上下文路径、数据库连接、日志级别等基础配置。另外两个 YAML 可能分别对应算法参数和文本处理参数。server: port: 8080 recommend: text: min-word-length: 2 stopword-file: stopwords.txt model: embedding-size: 128 hidden-layer: 256 learning-rate: 0.001 similarity: threshold: 0.6这些参数的意义很直观。embedding-size 决定了文本向量化的维度维度越高模型表达能力越强但训练和推理越慢threshold 是标签关联度阈值只有相似度超过 0.6 的书籍才会进入推荐候选集。调参是二次开发时最容易见效的切入点优先动这两个值就能感受推荐结果的变化。项目里还包含 XML 配置文件常见用途是存放 MyBatis 映射文件或 Spring 的 XML Bean 定义。4. 核心算法与关键实现从有效评论到标签关联度计算4.1 有效评论甄别文本分类如何过滤低质量书评书评文本的质量参差不齐大量短评、广告灌水、无意义内容直接影响标签提取质量。这个项目引入文本分类技术先对评论做质量分级把无效评论过滤掉再进入后续处理链路。文本分类常见做法是用朴素贝叶斯或轻量级 CNN 模型训练一个二分类器识别有效评论与无效评论。public class ReviewFilter { private TextClassifier classifier; private double threshold 0.7; public boolean isEffective(String reviewText) { double score classifier.predict(reviewText); return score threshold; } }逻辑说明classifier 是训练好的分类模型predict 方法返回该评论属于“有效评论”的概率值阈值定得越高过滤越严格。这里的 threshold 建议放在 YAML 配置中不要写死在代码里。如果不配置训练好的模型常见做法是先把分类器作为一个独立小服务训练好再集成到主工程里避免首次启动时在线训练时间过长。4.2 书籍标签集构建深度学习特征到标签映射贝叶斯之后是核心的标签集构建环节。这里用深度学习模型提取书评特征然后通过特征到标签的映射层输出一组权重化的标签。一个常见实现是清洗后的书评文本经过词嵌入层编码再经过双向 LSTM 或 Transformer Encoder 提取上下文特征最后通过全连接层输出每个标签的概率分布。public class TagExtractor { private DjlModel model; private ListString candidateTags; public MapString, Double extractTags(String cleanedText) { float[] embedding model.encode(cleanedText); MapString, Double tagScores new HashMap(); for (String tag : candidateTags) { double score model.scoreTag(embedding, tag); if (score 0.5) { tagScores.put(tag, score); } } return tagScores; } }逻辑说明encode 方法把文本转成向量scoreTag 计算该向量与某个标签的匹配分超过阈值的标签进入最终标签集。这里的 score 值可以理解为模型对“这本书符合该标签”的置信度。参数说明标签集合的大小直接受阈值影响阈值调低则标签更多更杂调高则标签更精准但可能丢失语义覆盖实际调优时要找到平衡点。深度学习模型输出的标签集与人工标签体系相比优势是能捕捉同义词和上下文隐含语义。4.3 标签关联度计算余弦相似度与推荐匹配标签集构建完成后每本书都对应一个标签权重向量。这一步要做的是计算两本书之间的标签关联度从而确定哪些书适合互相推荐。常用的相似度计算方法是余弦相似度因为它在向量长度不一致时依然能衡量方向一致性。public double calculateSimilarity(MapString, Double tagsA, MapString, Double tagsB) { SetString union new HashSet(tagsA.keySet()); union.addAll(tagsB.keySet()); double dotProduct 0.0; double normA 0.0; double normB 0.0; for (String tag : union) { double valueA tagsA.getOrDefault(tag, 0.0); double valueB tagsB.getOrDefault(tag, 0.0); dotProduct valueA * valueB; normA valueA * valueA; normB valueB * valueB; } if (normA 0.0 || normB 0.0) { return 0.0; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }逻辑说明两个 Map 结构分别承载两本书的标签权重先取并集保证所有标签都被纳入计算未出现的标签默认权重为 0。点积除以两个向量的模长乘积即得余弦相似度数值范围在 0 到 1 之间。相似度大于配置阈值的书籍会被加入推荐候选集合。这套方法在工程上的表现非常稳定配合阈值调参推荐的精确率与召回率都比较好控制。与协同过滤相比它不需要大量用户行为数据只要书评文本足够就能工作很契合这个项目的数据场景。4.4 系统接口调用链路一次请求的完整生命周期把上面的模块串起来后一次推荐请求从前端到返回结果的完整链路是这样的用户通过 REST 接口发起推荐请求Controller 层接收请求参数后调用 Service 层Service 层先把用户输入的偏好文本交给文本处理模块做清洗和分词然后交给标签提取模块生成用户偏好标签集再与图书库中每本书的标签集逐一计算相似度按相似度降序排序取 Top-N 返回给前端。RestController RequestMapping(/api/recommend) public class RecommendController { private final RecommendService recommendService; public RecommendController(RecommendService recommendService) { this.recommendService recommendService; } GetMapping(/books) public ListBook recommend(RequestParam String preferenceText) { return recommendService.recommendByText(preferenceText); } }逻辑说明preferenceText 是用户输入的偏好描述比如“我最近喜欢科幻和未来主题的书”收到请求后系统把它当作一条“用户书评”走同样的处理链路——清洗、分词、向量化、提取标签然后用这个临时生成的标签集去和书库里的每本书做相似度匹配。参数说明preferenceText 是核心入参注意不要传无关字符过长输入在进入模型前会被截断具体截断长度在文本处理器配置里定义。这样一个接口的实际价值是让整个推荐系统可以非常方便地嵌入到任何上层应用中无论是网页版还是客户端只需要请求这一个接口就能获得个性化推荐结果。5. 从源码到运行构建、启动与参数验证5.1 运行环境准备与 Maven 构建在本地跑起这个项目前需要准备 Java 开发环境。常见的配置是 JDK 8 或 JDK 11具体版本看 pom.xml 里 maven.compiler.source 标签的值。需要说明的是如果项目使用了较新版本的 Spring BootJDK 版本要求相应提高。准备妥当后进入项目根目录执行 Maven 构建操作。./mvnw clean package -DskipTests逻辑说明该命令会跳过测试并打包出可执行的 jar 文件构建产物出现在 target 目录下。mvnw 是 Maven Wrapper 脚本它保证了即使本地没有全局安装 Maven也能下载指定版本并完成构建。如果构建过程中报依赖下载失败检查网络或切换到国内 Maven 镜像源。如果使用 Windows 环境脚本变成 mvnw.cmd。5.2 配置数据源与启动应用项目如果涉及数据库存储书评和书籍信息需要在 YAML 配置文件中设置数据源。常见配置是使用 MySQL默认端口 3306需要提前建好数据库并导入项目附带的数据脚本。如果项目设计为纯内存模式运行即数据在启动时从本地文件加载则只需要把数据文件放在约定目录即可。java -jar target/book-recommend-system-1.0.0.jar启动成功后控制台会输出 Spring Boot 的启动横幅并显示监听的端口号默认一般是 8080。这时可以在浏览器访问项目提供的 API 接口文档路径或者直接用 curl 测试推荐接口。需要留意的是运行日志中的报错信息最常见的两类问题是端口占用和配置缺失端口被占用时修改 server.port 值配置缺失时检查 YAML 文件里的必要键是否都存在。5.3 接口调用与推荐结果验证启动完成后验证系统的核心功能是否正常用一条 curl 命令请求推荐接口即可。这部分是确认系统可用的最快路径。curl http://localhost:8080/api/recommend/books?preferenceText我喜欢科幻和未来主义的书如果返回的 JSON 数组包含书籍信息和关联度分数说明从文本处理到标签提取再到相似度计算的整条链路已经跑通。可以尝试更换不同的偏好文本查看结果变化观察推荐结果是否与输入偏好语义相关。如果返回空数组说明相似度阈值设置过高可能导致所有候选书的关联度都低于阈值这时把配置里的 threshold 调低后重启观察。6. 常见问题排查与踩坑记录6.1 启动时报 ClassNotFoundException 或 NoSuchMethodError现象应用启动到一半抛出 ClassNotFoundException或者在调用深度学习模型时出现 NoSuchMethodError。原因这几乎都是依赖版本冲突引起的。深度学习库和 Spring Boot 对某些基础库如 Guava、Netty的版本要求不一致Maven 在解析传递依赖时选中了不兼容的版本。解决用 mvn dependency:tree 查看依赖树找出冲突的依赖项在 pom.xml 中显式排除不需要的传递依赖或指定统一版本号。6.2 中文书评标签提取结果全为空现象接口正常启动但输入中文偏好文本后返回的推荐结果为空日志里可以看到标签集合为空。原因中文分词配置有误导致所有文本在分词后被过滤光了。这个项目如果内置的中文分词器词典文件路径配置不正确常见表现就是整条评论被切成了一个词然后又被停用词表过滤。解决检查 YAML 配置里分词器的词典路径确认 stopword 文件编码是 UTF-8 且不包含正常业务词汇。分词器的输入文本长度建议先做截断处理。调试时打印中间分词结果能快速定位是分词错误还是阈值过滤问题。6.3 相似度阈值默认值过高导致推荐结果为空现象系统能正常返回响应但返回的书籍列表始终是空数组切换偏好文本也一样。原因标签关联度阈值设置过高模型提取的标签权重比较保守所有候选书与输入偏好之间的余弦相似度都低于阈值。解决找到 YAML 配置中的 threshold 参数从 0.6 下调到 0.3 或 0.4重启服务后重新测试。这里要说明的是阈值过低会导致推荐结果泛化什么书都推经过调试后常见取值落在 0.4 到 0.6 之间比较合理。也可以用测试脚本在多次阈值下对比结果数量选一个符合推荐需求的值。6.4 Maven 构建下载依赖过慢或失败现象执行 ./mvnw clean package 时长时间卡住或报超时错误。原因Maven 默认从中央仓库下载依赖而部分依赖源在国内访问较慢尤其是深度学习框架的依赖体积较大。解决在项目根目录的 .mvn 目录下添加 settings.xml配置阿里云 Maven 镜像。这个操作不用改项目代码只影响本机的依赖下载路径。配置完成后重新执行构建命令如果仍有失败检查本地仓库的缓存是否有损坏的下载记录清理 ~/.m2/repository 下对应失败目录再重试。7. 二次开发思路把通用推荐内核换成你的业务场景拿到这份源码最有价值的改造方向不是调参而是把整个推荐内核从“书籍领域”抽象成“通用文本内容推荐引擎”。核心原因是推荐链路中的清洗、分词、向量化、标签提取、相似度计算这些环节与具体的推荐对象无关唯一需要换掉的是标签体系与数据源。第一步是替换数据源。把书籍与书评的加载逻辑改成通用的“内容条目 用户文本评论”结构例如换成电影影评推荐或技术文章推荐。Step 是保持 controller 和 service 层不动只改动数据访问层让 Book 实体变成泛化的 ContentItem。第二步是替换候选标签集。把原本的书籍标签表换成目标领域的关键词表比如电影领域换成“悬疑、爱情、科幻、纪实”等标签。第三步是调整模型的输入端和输出端。因为标签集变化深度学习模型的输出层大小也要对应调整需要重新训练或微调映射层。针对微调模型这部分代码层面需要关注的类在 algorithm 包下。假设目前模型输出的向量维度是 128改造后你可以直接修改配置项的 embedding-size 来适配新的文本规模。我一般会在 TagExtractor 里增加一个 rebuildIndex 方法在建库阶段批量重新提取所有候选项的标签集并缓存计算结果这样运行期推荐接口就不必每次重新计算全库相似度。public void rebuildIndex() { ListContentItem items contentRepository.findAll(); for (ContentItem item : items) { MapString, Double tags tagExtractor.extractTags(item.getDescription()); item.setTagVector(tags); contentRepository.update(item); } }这个方法的作用是预计算所有内容项的标签向量将计算结果更新到存储中。这样用户发起推荐请求时只计算用户标签集和所有预计算标签向量的相似度耗时从全链路缩短到一次矩阵运算。调试时建议把相似度计算结果输出到日志对比看哪些标签对匹配结果贡献最大借此反向调整标签生成逻辑。也可以增加一个 A/B 测试接口同时运行新旧两套算法参数比较同一批偏好输入的推荐列表差异。代码改动完成后沿着“数据导入 → 重建索引 → 调用推荐接口”这条链路跑一遍验收。最好用文本相近但标签不同的内容条目做测试验证过滤掉相似文本的能力。从那以后我每次改造这种推荐系统都会先在预计算索引层跑一次全量数据再调接口因为很多标签质量问题是索引层直接暴露出来的。希望这套拆解思路能帮你在自己的场景里少走几步弯路。本文还有配套的精品资源点击获取
RELATED

相关推荐

开源工具链搭建代码审查闭环:从提交前检查到合并门禁的落地实践

开源工具链搭建代码审查闭环:从提交前检查到合并门禁的落地实践

在研发团队里聊代码审查,讨论到最后往往不是“要不要做”,而是“怎么让审查真正落地”。代码审查这个东西,大家都知道重要,可真到了交付压力大的时候,最先被砍掉的流程往往就是它。我自己做过一段时间的研发效能和工程…

📅 2026/10/19 17:30:18
Grok国内使用指南:个人聊天入口与企业模型接入的两套答案

Grok国内使用指南:个人聊天入口与企业模型接入的两套答案

1. 先搞清楚问题到底在问什么“Grok 国内能不能用”这句话,我在不同场合听过至少几十遍,但每次追问一句“你具体想拿它干什么”,答案就分叉了。有人是想找个网页版聊天窗口,像用普通对话助手那样问点日常问题;有人是想…

📅 2026/10/19 17:30:18
从手动到自动:MCP与AI在日志诊断中的工程实践

从手动到自动:MCP与AI在日志诊断中的工程实践

1. 凌晨两点的告警电话:我决定把日志诊断交给AI先还原一个真实场景。某天凌晨两点半,手机震动,线上服务异常告警。我爬起来打开终端,熟练地敲下那串早就刻进肌肉记忆的命令:先看错误率曲线,再进日志平台搜E…

📅 2026/10/19 17:30:18
MORE NEWS

更多资讯

📰

终端安全检测原理与免杀对抗技术全解析

1. 这不是“绕过检测”的捷径,而是理解防御机制的起点 “免杀”这个词在技术社区里常被误读成某种黑箱技巧或玄学操作——仿佛只要套用某个混淆模板、换几行加密密钥,就能让程序在终端上悄然落地、无声执行。但实际工作中,我见过太多人花三天…

📰

Qt QFile文件读写全攻略:从打开模式到跨平台编码与性能优化

做Qt开发这几年,我接手过的跨平台桌面工具里,十个有七个绕不开文件读写。但说实话,真正能把QFile用明白的人比想象中少。我见过有人用readAll()读一个几百MB的文件把内存直接打爆,有人写的中文配置在Windows上打开全是乱码&#x…

📰

Product Hunt热榜复盘:从票数与评论区看产品冲榜策略

我每天早上都会打开 Product Hunt 的热榜页面,这已经是雷打不动的习惯。2026-03-31 的这份榜单,我点开之前其实没抱太大期待——毕竟“每日热榜”四个字听起来就像流水账。但真正逐条把当天上榜的产品翻完、把评论区看完、把背后几位创始人的发布策略捋清…

📰

C#类成员详解:字段、属性、构造函数及面试要点

1. 为什么"简述类成员"值得你认真准备 我以前参与技术面试时,经常用"简述类的成员"这道题做开场。原因很简单:它听起来基础得不像话,但实际考察的层次非常深。一个候选人如果连这道题都答得有结构、有主次,那…

📰

达梦8 DSC共享存储集群在VMware虚拟化环境中的部署实践与踩坑指南

1. 项目拆解:达梦8 DSC在虚拟化环境中的定位和难点1.1 达梦8 DSC到底是什么达梦8的DSC全称是Data Shared Cluster,也就是共享存储集群方案。它的逻辑跟很多人熟悉的Oracle RAC类似:两个或多个实例节点,通过共享存储访问同一份数据…

📰

自托管Sentry升级遇Django迁移冲突:table already exists排查与修复

自托管 Sentry 升级跑到一半,终端突然甩出一行 django.db.utils.OperationalError: table "labels_manager_label" already exists ,紧接着就是 Sentry is attempting ... 的提示。第一次碰到时,我也愣了一下:明明是…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬