<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>实践 on 撷时录</title><link>https://jiangbyte.github.io/tags/%E5%AE%9E%E8%B7%B5/</link><description>Recent content in 实践 on 撷时录</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Thu, 01 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://jiangbyte.github.io/tags/%E5%AE%9E%E8%B7%B5/index.xml" rel="self" type="application/rss+xml"/><item><title>Webhook 与飞书机器人集成</title><link>https://jiangbyte.github.io/p/webhook-%E4%B8%8E%E9%A3%9E%E4%B9%A6%E6%9C%BA%E5%99%A8%E4%BA%BA%E9%9B%86%E6%88%90.html</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0800</pubDate><guid>https://jiangbyte.github.io/p/webhook-%E4%B8%8E%E9%A3%9E%E4%B9%A6%E6%9C%BA%E5%99%A8%E4%BA%BA%E9%9B%86%E6%88%90.html</guid><description>&lt;p&gt;审计告警功能需要把检测到的安全事件推送到飞书群。飞书提供了自定义机器人 Webhook 的接口，支持文本和富文本消息格式，也可以配置 HMAC-SHA256 签名验证。&lt;/p&gt;&#10;&lt;p&gt;实现上需要解决三个问题：签名怎么算、参数放哪里、消息怎么写。&lt;/p&gt;&#10;&lt;h2 id="签名算法"&gt;签名算法&lt;/h2&gt;&#10;&lt;p&gt;飞书官方文档给出的签名示例：&lt;/p&gt;&#10;&lt;figure&#10; class="code-block code-block--chroma code-block--line-numbers"&#10; data-code-block&#10; data-language="python"&#10;&gt;&#10; &lt;figcaption class="code-block__toolbar"&gt;&#10; &lt;span class="code-block__language"&gt;python&lt;/span&gt;&#10; &lt;button class="code-block__copy" type="button" data-code-copy&gt;复制&lt;/button&gt;&#10; &lt;/figcaption&gt;&#10; &lt;div class="code-block__viewport"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;hashlib&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;base64&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 3&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;hmac&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 4&lt;/span&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 5&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gen_sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 6&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hmac_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 9&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;digestmod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;10&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;11&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hmac_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;12&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;pre class="code-block__fallback" hidden aria-hidden="true"&gt;&lt;code class="language-python"&gt;import hashlib&#10;import base64&#10;import hmac&#10;&#10;def gen_sign(timestamp, secret):&#10; string_to_sign = &amp;#39;{}\n{}&amp;#39;.format(timestamp, secret)&#10; hmac_code = hmac.new(&#10; string_to_sign.encode(&amp;#34;utf-8&amp;#34;),&#10; digestmod=hashlib.sha256&#10; ).digest()&#10; sign = base64.b64encode(hmac_code).decode(&amp;#39;utf-8&amp;#39;)&#10; return sign&lt;/code&gt;&lt;/pre&gt;&#10; &lt;button class="code-block__expand" type="button" data-code-expand hidden&gt;展开全部&lt;/button&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;注意这里的 &lt;code&gt;hmac.new&lt;/code&gt; 调用是 &lt;code&gt;hmac.new(key, msg=None, digestmod=sha256)&lt;/code&gt;，key 是 &lt;code&gt;timestamp\nsecret&lt;/code&gt;，msg 为空。和通常的 &lt;code&gt;hmac.new(secret, data, digestmod)&lt;/code&gt; 写法不一样，如果习惯性地把 secret 放第一个参数、string_to_sign 放第二个，签名就对不上。&lt;/p&gt;</description></item><item><title>用 Spring AI 实现 RAG 知识库：对话记忆、向量检索与文档入库</title><link>https://jiangbyte.github.io/p/%E7%94%A8-spring-ai-%E5%AE%9E%E7%8E%B0-rag-%E7%9F%A5%E8%AF%86%E5%BA%93%E5%AF%B9%E8%AF%9D%E8%AE%B0%E5%BF%86%E5%90%91%E9%87%8F%E6%A3%80%E7%B4%A2%E4%B8%8E%E6%96%87%E6%A1%A3%E5%85%A5%E5%BA%93.html</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0800</pubDate><guid>https://jiangbyte.github.io/p/%E7%94%A8-spring-ai-%E5%AE%9E%E7%8E%B0-rag-%E7%9F%A5%E8%AF%86%E5%BA%93%E5%AF%B9%E8%AF%9D%E8%AE%B0%E5%BF%86%E5%90%91%E9%87%8F%E6%A3%80%E7%B4%A2%E4%B8%8E%E6%96%87%E6%A1%A3%E5%85%A5%E5%BA%93.html</guid><description>&lt;p&gt;最近用 Spring AI 做了一套 RAG 知识库对话：后台管理多知识库和文件上传，对话侧支持会话记忆、按知识库过滤检索，以及 SSE 流式输出。骨架还是原来的 DDD-lite Spring Boot 分层。&lt;/p&gt;&#10;&lt;p&gt;技术选型大致是：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;能力&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;选型&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;框架&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Java 21 / Spring Boot 4.1 / Spring AI 2.0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Chat&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;阿里云百炼等 &lt;strong&gt;OpenAI 兼容&lt;/strong&gt;接口（&lt;code&gt;spring-ai-starter-model-openai&lt;/code&gt;）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Embedding&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;本地 &lt;strong&gt;Ollama&lt;/strong&gt;（如 &lt;code&gt;embeddinggemma&lt;/code&gt;，768 维）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;向量库&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;Milvus&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;对话记忆&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Spring AI &lt;strong&gt;Redis Chat Memory&lt;/strong&gt;（RediSearch）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;原文存储&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;MinIO&lt;/strong&gt;（可与 Milvus 栈共用）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;文档解析&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;Tika&lt;/strong&gt; + &lt;strong&gt;TokenTextSplitter&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;元数据&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;MySQL&lt;/strong&gt; + MyBatis-Plus&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;前端&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Vite + React + Ant Design，SSE 流式对话&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;下面按实现顺序写整条链路：模块怎么分、Spring AI 怎么接、文档如何切片入库、RAG 与记忆如何同时生效，以及默认 RAG 提示词会坑闲聊等问题。&lt;/p&gt;&#10;&lt;h2 id="目标与整体架构"&gt;目标与整体架构&lt;/h2&gt;&#10;&lt;p&gt;先明确要做的行为：&lt;/p&gt;</description></item><item><title>邮件集成与密码重置</title><link>https://jiangbyte.github.io/p/%E9%82%AE%E4%BB%B6%E9%9B%86%E6%88%90%E4%B8%8E%E5%AF%86%E7%A0%81%E9%87%8D%E7%BD%AE.html</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0800</pubDate><guid>https://jiangbyte.github.io/p/%E9%82%AE%E4%BB%B6%E9%9B%86%E6%88%90%E4%B8%8E%E5%AF%86%E7%A0%81%E9%87%8D%E7%BD%AE.html</guid><description>&lt;p&gt;项目中邮件功能用于发送密码重置链接，但此前只搭好了架子，缺少实际测试验证。本文将记录邮件集成的全链路配置、本地测试、以及过程中遇到并解决的几个问题。&lt;/p&gt;&#10;&lt;h2 id="邮件发送引擎"&gt;邮件发送引擎&lt;/h2&gt;&#10;&lt;p&gt;项目在 &lt;code&gt;app/platform/email/sender.py&lt;/code&gt; 中实现了 &lt;code&gt;send_mail&lt;/code&gt; 函数，支持普通 SMTP 和 STARTTLS 两种模式：&lt;/p&gt;&#10;&lt;figure&#10; class="code-block code-block--chroma code-block--line-numbers"&#10; data-code-block&#10; data-language="python"&#10;&gt;&#10; &lt;figcaption class="code-block__toolbar"&gt;&#10; &lt;span class="code-block__language"&gt;python&lt;/span&gt;&#10; &lt;button class="code-block__copy" type="button" data-code-copy&gt;复制&lt;/button&gt;&#10; &lt;/figcaption&gt;&#10; &lt;div class="code-block__viewport"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 2&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 3&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 4&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;BusinessError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Mail service is not configured&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 5&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 6&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;From&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;To&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_email&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Subject&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 9&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;10&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_send_sync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;pre class="code-block__fallback" hidden aria-hidden="true"&gt;&lt;code class="language-python"&gt;async def send_mail(to_email: str, subject: str, body: str) -&amp;gt; None:&#10; mail = settings.mail&#10; if not mail.host or not mail.from_email:&#10; raise BusinessError(&amp;#34;Mail service is not configured&amp;#34;)&#10; message = EmailMessage()&#10; message[&amp;#34;From&amp;#34;] = f&amp;#34;{mail.from_name} &amp;lt;{mail.from_email}&amp;gt;&amp;#34;&#10; message[&amp;#34;To&amp;#34;] = to_email&#10; message[&amp;#34;Subject&amp;#34;] = subject&#10; message.set_content(body)&#10; await asyncio.to_thread(_send_sync, message)&lt;/code&gt;&lt;/pre&gt;&#10; &lt;button class="code-block__expand" type="button" data-code-expand hidden&gt;展开全部&lt;/button&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;邮件配置通过 &lt;code&gt;MailSettings&lt;/code&gt; 集中管理，包括 SMTP 地址、端口、账号密码、TLS 开关等。配置项可在管理后台运行时修改，启动时从 &lt;code&gt;sys_config&lt;/code&gt; 表加载覆盖。&lt;/p&gt;</description></item></channel></rss>