沉梦听雨的编程指南 沉梦听雨的编程指南
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • RPC
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 多媒体框架
  • 虚拟机
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)

沉梦听雨

时间是最好的浸渍剂,而沉淀是最好的提纯器🚀
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • RPC
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 多媒体框架
  • 虚拟机
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)
  • Spring

  • SpringBoot

  • Mybatis

    • Mybatis基础知识小结
    • Mybatis映射文件解析
      • mybatis 中比较符号的写法
        • 第一种写法
        • 第二种写法
      • 顶级元素
      • 查询技巧
        • 获取参数值的两种方式
        • 动态设置表名
        • 新增返回主键 id⭐️
        • 1. 使用数据库自增主键
        • 2. 使用 MyBatis 的 useGeneratedKeys 属性
        • 3. 使用 MyBatis 的 @SelectKey 注解
        • 查询一条数据为 map 集合
        • 查询多条数据为 map 集合
        • 方法一
        • 方法二
      • 动态 sql
        • 1、if
        • 2、choose / when / otherwise
        • 3、where
        • 4、set
        • 5、trim
        • 6、foreach
        • 7、include
      • 模糊查询的 5 种方式
        • 方式一:&{}
        • 方式二:#{}
        • 方式三:字符串拼接函数
        • 方式四:bind 标签
        • 方式五:java 代码里写
      • 延迟加载
        • <collection> 标签
        • 写法示例
        • 查询语句
        • 结果对象示例
        • 写法分析
      • <if> 标签判断条件等于字符串的值
      • 学习参考
    • 获取中文字符串首字母
    • 计算分页数的几种方法
    • MP使用小记
  • Shiro

  • 常用框架
  • Mybatis
沉梦听雨
2024-03-13
目录

Mybatis映射文件解析

# Mybatis 映射文件解析

# mybatis 中比较符号的写法

# 第一种写法

直接在外层嵌套 <![CDATA[ ]]> 这个标签,例如 <![CDATA[ >= ]]>

标签说明:

CDATA(Character Data)标签,它在 XML 和 HTML 中用于定义一段文本数据,该数据不应被解析器解析为标记。CDATA 部分中的内容将原样输出,即其中的所有字符都将被视为普通文本,即使它们可能包含特殊字符或标记。

sql 举例如下:

select 
	*
from 
	user
where 
	age <![CDATA[ >= ]]> 18
1
2
3
4
5
6

# 第二种写法

符号替换,如下面表格所示

原符号 替换符号
< &lt;
<= &lt;=
> &gt;
>= &gt;=
& &amp;
' &apos
" &quot

符号区分:

  • 小于号 < -- &lt; -- 其中 l 代表 less,t 代表 than,合起来是 less than
  • 大于号 > -- &gt; -- 其中 g 代表 greater,t 代表 than,合起来是 greater than

sql 举例如下:

select 
	*
from 
	user
where 
	age &gt;= 18
1
2
3
4
5
6

# 顶级元素

SQL 映射文件有以下几个顶级元素(按照它们应该被定义的顺序):

  1. cache -- 给定命名空间的缓存配置
  2. cache-ref -- 其他命名空间缓存配置的引用
  3. resultMap -- 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象
  4. sql -- 可被其他语句引用的可重用语句块
  5. insert -- 映射插入语句
  6. update -- 映射更新语句
  7. delete -- 映射删除语句
  8. select -- 映射查询语句

# 查询技巧

# 获取参数值的两种方式

有两种形式:

  1. ${} -- 字符串替换
  2. #{} -- 预编译处理

区别:

  1. #{} 是预编译处理,像传进来的数据会加个 ''(# 将传入的数据都当成一个字符串,会对自动传入的数据加一个单引号)
    • 会按照预编译SQL语句(PreparedStatement)的方式来处理这些占位符,即将参数绑定到SQL语句中的问号(?)位置上,而不是直接将参数值拼接到SQL语句中
    • 对于数字类型的参数不会额外加引号,对于字符串类型则会加上单引号或者双引号
  2. ${} 就是字符串替换。直接替换掉占位符。$ 方式一般用于传入数据库对象,例如传入表名。使用 ${} 的话会导致 sq| 注入

什么是 SQL 注入呢?

比如:

select * from user where id = ${value}
1

value 应该是一个数值吧。然后如果对方传过来的是 001 and name= tom。这样不就相当于多加了一个条件嘛?

把 SQL 语句直接写进来了。如果是攻击性的语句呢?001;drop table user,直接把表给删了。

建议

所以为了防止 SQL 注入,能用 #{} 的不要去用 ${}

如果非要用 ${} 的话,那要注意防止 SQL 注入问题,可以手动判定传入的变量,进行过滤,一般 SQL 注入会输入很长的一条 SQL 语句。

# 动态设置表名

只能使用 ${},因为表名不能加单引号

例如:

<select id="getUserByTable" resultType="User">
	select * from t_info_${tableName}
</select>
1
2
3

# 新增返回主键 id⭐️

在 MyBatis 中,当你插入一条新记录并希望获取这条新记录的主键 ID 时,你可以使用数据库提供的自动生成主键的机制(例如自增字段),也可以通过 MyBatis 提供的一些特性来获取主键值。

以下是一些常见的方法来获取新增记录的主键 ID:

# 1. 使用数据库自增主键

这是最常见的方法。许多数据库系统(如 MySQL、PostgreSQL、SQL Server 等)都支持自动生成主键。在你的数据库表中,你可以设置一个字段为自增(例如,在 MySQL 中使用 AUTO_INCREMENT 属性),这样每次插入新记录时,数据库会自动为这个字段生成一个新的值。

你的表结构可能如下所示:

CREATE TABLE your_table (
    id INT NOT NULL AUTO_INCREMENT,
    other_column VARCHAR(255),
    PRIMARY KEY (id)
);
1
2
3
4
5

在你的 MyBatis 映射文件中,你可以这样写:

<insert id="insertUser" parameterType="User">
    INSERT INTO your_table (other_column) VALUES (#{otherColumn})
</insert>
1
2
3

然后,在你的 Java 代码中,你可以在插入记录后从数据库获取主键 ID:

在调用插入方法时,会自动将生成的主键 ID 设置到实体对象中,因此可以直接通过实体对象获取主键 ID。

User user = new User();
user.setOtherColumn("some value");
// sqlSession.insert("insertUser", user);
// sqlSession.insert("com.xxx.project.mapper.XxxMapper.insertUser", user);
userMapper.insertUser(user);

// 获取数据库自动生成的主键 ID
Long id = user.getId();
1
2
3
4
5
6
7
8

# 2. 使用 MyBatis 的 useGeneratedKeys 属性

如果你的数据库支持返回生成的主键,你可以在 MyBatis 映射文件中设置 useGeneratedKeys="true",这样 MyBatis 会在执行插入操作后返回生成的主键。

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO your_table (other_column) VALUES (#{otherColumn})
</insert>

# <insert id="insertEntity" parameterType="YourEntityClass" useGeneratedKeys="true" keyProperty="id">
#    INSERT INTO your_table (column1, column2, ...)
#    VALUES (#{property1}, #{property2}, ...)
# </insert>
1
2
3
4
5
6
7
8

在这个例子中,keyProperty 属性指定了要将生成的主键值设置到哪个属性上。

  • useGeneratedKeys="true":表示使用数据库自动生成的主键。
  • keyProperty="id":表示将自动生成的主键值设置到实体类的指定属性中。

在你的 Java 代码中,你可以直接获取这个属性的值:

User user = new User();
user.setOtherColumn("some value");
// sqlSession.insert("insertUser", user);
// sqlSession.insert("com.xxx.project.mapper.XxxMapper.insertUser", user);
userMapper.insertUser(user);

// 现在 user 对象的 id 属性已经被设置为新生成的主键值
Long id = user.getId();
1
2
3
4
5
6
7
8

# 3. 使用 MyBatis 的 @SelectKey 注解

如果你使用的是 MyBatis 注解而不是映射文件,你可以使用 @SelectKey 注解来获取生成的主键。

public interface UserMapper {
    @Insert("INSERT INTO your_table (other_column) VALUES (#{otherColumn})")
    @SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false)
    int insertUser(User user);
}
1
2
3
4
5

在这个例子中,

  • @SelectKey 注解指定了一个 SQL 语句 SELECT LAST_INSERT_ID() 来获取最后插入记录的 ID。
  • keyProperty 属性指定了要将这个值设置到哪个属性上。
  • before 属性指定了生成的主键是在执行插入操作之前还是之后被选择的。
User user = new User();
user.setOtherColumn("some value");
// sqlSession.insert("insertUser", user);
// sqlSession.insert("com.xxx.project.mapper.XxxMapper.insertUser", user);
userMapper.insertUser(user);

// 现在 user 对象的 id 属性已经被设置为新生成的主键值
Long id = user.getId();
1
2
3
4
5
6
7
8

# 查询一条数据为 map 集合

1、Mapper 接口

/**  
 * 根据用户id查询用户信息为map集合  
 * @param id  
 * @return  
 */  
Map<String, Object> getUserToMap(@Param("id") int id);
1
2
3
4
5
6

2、对应的映射文件

    <!--Map<String, Object> getUserToMap(@Param("id") int id);-->
    <select id="getUserToMap" resultType="map">
        select * from t_user where id = #{id}
    </select>
    <!--结果:{password=123456, sex=男, id=1, age=23, username=admin}-->
1
2
3
4
5

# 查询多条数据为 map 集合

# 方法一

1、Mapper 接口

    /**
     * 查询所有用户信息为map集合
     * <p>
     * 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,
     * 此时可以将这些map放在一个list集合中获取
     *
     * @return
     */
    List<Map<String, Object>> getAllUserToMap();
1
2
3
4
5
6
7
8
9

2、对应的映射文件

    <!--Map<String, Object> getAllUserToMap();-->
    <select id="getAllUserToMap" resultType="map">
        select * from t_user
    </select>
    <!--
        结果:
        {
        1={password=123456, sex=男, id=1, age=23, username=admin},
        2={password=123456, sex=男, id=2, age=23, username=张三},
        3={password=123456, sex=男, id=3, age=23, username=张三}
        }
    -->
1
2
3
4
5
6
7
8
9
10
11
12

# 方法二

1、Mapper 接口(用 @MapKey 注解)

    /**
     * 查询所有用户信息为map集合
     * <p>
     * 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,并且最终要以一个map的方式返回数据,
     * 此时需要通过 @MapKey 注解设置 map 集合的键,值是每条数据所对应的 map 集合
     *
     * @return
     */
    @MapKey("id")
    Map<String, Object> getAllUserToMap();
1
2
3
4
5
6
7
8
9
10

2、对应的映射文件

    <!--Map<String, Object> getAllUserToMap();-->
    <select id="getAllUserToMap" resultType="map">
        select * from t_user
    </select>
    <!--
        结果:
        {
        1={password=123456, sex=男, id=1, age=23, username=admin},
        2={password=123456, sex=男, id=2, age=23, username=张三},
        3={password=123456, sex=男, id=3, age=23, username=张三}
        }
    -->
1
2
3
4
5
6
7
8
9
10
11
12

# 动态 sql

MyBatis 动态 SQL 提供了多种标签,用于在运行时动态构建 SQL 语句,极大地提高了 SQL 语句的灵活性和可重用性。

以下是几个关键动态 SQL 标签及其用法的总结:

# 1、if

含义

用于根据表达式的值来决定是否包含某段 SQL 语句。

用法

    <if test="condition">
        SQL 片段
    </if>
1
2
3

解释

if 标签可通过 test 属性(即传递过来的数据)的表达式进行判断

  • 若表达式的结果为 true,则标签中的内容会执行;
  • 反之标签中的内容不会执行

# 2、choose / when / otherwise

含义

类似于 Java 中的 switch-case 结构,根据多个条件分支执行不同 SQL 片段。

用法

    <choose>
        <when test="condition1">SQL片段1</when>
        <when test="condition2">SQL片段2</when>
        <!-- 可以有多个when -->
        <otherwise>默认SQL片段(当所有when都不满足时执行)</otherwise>
    </choose>
1
2
3
4
5
6

# 3、where

含义

用于动态地添加 WHERE 子句,避免不必要的 AND 或 OR 关键词出现在 SQL 语句中。

用法

包裹多条可能的条件语句,MyBatis 会智能地忽略那些条件未满足(表达式结果为假)时产生的 AND 或 OR 关键词。

where 和 if 一般结合使用

  • 若 where 标签中的 if 条件都不满足,则 where 标签没有任何功能,即不会添加 where 关键字

  • 若 where 标签中的 if 条件满足,则 where 标签会自动添加 where 关键字,并将条件【最前方】多余的 and/or 去掉

注意:where 标签不能去掉【条件后】多余的 and/or

示例

    <!--List<Emp> getEmpByCondition(Emp emp);-->
    <select id="getEmpByCondition" resultType="Emp">
        select * from t_emp
        <where>
            <if test="empName != null and empName !=''">
                emp_name = #{empName}
            </if>
            <if test="age != null and age !=''">
                and age = #{age}
            </if>
            <if test="sex != null and sex !=''">
                and sex = #{sex}
            </if>
            <if test="email != null and email !=''">
                and email = #{email}
            </if>
        </where>
    </select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 4、set

含义

动态地添加 UPDATE 语句的 SET 部分,同样可以避免不必要的逗号问题。

用法

在 UPDATE 语句中,根据条件决定哪些列需要更新。(搭配 trim 标签使用。)

示例

    <update id="updateUserInfo">
        UPDATE
            t_user
        <set>
            <if test="dto.gender != null">
                gender = #{dto.gender},
            </if>
            <if test="dto.birthday != null">
                birthday = #{dto.birthday},
            </if>
        </set>
        WHERE
            user_id = #{userId}
    </update>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解释

在更新数据的时候,使用 <set> 标签,使用了 set 标签会自动帮你删除尾部的逗号。

# 5、trim

含义

用来【删除】SQL 片段首尾的特定字符或关键字,也可以在首尾【添加】字符或关键字。

常用属性

  1. prefix:在 trim 标签中的内容的前面【添加】某些内容
  2. suffix:在 trim 标签中的内容的后面【添加】某些内容
  3. prefixOverrides:在 trim 标签中的内容的前面【去掉】某些内容
  4. suffixOverrides:在 trim 标签中的内容的后面【去掉】某些内容(suffixOverrides="and|or")

用法

		<trim prefix="(" suffix=")" prefixOverrides="," suffixOverrides=",">
  		column1,
  		column2
		</trim>
<!-- 上述示例会在括号内去除列名间的逗号,并在前后分别添加括号。 -->

    <update id="updateUserInfo">
        UPDATE
            t_user
        <set>
            <if test="dto.gender != null">
                gender = #{dto.gender},
            </if>
            <if test="dto.birthday != null">
                birthday = #{dto.birthday}
            </if>
            <trim suffixOverrides=","/>
        </set>
        WHERE
            user_id = #{userId}
    </update>
<!-- 上述示例会删除内容最后的逗号(处理SET部分末尾可能出现的多余逗号) -->

    <trim prefix="SET" suffixOverrides=",">
        ...
    </trim>
<!-- 另一种写法 -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 6、foreach

含义

遍历集合,适用于 in 语句或者批量插入等场景。

用法

    <insert id="batchInsert">
        INSERT INTO tbl_user (id,name,age,sex,is_delete)
        VALUES
        <foreach collection="userList" item="item" index="index" open="(" separator="),(" close=")">
            #{item.id},#{item.name},#{item.age},#{item.sex},#{item.isDelete}
        </foreach>
    </insert>
1
2
3
4
5
6
7

上述示例为批量插入语句,会遍历名为 userList 的集合,依次将每个元素 item 插入 SQL 语句中,每一条数据用 "),(" 分隔。

# 7、include

  • sql 片段,可以记录一段公共 sql 片段,在使用的地方通过 include 标签进行引入
  • 声明 sql 片段:<sql> 标签
<sql id="empColumns">eid,emp_name,age,sex,email</sql>
1
  • 引用 sql 片段:<include> 标签
<!--List<Emp> getEmpByCondition(Emp emp);-->
<select id="getEmpByCondition" resultType="Emp">
	select <include refid="empColumns"></include> from t_emp
</select>
1
2
3
4

# 模糊查询的 5 种方式

# 方式一:&{}

&{} 这种方式,简单,但是无法防止 SQL 注入,所以不推荐使用

<if test="dto.name != null"> 
	AND name LIKE '%${dto.name}%'
</if>
1
2
3

# 方式二:#{}

在 #{} 左右两边写上字符串 "%"

<if test="dto.name != null"> 
	AND name LIKE "%" #{dto.name} "%"
</if>
1
2
3

# 方式三:字符串拼接函数

用 concat() 函数

<if test="dto.name != null"> 
	AND name LIKE concat('%', #{dto.name}, '%')
</if>
1
2
3

# 方式四:bind 标签

<select id="searchstudents" resultType="com.example.entity.studentEntity"
        parameterType="com.example.entity.studentEntity">
  
    <bind name="pattern1" value="'%' + parameter.name + '%'" />
    <bind name="pattern2" value="'%' + parameter.address + '%'" />
  
    SELECT * FROM test student
    <where>
        <if test="name != null and name != ''">
            name LIKE #{pattern1}
        </if>
        <if test="address != null and address != ''">
            AND address LIKE #{pattern2}
        </if>
    </where>
    ORDER BY id
  
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 方式五:java 代码里写

1、直接在 java 代码里写,在值的两边加上 %

dto.setName("%张三%");
1

2、然后,在映射文件中直接传参就行

<if test="dto.name != null"> 
	AND name LIKE #{dto.name}
</if>
1
2
3

# 延迟加载

# <collection> 标签

  • property: 指定父对象中的属性名,该属性应该是一个集合类型,如 List 或 Set。
  • ofType: 指定集合中元素的类型。
  • select: 指定一个 MyBatis 查询,该查询返回集合中的元素。
  • column: 用于传递参数给 <select> 查询的参数名称。(例如:column="objId=id",id 是父对象的元素,objId 是查询参数 #{objId})
  • 延迟加载: <collection> 标签通常用于延迟加载关联集合。
  • 关联查询: 通过 <collection> 标签,MyBatis 可以处理关联查询,将多个表的结果集映射到一个对象的属性中。
  • 参数传递: column 属性用于将查询参数传递给关联查询。

# 写法示例

# 查询语句

    <resultMap id="dVO" type="com.chenmeng.project.vo.MaintainerVO">
        <collection property="documentInfoVoS"
                    ofType="com.chenmeng.project.vo.DocumentInfoVO" select="getDocumentInfo"
                    column="objId=id">
        </collection>
    </resultMap>

    <select id="getDocumentInfo" resultType="com.chenmeng.project.vo.DocumentInfoVO">
        select info.certificate_name name,
               expire_date           expire,
               file.file_path        image
        from t_test_document_info info
            left join t_test_file file on file.obj_id = info.id
            and file.is_deleted = 0
        where info.obj_id = #{objId}
          and info.is_deleted = 0
    </select>

    <select id="queryByCreateUser" resultMap="dVO">
        SELECT mn.id, mn.name
               en.enterprise_name,
               en.field_type
        FROM t_test_enterprise en,
             t_test_maintainer mn
        WHERE mn.is_deleted = 0
          and en.id = mn.enterprise_id
          and en.is_deleted = 0
          and mn.create_user = #{userId}
    </select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 结果对象示例

/**
 * 人员信息视图
 */
@Data
public class MaintainerVO implements Serializable {

  private static final long serialVersionUID = 1L;

  @ApiModelProperty(value = "维保人id")
  private Long id;

  @ApiModelProperty(value = "维保人姓名")
  private String name;

  @ApiModelProperty(value = "证件信息")
  private List<DocumentInfoVO> documentInfoVoS;
}

/**
 * 证件信息视图
 */
@Data
public class DocumentInfoVO implements Serializable {

  private Long id;

  private String name;

  private String expire;

  private String image;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 写法分析

  • 在业务实现类层(impl),调用 queryByCreateUser 对应的 mapper 接口,最终返回 MaintainerVO 对象类型
  • <collection> 标签会延迟加载关联集合,自动查询 documentInfoVoS 属性的元素并加载。

# <if> 标签判断条件等于字符串的值

写法示例

# ...
        <if test="req.yearCheckCount != null and req.yearCheckCount != ''">
          <choose>
              <when test="req.yearCheckCount == '0'.toString()">
                  and t2.year_check_count is null
              </when>
              <when test="req.yearCheckCount == '1'.toString() or req.yearCheckCount == '2'.toString()">
                  and t2.year_check_count = #{req.yearCheckCount}
              </when>
              <otherwise>
                  and t2.year_check_count &gt;= 3
              </otherwise>
          </choose>
        </if>
# ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 学习参考

  • mybatis中大于等于小于等于的写法_mybatis大于小于-CSDN博客 (opens new window)

  • 归档 - MyBatis 教程 (javaboy.org) (opens new window)

  • MyBatis 中#{}和${}区别_w3cschool (opens new window)

  • Mybatis最全笔记知识点 - 知乎 (zhihu.com) (opens new window)

  • MyBatis 判断条件为不等于的问题(<if test=“变量!= ‘1‘.toString()“> xxx </if>_在mybatis中test不等于某个值-CSDN博客 (opens new window)

上次更新: 2024/9/25 11:16:13
Mybatis基础知识小结
获取中文字符串首字母

← Mybatis基础知识小结 获取中文字符串首字母→

Theme by Vdoing | Copyright © 2023-2025 沉梦听雨 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式