news 2026/10/10 9:31:02

校园反诈骗微信小程序:SSM全栈模板从零搭建实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
校园反诈骗微信小程序:SSM全栈模板从零搭建实战

简介:一套面向计算机相关专业毕业设计的校园反诈骗微信小程序完整资料包,涵盖微信小程序端与基于SSM框架的管理后台,可帮助从选题、功能设计、代码实现到论文撰写完成毕业设计,也适用于校园安全知识推广类课程实践。包内含小程序前端、SSM后端、MySQL数据库及后台管理界面,覆盖安全知识展示、知识竞赛、试题管理等核心功能,并附带毕业论文与运行演示视频,便于对照代码理解完整实现流程。资源共947个文件,总大小约44MB,文件类型以Vue前端页面、Java后端代码、JS脚本、WXML/WXSS页面、XML配置、SQL建库脚本及mp4演示视频为主,前后端结构与文档资料层次清晰,另有环境配置脚本可简化部署、快速启动项目。已有650人参与学习下载,能直观看到项目运行效果,可快速部署到微信开发者工具和本地环境,并围绕反诈宣传应用场景灵活扩展功能。

1. 校园反诈骗微信小程序:不是玩具项目,是一套可交付的SSM全栈模板

高校反诈宣传做得最累的环节是什么?纸质问卷发下去收不齐,知识讲座结束一问"刷单返利能不能信",台下还是有人点头。某高校保卫处之前就想把反诈教育搬到线上,于是这套校园反诈骗微信小程序被设计了出来:学生用小程序注册登录,刷安全知识、参加知识竞赛,管理员在后台配题库、发竞赛、看答题数据。技术栈选型很直接——微信小程序做移动端,SSM框架做后台服务,MySQL存数据,管理端用Vue.js搭界面。它不是只给评委看PPT的单页demo,而是把用户管理、安全知识管理、知识竞赛管理、竞赛试题管理、成绩统计全部串成闭环的毕业设计级模板。适合正在选题的计算机类学生,也适合想快速复刻一套"管理后台+小程序端"信息管理系统的开发者。这篇文章按"架构→搭建→改造→踩坑→交付"的顺序,把它拆开讲透。

2. 系统架构与数据模型:SSM+小程序的分工与八张核心表

2.1 双端口的分工边界:小程序端、Vue管理端、SSM服务端各管什么

这套系统最容易被误解的地方是"SSM就是后台",实际上它拆成了三个独立工程。小程序端只负责展示和交互,管理端负责配置和数据维护,SSM服务端是两者之间的接口层。拿到源码包后第一件事不是急着跑,而是先认清目录边界,不然改错地方会让你后面几个晚上都睡不好。

campus-fanzha/ ├── admin-web/ # 管理后台,Vue.js + Element UI │ ├── src/views/contest/ # 竞赛管理、试题管理页面 │ ├── src/views/knowledge/ # 安全知识管理页面 │ └── src/api/ # 封装所有HTTP请求 ├── miniprogram/ # 微信小程序端 │ ├── pages/index/ # 首页:安全知识列表 │ ├── pages/contest/ # 竞赛入口与答题页 │ ├── pages/user/ # 注册登录、个人中心 │ └── utils/request.js # wx.request统一封装 ├── server/ # SSM后端服务 │ ├── src/main/java/ # Controller / Service / Mapper │ ├── src/main/resources/ # Mapper.xml、jdbc.properties │ └── pom.xml # Maven依赖与插件配置 └── sql/ # 建库建表脚本与初始数据

看目录结构能直接得出一个结论:小程序端和管理后台是两个独立的客户端,它们都只通过HTTP协议访问server端的接口,互相之间没有直接依赖。这意味着你在调试管理端的时候,完全不需要打开小程序;反过来也一样,只要server端接口稳定,两个端可以并行开发。参数上留意server端默认端口是8080,admin-web开发服务器默认跑8081,小程序端请求的baseUrl指向8080。三个端口互不冲突,是这套模板能"一键启动"的前提。

2.2 题库与竞赛模块的表结构设计思路

整个系统的数据模型核心是"竞赛—试题—答题记录"三张表。竞赛表管场次,试题表存题目,答题记录表存用户每次交卷的答案。很多同学在这块翻车,是因为把题目直接写死在竞赛表的一个字段里,后面做统计时根本拆不出来。正确的做法是把试题独立成表,用contest_id做外键关联。

CREATE TABLE `tb_contest` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '竞赛名称', `start_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `status` tinyint(1) DEFAULT '0' COMMENT '0未开始 1进行中 2已结束', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `tb_question` ( `id` int(11) NOT NULL AUTO_INCREMENT, `contest_id` int(11) NOT NULL COMMENT '所属竞赛ID', `content` varchar(500) NOT NULL COMMENT '题干', `option_a` varchar(255) DEFAULT NULL, `option_b` varchar(255) DEFAULT NULL, `option_c` varchar(255) DEFAULT NULL, `option_d` varchar(255) DEFAULT NULL, `answer` char(1) NOT NULL COMMENT '正确选项字母', PRIMARY KEY (`id`), KEY `idx_contest` (`contest_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `tb_answer_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `contest_id` int(11) NOT NULL, `question_id` int(11) NOT NULL, `user_answer` char(1) DEFAULT NULL, `is_correct` tinyint(1) DEFAULT '0' COMMENT '0错误 1正确', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_contest_question` (`user_id`, `contest_id`, `question_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表SQL的关键点在UNIQUE KEYuk_user_contest_question,这个唯一索引从数据库层面拦住了同一个用户对同一道题的重复提交。answer字段用char(1)存单个字母而不是存整段文字,是因为判分时直接比对字符串最省事,也方便后续做正确率统计。MySQL版本建议5.7或8.0,字符集必须用utf8mb4,否则中文题干存进去会变成问号,后面第5章会专门讲这个坑。

2.3 管理员与用户双角色的权限落地方式

小程序端的用户走微信授权登录,服务端拿到openid后去用户表匹配,没匹配到就自动注册。管理端走标准账号密码登录,成功之后服务端签发一个token,后续请求在HTTP头里带上这个token。两个体系的权限完全隔离,避免出现"学生用小程序账号登录管理后台"这种事故。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行登录接口和静态资源 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/register") || uri.contains("/miniprogram/")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !TokenUtils.validate(token)) { response.setStatus(401); return false; } return true; } }

这段拦截器代码的逻辑分三块:先放行不需要登录的路径,再取请求头里的Authorization字段,最后校验token合法性。注意uri.contains("/miniprogram/")这段,小程序端的请求走的是openid登录,不走管理端的token体系,如果忘了放行,小程序端所有接口都会被拦成401。注册这个拦截器时还要在SpringMVC配置里设置exclude-path,否则静态资源CSS、JS也会被拦截。

3. 本地环境搭建:三个bat文件背后的完整启动链路

3.1 环境清单与版本选型

源码包里带的1-install.bat、2-run.bat、3-build.bat三个文件,对应的是安装依赖、启动服务、构建打包三个环节。在双击它们之前,电脑上必须配齐环境。这套模板用的不是Spring Boot,是传统的SSM框架,所以JDK版本不能太高,实测JDK 1.8最稳,JDK 11及以上会遇到Tomcat兼容性问题。

软件推荐版本用途
JDK1.8SSM后端编译与运行,版本过高会踩Tomcat坑
Maven3.6+后端依赖下载与打包
Tomcat8.5servlet容器,也可用maven插件内嵌启动
MySQL5.7或8.0数据存储,务必选utf8mb4字符集
Node.js12+管理端Vue.js构建与本地开发
微信开发者工具稳定版运行小程序端,导入miniprogram目录

这里有个选型理由值得说一句:为什么用Tomcat 8.5而不是Tomcat 10?因为SSM项目里大量依赖javax.servlet包,Tomcat 10起把javax包名改成了jakarta,老项目直接跑不起来。如果你电脑里已经装了Tomcat 10,要么换8.5,要么改Maven依赖并且全局替换import包名——后者工作量比你想象的大,别问我是怎么知道的。

3.2 数据库初始化与连接配置

打开sql目录,里面通常是init.sql和data.sql两个脚本。前者建表,后者灌初始数据。执行顺序不能反,建表脚本没跑就导数据会报"表不存在"的错误。

-- init.sql CREATE DATABASE IF NOT EXISTS fanzha DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE fanzha; -- 后续建表语句见第2章,脚本里已经按依赖顺序排好

数据库建好之后,去server/src/main/resources目录下找到jdbc.properties文件,改两个地方:密码和数据库名。源码包默认的账号密码一般是root/123456,如果你本机MySQL密码不同,不改这个文件的话后面所有接口都会报数据库连接失败。

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/fanzha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

三个参数要解释一下。characterEncoding=utf8是必须的,不加它会导致中文乱码;serverTimezone=Asia/Shanghai只对MySQL 8.0必需,5.7版本可加可不加;连接串里的fanzha是数据库名,跟init.sql里建的库名必须一致,否则连接会指向一个不存在的库。改完之后双击1-install.bat,它会先跑mvn clean install把后端依赖下好,再进admin-web目录执行npm install装前端依赖。这一步时间比较长,Maven首次下载依赖可能要十几分钟,是正常现象,不是卡死了。

3.3 小程序端AppID与接口地址的修改点

小程序端是最容易卡住新手的地方。下载源码后直接用微信开发者工具打开miniprogram目录,会弹出一堆红色报错,原因就两个:AppID不对、接口地址不对。先改AppID,打开project.config.json文件:

{ "appid": "touristappid", "projectname": "campus-fanzha", "setting": { "urlCheck": false } }

注意appid字段,如果你没有注册小程序账号,填touristappid可以开启游客模式,能用大部分API但部分功能受限。开发调试阶段用游客模式足够了。urlCheck字段是另一个关键开关,必须设成false,否则微信开发者工具会强制校验接口域名必须是HTTPS备案域名,你本地用http://localhost请求接口会被直接拦截,这是小程序本地调试最常见的翻车点之一。

改完AppID,再打开utils/request.js文件,把baseUrl指向你本机的后端服务:

const BASE_URL = 'http://127.0.0.1:8080/fanzha/api'

这个路径由三部分组成:127.0.0.1:8080是你Tomcat的监听地址,fanzha是后端服务的context-path,api是接口统一前缀。如果你的Tomcat改了端口或者把项目发布成了别的名字,这里必须同步修改。改完这两个文件,小程序端才算通了一半,另一半取决于后端服务是否已经启动。

4. 核心模块改造:把知识竞赛做成可复用的答题引擎

4.1 安全知识列表的分页与富文本展示

小程序首页展示安全知识列表,后台返回的数据是分页的。这个设计不是炫技,而是因为安全知识的条目会不断增长,一次拉全量数据会拖垮低端手机的渲染。看小程序端的请求封装:

// utils/request.js const request = (url, data = {}, method = 'GET') => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, data: data, method: method, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail: (err) => reject(err) }) }) } // pages/index/index.js Page({ data: { list: [], page: 1, limit: 10, total: 0 }, onLoad() { this.loadKnowledge() }, async loadKnowledge() { const res = await request('/knowledge/list', { page: this.data.page, limit: this.data.limit }) this.setData({ list: this.data.list.concat(res.rows), total: res.total }) }, onReachBottom() { if (this.data.list.length < this.data.total) { this.setData({ page: this.data.page + 1 }, () => this.loadKnowledge()) } } })

核心逻辑在onReachBottom触底加载那里。每次翻页page加1,limit保持10条,list累加而不是覆盖。后端返回的total是总条数,当前已经加载的条数小于total时才允许继续翻页,这个判断能避免最后一页重复请求。参数page和limit是后端Controller接收的分页参数,在Mapper.xml里对应limit #{limit}, offset #{offset},如果你想把每页改成20条,只需要把limit改成20,同时小程序端data里的limit也要同步改,两处不一致会导致数据对不上。

4.2 竞赛流程的状态机设计

知识竞赛不是简单的"打开页面开始答题",它有三个状态:未开始、进行中、已结束。后端Service层需要做状态校验,不能让学生限制时间结束后还能提交答案。这是整个项目里最值得反复改的模块。

public Result submitAnswer(Integer userId, Integer contestId, Integer questionId, String answer) { // 1. 校验竞赛当前状态 Contest contest = contestMapper.selectById(contestId); if (contest.getStatus() != 1) { return Result.error("竞赛不在进行中,无法提交答案"); } // 2. 校验用户是否参加过该竞赛 AnswerRecord count = answerRecordMapper.selectByUserAndContest(userId, contestId); if (count != null && count.getSubmitTime() != null) { return Result.error("本场竞赛已提交过答卷"); } // 3. 查题目正确答案并比对 Question q = questionMapper.selectById(questionId); boolean correct = q.getAnswer().equalsIgnoreCase(answer.trim()); // 4. 判定为正确时记1分,最后累加到用户成绩表 AnswerRecord record = new AnswerRecord(); record.setUserId(userId); record.setContestId(contestId); record.setQuestionId(questionId); record.setUserAnswer(answer); record.setIsCorrect(correct ? 1 : 0); answerRecordMapper.insert(record); return Result.success(correct ? 1 : 0); }

这段提交答案的代码里藏着两个关键判断。第一步状态校验保证只有status=1即"进行中"的竞赛才允许答题,防止管理员还没点发布学生就开始刷题;第二步用selectByUserAndContest先查用户是否已经交过卷,避免重复提交刷分。这里有个容易被忽略的细节:answer.trim()是必要的,因为小程序端传过来的答案可能是" A "这种带空格的字符串,不trim的话即使答案正确也会判错。判分结果用isCorrect字段存0或1,后续统计正确率只需要对isCorrect做sum和count,不需要再回去和正确答案比对第二遍。

4.3 管理端试题批量导入与竞赛配置

管理后台的竞赛管理页面有两个操作频率很高的功能:新建竞赛、往竞赛里塞试题。手工一道一道录入效率太低,所以设计成批量导入。后端Mapper层用foreach实现批量插入,而不是在Java代码里循环单条insert。

<insert id="batchInsert" parameterType="list"> INSERT INTO tb_question ( contest_id, content, option_a, option_b, option_c, option_d, answer ) VALUES <foreach collection="list" item="item" separator=","> ( #{item.contestId}, #{item.content}, #{item.optionA}, #{item.optionB}, #{item.optionC}, #{item.optionD}, #{item.answer} ) </foreach> </insert>

foreach批量插入的核心价值在separator=",",它负责在每条VALUES之间补逗号,拼成一条完整的INSERT语句。相比逐条insert,批量插入一次数据库连接就能完成所有数据的提交,100道题大约能快20倍。参数item对应Java实体类的属性名,注意optionA的A是大写,如果实体类字段写成了optiona,MyBatis反射时找不到属性会直接报错。管理端页面上给这个接口配的是表格上传组件,支持从Excel复制多行数据粘贴提交,接口单次最大接收200条,超过之后需要分批传,这是为了避免一次性拼接过长的SQL撑爆数据库连接。

5. 避坑排查:校园反诈小程序从下载到演示的六个翻车现场

这一章记录的是复现这套毕业设计时最容易遇到的排障记录,全部来自真实调试场景。

5.1 现象:Tomcat能启动,但管理端页面一直转圈

双击2-run.bat后,Tomcat正常启动,MySQL也连上了,但打开管理后台登录页时,浏览器控制台里一堆红色请求超时。

原因:admin-web的Vue项目默认开发端口是8080,后端Tomcat默认也监听8080,两个dev server打了架,其中一个被迫改用随机端口,前端页面自然找不到接口。

解决:打开admin-web目录下的vue.config.js,把devServer.port改成8081,同时把proxy配置里的target指向http://localhost:8080。改完重启2-run.bat,问题消失。

5.2 现象:小程序端全部接口请求失败,报"url not in domain list"

微信开发者工具的控制台里出现这个提示,中文意思是"域名不在合法列表中"。

原因:微信对小程序请求的域名有白名单机制,正式上线必须用备案过的HTTPS域名,本地调试的http://127.0.0.1不可能在名单里。

解决:开发者工具右上角"详情"→"本地设置",勾选"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书"。注意这个选项只对当前项目生效,重新导入项目后需要再勾一次。

5.3 现象:数据库里中文全是问号,英文正常

安全知识内容、用户昵称存进MySQL后变成一串"????"。

原因:init.sql里如果只写了CREATE DATABASE没指定字符集,MySQL 5.7会用默认的latin1,这个字符集没有中文字符映射。

解决:建库语句改成CREATE DATABASE fanzha DEFAULT CHARACTER SET utf8mb4,jdbc连接串加上characterEncoding=utf8。已经建出来的库不要直接改字符集,把数据导出后重建库再导回来,否则已损坏的数据是救不回来的。

5.4 现象:管理后台登录接口一直401

输入正确的管理员账号密码,登录接口返回401 Unauthorized。

原因:第2章写的AuthInterceptor拦截器没有放行login路径,请求在进入Controller之前就被拦掉了。

解决:打开拦截器配置类,把excludePathPatterns改成包含"/admin/login"和"/api/knowledge/list"等公开接口,或者按路径前缀整体放行登录相关请求。改完重启Tomcat再试。

5.5 现象:竞赛发布后小程序端看不到新加的题

管理员在后台成功导入了50道题,小程序端竞赛页面却只有旧的15道,刷新也没用。

原因:小程序端用了onReachBottom触底加载,每页10条,新导入的题排在旧数据后面。如果你已经翻到了最后一页,list长度等于total,新题加进来后total变大,但page还是原来的最大值,不会自动触发加载。同时后端接口没有禁用缓存,部分手机浏览器会缓存GET请求的响应。

解决:在竞赛页面加一个下拉刷新组件,onPullDownRefresh里把page重置为1、list清空、重新请求第一页。后端Controller对list接口加@ResponseHeader里设置Cache-Control: no-cache,或者干脆把list请求改成POST,POST请求默认不缓存。

5.6 现象:2-run.bat启动后窗口闪退

双击bat文件,弹出一个黑色窗口瞬间就没了,后端服务没起来。

原因:bat文件默认在当前目录执行,但2-run.bat里用了相对路径,如果双击时的工作目录不在项目根目录,mvn命令找不到pom.xml就直接退出了。

解决:用管理员身份打开命令行,cd到项目根目录后手动执行mvn tomcat7:run。想看具体报错信息,在2-run.bat的@echo off下面加一行pause命令,让窗口停住不消失。这也是排障的第一步——先让错误信息露出来,才能知道下一步查什么。

6. 交付与验证:论文结构和演示录制的最后一公里

6.1 论文五章结构与答辩问题准备

这套资源答辩的时候重点看眼界的提升,看你有没有思考过架构选择。答辩一定会问你"为什么选SSM而不是Spring Boot",准备思路可以是:SSM的MyBatis对复杂SQL控制更灵活,竞赛试题批量导入这种需求用XML手写SQL更好调试。另外一个高频追问是"如何防止学生重复刷答案",对应的回答是第2章里那个唯一索引加上Service里的状态校验,两句话能说清楚就达标了。

6.2 演示录制的验收清单

按照下面的顺序录制,操作最顺畅:

步骤操作预期结果
1初始化数据库并启动Tomcat管理端登录页能打开
2用管理员登录后台左侧菜单显示用户管理、知识管理等
3进入竞赛管理,新建一场竞赛列表出现新纪录,状态为"未开始"
4进入试题管理,批量导入10道题返回成功条数与导入条数一致
5回到列表,把竞赛状态改为"进行中"状态字段更新成功
6小程序端登录学生账号首页显示安全知识列表
7进入竞赛,逐题作答提交每题都返回正确/错误提示
8交卷后回个人中心查看成绩得分与答对题数一致,且无法重复交卷

这套验收顺序的好处是把"管理端配置"放在"学生端使用"前面,演示时不会出现学生进了竞赛却看不到题的尴尬。我养成的习惯是拿到源码包的第一天先不看代码,按这个清单跑通一遍,确认数据能写进库、页面能刷出来,再开始读核心模块。这比任何文档都更能判断一套资源能不能安全落地。希望帮到你。

本文还有配套的精品资源,点击获取

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

Win32 ListBox日志窗口:字体、刷新与性能优化实战

简介&#xff1a;适用于Windows桌面开发者的ListBox控件自定义示例&#xff0c;聚焦日志列表框的字体与颜色定制。面向初涉MFC或Win32控件扩展的开发者&#xff0c;演示如何让日志条目按错误、警告、信息等级别清晰区分&#xff0c;从而提升界面可读性与用户体验&#xff0c;尤…

作者头像 李华
网站建设 2026/10/10 9:28:52

Java超级签名系统源码解析:iOS内测分发与APK分发平台搭建

简介&#xff1a;这套源码实现了一个基于 Java 的 Android 超级签名与 APK 分发系统&#xff0c;核心解决 APK 批量签名、自动打包和企业内部分发问题&#xff0c;适合需要自建签名服务的开发者、运维工程师或移动端技术管理者。压缩包共 434 个文件&#xff0c;约 48.82MB&…

作者头像 李华
网站建设 2026/10/10 9:27:50

ASP+Access服装系统搭建与调试实战指南

简介&#xff1a;本资源是一套面向Web开发初学者与毕业设计学生的ASPACCESS网上服装销售系统完整实践方案&#xff0c;聚焦传统动态网站开发技术栈的学习与复现。资源包含系统设计论文、可运行源代码、开题报告、中期检查表及答辩PPT五大核心模块&#xff0c;覆盖需求分析、数据…

作者头像 李华
网站建设 2026/10/10 9:27:43

Linux下手动编译安装GCC 9.1.0:从configure到排坑全攻略

简介&#xff1a;gcc-9.1.0.tar.gz 是 GNU 编译器集合 9.1.0 版本的完整源码压缩包&#xff0c;适用于 Linux/类 Unix 环境下的开发者、运维人员与计算机专业学习者&#xff0c;解决从源码编译、安装自定义 GCC 工具链&#xff0c;到理解现代编译器内部结构的问题。包体约 118.…

作者头像 李华
网站建设 2026/10/10 9:27:01

Java元空间Metaspace泄漏排查:jstat+jcmd+Arthas三重定位法

线上Java服务如果反反复复出现重启&#xff0c;重启前日志里飘着一句java.lang.OutOfMemoryError: Metaspace&#xff0c;监控面板上Metaspace的committed一路爬升&#xff0c;used却低得像在嘲笑你&#xff0c;那基本可以断定&#xff1a;元空间正在泄漏。这种事我遇到不止一次…

作者头像 李华
网站建设 2026/10/10 9:26:54

JSP+Servlet+MySQL学生管理系统教学实践指南

简介&#xff1a;这是一套基于JSPServletMySQL实现的完整学生信息管理系统源码&#xff0c;专为Java Web初学者及高校课程设计、期末大作业需求打造&#xff0c;覆盖用户登录、学生增删改查、教师管理、密码找回等核心功能模块&#xff0c;代码结构清晰、逻辑完整&#xff0c;已…

作者头像 李华