news 2026/9/29 15:57:49

若依后端Docker化部署实战:Spring Boot+MySQL+Redis容器编排

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
若依后端Docker化部署实战:Spring Boot+MySQL+Redis容器编排

手头一个内部管理系统是基于若依前后端分离版开发的,交付准备阶段客户只给了台全新测试机,要求把后端、数据库、缓存一次性跑起来。当时临时去查各种教程,断断续续折腾了两天,后来把整个流程整理成一份可直接复用的部署方案。这篇指南就把这套方案完整写出来,涉及镜像选型、Dockerfile 编写、Compose 编排、常见坑位排查,适合需要把若依后端以 Docker 方式部署到服务器或本地开发机的读者。后端环境是 Spring Boot + MySQL + Redis 的经典组合,你照着一步步操作,最后能得到一个可随时重建的后端运行环境。

1. 部署前的准备与整体思路

1.1 若依后端容器化要解决什么问题

若依(RuoYi)本身是一套基于 Spring Boot 的后台权限管理系统,代码里包含了用户、角色、菜单、字典、定时任务等一堆通用模块。平时本地开发,电脑上得装 JDK、Maven、MySQL 5.7 或 8.0、Redis,还要把数据库脚本手工导入。问题在于每个人电脑环境不一样,JDK 版本不同、MySQL 字符集不同、Redis 有没有密码也说不清,协作时经常出现“我这跑得好好的,你那怎么报错”。

Docker 化部署的核心价值不是“看起来很现代”,而是把整个运行环境固化成文件。前端分离开发完成后,后端 jar 包、MySQL 初始化脚本、Redis 密码、端口映射、环境变量全部通过 Dockerfile 和 Compose 文件描述出来。任何人拿到项目目录,执行一条docker compose up -d,就能得到与开发环境基本一致的后端运行态。交付、回滚、扩容都会轻松很多。

这里我也要提醒一句:容器化解决的是“环境不一致”问题,但不解决“代码本来就有 bug”的问题。先把业务功能在本地跑通,再来做 Docker 化,别指望容器来兜底。

1.2 整体架构与容器清单

若依前后端分离版的后端是一个标准 Spring Boot 单体应用,依赖 MySQL 存业务数据,依赖 Redis 放验证码、登录 token、在线用户、定时任务锁这一类缓存数据。

本次部署方案里我会创建三个容器:

容器镜像职责
ruoyi-mysqlmysql:8.0存储全部业务表、菜单权限、定时任务配置
ruoyi-redisredis:7.0-alpine缓存登录用户信息、验证码、session
ruoyi-backend基于 JDK8 自定义镜像运行 ruoyi-admin.jar,提供 REST API

我没有选择把 MySQL 和 Redis 部署在宿主机上,而是统一容器化,原因有三个:一是交付环境是台干净测试机,宿主机不一定预装数据库;二是 MySQL 数据目录和 Redis 数据目录都能用 volume 持久化,后续换机器直接挂上旧数据就能恢复;三是整个依赖关系可以用 Compose 的depends_on和健康检查控制启动顺序,避免后端容器起来时数据库还没初始化好。

1.3 环境准备与项目初始状态

动手之前先确认几件事。第一,Docker 版本建议不低于 20.10,Compose 插件最好用 v2 版本,也就是docker compose(中间没横杠)这种命令。第二,Windows 用户建议直接用 Docker Desktop 并启用 WSL2 后端,Linux 用户装 docker-ce 即可。第三,准备好若依前后端分离版的源码,至少包含后端代码和sql目录下的数据库脚本,通常是一个ry_2024xxxx.sql和一个quartz.sql。

还需要在后端源码里确认一点:若依是典型的 Maven 多模块工程,模块名包括ruoyi-admin、ruoyi-common、ruoyi-framework、ruoyi-generator、ruoyi-quartz、ruoyi-system,真正可执行的 Spring Boot 入口在ruoyi-admin模块。所以后面 Dockerfile 里拷贝 jar 时,路径要写成ruoyi-admin/target/ruoyi-admin.jar,千万别拿整个工程根目录当成启动目录。

2. 基础镜像选型和依赖组件配置

2.1 JDK 镜像的选择逻辑

若依 3.8.x 依赖的 Spring Boot 版本主要是 2.x,在 JDK 8 下运行是最省心的。我自己做镜像时优先选择eclipse-temurin:8-jre,而不是openjdk:8-jre-alpine,原因是 Alpine 发行版用的是 musl libc,个别 Java 类库在做文件操作、时区计算时会出现难以描述的异常,排查成本极高。如果你在本地测试时没发现问题,生产环境最好也别赌这个概率,选 Debian 或 Ubuntu 底座的 JRE 镜像更可靠。

这里顺便说一下镜像体积问题。完整 JDK 镜像动不动就五六百兆,运行时其实只需要 JRE。eclipse-temurin:8-jre的体积比 JDK 小很多,装完 jar 后镜像大概二三百兆,对你没坏处。构建时用 Maven 容器完成编译,运行镜像里不保留 JDK、不保留源码,这也是后面多阶段构建要做的事。

2.2 MySQL 8.0 容器的初始化安排

MySQL 的 Docker 镜像有一个官方支持的初始化行为:容器首次启动时,会自动执行/docker-entrypoint-initdb.d目录下的.sql、.sh文件。若依源码的sql目录里有两个脚本,命名大致是ry_2024xxxx.sql和quartz.sql,把整个sql目录挂载到/docker-entrypoint-initdb.d,首次启动时两个脚本会按文件名字母序执行。宁可让文件名保持像1_ry.sql、2_quartz.sql这样的可排序前缀,也不要依赖目录顺序。

创建数据库时要注意字符集。若依默认要求 utf8mb4,否则中文、表情符号、特殊字符都可能出现乱码或存储异常。Compose 里给 MySQL 容器加command参数,指定--character-set-server=utf8mb4和--collation-server=utf8mb4_general_ci,这部分我会在后面的编排文件里直接给出完整写法。

还有一点:MySQL 8.0 默认的认证插件是caching_sha2_password,如果你的后端 JDBC 驱动版本比较老,会出现 “Public Key Retrieval is not allowed” 这类报错。这个问题我在第 6 节会专门展开,先记住一个思路:在 JDBC 连接串上增加allowPublicKeyRetrieval=true&useSSL=false,同时依赖若依自带的 JDBC 驱动版本,一般都能解决。

2.3 Redis 单节点配置要点

若依后端对 Redis 的要求不高,主要存验证码、登录令牌和在线用户信息,单节点完全够用,不需要主从复制。但是 Redis 容器必须开启密码认证,否则后端连接周期会可能会被暴露,尤其是测试机映射了公网端口时特别危险。

在 Compose 文件里给 Redis 容器加启动命令即可:

redis-server --requirepass redis123456 --appendonly yes

--appendonly yes表示开启 AOF 持久化,配合 volume 挂载,容器销毁后数据还能保留。后端application.yml里的 Redis 密码要与此一致,我会把密码也做成环境变量,这样不同环境切换时不用改 jar 包里的配置。

3. 编写 Dockerfile 构建后端镜像

3.1 多阶段构建的核心思路

后端镜像如果只写一层“拷贝 jar 运行”,那下载依赖和编译还得在宿主机完成,换一台机器就又得装 Maven。多阶段构建的思路是:第一个阶段用 Maven 镜像把源码编译成 jar,第二个阶段用精简 JRE 镜像只拷 jar,最终镜像里不含 Maven、不含源码、不含编译缓存。

这样有几个直接好处:镜像体积小、交付内容干净、构建过程可重复。坏处是每次构建都要重新拉依赖,第一次会慢一些。如果经常改代码,我可以给你一个优化建议:把mvn dependency:go-offline单独提出来缓存依赖层,但这会让 Dockerfile 复杂度上升,小项目没必要一开始就上。

3.2 Dockerfile 具体内容与逐段说明

下面这份 Dockerfile 是我在若依项目中实际跑过的版本,直接放到后端工程根目录即可:

# 第一阶段:编译 FROM maven:3.8.6-jdk-8 AS build WORKDIR /build COPY . . RUN mvn clean package -DskipTests -pl ruoyi-admin -am # 第二阶段:运行 FROM eclipse-temurin:8-jre ENV TZ=Asia/Shanghai WORKDIR /app COPY --from=build /build/ruoyi-admin/target/ruoyi-admin.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-jar", "app.jar"]

逐个说下关键点。-pl ruoyi-admin -am表示只构建ruoyi-admin模块,同时构建它依赖的其他模块,最终产出的 jar 就在ruoyi-admin/target下。

ENV TZ=Asia/Shanghai是必须写的。不带时区的话,容器默认 UTC,若依后台的时间显示会比北京时间晚 8 小时,登录日志、操作日志、定时任务调度时间全乱套。在 Debian 系 JRE 镜像里设置这个环境变量就够了,不需要再额外拷贝时区文件。

-Xms512m -Xmx1024m是最小堆和最大堆大小。若依默认内存占用不算夸张,512M 起跑没压力,但如果你的服务器内存紧张,可以把-Xmx降到 512m。堆大小不是越大越好,JVM 如果超出容器可用内存,可能导致容器被杀。

3.3 构建运行的完整命令

Dockerfile 放在后端工程根目录后,执行下面两条命令就能完成镜像构建:

docker build -t ruoyi-backend .

如果 Maven 依赖下载特别慢,你可以在后端工程根目录的settings.xml或 Maven 全局配置里配置镜像仓库。这里不展开,只是提醒:依赖拉不下来时别怀疑代码,先看 Maven 日志里卡在哪个包。

构建完成后,可以用docker images看到本地多了一个ruoyi-backend镜像。此时单独运行它一定会报数据库连不上,因为没有 MySQL 和 Redis 可连。所以下一步直接用 Compose 把它们编排到一起,而不是单独docker run后端。

4. 用 Docker Compose 编排整套环境

4.1 目录规划与编排文件

我为项目建的目录结构是这样的:

ruoyi-docker/ ├── docker-compose.yml ├── Dockerfile ├── sql/ │ ├── ry_2024xxxx.sql │ └── quartz.sql └── backend/ └── 后端源码或构建上下文

实际用的时候把Dockerfile和后端源码放在同一个构建上下文中,我习惯把整个后端工程目录作为 Compose 文件所在目录的子目录,然后在docker-compose.yml里用build: ./backend指过去。下面是一份可参考的编排文件:

services: mysql: image: mysql:8.0 container_name: ruoyi-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: ruoyi TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_general_ci ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./sql:/docker-entrypoint-initdb.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] interval: 5s timeout: 3s retries: 10 redis: image: redis:7.0-alpine container_name: ruoyi-redis restart: unless-stopped command: ["redis-server", "--requirepass", "redis123456", "--appendonly", "yes"] ports: - "6379:6379" volumes: - ./redis-data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "redis123456", "ping"] interval: 5s timeout: 3s retries: 10 backend: build: ./backend container_name: ruoyi-backend restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: DB_HOST: mysql DB_PORT: 3306 DB_NAME: ruoyi DB_USER: root DB_PASSWORD: root123456 REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: redis123456 ports: - "8080:8080"

这里需要解释几个容易忽略的设计。MySQL 和 Redis 都映射了宿主机端口,理论上生产环境可以不映射数据库端口,只让后端容器通过内部网络访问,但既然讲的是通用部署方案,保留端口能让排错更方便。真正对外提供服务只需要映射 8080 即可。如果担心安全,内网环境里建议把3306和6379的宿主机端口映射删掉。

MYSQL_DATABASE=ruoyi表示容器启动时自动创建名为ruoyi的库。后面执行ry_2024xxxx.sql时,脚本里通常也有CREATE DATABASE IF NOT EXISTS ruoyi之类的语句,二者互不冲突。

4.2 健康检查与启动顺序

初次布若依后端的人都容易踩同一个坑:MySQL 容器刚running,后端就开始连数据库,结果 MySQL 还没完成初始化,后端启动失败。Compose 的depends_on默认只能保证启动顺序,不能保证“可服务”,必须配合健康检查。

healthcheck的作用是让 Docker 每隔几秒探测一次容器状态,探活成功后其他容器才会被拉起。我在 MySQL 的探活命令里用mysqladmin ping,在 Redis 里用redis-cli ping,都是最轻量的方式。retries设为 10,间隔 5 秒,给数据库初始化留了足够时间。

如果不用健康检查,还有一种省事的写法是在后端启动脚本里等待,但那样代码侵入性太强,不值得。Compose 自带机制就能解决,不要自己造轮子。

4.3 数据库初始化和数据持久化

./sql目录挂载到/docker-entrypoint-initdb.d后,只有第一次创建 MySQL 数据目录时才会执行初始化脚本。后面重启容器,不会重复执行。

数据持久化主要通过两个目录:./mysql-data对应 MySQL 的数据文件,./redis-data对应 Redis 的 AOF 文件。这两个目录一旦生成,就包含了真实的业务数据。备份时不要只拷贝 sql 脚本,要把整个mysql-data目录打包。恢复时先停掉 MySQL 容器,清空或替换mysql-data,再重新启动。

有两点经验值得记一下:第一,MySQL 容器以mysql用户运行,宿主机上挂载目录的属主如果不是 uid 999,可能会因权限问题启动失败,直接chown -R 999:999 mysql-data有时比纠结授权更快。第二,初始化脚本执行时间较长,不要一看到后端报“数据库不存在”就急着删卷重建,先看 MySQL 日志是否还停在初始化阶段。

5. 启动、验证与前后端联调

5.1 一键启动与日志排查

所有准备工作完成后,执行:

docker compose up -d --build

--build会先构建后端镜像,再按下依赖顺序启动三个容器。首次执行耗时较长,主要时间花在 Maven 拉依赖和 MySQL 初始化脚本上,不要中途按 Ctrl+C,否则未必是失败。

启动过程中最常用的两个命令:

docker compose ps docker compose logs -f backend

docker compose ps能看每个容器当前状态。三个容器都显示running且没有不断重启的迹象后,再用日志确认后端是否真正启动完成。

5.2 后端健康状态验证

若依后端启动成功的标志是日志里出现类似Started RuoYiApplication in xx seconds这样一行。但只凭日志还不够,我习惯再打一个接口验证。

若依默认开启验证码接口,不需要登录就能访问。下列请求如果能返回包含uuid和img字段的 JSON,说明 Spring Boot 正常、数据库连接和 Redis 连接都通了:

curl http://localhost:8080/captchaImage

如果返回的是空白页或者直接连接失败,分几种情况看:docker compose logs -f backend里有异常堆栈,重点看是数据库拒绝连接还是 Redis 超时;如果完全没有请求日志,确认防火墙是否放行了 8080 端口;如果在服务器上 curl 通但外部访问不通,检查云安全组的入站规则。

5.3 前端联调的重点配置

若依前后端分离工程里,前端开发环境和生产环境的接口地址存在不同位置。Vue2 老版本在vue.config.js里配置转发规则,Vue3 版本在vite.config.ts的 server 配置里把/prod-api转发到后端地址。无论哪一版,核心思路都一样:前端请求统一走/prod-api前缀,本地开发时由前端构建工具把该前缀替换成后端实际地址。

这里有个高频误区:很多人改了后端接口地址后,前端页面还在请求 8080 之外的端口,或者请求返回 404。正确做法是确认前端配置里的目标地址就是http://后端服务器IP:8080,并且确保若依前端路由里请求的 path 与后端 controller 的@RequestMapping匹配。

还有一点,初次登录时如果前端报跨域错误,先别急着在后端加全局跨域配置,除非你明确知道自己在干什么。若依后端默认对大多数来源做了放行,但生产上更推荐由 Nginx 做统一流量转发,让前端与后端同源,避免在代码里裸奔 CORS。

5.4 从部署角度看前端 TS 报错

热搜里经常能看到“若依 vue3 ts 报错”。这类问题虽然在前端项目里出现,但部署联调时也常常被误判为后端问题。实际踩过以后,我总结出三个常见方向。

第一是依赖版本问题,若依 Vue3 版本的package.json对 Node 版本有要求,如果你是 Node 20+,安装依赖时可能提示engines不满足或node-sass类依赖构建失败,建议对照官方 README 使用匹配的 Node 版本,推荐 16 或 18。第二是类型编译报错,Vue3 + TS 工程里偶发的类型推导问题,npm run build时才能暴露,后端部署正确但前端构建失败时,先从tsconfig.json的skipLibCheck和路径别名排查。第三是接口地址与路由 base 拼错,前端VITE_APP_BASE_API必须与后端实际 context-paht 对齐,若依后端默认没有上下文前缀,因此通常是/prod-api加上后端具体路径。

6. 部署现场问题清单与排查方法

6.1 MySQL 连接失败的几种典型报错

后端日志里出现Communications link failure时,先从网络连通性排查:在 backend 容器里执行ping mysql看能不能解析到 MySQL 容器 IP,再确认账号密码是否匹配。若依默认账号是root,密码是 Compose 里MYSQL_ROOT_PASSWORD设置的值。

如果报错是Public Key Retrieval is not allowed,说明 MySQL 8 使用了caching_sha2_password认证,而 JDBC 驱动没有启用公钥检索。修改后端application.yml的数据源连接串,增加参数:

url: jdbc:mysql://${DB_HOST:mysql}:${DB_PORT:3306}/${DB_NAME:ruoyi}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

需要注意:serverTimezone必须与容器时区一致,否则日期时间字段可能会有偏差。如果你对安全要求较高,useSSL=false会降低连接传输的加密级别,内网环境可接受,公网环境建议改用其他加密方式。

6.2 Redis 连接异常的排查路径

若依启动时如果 Redis 连不上,日志通常会直接报Unable to connect to Redis或者JedisConnectionException。最常见原因是密码不匹配。很多教程里 Redis 是不设密码的,但若依的application.yml默认要求填password字段,要么留空,要么填上你启动 Redis 时设置的密码。

排查步骤建议按这个顺序:先看 Compose 里 Redis 的密码是什么;再打开后端配置里 Redis 密码是否一致;最后进入容器手动验证:

docker exec -it ruoyi-redis redis-cli -a redis123456 ping

如果返回PONG,Redis 服务本身没问题,问题大概率出在配置或网络。localhost和redis这两个主机名在容器环境里是完全不同的地址,后端容器内不能用localhost连接 Redis 容器,必须用服务名redis。

6.3 Windows 下 Docker Desktop 无法启动

Windows 上安装 Docker Desktop 后,有时会弹窗报错:Docker Desktop failed to start because virtualization support is not detected。这通常不代表系统不支持虚拟化,而是 Hyper-V 或“Windows 虚拟机监控程序平台”没启用,或者 BIOS 里的 VT-x/AMD-V 被关了。

处理路径是:先进入 BIOS 开启虚拟化选项;再到 Windows“启用或关闭 Windows 功能”里勾选“虚拟机平台”和“适用于 Linux 的 Windows 子系统”;装好 WSL2 后执行wsl --set-default-version 2;最后重启 Docker Desktop。老机器如果 BIOS 里找不到虚拟化选项,也可以尝试 Docker Desktop 的旧版本或改用 WSL 原生方式,但这一步属于环境适配,和若依本身无关。

6.4 从热搜词看若依部署后的高频话题

部署完成后,我偶尔会在搜索热词里看到一些若依相关的问题,和这次的容器化流程有交叉,这里一并做个速查:

热搜场景答案与关联点
ruoyi 在哪里写入登录用户的信息登录成功后后端将LoginUser对象写入 Redis,key 通常为login_tokens:加 UUID。请求头带Authorizationtoken,后端从 Redis 取对应用户信息
若依框架集成 druid 实现数据库密码加密在application.yml中配置 Druid 的publicKey和加密后的密码,部署时用环境变量覆盖即可,不必把明文写在镜像里
若依主子表导出功能后端基于 EasyExcel 动态生成表头,部署时只要保证导出目录可写,容器内没有特殊依赖
若依新加模块在ruoyi-system下新增业务模块,重新mvn package生成 jar,再docker build覆盖镜像
若依 Vue3 去掉验证码验证码开关一般存在数据库配置表sys_config中,修改sys.account.captchaEnabled的值为 false,重启后端生效

这些点如果展开,每一个都能单独写一篇文章。部署阶段你只需要知道:后端容器本质是“JDK8 + jar + 环境变量”,你改代码后重新构建镜像,就是一次标准的迭代发布。

7. 写在最后

这次把若依后端做成 Docker 部署,回头看我个人最大的体会是:大部分人第一次失败不是错在 Dockerfile 或 Compose 语法上,而是错在没理解容器之间的网络关系。后端容器内看到的主机名是mysql、redis,不是localhost,这三者之间靠 Compose 内部网络通信;数据库密码、Redis 密码、时区、字符集这几类“慢变量”只要有一个不一致,排查时间就会成倍上涨。

最后再分享一个小技巧:正式发布前,我会把docker compose config的输出检查一遍,这个命令会展开所有配置并做语法校验。然后执行一次docker compose down -v再up -d,验证从零开始的完整初始化。这套流程能保证另一台机器上跑起来的行为一致,交付后基本不会再被半夜叫起来改环境。

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

智能零售柜商品检测:1000张图、三种标签格式与YOLO11一键训练落地指南

简介:本资源面向智能零售柜商品检测项目开发者与目标检测学习者,提供真实零售柜监控场景采集的1000张高质量商品图片,覆盖罐装饮料、袋装零食等常见品类,标注标签包含113个商品类别,可作为新零售场景通用商品检测数据的…

作者头像 李华
网站建设 2026/9/29 15:55:52

J-Link V9被识别为克隆版?SEGGER V6.22回滚固件恢复教程

J-Link V9用了好几年一直挺稳的,结果某天心血来潮点了SEGGER软件里的“Update Firmware”,然后Keil里直接报错,调试器怎么都连不上目标板——那一刻我才意识到,自己手里这支“老伙计”大概率已经被官方新版软件标记成了克隆版。这…

作者头像 李华
网站建设 2026/9/29 15:54:38

银河麒麟V10上编译e1000e与rtl8125网卡驱动:从环境检查到避坑

简介:面向银河麒麟V10系统维护者与网卡驱动开发者的可编译网卡驱动资源包,解决e1000e与RTL8125两款网卡在国产操作系统上因内核适配导致的编译失败问题。包内已提前处理重复定义删除、函数参数修改等兼容性调整,可直接用于编译安装。资源共56…

作者头像 李华
网站建设 2026/9/29 15:54:12

Eclipse SVN插件完全指南:从选型安装到冲突解决的一线避坑经验

开工前先聊聊:我为什么写这篇Eclipse SVN插件指南 如果你打开这篇博文,大概率正被Eclipse里的SVN插件折磨着——要么安装半天连不上仓库,要么提交代码时弹一堆看不懂的英文报错,要么界面上的图标和菜单全消失,项目右键…

作者头像 李华
网站建设 2026/9/29 15:53:40

Polyline与Polygon实战:地图覆盖物绘制、编辑与坐标转换全解析

1. 项目概述:为什么Polyline和Polygon是地图开发的基本功今年我在给一个物流调度平台做前端地图功能时,接到一个看起来很普通的任务:在业务地图上画出车辆历史行驶轨迹(折线),同时把各配送区域用不同颜色的…

作者头像 李华
网站建设 2026/9/29 15:53:17

手搓Java反序列化Gadget链:CC2、CC4、CC5原理与构造详解

1. 先把Gadget链这回事说透Java反序列化攻防里,最劝退新人的不是反序列化本身,而是Gadget链那堆绕来绕去的类名。CC2、CC4、CC5这些名字,懂的人觉得就几行代码的事,不懂的人看ysoserial源码像看天书。我见过不少人拿着现成POC跑通…

作者头像 李华