news 2026/9/26 13:03:40

SpringBoot+Vue热门文创推荐平台:热度算法与前后端联调实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue热门文创推荐平台:热度算法与前后端联调实战

简介:这是一个基于Spring Boot与Vue.js的热门文创内容推荐平台项目,面向需要完成毕业设计、课程设计、大作业或工程实训的开发者,也适合希望学习前后端分离架构的入门与进阶者。项目源码已经过调试,压缩包约36.31MB,内部主要包含Java后端源码、Vue前端代码、SQL初始化脚本及配套说明文档,便于导入IDE后结合数据库快速搭建运行环境。整体采用SpringBoot+Vue实现前后端分离,覆盖接口调用、业务逻辑处理、数据持久化和页面交互等关键环节,目录结构清晰,易于按模块阅读与改造。开发环境基于JDK8、MySQL5.7、Tomcat7和Maven3.3.9,可复现性较强;SQL脚本与说明文档能帮助快速完成数据库初始化及部署配置,有效降低环境搭建门槛。目前已有79人浏览学习,适合作为毕业设计参照、课程设计素材或全栈实战练手项目,也可在其基础上继续扩展推荐功能或后台管理模块,进行二次开发与功能迭代。

1. 解压后这堆代码到底做了什么:基于SpringBoot+Vue的热门文创内容推荐平台的第一个30分钟

拿到「5b263基于SpringBoot+Vue的热门文创内容推荐平台.zip」这种压缩包,第一件事不是急着解压跑demo,而是先弄清楚里面装的是一个能交货的项目,还是只供课程作业参考的骨架。前后端分离已经是SpringBoot+Vue项目的默认玩法:后端SpringBoot把推荐算法、接口和缓存扛下来,前端Vue负责把推荐结果渲染成轮播图、瀑布流或信息流。所谓“热门文创内容推荐”,核心不是安卓壳子套网页,而是服务器端有一套能对文创商品、展览、文章按热度打分排序的规则,前端再通过接口把结果展示出来。如果你正准备接手或者复现这类平台,这篇笔记能帮你把运行路径、打分逻辑、联调技巧一次捋清楚。

2. 先拆后装:从SpringBoot后端理解热门推荐的数据流与打分规则

2.1 为什么SpringBoot+Vue足够支撑“热门文创”这个场景

很多第一次做推荐平台的人会纠结要不要上Elasticsearch、Redis、Spark这些重型组件。对“热门文创内容推荐”这个场景,我的结论是:SpringBoot加MySQL加Redis够用,Vue负责展示,不需要一上来就引入大数据计算。

文创内容的特点是更新频率中等、数量不是特别大,热门推荐本质上是把用户行为数据(浏览量、收藏量、点赞量)按照时间衰减规律计算出一个热度值,再做倒序排列。这个计算量用SpringBoot的定时任务加SQL分页就能顶住。相对而言,如果上了Spark或者Flink,反而把部署和运维复杂度拉高,对中小型文创平台来说不划算。

SpringBoot在这个项目里的职责是提供REST接口、执行打分定时任务、管理MySQL中的数据、用Redis缓存热门榜单结果。Vue则负责向用户呈现“今日热门”“本周文创精选”等内容位。前后端分离的好处在于,后端接口和前端页面可以分别迭代,热门算法调整不牵扯前端重构,页面改版不需要动后端逻辑。

2.2 后端模块与数据模型:评分表、点击表、内容表的关系

在正式写代码前,先看数据模型。一个典型的热门文创推荐平台至少需要四张核心表:

CREATE TABLE `creative_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '文创内容ID', `title` varchar(200) NOT NULL COMMENT '标题', `category` varchar(50) DEFAULT NULL COMMENT '分类:展览/非遗/文创商品/文章', `cover_url` varchar(500) DEFAULT NULL COMMENT '封面图URL', `publish_time` datetime DEFAULT NULL COMMENT '发布时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文创内容表'; CREATE TABLE `user_behavior` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `item_id` bigint(20) NOT NULL COMMENT '文创内容ID', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `behavior_type` tinyint(4) NOT NULL COMMENT '行为类型:1浏览 2收藏 3点赞 4分享', `create_time` datetime DEFAULT NULL COMMENT '行为时间', PRIMARY KEY (`id`), KEY `idx_item_time` (`item_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表'; CREATE TABLE `hot_score_cache` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `item_id` bigint(20) NOT NULL COMMENT '内容ID', `score` decimal(10,2) DEFAULT NULL COMMENT '计算出的热度分', `calc_time` datetime DEFAULT NULL COMMENT '计算时间', PRIMARY KEY (`id`), KEY `idx_calc_time` (`calc_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='热度分缓存表';

这三张表的关系非常直白:creative_item存的是文创内容本身,比如一件非遗手作、一篇文创深度文章或一场线上展览;user_behavior记录用户对这些内容做过什么操作;hot_score_cache是定时任务算出来的临时热度结果。

其中user_behavior表的索引很关键。很多新手抄作业时只建主键索引,到线上跑热门榜单统计时,一张百万级行为表按item_id分组汇总能卡到好几秒。idx_item_time这个联合索引是让定时任务从“最近7天行为数据”里捞出统计结果的核心。这里有个经验:行为表的create_time一定要用datetime类型且默认CURRENT_TIMESTAMP,否则前端联调时传时间格式会有各种幺蛾子。

3. 被浏览权重怎么玩:SpringBoot热门榜单接口的最小实现

3.1 热门评分公式:用衰减和权重把“最近的热门”算出来

所谓热门,不能简单按总量排序,否则老内容永远压在头上,新内容永无出头之日。常见的做法是引入“时间衰减因子”,让越久远的行为权重越低,此分数随时间推移以指数衰减。公式可以简化为:

热度分 = 浏览量权值 + 收藏量权值 + 点赞量权值 + 分享量权值,其中每个权值还要乘以时间衰减系数。

对应的Java实现如下:

/** * 计算单个文创内容的热度分 */ public Double calculateHotScore(Long itemId, List<UserBehavior> behaviors) { // 行为权重配置,实际开发中放到application.yml里更方便调整 double browseWeight = 1.0; double favoriteWeight = 3.0; double likeWeight = 2.5; double shareWeight = 4.0; // 衰减系数:通常取0.95~0.98,表示每过一天的权重折损 double decayFactor = 0.96; double score = 0.0; for (UserBehavior behavior : behaviors) { long daysBetween = Duration.between( behavior.getCreateTime().toInstant(), LocalDateTime.now().toInstant() ).toDays(); // 超过30天的行为直接忽略,减少计算量 if (daysBetween > 30) { continue; } double ageWeight = Math.pow(decayFactor, daysBetween); switch (behavior.getBehaviorType()) { case 1: // 浏览 score += browseWeight * ageWeight; break; case 2: // 收藏 score += favoriteWeight * ageWeight; break; case 3: // 点赞 score += likeWeight * ageWeight; break; case 4: // 分享 score += shareWeight * ageWeight; break; default: break; } } return score; }

这个实现的关键参数有三个:行为权重、衰减系数decayFactor、时间窗口。行为权重决定哪种营销动作对热度拉升最有效,decayFactor决定热门的“新鲜度”,时间窗口决定参与计算的数据范围。

这里有一个新手常踩的坑:Duration.between()对时间敏感,如果数据库中create_time存的是本地时间,而服务器时区设置成UTC,算出daysBetween会差8小时,导致评分出现很离谱的边界抖动。解决办法是在SpringBoot的application.yml中把JDBC连接串显式加上serverTimezone=Asia/Shanghai,并保证MySQL会话时区与JVM时区一致。

3.2 热门榜单接口:Redis缓存+定时任务+Controller三层实现

评分逻辑写完之后,需要把它包装成一个可调用的接口。常见做法是三层结构:定时任务批量算分、Redis缓存结果、Controller接收参数并返回榜单。

/** * 定时任务:每30分钟重算一次热度分,避免每次请求都去跑全量行为表 */ @Component public class HotScoreScheduler { private final String HOT_LIST_KEY = "hot:creative:list"; @Autowired private StringRedisTemplate redisTemplate; @Autowired private CreativeItemMapper itemMapper; @Scheduled(cron = "0 */30 * * * ?") public void refreshHotList() { // 1. 查出所有在售/上架的文创内容 List<CreativeItem> items = itemMapper.selectAllPublished(); // 2. 对每个item计算热点分,并缓存到Redis for (CreativeItem item : items) { List<UserBehavior> behaviors = behaviorMapper.selectByItemAndTime( item.getId(), LocalDateTime.now().minusDays(30)); Double score = calculateHotScore(item.getId(), behaviors); // 3. ZSet存储,score直接作为排序权重 redisTemplate.opsForZSet().add(HOT_LIST_KEY, item.getId().toString(), score); } // 4. 保留前100条,淘汰尾部数据 redisTemplate.opsForZSet().removeRange(HOT_LIST_KEY, 100, -1); } }
/** * 热门榜单接口:支持分页与分类过滤 */ @RestController @RequestMapping("/api/creative") public class CreativeRecommendController { @Autowired private StringRedisTemplate redisTemplate; @GetMapping("/hot") public Result<List<CreativeItemVO>> hotList( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String category) { // 从Redis ZSet倒序取出当前热门ID Set<String> hotIds = redisTemplate.opsForZSet().reverseRange( "hot:creative:list", (long) (page - 1) * size, (long) page * size - 1); if (hotIds == null || hotIds.isEmpty()) { return Result.success(Collections.emptyList()); } List<Long> ids = hotIds.stream().map(Long::valueOf).collect(Collectors.toList()); List<CreativeItemVO> items = itemMapper.selectByIds(ids); // 按Redis中的顺序重新排列,避免MySQL in查询乱序 Map<Long, CreativeItemVO> itemMap = items.stream() .collect(Collectors.toMap(CreativeItemVO::getId, item -> item)); List<CreativeItemVO> sorted = new ArrayList<>(); for (String idStr : hotIds) { CreativeItemVO vo = itemMap.get(Long.valueOf(idStr)); if (vo != null && (category == null || category.equals(vo.getCategory()))) { sorted.add(vo); } } return Result.success(sorted); } }

第一个代码块说明了定时任务的核心调用逻辑。用Redis的ZSet(有序集合)来存热度榜单,好处是ZSet天然支持按分数倒序排列,reverseRange()接口直接可以取分页数据,不用再额外做排序。第二个代码块暴露接口给前端。注意看第3章代码中的Result类,这是SpringBoot项目里常见的统一响应包装,实际实现时用HashMap也可以,但在大项目中提前统一结构能省很多联调扯皮。

参数说明方面,cron = "0 */30 * * * ?"表示每30分钟执行一次。文创内容热度的实时性要求不高,半小时的缓存窗口完全能接受。如果换成新闻类推荐,这个时间间隔就要缩到5分钟以下。

4. Vue端怎么把推荐做成轮播:路由、组件、请求与联调细节

4.1 Vue路由与页面骨架:把“推荐首页”接入VueRouter并设置动态参数

SpringBoot接口准备就绪后,Vue端要做的第一件事是搭路由。热门文创推荐平台一般会把首页、文创详情页、分类页分开,其中首页的推荐位是最核心的。

// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('../views/HomeView.vue'), meta: { title: '热门文创推荐' } }, { path: '/detail/:id', name: 'Detail', component: () => import('../views/DetailView.vue'), meta: { title: '文创详情' } }, { path: '/category/:type', name: 'Category', component: () => import('../views/CategoryView.vue'), meta: { title: '分类浏览' } } ] const router = createRouter({ history: createWebHistory(), routes }) export default router

这里使用路由懒加载() => import(),这是大型SpringBoot+Vue项目的基本操作。懒加载的效果是首屏只加载首页需要的JS文件,而不是把整个项目所有页面的代码一次性打包给浏览器。对于文创推荐平台这种以图片为主的场景,首屏加载速度直接决定用户会不会留下来多看几个推荐位。

动态路由参数/detail/:id与/category/:type是联调时的常见关注点。比如点击轮播图进入详情页,detail/123中的123就是后端返回的itemId。很多新手从详情页返回首页时会发现滚动位置丢失,那是没有在HomeView组件里做keep-alive处理,这个后面踩坑部分还会提到。

4.2 推荐位组件:轮播图或瀑布流的请求与渲染

首页推荐位的组件化写法,是这套东西里最容易出效果的地方。这里给出一个基于Element Plus的轮播图封装方案,它替代了传统“图片+文字链接”的堆砌,让推荐位有“运营感”。

<template> <div class="hot-recommend"> <el-carousel height="320px" :interval="5000" indicator-position="outside"> <el-carousel-item v-for="item in hotItems" :key="item.id"> <div class="recommend-card" @click="goDetail(item.id)"> <img :src="item.coverUrl" :alt="item.title" /> <div class="recommend-info"> <h3>{{ item.title }}</h3> <span class="category-tag">{{ item.category }}</span> <span class="score">热度值 {{ item.score }}</span> </div> </div> </el-carousel-item> </el-carousel> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import axios from 'axios' const router = useRouter() const hotItems = ref([]) const loading = ref(false) // 注意这里的baseURL应与SpringBoot接口前缀保持一致 const apiClient = axios.create({ baseURL: '/api', timeout: 10000 }) const fetchHotItems = async () => { loading.value = true try { const response = await apiClient.get('/creative/hot', { params: { page: 1, size: 5 } }) hotItems.value = response.data.data } catch (error) { console.error('推荐位加载失败', error) } finally { loading.value = false } } const goDetail = (id) => { router.push({ path: `/detail/${id}` }) } onMounted(() => { fetchHotItems() }) </script>

这一段代码的关键点是baseURL: '/api'。在开发环境下,Vue默认端口是5173,SpringBoot接口跑在8080,跨域问题就出在这里。常规操作是在vite.config.js中配置代理,把/api开头的请求转发到后端:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

如果生产环境前后端不在同一个域名下,则需要在Nginx层做同样的反向代理配置。这里要当心:代理只解决开发环境的跨域,真正上线时如果前端部署在https://www.example.com,后端在https://api.example.com,那么必须让后端接口支持CORS,且前端请求要带withCredentials: true。

4.3 页面接口联调时的时间炫耀:格式化后端返回的时间不再头大

后端返回的publishTime通常是一串时间戳或者带T的ISO格式字符串。Vue中显示“发布于xx小时前”最常见的方式是写一个formatTime过滤器。这里不争论要不要引入day.js,直接用原生Date可以解决大部分需求:

export function formatTime(dateTimeStr) { const date = new Date(dateTimeStr) const now = Date.now() const diff = now - date.getTime() // 小于1小时 if (diff < 3600 * 1000) { return `${Math.floor(diff / 60000)}分钟前` } // 小于24小时 if (diff < 24 * 3600 * 1000) { return `${Math.floor(diff / (3600 * 1000))}小时前` } // 小于7天 if (diff < 7 * 24 * 3600 * 1000) { return `${Math.floor(diff / (24 * 3600 * 1000))}天前` } // 过久的内容直接返回完整日期 return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}` }

这里有一个非常隐蔽的坑:SpringBoot默认返回的LocalDateTime会被Jackson序列化成"2025-02-18T14:30:00"这种带T的格式,Vue的new Date()可以直接解析。但如果后端字段类型是Date且配置了spring.jackson.date-format,返回的可能是时间戳数字,那就要注意单位是秒还是毫秒。秒需要乘以1000才能交给new Date(),这是联调时最容易出现“显示NaN”的原因。

5. 避坑记录:SpringBoot+Vue联调中最常翻车的5个位置

5.1 页面白屏:路由懒加载与Element Plus按需引入冲突

现象:npm run build后部署到Nginx,首页能打开,但点击进入详情页时白屏,控制台报“Cannot find module chunk”。

原因:路由懒加载和Element Plus组件库按需引入配合不当。常见于主包引用ElCarousel而子页面引用ElButton,当构建分包时,公共组件没有被正确提取,子路由加载时找不到对应的JS chunk。

解决:确认使用unplugin-vue-components的ElementPlusResolver实现按需引入,同时不要手动在main.js里全量引入ElementPlus,两套方案同时用会导致chunk依赖错乱。另外Nginx的try_files也要配index.html,否则刷新子路由时返回404,页面自然白屏。

5.2 前后端联调时“已登录”状态丢失:Cookie跨域配置

现象:前端登录成功,切换到其他接口后提示未授权;或者SpringBoot后端打印出的HttpSession每次都是新会话。

原因:前端与后端域名不同,请求拦截器里没有携带Cookie。Vue的axios默认跨域请求不带Cookie,而后端SpringBoot又没有配置Access-Control-Allow-Credentials: true,所以登录态根本传不到后端。

解决:前端axios实例加上withCredentials: true,后端配置CORS时不能让AllowedOrigin为*,必须指定具体前端域名,同时开启allowCredentials。Nginx代理模式下,还要检查proxy_set_header Cookie $http_cookie;是否被注释掉。这个坑非常隐蔽,往往查一个下午最后发现是Nginx把Cookie吞了。

5.3 热门推荐接口返回404:@PathVariable和路由参数对不上

现象:点击详情页请求/api/creative/detail/123,SpringBoot返回404,或者前端请求发出的是/api/creative/detail?id=123。

原因:前端router.push({ path: '/detail/' + id })拼的是路径参数,但后端Controller写的是@RequestParam("id") Long id,两者形式不一致。

解决:后端详情接口统一使用@PathVariable:

@GetMapping("/detail/{id}") public Result<CreativeItemDetailVO> detail(@PathVariable("id") Long id) { ... }

联调时最简单的方式是看浏览器Network面板中URL的形状,后缀是/123还是?id=123一目了然。这个问题本质上是前后端约定的问题,建议项目启动时就把接口文档格式定死,省得后面反复调。

5.4 接口正常但页面显示“数据不在”:Long型主键精度丢失

现象:列表接口正常返回,但点击详情页后显示的内容与列表不一致,甚至跳到默认404页。

原因:Java后端主键是Long类型,当ID超过JavaScript的Number.MAX_SAFE_INTEGER(9007199254740991)时,JSON序列化后前端拿到的是精度丢失的数字。

解决:在SpringBoot后端的实体类主键字段上增加注解:

@JsonSerialize(using = ToStringSerializer.class) private Long id;

如果项目使用Jackson,也可以配置全局规则,把Long统一转成String输出。主键不参与计算,字符串形式对前端更友好。

5.5 定时器跑完后榜单没变化:Redis序列化策略不一致

现象:重启SpringBoot后第一次接口返回正常,之后修改了数据库数据,重跑定时任务,榜单依然是重启前的数据。

原因:SpringBoot默认的RedisTemplate使用JDK序列化,而StringRedisTemplate使用String序列化。如果定时任务往Redis写入时用的是StringRedisTemplate,读取时用的是RedisTemplate,两个客户端存取键值时的编码不同,导致实际读不到新数据。

解决:统一使用StringRedisTemplate,假若你要主动用RedisTemplate,则需显式设置序列化器。推荐方案是封装一个专用的RedisService,内部统一使用StringRedisTemplate,这样避免不同开发人员直接操作RedisTemplate造成风格混用。

6. 收尾技巧:把人工运营权重和后端验证方法拧进推荐逻辑

热门平台活下来的关键在于推荐位不只是算法说了算,还需要人工干预。文创内容有其特殊性,比如某件非遗作品正在参加线下展览,短期曝光价值极高,但它的基础浏览数据远低于老内容。常见操作是给creative_item表加一个manual_weight字段,在打分时叠加一个“运营加权重”,让编辑推的内容能临时置顶。

public Double calculateFinalScore(CreativeItem item, Double baseHotScore) { // 运营加权:范围建议0~10,数值越大越优先展示 Double manualWeight = item.getManualWeight(); if (manualWeight == null) { return baseHotScore; } // 加一个时间窗,避免手动置顶内容永久霸榜 if (item.getWeightExpireTime() != null && item.getWeightExpireTime().isAfter(LocalDateTime.now())) { return baseHotScore * (1 + manualWeight); } return baseHotScore; }

这是我在实际项目里运用的方式:运营给出的不是固定置顶而是加权系数,保证了推荐列表仍然是“热度为主,人工为辅”,不会破坏算法整体的排序结构。

验证推荐效果是很多团队忽略的一步。上线后看两个指标就够:推荐位内容的点击率(CTR)和用户停留时长。以一周为周期对比推荐位改版前后的数据,发现点击率提升超过10%才算有效果。如果只是换了皮肤没有数据变化,建议去检查推荐内容是否符合目标用户的喜好,而不是继续调权重。

最后说一个调试习惯:SpringBoot项目排查热门接口问题,别只看Controller日志。把application.yml中的日志级别调到debug,重点观察HotScoreScheduler的执行时间和Redis操作耗时。我曾遇到过定时任务被Spring的默认单线程执行器阻塞,原因是同时跑了多个@Scheduled任务且没有配置线程池,热门榜单整整两个小时没更新,线上用户全都看到了旧数据。后来在配置类里增加TaskScheduler线程池参数,问题才彻底解决。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 13:02:56

2026 企业 AI 办公工具选型指南:从需求匹配到平台盘点

一、企业AI办公工具选型的常见误区与核心逻辑 很多企业在启动AI办公工具调研时&#xff0c;习惯沿用过去采购传统办公自动化系统的思路&#xff0c;对着功能清单逐行打勾&#xff0c;优先对比单席位采购成本&#xff0c;或是直接选择日常消费级产品知名度最高的选项&#xff0c…

作者头像 李华
网站建设 2026/9/26 13:02:42

营销应届生简历如何用AI项目加分?三类高价值项目实战指南

每年校招季我都会翻几百份营销岗简历&#xff0c;今年最明显的变化是&#xff1a;几乎每个人都在技能栏里写了“AI”“大模型”“智能营销工具”。但真正能坐下来把AI怎么用讲清楚的人&#xff0c;十个里面未必有一个。很多人觉得“用过ChatGPT、Midjourney、剪映AI”就算证明了…

作者头像 李华
网站建设 2026/9/26 13:02:22

基于MATLAB的异构固定翼无人机集群协同搜索与避障仿真

1. 项目概述与整体思路拆解1.1 核心需求解析先说清楚这个项目到底是干什么的&#xff1a;一套在复杂环境下&#xff0c;让多架性能不同的固定翼无人机组成集群&#xff0c;通过自适应决策完成协同搜索&#xff0c;同时具备避障能力的Matlab仿真方案。标题里的“异构”是灵魂&am…

作者头像 李华
网站建设 2026/9/26 13:01:32

椅子人物桌子目标检测数据集:YOLOv8训练与ONNX部署全流程

简介&#xff1a;这份椅子人物桌子目标检测数据集面向计算机视觉开发者、智能家居与安防方向的研究人员&#xff0c;以及需要室内场景物体识别训练素材的高校师生&#xff0c;帮助解决椅子、人物、桌子三类常见目标的检测模型训练与验证问题。压缩包共854个文件&#xff0c;包含…

作者头像 李华
网站建设 2026/9/26 13:01:32

中国开源年会COSCon参会指南:从注册到提交PR的实战攻略

又到一年开源圈的大型聚会时段。每年这时候&#xff0c;群消息列表里总会出现一堆类似"COSCon 票怎么买""今年哪个分论坛值得蹲"的问题。作为从第四届开始就年年守在会场的半个老油条&#xff0c;我可以很负责任地说&#xff1a;中国开源年会&#xff08;C…

作者头像 李华