Xuan's blog - API
type: channel
link: https://blog.ybyq.wang/tag/API/
items: 软件工程实践八：Web 前端项目实战（SSE、Axios 与代理）
items: 软件工程实践七：AI 大模型 API 合并与适配层设计
items: 软件工程实践五：Spring Boot 接口拦截与 API 监控、流量控制
items: 软件工程实践三：RESTful API 设计原则
items: FriendsRSS - Typecho友链RSS自动订阅展示插件
items: Typech handsome 侧边栏添加QQ等级和信息显示
items: 分享60个有趣实用的API及其Typecho博客集成指南

软件工程实践八：Web 前端项目实战（SSE、Axios 与代理）
type: item
date: 2025-09-19T14:48:42+08:00
description: 适合人群：具备 Vue 基础，希望接入 AI 文本/图片与流式输出的开发者或技术爱好者。建议先看：实践三（RESTful API 设计原则）、实践五（接口监控与限流）。你将收获：SSE 两种实现、Axios 标准化封装、vite 代理配置与常见坑。创建项目npm create vue@latest实现文本聊天页面目标：实现基础的文本输入、发送与消息列表展示。要点：输入框与发送按钮、消息时间顺序、滚动定位到最新消息。组件建议：ChatInput（输入区）、ChatList（消息区）。实现图片目标：支持根据文本生成图片并展示缩略图。要点：提交参数（prompt、size、数量）、生成态 loading、失败重试、下载按钮。组件建议：ImageGenerateForm、ImageGrid。实现文本聊天SSE的方式sse 前端实现方式第一种 EventSourceconst url = "/api/ai/chat/stream?platform=openai&model=gpt-4o&message=" + encodeURIComponent(message); const sse = new EventSource(url); const handleDelta = (event: MessageEvent) => { const data = (event as MessageEvent).data; }; const handleDone = () => { sse.close(); }; // Some servers don't set a named event; treat default messages as delta sse.onmessage = handleDelta; sse.addEventListener("delta", handleDelta as EventListener); sse.addEventListener("done", handleDone as EventListener); sse.onerror = (err) => { console.error("SSE error", err); sse.close(); };第二种 使用自带的fetch const res = await fetch("/api/ai/chat/stream", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ platform: "openai", model: "gpt-4o", message }), }); if (!res.ok || !res.body) throw new Error(`请求失败: ${res.status}`); const reader = res.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Try to parse as SSE events: data: {json}\n\n let sepIndex: number; while ((sepIndex = buffer.indexOf("\n\n")) !== -1) { const rawEvent = buffer.slice(0, sepIndex); buffer = buffer.slice(sepIndex + 2); const dataLines = rawEvent .split("\n") .filter((l) => l.startsWith("data:")) .map((l) => l.replace(/^data:\s?/, "")) .join(""); console.log("dataLines", dataLines); if (!dataLines) continue; consumeChunk(dataLines, assistantId); } // Fallback: handle NDJSON lines if server doesn't use blank-line separators // let lineIndex: number; // while ((lin…

软件工程实践七：AI 大模型 API 合并与适配层设计
type: item
date: 2025-09-19T14:36:25+08:00
description: 适合人群：希望统一多厂商（OpenAI、阿里、科大讯飞等）调用体验的后端开发者或技术爱好者。建议先看：实践三（RESTful API 设计原则）、实践六（阿里大模型对接）。你将收获：一套可扩展的适配层模型与返回体规范，最小变更对接新厂商。设计目标与边界统一接口：隐藏厂商差异，暴露稳定域模型（模型、聊天、流式聊天、图片）。易于扩展：新增厂商仅需实现适配器，不影响上层调用与返回结构。一致的错误语义：统一 success/code/message，流式以 [process]/[done] 信号闭环。安全与合规：不在代码中硬编码密钥，敏感信息通过环境变量或配置中心注入。对输入参数进行统一几乎不改返回值统一模型@Data public class BigModel { private String id; public BigModel(String id) { this.id = id; } public BigModel() {} } 文字聊天@Data public class ChatMessage { private String id; private String content; private String status; // success fail private String error; }文字聊天流@Data public class ChatMessageStream { private String id; private String content; private String status; // done process }图片@Data public class ChatImage { private String id; private String imageUrl; private String status; // success fail private String error; }流程合并使用统一接口OpenAI@Service public class OpenAiAdopter { @Autowired private OpenAiService openAiService; public List<BigModel> getModels() { Map model = openAiService.getModel(); List<Map> models = (List<Map>) model.get("data"); return models.stream().map(m -> new BigModel(m.get("id").toString())).collect(Collectors.toList()); } public ChatMessage chat(String model, String message) { Map chat = openAiService.chat(model, message); String id = chat.get("id").toString(); List<Map> output = (List<Map>) chat.get("output"); ChatMessage chatMessage = new ChatMessage(); chatMessage.setId(id); String contentStr = ""; chatMessage.setStatus("completed"); for (Map map : output) { if (!map.get("type").toString().equals("message")) { continue; } if (!map.get("status").toString().equals("completed")) { continue; } List<Map> contents = (List<Map>) map.get("content"); for (Map content : contents) { contentStr += content.get("text").toString(); } } chatMessage.setContent(contentStr); return chatMessage; } public void chatStream(String model, String message, Consumer<ChatMessageStream> onDelta){ openAiService.chatStream(model, message, (delta)->{ ChatMessageStream chatMessag…

软件工程实践五：Spring Boot 接口拦截与 API 监控、流量控制
type: item
date: 2025-09-18T14:55:00+08:00
description: 适合人群：具备 Spring Web 基础、希望在接口层做统一治理的开发者或技术爱好者。建议先看：软件工程实践二（Spring Boot 知识回顾）、实践三（RESTful API 设计原则）。1. 方案选型速览Filter（Servlet 层）：最靠前，处理包括静态资源；适合全局跨域、XSS、统一编码等。HandlerInterceptor（Spring MVC 层）：在进入 Controller 前后执行；适合接口打点、鉴权、限流、审计。AOP（方法层）：对注解/切点进行横切；适合注解式限流、审计、幂等等。API Gateway（边界层）：最靠外；适合全局流控、黑白名单、熔断、灰度，一处管多服务。一般应用内优先用 Interceptor + AOP；如果有网关，也可在网关做粗粒度限流，在应用内做精细化限流与监控。国内比较熟悉的：Sentinel2. 基于 HandlerInterceptor 的接口监控目标：记录每次请求的关键信息与耗时，并输出到日志或指标系统。示例：// RequestLoggingInterceptor.java // 功能：为每个进入的 HTTP 请求生成请求ID、记录入站/出站日志与耗时 package com.example.demo.web; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import java.util.UUID; public class RequestLoggingInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class); private static final String REQ_ID = "reqId"; // MDC 中的请求 ID 键 private static final String START = "startAt"; // 请求开始时间属性键 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String requestId = UUID.randomUUID().toString(); // 生成请求唯一 ID，便于链路追踪 MDC.put(REQ_ID, requestId); // 放入日志 MDC request.setAttribute(START, System.currentTimeMillis()); // 记录起始时间 String method = request.getMethod(); String uri = request.getRequestURI(); String ip = getClientIp(request); // 识别真实客户端 IP（支持代理场景） log.info("[IN] {} {} ip={} ua={}", method, uri, ip, request.getHeader("User-Agent")); return true; // 返回 true 放行请求 } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { // 可在此处添加对响应体或模型的额外处理 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Long start = (Long) request.getAtt…

软件工程实践三：RESTful API 设计原则
type: item
date: 2025-09-11T10:30:00+08:00
description: 目标与范围明确一致的接口风格，降低客户端心智负担，提升跨团队协作效率。规范涵盖：资源建模、URI 设计、HTTP 方法与状态码、请求/响应、错误、分页过滤排序、版本化、安全、缓存、可观测性与文档。1. 核心理念资源导向：一切皆资源（名词复数命名），动作用 HTTP 方法表达。统一接口：方法、状态码、媒体类型、错误结构一致。无状态：每个请求自包含认证与上下文，不依赖服务器会话。可缓存：合理使用条件请求与缓存头，降低延迟与负载。可演进：通过版本化与向后兼容策略平滑升级。2. 资源建模与 URI 规范资源命名：使用复数、短小、层级表达从属关系。/api/v1/users/api/v1/users/{userId}/api/v1/users/{userId}/orders关联过滤：也可使用查询参数表达关联（优先简单方案）。/api/v1/orders?userId=123避免在路径中使用动词；确需动作，用子资源表达。POST /api/v1/invoices/{id}/pay标识符：建议使用不可泄漏信息的 ID（UUID/雪花），避免自增 ID 暴露业务规模。路径示例与反例：推荐：GET /api/v1/users/{id}，PATCH /api/v1/users/{id}，DELETE /api/v1/users/{id}不推荐：POST /api/update/users/{id}、POST /api/create/users/{id}（应分别使用 PUT/PATCH 与 POST /api/v1/users）3. HTTP 方法语义与幂等性GET（安全、幂等）：获取资源或集合。POST（非幂等）：创建资源、触发计算/异步任务。PUT（幂等）：整体替换资源（客户端提供完整表述）。PATCH（建议近幂等）：局部更新，使用 JSON Merge Patch 或 JSON Patch。DELETE（幂等）：删除资源（软删/硬删在语义上对客户端保持透明）。4. 标准状态码2xx：200 OK：成功，返回资源或结果；201 Created：创建成功，Location 指向新资源；202 Accepted：已受理异步任务；204 No Content：成功但无响应体（删除、幂等更新）。4xx：400 Bad Request：参数错误/校验失败；401 Unauthorized：未认证或凭证无效；403 Forbidden：已认证但无权限；404 Not Found：资源不存在；409 Conflict：资源状态冲突（如唯一键冲突）；412 Precondition Failed：条件请求失败（ETag 并发控制）；415 Unsupported Media Type：媒体类型不支持；422 Unprocessable Entity：语义错误（校验未通过）；429 Too Many Requests：限流触发。5xx：服务器错误，尽量避免；记录告警并快速恢复。5. 请求与响应规范媒体类型：请求/响应 Content-Type/Accept 统一使用 application/json; charset=utf-8。字段命名与格式：推荐 camelCase；时间使用 ISO 8601（UTC），如 2025-08-20T10:30:00Z。大整数（如 ID）防止前端精度丢失可按 string 传输。统一返回结构（可选但推荐，便于一致性与观测）：{ "code": "OK", // 可选 "message": "success", // 可选 "data": { /* 资源对象或结果 */ }, "requestId": "f7a2b..."， }错误返回结构：{ "code": "VALIDATION_ERROR", // 可以是数字，字符串 "message": "email is invalid",// 提示 "requestId": "9e6d7...", // 可选 "details": [ // 可选 { "field": "email", "issue": "must be a valid email" } ] }6. 示例：Todo 列表接口（内存 List）用途：演示使用内存 List 存储并返回 todo 集合（示例代码，非生产）。路径：GET /api/v1/todos返回：200 OK，application/json; charset=utf-86.1 控制器示例（Java / Spring）package com.example.demo.todo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.w…

FriendsRSS - Typecho友链RSS自动订阅展示插件
type: item
date: 2025-08-25T16:18:00+08:00
description: 概述FriendsRSS 是一款专为 Typecho 博客系统设计的智能友链 RSS/Atom 聚合插件。它能够自动检测、解析和聚合您友链博客的最新文章，通过多种展示方式（短代码、API接口、RSS输出、前台页面）让您的博客成为友圈动态的聚合中心。插件功能核心功能友链来源集成：从 Typecho links 表的指定分类读取友链，自动参与聚合。兼容性增强：当指定分类不存在时，自动获取所有友链，完美兼容只有一种友链的主题博客。排除功能：支持配置排除检测的博客网址，每行一个，支持部分匹配。RSS 自动检测：采用"三轮检测"策略（常用路径/feed和/feed/ → HTML 解析 → 其他常见路径），并对检测结果进行 10天缓存（RSS地址相对稳定）。定时检测功能：支持配置定时检测RSS地址间隔（默认240小时/10天），RSS地址缓存时间自动与定时检测间隔保持一致。定时解析功能：支持配置定时解析间隔（默认6小时），提供独立的定时任务脚本，支持命令行和HTTP调用。手动配置 RSS：后台提供"选择友链 + 输入 RSS 地址"的表单，保存到 rss_config.json 作为覆盖与纠偏。短代码功能：在任意文章或页面中使用 rss 短代码来显示友链RSS聚合内容，支持 limit 参数控制显示文章数量。多格式支持：兼容 RSS 2.0 / Atom 1.0 / RSS 1.0，解析标题、链接、作者、摘要与发布时间等字段。文章聚合与排序：为每个博客限制抓取数量，并按发布时间 倒序 排列，支持聚合总数上限。缓存与并发保护：聚合结果持久化缓存，内置 锁文件 防止并发重复任务；可读取已有缓存以快速展示。日志记录：记录检测、解析与网络状态，便于排查异常与性能问题。输出功能RSS订阅地址：/action/friends-rss?do=rss - 生成标准RSS 2.0格式的聚合订阅源API接口地址：/action/friends-rss?do=page - 返回JSON格式的聚合文章数据前台展示页面：/action/friends-rss?do=pageview - 美观的前台展示页面，支持响应式设计[collapse status="false" title="示例图片查看"][/collapse]管理界面统计卡片：显示友链数量、文章数量、最后更新时间、已配置RSS数量等统计信息操作卡片：定时检测RSS地址：显示当前检测间隔和下次执行时间，提供"立即执行"按钮定时解析RSS内容：显示当前解析间隔和下次执行时间，提供"立即执行"按钮添加RSS地址：手动配置友链RSS地址的表单界面访问地址区块：显示所有可用的访问地址和友链分类信息友链与文章列表：显示友链列表和最新聚合文章，支持RSS配置状态标识日志显示区域：实时显示操作日志，支持自动滚动到最新内容短代码功能基本用法：在任意文章或页面中使用 rss 短代码来显示友链RSS聚合内容参数支持：支持 limit 参数控制显示文章数量块状设计：使用简洁美观的块状设计，包含博客信息、文章标题、摘要等响应式布局：自动适配移动端设备，优化显示效果网络与兼容性优化请求优化：优先使用 cURL，请求超时与重定向受控内容清理：自动清理与修复常见 XML/文本问题、摘要长度截断错误处理：完善的异常处理和日志记录机制并发保护：使用锁文件防止重复任务执行缓存策略：智能缓存管理，支持缓存统计和清理定时任务功能独立脚本：提供 cron.php 独立定时任务脚本HTTP调用：支持通过HTTP请求触发定时任务状态管理：记录定时任务执行状态和下次执行时间安全验证：支持密钥验证防止未授权访问Ajax功能实时检测：支持SSE（Server-Sent Events）实时显示RSS检测进度批量操作：支持批量检测RSS地址和清除缓存进度反馈：实时显示操作进度和结果错误处理：完善的错误处理和用户反馈机制安装与配置将插件文件夹上传到 usr/plugins/ 目录。在 Typecho 后台启用插件 FriendsRSS。在插件设置页配置友链来源、每个博客抓取文章数、聚合总数上限与缓存时间等参数。Github地址：[hide]https://github.com/BXCQ/FriendsRSS[/hide]配置参数说明友链分类：指定需要聚合的友链分组。兼容性说明：Handsome主题：填写"ten"获取全站链接，填写"one"获取内页链接，填写"good"获取推荐链接其他主题：通常留空即可，插件会自动获取所有友链当指定分类不存在时，插件会自动获取所有友链，确保兼容性排除检测的博客网址：每行一个网址，支持部分匹配。例如：example.com - 排除所有包含example.com的友链https://blog.example.com - 排除特定…

Typech handsome 侧边栏添加QQ等级和信息显示
type: item
date: 2025-07-17T21:13:00+08:00
description: 前言在个人博客的侧边栏展示自己的QQ信息，包括头像、昵称、QQ号、等级和Q龄等，不仅可以增加博客的个性化，还能让访客更好地了解博主。本教程将详细介绍如何在Typecho博客的Handsome主题侧边栏中添加QQ信息卡片功能。效果展示实现后的效果是在侧边栏显示一个包含以下信息的QQ卡片：QQ头像QQ昵称QQ号码Q龄（注册年份）QQ等级（使用emoji表情：👑☀️🌙⭐表示不同等级）实现步骤1. 准备工作首先，我们需要找到合适的API来获取QQ信息。本教程使用的是jkapi.com提供的API服务，它可以根据QQ号获取相关信息，这个用了半个月就不行了，换成了api.dwo.cc的。2. 修改sidebar.php文件在Handsome主题中，侧边栏的内容是由sidebar.php文件控制的。我们需要编辑这个文件来添加QQ信息卡片。2.1 找到合适的位置通常，我们会将QQ信息卡片添加到侧边栏的个人信息模块附近。在sidebar.php文件中找到合适的位置，例如在博主信息卡片之后。2.2 添加QQ信息卡片代码将以下代码添加到选定的位置：[hide] <?php _me("QQ信息") ?> <?php function getQQInfoFromDwoApi($qq) { $url = "https://api.dwo.cc/api/aiys_qq?qq=" . $qq; $ch = curl_init(); // 设置cURL选项 curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 5); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'ckey: *************', // 在header中传递秘钥 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' ]); $response = curl_exec($ch); curl_close($ch); if ($response) { $data = json_decode($response, true); if (isset($data['code']) && $data['code'] == 200) { return $data; } } return null; } function generateQQLevelIconsHtml($level) { $html = ''; $level = intval($level); // 等级计算规则 $crowns = floor($level / 64); $suns = floor(($level % 64) / 16); $moons = floor(($level % 16) / 4); $stars = $level % 4; // 使用emoji表情替代图片，更加稳定可靠 $crown_icon = '👑'; // 皇冠 $sun_icon = '☀️'; // 太阳 $moon_icon = '🌙'; // 月亮 $star_icon = '⭐'; // 星星 $icon_style = 'style="font-size:16px; margin:0 1px;"'; for ($i = 0; $i < $crowns; $i++) { $html .= '' . $crown_icon . ''; } for ($i = 0; $i < $suns; $i++) { $html .= '' . $sun_icon . ''; } for ($i = 0; $i < $moons; $i++) { $html .= '' . $moon_icon . ''; } for ($i = 0; $i < $stars; $i++) { $html .= '' . $star_icon . ''; } return $html; } // 缓存文件路径 - 使用Typecho的缓存目录 $cache_dir = defined('__TYPECHO_TMP_DIR__') ? __TYPECHO_TMP_D…

分享60个有趣实用的API及其Typecho博客集成指南
type: item
date: 2025-07-17T16:26:00+08:00
description: 引言突发奇想，搜集了一些公开API，为博客添加一些动态和个性化的元素让网站更加生动有趣。本文介绍60个有趣的API，它们都可以集成到Typecho博客中，让您的博客脱颖而出。目录个人社交账号API热搜榜单API工具类API内容增强类API博客功能增强类API游戏与娱乐类API个人社交账号API1. QQ等级显示通过QQ空间API获取您的QQ等级、成长值等信息，并在博客中展示。<?php // 在sidebar.php或自定义页面中添加 function getQQLevel($qq) { $url = "https://r.qzone.qq.com/fcg-bin/cgi_get_score.fcg?mask=7&uins=".$qq; $data = file_get_contents($url); $data = mb_convert_encoding($data, "UTF-8", "GBK"); preg_match('/lvtext:"(\d+级)"/', $data, $level); return isset($level[1]) ? $level[1] : '获取失败'; } $qq = '你的QQ号'; echo '我的QQ等级: '.getQQLevel($qq); ?>2. QQ音乐状态API展示您正在听的QQ音乐歌曲信息。<?php function getQQMusicStatus($qq) { // QQ音乐状态获取 $url = "https://c.y.qq.com/rsc/fcgi-bin/fcg_get_profile_homepage.fcg?format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=yqq&uin=".$qq; $context = stream_context_create([ 'http' => [ 'header' => 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' ] ]); $data = json_decode(file_get_contents($url, false, $context), true); // 判断是否有正在收听的歌曲 if (isset($data['data']['listen']) && !empty($data['data']['listen']['now'])) { return [ 'status' => true, 'song_name' => $data['data']['listen']['now']['song_name'], 'singer' => $data['data']['listen']['now']['singer'][0]['name'], 'cover' => $data['data']['listen']['now']['cover'] ]; } else { return [ 'status' => false ]; } } $qq = '你的QQ号'; $music = getQQMusicStatus($qq); if ($music['status']) { echo ' '; echo ' 我正在听 '; echo ' '; echo ' '; echo ' '.$music['song_name'].' '; echo ' - '.$music['singer'].' '; echo ' '; echo ' '; echo ' '; } else { echo '暂无音乐播放记录'; } ?>3. 哔哩哔哩UP主数据展示您的B站粉丝数、视频数、播放量等数据。<?php function getBiliStats($uid) { $url = "https://api.bilibili.com/x/relation/stat?vmid=".$uid."&jsonp=jsonp"; $data = json_decode(file_get_contents($url), true); return $data['data']; } $uid = '你的B站UID'; $stats = getBiliStats($uid); echo "粉丝数: ".$stats['follower']; ?>4. 网易云音乐状态展示您最近听的歌曲或者创建一个迷你播放器。<?php // 网易云音乐用户歌单获取 function get…