简介信息发布系统源码包面向高校计算机相关专业学生以及希望熟悉 C#、ASP.NET 与 .NET Web 开发的初中级开发者可用于毕业设计、课程设计或自主研习。项目围绕信息发布、分类管理、检索和后台审核展开涵盖用户注册登录、个人信息维护、信息增删改查、分类关联、关键词搜索、分页排序过滤及管理员审核等模块便于理解数据库设计、身份验证与授权、前后端交互以及安全性和性能优化等要点。压缩包共 577 个文件约 53.3MB以 class、java、png、xml、jar 等类型为主分别对应编译类文件、源码、界面图片、配置清单和依赖库其中还包含 vsplayer 视频播放相关子项目可参考视频内容嵌入思路。已有 713 人学习读者可据此分析用户表、信息表与分类表之间的关联借鉴身份验证、权限控制、分页查询和缓存优化的实现方式也能结合响应式界面与视频子项目扩展展示形态。1. 拿到这套信息发布系统源码先别急着打开解决方案三个判断决定它值不值得跑压缩包解开通常是一个.sln、一份数据库备份、一个空的UploadFiles目录——这就是多数信息发布系统源码 C#版本的常见配置。它属于典型的 ASP.NET WebForms 三层结构项目前台负责栏目导航和文章详情后台负责栏目管理、稿件录入、审核发布和用户权限数据库用 SQL Server。放在毕业设计场景里它的价值不在代码写得多漂亮而在于把“内容从录入到上线”这条链路的每个环节都摆出来了包括权限、状态、附件、日志。判断它值不值得花时间先看三件事目标框架是不是 .NET Framework 4.x不是就别指望在旧版 VS 里直接打开数据库是不是 SQL Server 且给了.bak或.mdf而不是只给建表语句权限是写死在页面里的if (role admin)还是落到角色-栏目-操作三张关联表上。前两条决定你能不能跑起来第三条决定你后续改起来会不会被硬编码绑住手脚。适合谁需要一套能跑通、能改栏目、能加审核流的 CMS 底子的人带学生做课题、要演示完整业务闭环的人。不适合谁指望拿它直接扛生产流量的人——分页、缓存、富文本过滤这几块默认实现基本都要重做。2. 拆开解决方案看分层ASP.NET WebForms 的目录职责与数据表映射把项目在 Visual Studio 里加载完先别按 F5。花二十分钟把目录和表结构对一遍比后面 debug 半天更省事。这套代码的骨架不复杂但目录命名各家不一样有的叫BLL/DAL/Model有的叫Business/DataAccess/Entity还有的直接把 SQL 拼在.aspx.cs里。先摸清它到底分了几层决定了你后面改一个字段要动几个文件。2.1 从 .sln 到页面五个目录的职责边界按 WebForms 的惯例一份能跑的信息发布系统源码大致会落到这几块目录 / 文件职责改动频率是否建议动*.sln/*.csproj解决方案与程序集引用低只在加第三方库时动Model/或App_Code/实体类字段与表列一一对应中加字段时必动DAL/拼 SQL、执行SqlCommand、返回DataTable高改查询、加索引必动BLL/业务判断如“待审才能改”“已发布不能删”高审核流改造的主战场Admin/后台页面.aspx.aspx.cs.designer.cs高界面与交互Web/或根目录前台栏目页、列表页、详情页中模板改造UploadFiles/附件与图片落盘位置低权限配置重点WebForms 的一个特点是每个页面三个文件.aspx写服务器控件和绑定表达式.aspx.cs写生命周期事件Page_Load、btnSave_Click.designer.cs是设计器自动生成的控件声明。改界面时只动.aspx改逻辑只动.aspx.cs.designer.cs手改容易和设计器打架一般不碰。判断分层是否真实存在最快的办法是搜一遍SqlConnection。如果它只出现在DAL目录里说明分层是干净的如果Admin下的每个btnSave_Click里都有一份那这套代码本质上是“页面直连数据库”后面加一个字段你要改十几个页面。这种情况不用急着重构先把新功能用DAL写老的慢慢替换。2.2 数据表与实体的对应关系信息发布系统的表不多但关联关系是全的。把下面这几张表认全后面调权限和状态就不会迷路表名常见命名主键关键字段说明CMS_ColumnColumnIdColumnName、ParentId、Sort、IsNav栏目树ParentId0为顶级CMS_ArticleArticleIdTitle、Content、ColumnId、State、AuthorId稿件主表CMS_AttachmentAttIdArticleId、FileName、SavePath、FileSize一稿多附件Sys_UserUserIdUserName、Pwd、RoleId、IsLock密码常见是 MD5后面要换Sys_RoleRoleIdRoleName编辑、审核、管理员Sys_RoleColumn联合主键RoleId、ColumnId、CanAdd/CanEdit/CanAudit数据级授权Sys_LogLogIdUserId、Action、TargetId、CreateTime操作留痕实体类字段和表列对不上是常见现象Content在实体里可能叫BodyState可能叫Status还有的把CreateTime写成字符串类型取出来再Convert.ToDateTime。这不会报错但会让你在写条件判断时踩空。做法是先打开一个实体类对照表结构逐个核一遍类型尤其是decimal、DateTime、bit这三种映射错了在 GridView 里显示正常写回数据库时才炸。栏目树还有一个隐藏约定ParentId是不是允许无限层级。多数实现只支持两级前台导航写死了“一级栏目 二级栏目”的循环。你如果加三级前台模板不会自动展开需要改绑定逻辑。这一点在改造前先用数据试手动插一条ParentId指向二级栏目的记录看前台是否显示。2.3 一次发布请求的完整链路把“编辑点保存”这条线走通整套代码的运行方式就清楚了。典型链路如下Admin/ArticleEdit.aspx加载Page_Load里判断Session[UserId]是否为空空则跳登录页用户填写标题、选栏目、富文本编辑器写入正文点保存触发btnSave_Click从控件取值组装实体调BLL.Article.Save(entity)内部先做状态判断再调DALDAL用参数化 SQL 执行INSERT或UPDATE返回受影响行数页面根据返回值ClientScript.RegisterStartupScript弹提示并跳列表页。其中第 4 步是整套代码的业务核心。判断逻辑通常长这样// BLL 层保存区分新增与修改并限制已发布稿件不能直接覆盖 public int Save(Article model) { if (model.ArticleId 0) { model.State 0; // 新增一律进草稿 model.CreateTime DateTime.Now; return dal.Insert(model); } var old dal.GetById(model.ArticleId); if (old null) throw new Exception(稿件不存在); if (old.State 2) // 已发布先下架再改 throw new Exception(已发布稿件请先下架); return dal.Update(model); }这段代码里有两个参数值得注意State 0是硬编码的草稿状态如果后面要加“直接送审”的入口这个赋值就得改成从页面传入old.State 2用的是魔法数字改造时建议先把它换成枚举否则状态一多就没法读了。dal.Insert返回的是IDENTITY或SCOPE_IDENTITY()前者在表上有触发器时会拿到错误的自增值这是老代码里常见的隐患看到就换成后者。链路走通之后再看一个细节前台的列表页是不是每次都实时查库。如果Page_Load里没有Cache相关代码那么后台一发布前台刷新就能看到调试方便但性能一般如果用了Cache.Insert且没设过期策略就会出现“改了看不到”的经典问题排查时先去Global.asax里找有没有统一的缓存清理逻辑。3. 把环境跑起来数据库附加、连接串与目录权限的三处必改项代码看得差不多了开始动手。这一步的目标很明确让登录页出现在浏览器里。整个过程会卡在三个地方——依赖版本、数据库附加、目录权限每一个都有明确的报错特征认出来就好办。3.1 依赖清单与版本对齐先确认本机环境。多数这类项目是 .NET Framework 4.5 到 4.8 之间需要装对应版本的开发工具包数据库用 SQL Server Express 就够但要注意实例名。依赖项常见要求检查方式不满足时的表现.NET Framework4.5 / 4.6.1 / 4.8注册表或 VS 安装器项目显示“不兼容”Visual Studio2017 及以上打开.sln提示需要迁移SQL Server2012 及以上SSMS 连接附加时报版本号错误IIS Express随 VS 安装项目属性里的 Web 选项卡启动时无端口第三方 DLLAjaxControlToolkit、 Newtonsoft.Json 等bin/与packages/对照“未能加载程序集”版本不对齐是第一个坑。.sln文件开头有一行Format Version还有TargetFrameworkVersion写在.csproj里用记事本就能看。如果显示v4.8而本机只有 4.5要么装 4.8 的开发工具包要么把目标框架降下来重新编译——但降框架可能导致某些语法特性不可用所以优先升级环境。第三方 DLL 的处理有个省事的做法先看bin目录里有没有编译好的 DLL有的话直接跑不用管packages里的源码。只有在你需要改第三方库或者它引用了错误版本时才去 NuGet 里还原。判断依据是页面报错里那句Could not load file or assembly ... Versionx.x.x.x报错里的版本号和bin里实际的版本号对一下就知道差在哪。3.2 附加数据库与初始化脚本数据库给的形式通常两种.mdf.ldf文件或者一个.bak备份。前者用FOR ATTACH后者用RESTORE。把路径换成你本机解压后的实际目录再执行。-- 方式一只有 mdf/ldf直接附加 -- 注意mdf 与 ldf 必须同时存在且 SQL Server 服务账号要有读取权限 CREATE DATABASE InfoPub ON (FILENAME ND:\Src\App_Data\InfoPub.mdf), (FILENAME ND:\Src\App_Data\InfoPub_log.ldf) FOR ATTACH; GO-- 方式二给了 .bak 备份用 RESTORE 并指定新路径 -- WITH MOVE 的两个逻辑名要与备份里的逻辑文件名一致写错会报“无法覆盖” RESTORE DATABASE InfoPub FROM DISK ND:\Src\db\InfoPub.bak WITH MOVE InfoPub TO ND:\DbData\InfoPub.mdf, MOVE InfoPub_log TO ND:\DbData\InfoPub_log.ldf, REPLACE; GO两段脚本的关键参数说明FILENAME必须写绝对路径写成相对路径会报操作系统错误 5拒绝访问本质是 SQL Server 服务账号没权限访问你用户目录下的文件MOVE后面的逻辑名不是数据库名要看备份内部的名字用RESTORE FILELISTONLY FROM DISK N...先列一遍最稳妥。REPLACE表示覆盖同名库第一次执行可以不加重复执行时加。如果只给了.sql建表脚本那就先CREATE DATABASE InfoPub再在 SSMS 里切到该库执行脚本。注意脚本里的GO是 SSMS 的批处理分隔符不是 T-SQL 语句用代码执行时要按GO切分。附加完成后做一次数据核对SELECT COUNT(*) FROM CMS_Article看有没有演示数据。有数据的话后台登录账号通常藏在Sys_User表里密码是 MD5直接改一个新值比猜原密码快——用SELECT HASHBYTES(MD5, NewPass123)生成后UPDATE进去。3.3 Web.config 三处改动与目录授权数据库通了接下来改配置。Web.config里必须动的是三处configuration connectionStrings !-- 改动点 1数据源改成自己的实例名Express 版通常是 .\SQLEXPRESS -- add nameConnStr connectionStringData Source.\SQLEXPRESS;Initial CatalogInfoPub;User IDsa;PasswordYourStrongPwd; providerNameSystem.Data.SqlClient / /connectionStrings system.web !-- 改动点 2调试期开 true交付前必须改回 false -- compilation debugtrue targetFramework4.8 / !-- 改动点 3富文本正文较大默认 4MB 不够单位是 KB -- httpRuntime maxRequestLength20480 requestValidationMode2.0 / /system.web /configuration三处的含义分别是Data Source里的点代表本机如果用的是命名实例必须带反斜杠debugtrue会让每个请求多做一些检查交付前改成false是基本要求否则出错页会把堆栈直接吐给访问者maxRequestLength单位是 KB20480 就是 20MB配合system.webServer下的maxAllowedContentLength单位是字节一起改才生效只改一处会出现“小文件能传、大文件 404”的怪现象。接着给上传目录授权。IIS Express 跑的时候用的是当前用户身份问题不大换成完整 IIS 之后应用程序池的虚拟账号需要对UploadFiles有写权限# 给 IIS_IUSRS 组授予修改权限/T 递归到子目录 # (OI)(CI) 表示对象继承与容器继承M 表示修改 icacls D:\Src\UploadFiles /grant IIS_IUSRS:(OI)(CI)M /T # 查看授权结果确认输出里有 IIS_IUSRS 一行 icacls D:\Src\UploadFiles如果授权后仍然上传失败去看应用程序池的“标识”是什么。默认是ApplicationPoolIdentity对应的虚拟账号是IIS AppPool\池名称也可以直接给这个账号授权比给整个IIS_IUSRS组更收敛。三处改完在 VS 里把起始页设成Admin/Login.aspx或Default.aspxF5。能看到登录框这一步就算过了。看不到的话按报错类型分黄页是编译错误看具体行号白页多半是Session或数据库连接超时404 是路由或处理程序没注册检查system.webServer里的handlers节。4. 权限与审核流程落地角色表、状态机与两道校验跑起来之后真正决定这套源码能不能改成可用系统的是权限和状态这两块。毕业设计里这两块常常是半成品权限靠页面顶部的角色判断审核靠一个IsPass字段。跑演示够用一加栏目、一加角色就散架。4.1 表驱动的三级权限角色-栏目-操作先分清三层概念。功能权限决定你能看到哪些菜单数据权限决定你能操作哪些栏目的稿件操作权限决定你能新增还是只能审核。三层里最容易缺的是第二层——很多代码只做了菜单控制结果编辑 A 栏目的账号能改 B 栏目的稿子。判断现有实现够不够用看Sys_RoleColumn这张表在不在。在的话授权粒度就到位了不在的话建议补上改动量不大层级载体控制对象校验时机功能权限Sys_RoleMenu后台菜单项、页面入口菜单渲染、页面Page_Load数据权限Sys_RoleColumn栏目、稿件集合列表查询条件、保存前判断操作权限Sys_RoleColumn的四个bit列增、改、审、删按钮点击事件内补表的时候有个细节要注意Sys_RoleColumn的主键应该是RoleId ColumnId的联合主键而不是自增 ID。用自增 ID 的话同一条授权重复插入不会报错权限表里会出现重复行判断时取到哪一条全看运气这种玄学问题排查起来很费时间。加联合主键之后重复插入直接抛主键冲突问题在插入那一步就暴露了。菜单权限的校验方式通常是页面基类或母版页里做一次判断。如果代码里每个页面开头都是重复的三行if那说明没抽基类这时候新增页面就必须记得复制这三行漏一个就是一个越权入口。改造思路后面 4.3 会讲。4.2 发布状态机四个状态与合法迁移审核流的核心不是“有没有审核按钮”而是状态之间能不能随便跳。一个够用的状态机至少四个状态状态值名称谁可以进入允许迁往0草稿编辑新增或驳回后1 待审1待审编辑提交0 草稿驳回、2 已发布2已发布审核通过1 待审修改重审、3 已下架3已下架管理员下架2 已发布重新上架用表格把规则写死代码就只是翻译这张表。常见的翻车点是允许“已发布”直接跳回“草稿”表面上是撤回实际上前台缓存和静态页不会同步失效用户看到的是旧内容而数据库里已经是草稿状态两边对不上。所以合法迁移里必须把这条排除掉改稿走“下架 → 修改 → 重新提交”的路径。另一个细节是驳回要不要记原因。至少加一列AuditRemark否则被驳回的编辑只能猜哪里不合格。这列在CMS_Article上加就行nvarchar(200)够用。4.3 把校验收口到一个静态入口规则清楚了代码就要收口。别在每个按钮事件里重复写判断抽两个静态类一个管权限一个管状态迁移。// 状态迁移守卫规则集中在这里页面只问“能不能”不自己写 if public enum ArticleState { Draft 0, Pending 1, Published 2, Offline 3 } public static class ArticleFlow { // 字典的键是当前状态值是可到达的目标状态集合 private static readonly DictionaryArticleState, ArticleState[] Map new DictionaryArticleState, ArticleState[] { { ArticleState.Draft, new[] { ArticleState.Pending } }, { ArticleState.Pending, new[] { ArticleState.Published, ArticleState.Draft } }, { ArticleState.Published, new[] { ArticleState.Offline, ArticleState.Pending } }, { ArticleState.Offline, new[] { ArticleState.Published } } }; public static bool CanTransit(ArticleState from, ArticleState to) { ArticleState[] targets; if (!Map.TryGetValue(from, out targets)) return false; return Array.IndexOf(targets, to) 0; } }Map这个字典就是 4.2 表格的代码化。TryGetValue比先ContainsKey再取值的写法少一次哈希查找执行频率高的地方值得这么写。返回false时页面要给出明确提示比如“已发布稿件请先下架”而不是笼统的“操作失败”。权限部分同理// 栏目级权限校验action 取 add / edit / audit / delete public static class Auth { public static bool HasColumnRight(int userId, int columnId, string action) { int roleId UserRepo.GetRoleId(userId); // 可加 60 秒缓存 var right RightRepo.Get(roleId, columnId); // 查 Sys_RoleColumn if (right null) return false; switch (action) { case add: return right.CanAdd; case edit: return right.CanEdit; case audit: return right.CanAudit; case delete: return right.CanDelete; default: return false; } } }页面里的用法是把权限判断放在数据库操作之前而不是放在按钮的Enabled属性上——前端隐藏按钮只是体验优化服务端不校验等于没校验直接构造请求照样能提交。protected void btnSubmit_Click(object sender, EventArgs e) { int uid Convert.ToInt32(Session[UserId]); int columnId Convert.ToInt32(ddlColumn.SelectedValue); if (!Auth.HasColumnRight(uid, columnId, add)) { ClientScript.RegisterStartupScript(GetType(), noauth, alert(无权在该栏目发布);, true); return; } var model new Article { ColumnId columnId, AuthorId uid, Title txtTitle.Text }; // 后续保存逻辑…… }ClientScript.RegisterStartupScript比Response.Write(script)靠谱后者在页面有 AJAX 局部刷新时可能不执行。第三个参数true表示自动加script标签别写成false又自己拼标签容易拼出两个嵌套标签导致整页脚本失效。GetType()作为注册键同一页面注册多个脚本时用不同的键否则后面的会覆盖前面的。5. 避坑与排查这套源码最容易翻车的五处下面五条都是复现过程中真实会撞上的按“看到什么现象、为什么、怎么处理”记录。前三条偏环境后两条偏数据与权限。5.1 跑不起来的三个现场现象一附加数据库时报“数据库版本为 869无法打开此服务器支持 852 及更低版本”。原因是.mdf来自更高版本的 SQL Server而本机实例版本偏低文件头里的内部版本号对不上。解决办法有两条装一个同版本或更高版本的实例来附加或者找提供方导出一份.sql脚本在本机建库后执行。后者更稳因为脚本里通常是纯 T-SQL不携带版本信息。如果只能拿到 mdf别尝试用兼容级别参数去糊那个参数控制的是语法行为不是文件格式。现象二页面能打开但一进后台就报“未能加载文件或程序集 xxx版本x.x.x.x”。原因是bin目录里的 DLL 实际版本与web.config的bindingRedirect或.csproj里的引用版本不一致。老项目升级过 NuGet 包但bin里留着旧的编译时又不覆盖。处理办法先看报错里的版本号再对bin下同名 DLL 右键看属性版本两者不一致就删掉旧的重新生成。如果项目引用了packages目录确认该目录存在且路径正确——HintPath写的是相对路径换个解压位置就失效了。现象三前台首页 404但后台能进。原因是默认文档没配对或者前台的.aspx文件不在站点根目录而在子目录里。先看项目属性里的“起始页”设的是什么再检查 IIS 的“默认文档”列表里有没有Default.aspx。还有一种情况是用了 URL 重写但没装对应模块web.config里有rewrite节而服务器没有URL Rewrite模块时会整站 500.19报错信息里会写明缺少哪个模块。5.2 数据与权限的两类隐性错误现象四富文本编辑器里存进去的正文保存后只剩前几百字或者直接报“检测到有潜在危险的 Request.Form 值”。原因有两层。第一层是请求验证ASP.NET 默认拦截含 HTML 标签的表单值富文本提交的正是标签所以要在该页面的% Page %指令里加ValidateRequestfalse同时在web.config的httpRuntime上设requestValidationMode2.0缺一个都不生效。第二层是字段长度Content列如果建成了nvarchar(4000)超出部分会被截断或抛异常正确类型是nvarchar(max)。关掉请求验证之后必须补输入过滤否则等于自己开了个存储型 XSS 的口子白名单方案在第 6 章讲。现象五后台显示“发布成功”前台刷新还是旧内容等十几分钟才变。原因通常是缓存没失效。列表页在Page_Load里用了Cache.Insert(article_list_ columnId, dt)但没有对应的清理逻辑后台发布只更新了数据库。排查顺序是先看Global.asax有没有Application_Start里注册的缓存依赖再全局搜Cache.Insert找到所有键名在发布、下架、删除三个动作后面统一调一次清理。临时验证的办法是改web.config的compilation节加个空格触发应用重启缓存清空后如果内容正常就确认是缓存问题。长期方案是把键名收敛到一个CacheKey静态类里避免手写字符串拼错导致漏清。还有一种更隐蔽的情况稿件状态字段存的是2字符串而查询条件写的是State 2数字SQL Server 做隐式转换后能查到但如果该列上有索引可能因为类型转换不走索引数据量一大列表页就变慢。核对办法是看表设计器里State的实际类型tinyint或int是正常的varchar就要改。6. 二次开发验收把毕业设计底子改到能交付的四项收口跑通、改完权限剩下的工作是把“能演示”变成“能交付”。这一步我一般盯四个点每个点都有明确的验证方式不是靠感觉。第一是富文本的输入过滤。关掉请求验证之后正文是原样入库的必须加白名单。做法是保存前用正则或 HTML 解析器只保留允许的标签其余转义// 白名单过滤只保留排版常用标签其余标签与事件属性一律清掉 private static readonly Regex SafeTag new Regex((?!\/?(p|br|strong|em|ul|ol|li|img|a)\b)[^]*, RegexOptions.IgnoreCase | RegexOptions.Compiled); public static string CleanHtml(string html) { if (string.IsNullOrEmpty(html)) return string.Empty; // 先干掉 script/style 整块再干不掉白名单外的标签 html Regex.Replace(html, (script|style)[^]*.*?/\1, string.Empty, RegexOptions.Singleline | RegexOptions.IgnoreCase); html SafeTag.Replace(html, string.Empty); // on 开头的事件属性单独清一遍避免 img onerror 这类 return Regex.Replace(html, \son\w\s*\s*[][^]*[], string.Empty, RegexOptions.IgnoreCase); }RegexOptions.Compiled适合调用频繁的静态正则代价是首次编译稍慢Singleline让点号匹配换行否则跨行的script块清不掉。这套过滤不追求完备够挡住最常见的注入就行真要严格就用成熟的 HTML 净化库。第二是分页。默认实现常见两种一次性查全表再DataTable分页或者用GridView自带的AllowPaging。前者在几千条数据时就开始卡。改成数据库端分页-- SQL Server 2012 及以上按发布时间倒序取第 page 页 -- page 从 1 开始size 是每页条数 SELECT ArticleId, Title, PublishTime FROM CMS_Article WHERE State 2 AND ColumnId columnId ORDER BY PublishTime DESC, ArticleId DESC OFFSET (page - 1) * size ROWS FETCH NEXT size ROWS ONLY;ORDER BY里的第二排序键ArticleId不能省发布时间相同的稿件如果没有稳定次序翻页时会出现重复或漏条这种问题在测试数据少的时候完全看不出来。配套索引CREATE INDEX IX_Article_Column_State_Time ON CMS_Article (ColumnId, State, PublishTime DESC) INCLUDE (Title);列顺序按“等值条件在前、范围与排序在后”排INCLUDE把列表页要显示的Title带上避免回表。如果实例是 2008 版本OFFSET/FETCH不支持得换成ROW_NUMBER() OVER (ORDER BY ...)再套一层筛选。第三是密码存储。老代码里 MD5 很常见改造时至少加盐SHA256加每用户随机盐也比裸 MD5 强改动只在注册和登录两个方法里。第四是回归验收。我习惯用一张固定清单过一遍比凭记忆靠谱验收项操作预期越权新增用只读角色账号调新增接口返回无权库中无新记录状态回退对已发布稿件执行“撤回草稿”操作被拒状态不变大附件上传 15MB 文件成功且maxAllowedContentLength生效分页边界总数正好为每页条数的整数倍最后一页无空白页缓存失效发布后在另一浏览器刷新5 秒内可见新内容这套源码我本地一直留着改新项目时拿它当骨架比从零搭快得多。血泪经验是每次动手改之前先把原版压缩包另存一份改坏了能退回去——这个后悔药成本几乎为零但真出事的时候能省一整天。从那以后我每次拆这类老项目都先跑一遍上面的验收清单再动代码。希望帮到你。本文还有配套的精品资源点击获取