1. 项目概述在个人博客中添加每日一句功能是个提升用户体验的好方法。这个功能每天展示不同的中英文双语名言警句既能增加网站的内容丰富度又能给访客带来一些思考和启发。我最近在自己的博客上实现了这个功能经过几周的运行测试效果相当不错。这个功能的实现涉及前后端多个技术点包括API代理、数据缓存、响应式设计和交互优化等。最核心的是采用了双层缓存架构服务端使用ConcurrentHashMap缓存API返回的数据客户端使用localStorage存储当天的句子这样能最大程度减少对第三方API的调用提高响应速度。2. 功能需求与技术选型2.1 核心功能需求在设计这个功能时我确定了以下几个核心需求每日自动更新句子内容需要每天自动更换不需要人工干预高可用性即使第三方API不可用也要有备用方案保证功能正常性能优化避免重复请求API减少网络开销响应式设计在各种设备上都能良好显示交互体验长句子支持展开/收起方便阅读2.2 技术选型分析基于上述需求我选择了以下技术栈后端Spring Boot RestTemplateSpring Boot提供了快速开发REST API的能力RestTemplate适合简单的HTTP请求场景前端原生JavaScript CSS3不依赖任何框架减少资源加载CSS3提供了丰富的样式和动画效果数据源有道词典每日一句API提供高质量的中英文双语内容接口相对稳定缓存策略服务端ConcurrentHashMap客户端localStorage选择这些技术主要考虑以下几点轻量级不会给博客增加太多负担成熟稳定减少维护成本性能良好响应速度快3. 后端实现详解3.1 API接口设计后端API设计遵循RESTful风格返回JSON格式数据GetMapping(value /api/daily-quote, produces application/json;charsetUTF-8) ResponseBody public MapString, String getDailyQuote() { // 实现逻辑... }接口返回的数据结构包含三个字段en英文句子cn中文翻译success是否成功获取数据这样设计的好处是统一了API响应格式前端可以方便地判断请求是否成功支持后续扩展更多字段3.2 缓存策略实现3.2.1 ConcurrentHashMap缓存服务端使用ConcurrentHashMap作为内存缓存private static final ConcurrentHashMapString, MapString, String quoteCache new ConcurrentHashMap(); String today java.time.LocalDate.now().toString(); if (quoteCache.containsKey(today)) { return quoteCache.get(today); }选择ConcurrentHashMap的原因线程安全支持多线程并发访问适合Web应用场景高性能内存操作速度快减少IO开销简单易用不需要额外依赖Spring环境原生支持缓存键使用当前日期字符串这样每天会自动生成新的缓存条目旧数据会被自动覆盖。3.2.2 API请求实现调用有道词典API的代码String apiUrl https://dict.youdao.com/infoline/style/cardList?modepublishclientmobilestyledaily; HttpHeaders headers new HttpHeaders(); headers.add(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); headers.add(Accept, application/json, text/plain, */*); headers.add(Host, dict.youdao.com); HttpEntityString requestEntity new HttpEntity(headers); ResponseEntityString responseEntity restTemplate.exchange( apiUrl, HttpMethod.GET, requestEntity, String.class );这里有几个关键点请求头伪装模拟浏览器请求避免被识别为爬虫异常处理需要对各种异常情况进行捕获和处理超时设置建议添加合理的超时时间避免请求卡死3.3 数据解析与处理API返回的是JSON数组我们需要解析第一个元素JsonNode rootNode objectMapper.readTree(responseStr); if (rootNode.isArray() rootNode.size() 0) { JsonNode firstItem rootNode.get(0); String en firstItem.get(title).asText().trim(); String cn firstItem.get(summary).asText().trim(); result.put(en, en); result.put(cn, cn); result.put(success, true); quoteCache.put(today, result); }解析时需要注意检查JSON结构是否符合预期对文本内容进行trim处理去除多余空格处理可能的null值情况3.4 降级策略实现为了保证功能的高可用性必须实现降级策略} catch (Exception e) { log.error(获取每日一句失败, e); } // 所有方案都失败时返回备用数据 result.put(en, The best time to plant a tree was 10 years ago. The second best time is now.); result.put(cn, 种一棵树最好的时间是十年前其次是现在。); result.put(success, false);降级策略的设计要点捕获所有可能的异常记录错误日志便于排查问题提供有意义的默认内容通过success字段告知前端这是降级数据4. 前端实现详解4.1 HTML结构设计前端HTML结构保持简洁div classdaily-quote iddaily-quote div classdaily-quote-content div classdaily-quote-en idquote-enLoading.../div div classdaily-quote-cn idquote-cn正在获取每日一句.../div /div /div设计考虑使用语义化的class命名提供有意义的加载状态提示保持结构简单便于样式控制4.2 CSS样式实现4.2.1 基础样式.daily-quote { display: flex; flex-direction: column; justify-content: center; padding: 16px 20px; height: 80px; width: 360px; min-width: 360px; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border-radius: 16px; border: 1px solid #bae6fd; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(14, 165, 233, 0.06); position: relative; overflow: hidden; } /* 引号装饰 */ .daily-quote::before { content: ; position: absolute; top: 8px; left: 12px; font-size: 3rem; color: rgba(14, 165, 233, 0.15); font-family: Georgia, serif; line-height: 1; pointer-events: none; }样式设计要点使用渐变色背景提升视觉效果添加微妙的阴影和边框增强层次感使用伪元素添加装饰性引号设置合理的过渡动画4.2.2 文字截断与展开.daily-quote-en { font-size: 1.05rem; color: #0369a1; font-weight: 500; line-height: 1.4; font-style: italic; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; position: relative; transition: all 0.3s ease; } /* 悬浮/展开状态 */ .daily-quote-en:hover, .daily-quote-en.expanded { white-space: normal; overflow: visible; text-overflow: clip; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); padding: 4px 8px; border-radius: 6px; box-shadow: 0 4px 12px rgba(14, 165, 233, 0.15); z-index: 10; }文字处理技巧使用text-overflow实现省略号效果通过改变white-space属性实现展开/收起添加悬停状态提升交互体验使用z-index确保展开内容不被遮挡4.3 JavaScript交互实现4.3.1 数据获取与缓存(function fetchDailyQuote() { var quoteEn document.getElementById(quote-en); var quoteCn document.getElementById(quote-cn); // 检查元素是否存在不存在则重试 if (!quoteEn || !quoteCn) { setTimeout(fetchDailyQuote, 500); return; } // 检查本地缓存 var cachedDate localStorage.getItem(dailyQuoteDate); var today new Date().toDateString(); if (cachedDate today) { var cachedData localStorage.getItem(dailyQuote); if (cachedData) { try { var data JSON.parse(cachedData); quoteEn.textContent data.en; quoteCn.textContent data.cn; return; } catch (e) { console.error(解析缓存数据失败, e); } } } // 调用后端接口 fetch(/api/daily-quote) .then(function(response) { if (!response.ok) { throw new Error(Network response was not ok); } return response.json(); }) .then(function(data) { if (data data.en data.cn) { quoteEn.textContent data.en; quoteCn.textContent data.cn; // 缓存到 localStorage localStorage.setItem(dailyQuote, JSON.stringify({ en: data.en, cn: data.cn })); localStorage.setItem(dailyQuoteDate, today); } }) .catch(function(error) { console.error(Error:, error); // 使用备用数据 quoteEn.textContent The best time to plant a tree was 20 years ago...; quoteCn.textContent 种一棵树最好的时间是十年前其次是现在。; }); })();缓存策略实现要点首先检查本地是否有当天的缓存有缓存则直接使用减少网络请求无缓存或缓存过期才请求后端API请求成功后将数据缓存到localStorage所有环节都有错误处理和降级方案4.3.2 点击展开功能// 移动端点击展开/收起 function toggleExpand(element) { element.classList.toggle(expanded); } quoteEn.addEventListener(click, function(e) { e.stopPropagation(); toggleExpand(quoteEn); }); quoteCn.addEventListener(click, function(e) { e.stopPropagation(); toggleExpand(quoteCn); }); // 点击其他地方收起 document.addEventListener(click, function() { quoteEn.classList.remove(expanded); quoteCn.classList.remove(expanded); });交互设计考虑点击句子展开/收起完整内容点击其他区域自动收起使用stopPropagation防止事件冒泡通过classList动态添加/移除类名5. 双层缓存架构详解5.1 缓存层次结构┌─────────────────────────────────────────┐ │ 客户端 localStorage │ │ - 当天数据缓存 │ │ - 减少重复请求 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ 服务端 ConcurrentHashMap │ │ - 当天数据缓存 │ │ - 减少第三方 API 调用 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ 第三方 API │ │ - 有道词典每日一句 │ └─────────────────────────────────────────┘5.2 缓存策略对比缓存层存储位置有效期作用localStorage浏览器1天减少页面刷新后的请求ConcurrentHashMap服务端内存1天减少第三方API调用这种双层缓存架构的优势减少网络请求大部分情况下只需要访问本地缓存降低API压力多个用户共享同一个服务端缓存提升响应速度内存操作远快于网络请求增强稳定性即使一方缓存失效另一方仍可工作6. 响应式设计实现6.1 断点设置/* 桌面端 */ .daily-quote-en { font-size: 1.05rem; } .daily-quote-cn { font-size: 1rem; } /* 平板端 (max-width: 768px) */ media (max-width: 768px) { .daily-quote-en { font-size: 1rem; } .daily-quote-cn { font-size: 0.95rem; } } /* 手机端 (max-width: 480px) */ media (max-width: 480px) { .daily-quote-en { font-size: 0.95rem; } .daily-quote-cn { font-size: 0.9rem; } }响应式设计要点根据屏幕宽度调整字体大小设置合理的断点768px和480px使用相对单位rem便于缩放6.2 布局适配.daily-quote { width: 360px; min-width: 360px; } media (max-width: 768px) { .daily-quote { width: 100%; max-width: 340px; } } media (max-width: 480px) { .daily-quote { width: 100%; max-width: 100%; } }布局调整策略桌面端固定宽度平板端适当缩小手机端全宽度显示使用max-width防止元素过大7. 完整代码实现7.1 后端完整代码RestController public class DailyQuoteController { private static final ConcurrentHashMapString, MapString, String quoteCache new ConcurrentHashMap(); Autowired private RestTemplate restTemplate; Autowired private ObjectMapper objectMapper; GetMapping(value /api/daily-quote, produces application/json;charsetUTF-8) ResponseBody public MapString, String getDailyQuote() { String today java.time.LocalDate.now().toString(); // 1. 检查缓存 if (quoteCache.containsKey(today)) { return quoteCache.get(today); } MapString, String result new HashMap(); String apiUrl https://dict.youdao.com/infoline/style/cardList?modepublishclientmobilestyledaily; try { HttpHeaders headers new HttpHeaders(); headers.add(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); headers.add(Accept, application/json, text/plain, */*); HttpEntityString requestEntity new HttpEntity(headers); ResponseEntityString response restTemplate.exchange( apiUrl, HttpMethod.GET, requestEntity, String.class); if (response.getStatusCode() HttpStatus.OK) { JsonNode rootNode objectMapper.readTree(response.getBody()); if (rootNode.isArray() rootNode.size() 0) { JsonNode item rootNode.get(0); result.put(en, item.get(title).asText().trim()); result.put(cn, item.get(summary).asText().trim()); result.put(success, true); quoteCache.put(today, result); return result; } } } catch (Exception e) { log.error(获取每日一句失败, e); } // 备用数据 result.put(en, The best time to plant a tree was 10 years ago. The second best time is now.); result.put(cn, 种一棵树最好的时间是十年前其次是现在。); result.put(success, false); return result; } }7.2 前端完整代码div classdaily-quote iddaily-quote div classdaily-quote-content div classdaily-quote-en idquote-enLoading.../div div classdaily-quote-cn idquote-cn正在获取每日一句.../div /div /div script (function fetchDailyQuote() { var quoteEn document.getElementById(quote-en); var quoteCn document.getElementById(quote-cn); if (!quoteEn || !quoteCn) { setTimeout(fetchDailyQuote, 500); return; } // 检查本地缓存 var cachedDate localStorage.getItem(dailyQuoteDate); var today new Date().toDateString(); if (cachedDate today) { var cachedData localStorage.getItem(dailyQuote); if (cachedData) { try { var data JSON.parse(cachedData); quoteEn.textContent data.en; quoteCn.textContent data.cn; return; } catch (e) { console.error(解析缓存数据失败, e); } } } // 点击展开功能 function toggleExpand(el) { el.classList.toggle(expanded); } quoteEn.addEventListener(click, function(e) { e.stopPropagation(); toggleExpand(quoteEn); }); quoteCn.addEventListener(click, function(e) { e.stopPropagation(); toggleExpand(quoteCn); }); document.addEventListener(click, function() { quoteEn.classList.remove(expanded); quoteCn.classList.remove(expanded); }); // 获取数据 fetch(/api/daily-quote) .then(r r.json()) .then(data { if (data.en data.cn) { quoteEn.textContent data.en; quoteCn.textContent data.cn; localStorage.setItem(dailyQuote, JSON.stringify({ en: data.en, cn: data.cn })); localStorage.setItem(dailyQuoteDate, today); } }) .catch(() { quoteEn.textContent The best time to plant a tree was 20 years ago...; quoteCn.textContent 种一棵树最好的时间是十年前其次是现在。; }); })(); /script8. 技术亮点与扩展思路8.1 技术亮点总结双层缓存架构服务端和客户端双重缓存极大提升了性能完善的降级策略从API调用到前端展示都有备用方案优雅的交互设计文字截断与展开功能提升了用户体验响应式布局完美适配各种设备屏幕轻量级实现不依赖大型框架加载速度快8.2 扩展思路这个功能还有不少可以扩展的方向多数据源支持接入多个名言API随机或轮询使用当主API不可用时自动切换到备用API历史记录功能将每日句子保存到数据库提供历史句子查询界面用户自定义允许用户收藏喜欢的句子支持用户提交自己的句子社交分享添加分享到社交媒体的功能生成带有句子和博客链接的分享图片主题切换提供多种显示主题供用户选择根据时间自动切换白天/夜间模式在实际使用中我发现这个功能确实能提升博客的互动性。很多访客会特意来看当天的句子有些还会在评论区分享自己的感悟。这种小而美的功能往往能带来意想不到的效果。
企业数字化 ERP 产品动态
相关推荐
机器学习微调中数据质量与数量的平衡艺术 1. 数据集的迷思:为什么"越多越好"不总是对的在机器学习领域,尤其是模型微调(Fine-tuning)过程中,新手最容易犯的一个错误就是盲目追求数据量。我刚入行时也踩过这个坑——当时为了提升文本分类模型的准确率… · 2026/11/4 7:41:37
鸿蒙生态下m3u_nullsafe的HLS流媒体深度适配实践 1. 项目概述:m3u_nullsafe在鸿蒙生态的深度适配作为一名长期从事跨平台流媒体开发的工程师,我最近在将Flutter项目迁移到鸿蒙平台时遇到了一个棘手问题:如何高效处理复杂的HLS流媒体协议。m3u_nullsafe这个Flutter组件在基础解析方面表现不错… · 2026/11/4 7:41:36
LL(1)语法分析器原理与实现:查表+栈的确定性解析机制 简介:本资源是一份面向计算机专业本科生与编译原理初学者的语法分析实验教学文档,聚焦编译器前端核心环节——语法分析程序的设计与实现。文档以算术表达式简化子集为分析对象,系统讲解LL(1)文法改写、预测分析表构建、分析栈与符号栈协同工作… · 2026/11/4 7:41:36
Codex CLI接入DeepSeek:科研党十分钟配置指南 上周我帮一位师弟配置Codex来跑数据分析,整整折腾了两个晚上,最后发现卡住的不是AI本身,而是安装和认证这一整串前置环节。node版本不合适,npm全局安装报权限错误,登录官方账号又碰到手机验证和token刷新问题ÿ… · 2026/11/4 8:16:50
轻量级自动化代理工具helloagent06架构与实践指南 1. 项目概述:helloagent06 的定位与核心价值helloagent06 是一个典型的轻量级自动化代理工具名称,这类工具通常用于简化重复性任务或实现特定场景下的流程自动化。从命名惯例来看,"hello"前缀暗示了其入门友好的特性,&q… · 2026/11/4 8:16:50
AI辅助学术写作:开题报告效率提升方案 1. 学术写作痛点与解决方案剖析 写开题报告时最让人头疼的莫过于逻辑框架搭建和文献综述部分。我指导过上百份研究生开题报告,发现80%的返工问题都集中在三个环节:研究问题表述不清(42%)、技术路线设计不合理(35%&… · 2026/11/4 8:16:50
反射列定位工具包实战:从算法原理到工程化部署 简介:该资源是面向ROS开发者与激光雷达定位学习者的三边定位算法功能包代码,基于反光柱特征实现移动机器人位姿解算,适合具备一定ROS基础、希望将反光柱定位方案落地到实际工程的中高级开发者参考。压缩包共18个文件,约24KB&#… · 2026/11/4 8:16:50
大模型规模膨胀下的AI芯片全栈协同:从算力到部署的实战洞察 过去两年,我大部分时间都花在给大模型搭环境、调算子、补框架适配这些"脏活"上。从本地部署一个几B的小模型,到帮着跑千亿参数的分布式训练,再到把推理服务稳定上线,最直观的感受是:大模型规模膨胀这件事&am… · 2026/11/4 8:16:50
企业业财系统对接实战:金蝶与百胜数据通道建设 1. 项目背景与核心价值最近刚完成一个企业数字化转型的实战项目,帮一家中型制造企业打通了金蝶云星空ERP与百胜E3OMS系统的数据通道。这个案例特别典型——财务部门用金蝶做账,供应链团队用百胜管订单,两边数据长期对不上,月底对账… · 2026/11/4 8:15:49
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38