简介一份面向网站开发者的免费开源CMS内容管理系统基于Java技术构建可能对应jspxcms。它提供完整的前后台代码后台涵盖内容发布、用户管理、权限控制等管理功能前台则负责内容展示与用户交互方便非技术人员或开发者快速搭建网站。压缩包整体约13.38MB内含Java源码、配置信息及数据库相关文件夹可借此梳理CMS的表结构、数据访问与核心逻辑。已有244人浏览学习。读者通过阅读代码可理解CMS分层架构与权限设计并按开源许可如GPL/Apache License进行二次开发、界面改造或性能优化适合入门至中级Web开发者及有企业站建设需求者参考。1. 这不是又一个后台框架LiteCMS 把内容管理做成了“开箱即用”的完整站点某开发者接了一个小型企业官网项目需求是“能发布新闻、管理产品、后台要简单”。当时面临两个选择用现成的开源 CMS还是从零写一个后来在某开源仓库里翻到 LiteCMS——一个基于原生 PHP MySQL 的内容管理系统没有依赖 Composer 包、没有复杂的框架约束下载下来解压就能安装。跑通之后发现它不光是“后台代码 前台页面”的拼凑而是把路由、模板、权限、安装向导全部打通了。这套代码特别适合两类人一是想快速交付企业站、不想被重型框架绑住手脚的开发者二是刚学完 PHP 基础、想看懂完整前后台代码逻辑的初学者。免费、开源、结构清晰这是它最直接的标签。2. 整体架构与技术选型原生 PHP MySQL 的零依赖设计2.1 工作原理一次请求如何命中文章和栏目每个 CMS 都有自己的一套“路由哲学”。LiteCMS 的设计很朴素所有前台请求都通过根目录下的 index.php 进入入口文件根据 URL 参数解析出控制器、方法和参数。它没有采用 PATH_INFO 那种index.php?carticleaviewid1的传统查询字符串而是配合 Nginx/Apache 的伪静态规则把 URL 重写成/article/1.html这种形式。伪静态规则只负责“让 URL 好看”真正干活的是入口文件里的一个 Route 类。这个 Route 类并不复杂核心方法 dispatch() 负责拆路径。完整实现里还会先去掉.html后缀、过滤空字符串、检查静态资源目录。基础逻辑是这样的// 通过解析请求的路径拆出控制器、方法、参数 public function dispatch(string $uri): array { $path trim($uri, /); // 根路径直接访问首页 if ($path ) { return [controller Index, action index, params []]; } // 去掉 .html 后缀例如 article/1.html - article/1 $path preg_replace(/\.html$/, , $path); $parts explode(/, $path); $controller ucfirst($parts[0]) ?? Index; $action isset($parts[1]) $parts[1] ! ? $parts[1] : index; $params array_slice($parts, 2); return [$controller, $action, $params]; }这里返回的是三个值控制器名、方法名、参数数组。前端控制器拿到它们之后拼出控制器类的完整类名实例化后调用对应方法。因为光靠路径里的第一段还不足以决定走哪个控制器所以还会先通过一个Route::map()方法把常见路由映射提前注册好。比如/category/3要映射到 CategoryController 的 show 方法而不是僵硬地用$parts[1]当方法名。映射表长这样// 系统默认路由表别名 [控制器, 方法] protected $map [ category [CategoryController, show], tag [TagController, list], article [ArticleController, view], page [PageController, view], ];参数说明$uri是剔除了域名和查询参数之后的路径例如/article/1.html在伪静态解析后传入的是article/1.html框架内部再去掉.html后缀。ucfirst是为了和控制器类名命名规范对齐比如article对应ArticleController。这段代码在 LiteCMS 里还有一个前置处理解析静态资源请求凡是/static/开头的路径直接放行不进入控制器逻辑。也就是说路由层同时承担了“请求入口”和“资源拦截”两个职责避免图片和 CSS 也被 PHP 接管白白消耗一次进程。2.2 目录结构前后台代码的边界划分很多 CMS 把前后台代码混在一个 admin 目录下LiteCMS 采用的是“同一套入口、后台单独目录”的结构。根目录看起来是这样的litecms/ ├── index.php # 前台入口 ├── admin.php # 后台入口 ├── install/ # 安装向导 ├── app/ │ ├── Controllers/ # 前台控制器 │ ├── Models/ # 数据模型 │ ├── Views/ # 前台模板 │ └── admin/ # 后台专属控制器与模板 ├── system/ │ ├── Core/ # 核心类库路由、数据库、模板引擎 │ ├── Config/ # 配置文件 │ └── Helpers/ # 辅助函数 ├── static/ │ ├── css/ │ ├── js/ │ └── uploads/ # 上传文件目录 └── backup/ # 备份目录这个划分有个很聪明的地方前台控制器都在app/Controllers后台控制器放在app/admin它们共享同一套system/Core里的基础类。模板方面前台模板在app/Views后台模板放在app/admin/views两者分别继承不同的基础布局互不干扰。install目录在安装完成后会被要求重命名或删除防止重复安装造成数据覆盖。我一般在拿到源代码后第一件事不是看代码而是先看这个目录结构。LiteCMS 的结构能让我很快判断要改文章列表页去app/Views/article/list.php要加后台菜单去app/admin/controllers/MenuController.php。边界清晰二次开发时不需要从头摸索。另外控制器文件名的命名规则也很统一ArticleController.php对应article这个路由前缀模型文件ArticleModel.php对应数据库表article。这套命名约定让自动加载器可以不依赖命名空间映射表直接通过类名拼路径。2.3 数据表设计文章的上下级关系和扩展字段数据库是内容管理系统的灵魂。LiteCMS 默认的表结构不算复杂核心表有六张表名作用category栏目表包含pid字段支持无限级分类article文章表存储标题、内容、封面图、发布时间等tag标签表article_tag文章和标签的关联表user后台管理员表setting系统设置表用来存站点标题、SEO 关键词等其中值得关注的是category表。它没有采用闭包表或者邻接表的复杂设计而是用了一个简单的parent_id指向父分类查询时在 PHP 层递归拼接出树形结构。这在文章量不大的场景下完全够用还省掉了递归 SQL 的麻烦。建表语句大致是CREATE TABLE category ( id int(11) NOT NULL AUTO_INCREMENT, parent_id int(11) NOT NULL DEFAULT 0 COMMENT 父栏目ID0表示顶级, name varchar(50) NOT NULL COMMENT 栏目名称, alias varchar(50) DEFAULT NULL COMMENT URL别名留空则使用ID, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序值越大越靠前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;article表留了一个extend字段类型是TEXT用来存 JSON 格式的扩展属性比如文章访问量、自定义摘要、是否推荐。这样做的好处是当你想给文章增加一个“推荐到首页”的开关时不需要修改表结构直接在后台表单里加上 checkbox提交时把值写进 JSON 即可。控制器读取时用json_decode($article[extend], true)拆开里面的字段就可以当普通数组用。这种设计对新手非常友好不需要理解高深的数据库范式也不需要在初期就把所有字段定死。等到业务复杂度上来再考虑把extend里的高频字段拆到独立列。LiteCMS 的安装脚本会在install/install.sql里定义好这些表执行时还会自动检查每个字段的字符集避免因为服务器默认字符集不同导致中文乱码。实际使用中我最看重的是article表里status字段。它用 0/1 控制文章的上线/下线状态所有查询前台列表的地方都强制带上status 1这就从数据层挡住了“草稿被前台看到”的可能。如果你要二次开发第一件事就是搞清楚每个表里哪个字段是状态开关否则很容易漏掉过滤条件造成数据越权展示。3. 核心模块拆解从 URL 到 HTML 的完整渲染链路3.1 前端控制器入口文件如何做路由分发前文已经提到了 Route 类的 dispatch 方法但真正跑起来还需要看入口文件 index.php 的完整流程。它大概是这个样子的// index.php - 前台入口 require_once __DIR__ . /system/Core/App.php; use LiteCMS\Core\App; $app new App(); $app-loadConfig(); // 加载数据库配置、站点配置 $app-initSession(); // 开启会话判断登录状态 $uri $_SERVER[REQUEST_URI]; // 去掉查询字符串例如 ?page2 if (strpos($uri, ?) ! false) { $uri strstr($uri, ?, true); } // 去掉伪静态重写后可能遗留的 .html 后缀 $uri preg_replace(/\.html$/, , $uri); $route $app-route-dispatch($uri); $controllerName App\\Controllers\\ . ucfirst($route[0]) . Controller; if (!class_exists($controllerName)) { $controllerName App\\Controllers\\IndexController; } $controller new $controllerName($app); $controller-{$route[1]}($route[2] ?? []);这里有个细节初始化时把$app传给了控制器这样控制器里可以随时访问数据库连接、配置、模板引擎等核心对象避免到处写单例。和很多现代框架用容器注入不同LiteCMS 采用的是“手工打造”的依赖传递简单直接适合阅读源码入门。控制器基类里封装了model()、view()、assign()这些常用方法子类只需要调用它们即可。注意preg_replace这句它就是处理伪静态 URL 中.html后缀的核心。如果部署环境没有伪静态规则而是用index.php?rarticle/1这样的形式那入口文件还需要多一步从$_REQUEST中读取r参数作为备用 URL。LiteCMS 里把这两种方式都兼容了判断逻辑是如果$_GET[r]存在就把它当作 URL否则使用REQUEST_URI。为了调试方便你可以在入口文件里临时加一行打印// 临时调试打印实际解析出来的控制器和方法 var_dump($route); exit;把这段代码放在调用控制器之前能直接看到路由结果排查“为什么访问这个地址却进了另一个页面”的问题特别有用。3.2 数据库封装用预处理语句撑起所有查询LiteCMS 没有引入 PDO 之外的任何抽象层它自己写了一个非常轻的 Database 类。这个类做的事情只有三件连接 MySQL、执行预处理语句、封装查询结果。核心方法 query 长得是这个样子public function query(string $sql, array $params []): ?array { $stmt $this-pdo-prepare($sql); if (!empty($params)) { foreach ($params as $key $value) { // 数字索引绑定位置字符串索引绑定命名占位符 $stmt-bindValue( is_int($key) ? $key 1 : $key, $value ); } } $stmt-execute(); return $stmt-fetchAll(PDO::FETCH_ASSOC); }这个方法的特点在于所有 SQL 都必须通过$params传参不允许直接拼接字符串。例如查询文章详情时代码是这样写的$sql SELECT * FROM article WHERE id :id AND status 1; $article $db-query($sql, [id $articleId]);参数说明:id是命名占位符bindValue 时传入键名对应即可。如果是数字索引则绑定到对应位置兼容?占位符。这一层虽然薄但能挡掉 90% 以上的 SQL 注入风险。我拿到源码后先通读了所有模型文件确认没有任何一处直接拼接变量进 SQL才放心把它用到生产环境。你可以在改造时保留这个方法但也可以根据需要加上日志记录把每次执行的 SQL 和耗时记录下来方便排查性能问题。最简单的方式是在 execute 之后写一句error_log($sql . [ . implode(,, $params) . ] . $stmt-rowCount() . 条);在模型层每个业务表对应一个类比如ArticleModel继承一个基础 Model基础 Model 提供find、findAll、where这些魔法方法。实际查询时模型会把当前类的表名自动转换过来ArticleModel对应article表不需要每次写表名。这个约定虽然老套但非常实用尤其是维护由别人写的老 CMS 代码一眼就能看出哪张表在处理。3.3 模板引擎原生 PHP 语法下的继承与循环模板引擎是 CMS 对开发者友好度的关键。LiteCMS 没用任何第三方模板引擎而是直接用 PHP 作为模板语言做了一层很薄的封装。这层封装主要提供三个能力模板继承、变量转义、区块占位。基础布局文件app/Views/layout.php大概是这样的!DOCTYPE html html head title? htmlspecialchars($pageTitle ?? $siteTitle) ?/title /head body header? include __DIR__ . /partials/header.php; ?/header div classcontent ? $content ?? ? /div footer? include __DIR__ . /partials/footer.php; ?/footer /body /html这个写法非常直白公共头部和底部通过原生 include 引入中间区域用$content变量占位。而文章列表页article/list.php通过一个renderLayout函数挂到布局上?php $this-extends(layout); ? div classnews-list ?php foreach ($articles as $a): ? div classnews-item h3a href/article/? $a[id] ?.html? htmlspecialchars($a[title]) ?/a/h3 p? mb_substr(strip_tags($a[content]), 0, 100) ?.../p span? date(Y-m-d, strtotime($a[create_time])) ?/span /div ?php endforeach; ? /div ? $pagination ?? ?这里没有复杂的语法extends告诉模板引擎当前视图应该插入母模板的哪个位置默认就是$content。变量输出都经过htmlspecialchars防止 XSS。如果你在二次开发时想换用别的模板引擎只要保证$this-extends和变量赋值这些方法兼容就可以做到无缝替换。循环输出列表时LiteCMS 在控制器里已经做过分页模板里拿到的$articles是当前页的数据数组分页 HTML 由 Pagination 类单独生成。模板引擎最需要注意的一点是它不会自动转义输出变量所以每一个? ?里的内容都必须手工调用htmlspecialchars或htmlentities。这一点和 Laravel 的 Blade 模板默认转义不同容易被新手忽略。4. 本地跑通全流程环境搭建、安装向导与后台操作4.1 环境准备PHP 版本、扩展和伪静态配置LiteCMS 对 PHP 版本的要求不高PHP 7.4 及以上就可以跑起来推荐 8.1 或 8.2。需要开启的扩展包括pdo_mysql、mbstring、gd图片缩略图用。如果你用的是集成环境这些默认都开着只需要确认一下。检查扩展是否开启可以在命令行执行php -m | grep pdo_mysql php -m | grep mbstring如果有输出说明已经安装如果没输出就打开你的 PHP 配置文件把对应扩展前面的分号去掉再重启 PHP-FPM。LiteCMS 里用到了mb_substr做中文截断没有这个扩展的话文章列表页会直接白屏这个坑下面专门讲。伪静态配置是让人最容易翻车的点。Nginx 环境下站点配置文件的 server 块里要加上这样一段server { listen 80; server_name localhost; root /var/www/litecms; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; } }Apache 环境则依赖.htaccess文件。LiteCMS 源码包自带了一个 Apache 用的.htaccess内容大致是RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?/$1 [L]注意这段规则把路径塞给了index.php入口文件里再去解析$_SERVER[REQUEST_URI]。如果你在 Nginx 上用了 try_files那么$_SERVER[REQUEST_URI]依然保留原始 URL解读起来没问题。要避免的坑是同时开启两套伪静态规则导致路由参数重复最常见的表现是文章详情页总是跳到首页。4.2 安装向导配置文件和数据表的初始化过程把源码解压到 web 根目录后浏览器访问http://localhost/install就能进入安装向导。向导页面分三步检查环境、填写数据库信息、初始化数据。环境检查会逐项列出 PHP 版本、扩展、目录写入权限。如果某个扩展没装页面会标红提示。数据库信息填写的是 MySQL 的 host、端口、库名、用户名和密码。提交后安装程序会先测试连接再执行install.sql。这个 SQL 文件里不仅创建表还会插入初始的站点配置和管理员账号。初始化完成后安装程序会在system/Config/database.php里写入实际配置。system/Config/database.php的内容大致是?php return [ host 127.0.0.1, port 3306, dbname litecms, username root, password your_password, charset utf8mb4, ];这里有一个我每次都会检查的点安装程序写入配置文件时会用什么方式如果用的是file_put_contents那么要注意文件权限。如果 web 服务器用户对system/Config目录没有写权限安装程序就会卡在“写入配置”这一步。我的做法是先在命令行手动创建database.php并设置好内容再跳过安装向导直接进入安装。不过对于大多数场景向导够用了。安装完成后我强烈建议把install目录改名或直接删除。否则别人访问/install/可以重新安装你的数据表和配置会被覆盖。这属于最基础的安全习惯比设置复杂密码还重要。4.3 后台操作从创建栏目到发布第一篇文章安装完成后访问http://localhost/admin.php用安装时设置的管理员账号登录。后台左侧菜单是标准的“栏目管理、文章管理、标签管理、系统设置”。发布文章的流程是先在“栏目管理”里创建一个“行业新闻”栏目保存后得到一个栏目 ID。然后进入“文章管理”点击“新增文章”填写标题、正文选择所属栏目上传封面图。保存时后台会调用ArticleModel::generateSlug生成 URL 别名如果你没填别名就用文章 ID。这里的逻辑是默认情况下文章链接是/article/12.html而如果你填写了“alias”字段则链接变成/article/my-first-news.html。这个别名功能在很多轻量 CMS 里没有LiteCMS 选择把它放在文章表单的“高级选项”里。文章发布后前台首页会根据栏目 ID 拉取每个栏目下的最新文章。这个过程涉及CategoryController的index方法它先从category表取出所有栏目然后对每个栏目执行一次文章查询。如果栏目很多N1 查询问题就来了。LiteCMS 的优化做法是在一次查询里用IN子句把所有栏目的文章都拿出来再在 PHP 层按栏目分组。控制器代码类似public function index() { $categories $this-model(Category)-getAll(); $ids array_column($categories, id); $articles $this-model(Article)-getByCategoryIds($ids, 10); // 按栏目分组方便模板遍历时快速取数 $grouped []; foreach ($articles as $a) { $grouped[$a[category_id]][] $a; } $this-assign(categories, $categories); $this-assign(groupedArticles, $grouped); $this-view(home); }这个分组技巧是我在这个项目中学到的第一个性能优化点。如果你直接 foreach 栏目再查数据库10 个栏目就要 10 条 SQL换成IN查询后只要一条速度是多少倍的差距自己体会。5. 部署与维护避坑指南从本地到服务器常见的 6 个坑5.1 伪静态配置失效导致文章 404现象本地环境跑得好好的部署到服务器后访问http://example.com/article/1.html返回 404但访问index.php?rarticle/1正常。原因大多数情况下是服务器的 web 软件没加载重写模块。比如 Nginx 配置里没有把try_files加到location /Apache 没开启mod_rewrite且.htaccess没被允许。解决先确认 web 服务器类型。Nginx 就在站点配置的 server 块内加上前面说的try_files行然后nginx -t测试配置语法最后重载 Nginx。Apache 则在虚拟主机配置里把AllowOverride设为All。如果还不行打开浏览器的开发者工具看网络请求确认返回 404 的是 HTML 页面还是静态资源如果是 HTML 页面说明重写规则匹配到了index.php但控制器处理时没找到文章再去排查文章表里的数据是否完整。5.2 上传图片后无法访问的权限问题现象后台成功上传了图片但前台加载图片时浏览器报 403或者直接在 HTML 里看到图片裂开。原因static/uploads目录或文件权限不对。很多新手把整个目录设成 777但服务器上一般不允许这么干。上传用的脚本是用 PHP 用户执行的如果目录是 root 所有者PHP 就没法写上传就会失败如果目录是 777则可能被 web 服务器拒绝执行 PHP 文件为了安全。解决把static/uploads目录归属定为 web 服务器运行用户例如 Nginx 和 PHP-FPM 都用www-data则执行chown -R www-data:www-data static/uploads chmod -R 755 static/uploads如果上传时机是后台创建文章还要看 PHP-FPM 的运行用户是否一致。不一致时用ps aux | grep php-fpm查看。这问题最明显的特征是本地正常、服务器上传失败十有八九是权限和属主问题。5.3 数据库连接配置被缓存导致迁移失败现象把网站从一台服务器迁移到另一台改了system/Config/database.php里的数据库地址和密码但访问前台仍然报连接旧库失败或者一直提示连接超时。原因LiteCMS 的App::loadConfig()在加载配置后会把结果缓存到临时目录的一个 PHP 文件里只要缓存文件存在它就不重新读取配置文件。很多人在迁移时完全忘了清这个缓存。解决进入system/下的 cache 目录一般在system/Cache或temp删除所有缓存文件。或者直接在配置类里把缓存开关临时关掉等确认迁移完成再开。我一般会在迁移清单里加上“清除缓存”这一条另外要注意缓存文件的写入权限如果当前目录不可写加载配置时会报错并回退到直接读配置。5.4 后台登录一直跳回登录页的会话问题现象输入正确的账号密码提交后又被弹回登录页没有任何错误提示。原因后台会话基于 PHP Session如果session.save_path指向的目录不存在或不可写会话无法写入登录状态就保存不了。另外如果站点启用了 HTTPS但后台在 HTTP 和 HTTPS 之间来回跳转session.cookie_secure设置为 1 时HTTP 请求不会携带 Session Cookie。解决检查 PHP-FPM 配置里的session.save_path确保目录存在且属主正确。同时把站点统一到 HTTPS 访问登录链接也改成 HTTPS。在本地调试时如果用了localhost和127.0.0.1两种地址访问Session 也会视为不同站点用同一个域名访问就能排掉这个雷。5.5 安装向导打不开一直白屏现象浏览器访问http://localhost/install时页面空白F12 看到 500 错误。原因多半是 PHP 解析出错但错误提示被显示成display_errors关闭了。另外install目录里的index.php引入了某些扩展未启用的函数例如mb_*系列。解决先临时开启错误显示在入口文件顶部加一行ini_set(display_errors, 1);刷新页面看到具体报错。如果是mb_substr未定义安装php-mbstring扩展并重启 PHP-FPM。如果是权限造成确认install目录可读。总之先让错误显示出来再对症下药。6. 二次开发实战给 LiteCMS 增加一个“精选文章”接口6.1 新增数据表与模型方法假设业务需要在前台首页展示“编辑推荐”的文章原始表没有这个字段。最稳妥的方式是加一个标记位而不是新开一张表。可以用安装向导里的install.sql做增量 SQL或者直接在 MySQL 里执行ALTER TABLE article ADD COLUMN is_featured TINYINT(1) NOT NULL DEFAULT 0 COMMENT 是否精选1是0否;然后在ArticleModel里加一个方法public function getFeatured(int $limit 5): array { $limit (int) $limit; // 强制转成整型避免预处理把LIMIT当字符串 $sql SELECT id, title, cover, create_time FROM article WHERE is_featured 1 AND status 1 ORDER BY create_time DESC LIMIT {$limit}; return $this-db-query($sql); }注意这里 LIMIT 的值是通过方法参数传入的我没有用预处理绑定而是直接转整型后拼接。因为$limit来自控制器内部调用不是用户输入这种写法更简洁也能避免 PDO 把LIMIT 5当字符串导致 SQL 语法错误。如果你坚持用绑定记得 bindValue 时指定PDO::PARAM_INT。这是经验之谈预处理是好习惯但不要对所有参数盲目使用绑定尤其在 LIMIT 这种场景转整型更靠谱。6.2 挂载路由与模板调用模型方法写好后需要在控制器里调用并传值给模板。打开IndexController的index方法在返回首页模板前查询一次public function index() { $articles $this-model(Article)-getFeatured(5); $this-assign(featuredArticles, $articles); $this-view(index); }然后在首页模板顶部加一个栏目块?php if (!empty($featuredArticles)): ? div classfeatured-list ?php foreach ($featuredArticles as $item): ? a href/article/? $item[id] ?.html? htmlspecialchars($item[title]) ?/a ?php endforeach; ? /div ?php endif; ?这样首页就能展示最多 5 篇精选文章。如果你希望这个接口还能被 APP 端调用可以在控制器里再加一个json方法public function json() { $articles $this-model(Article)-getFeatured(10); header(Content-Type: application/json); echo json_encode([code 0, data $articles]); }这种改造不需要动路由配置因为控制器文件已经存在只需增加一个方法并让它对应/index/json这个 URL。如果希望更正规的 REST 接口可以把路径改成/api/featured.json只要在路由映射里加一条api/featured [IndexController, json]即可。到这里整个改动涉及三层表加字段、模型加方法、控制器加动作。每一步都有独立验证方法改完表后用 SQL 查询能否过滤出数据模型方法在控制器里 dump 一下返回数组模板改完重新访问首页看是否渲染。不要把三层改动一次堆完再去调那样出了 bug 很难定位。我自己的习惯是每改一层先在对应层写一个临时的exit(json_encode(...))来验证数据确认没有问题后再清掉。这个习惯在框架开发里特别有用因为 CMS 系统的坑常出在模板缓存上改完模板有时候不刷新先要清缓存再继续排查不然你会带着一个旧页面找 bug纯粹是浪费时间。后来我每次做这类 CMS 二次开发都强制先更新目录结构、再清缓存、再走一遍前端展示希望帮到你。本文还有配套的精品资源点击获取