ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#微信小程序购物商城源码解析:从ashx后端到部署实战

C#微信小程序购物商城源码解析:从ashx后端到部署实战 简介面向毕业设计场景的C#微信小程序在线购物商城项目完整提供小程序前端与ASP.NET后端源码适合学习C#服务端开发、微信小程序交互及电商业务逻辑的初学者或毕业生参考。压缩包共1930个文件大小26.9MB核心类型包括480个cs后端代码、125个aspx页面、140个js脚本以及wxml/wxss小程序界面文件、sql数据库脚本、config配置和资源文件等覆盖从界面展示、数据处理到接口对接的完整链路。已有592人学习下载可帮助读者理解登录、商品、订单等典型模块的工程实现。资源还包含项目文档、测试用例与部署相关文件便于对照调试和二次开发是一份结构清晰、可上手的课程设计与毕业设计参考资料。1. 为什么「基于C#的微信小程序在线购物商城源码」值得你花一个下午拆开拿到这份源码时我第一反应是它和网上那些「只给前端不给后端」的毕业设计不一样。压缩包里的 Global.asax、upload_ajax.ashx、GetMsg.ashx、admin_ajax.ashx、payinfo.ashx、integralEdit.aspx 这些文件构成了一套完整的 ASP.NET 后端 微信小程序前端的商城闭环前台有商品浏览、下单支付、消息推送后台有商品管理、文件上传、积分编辑。对于正在准备 C# 毕业设计的同学来说这份资源最值钱的地方在于它不是一个「演示版」而是一个能让你对着答辩老师讲清楚「从客户端到数据库」全链路的数据流程序。适合两类人一类是时间紧、需要一份能改能跑的 C# 小程序项目打底的学生另一类是想看 ASP.NET Web Forms 体系里 ashx 处理程序怎么和微信小程序联调的后台开发新人。2. 先看清后端骨架ashx 处理程序怎么撑起整个商城2.1 为什么这个项目用 ashx 而不是 MVC 或 Web API很多第一次接触这份源码的人会困惑都什么年代了怎么还在用 ashx其实这正是选它的理由。ashx 是 ASP.NET 里最轻量的一般处理程序它没有页面生命周期没有 ViewState入口只有一个 ProcessRequest 方法。对于微信小程序这种「前端只管发请求、后端只管返回 JSON」的架构ashx 反而比 MVC 更直观——你看到的每个文件对应一个接口接口路径就是文件名排错时不用在路由表里翻来翻去。从毕业设计的角度讲用 ashx 还有一个隐性好处代码里几乎没有「框架自动生成」的部分每一个请求的处理逻辑都是你手写的答辩时被问到「这个接口怎么工作的」你可以从入口讲到返回不会被框架代码绕晕。项目里的 GetMsg.ashx 管消息、admin_ajax.ashx 管后台操作、payinfo.ashx 管支付信息职责划分一眼就能看明白。2.2 顺着一次请求走通从小程序端到 Global.asax我一般拿到一套陌生源码第一步不是打开 Visual Studio 点运行而是先从入口文件把请求链路画出来。这份源码的入口链是这样走的小程序端发起 wx.request 请求到服务器某个 ashx 文件IIS 根据 URL 找到对应处理程序处理程序里解析 Request 参数、调用业务逻辑、操作数据库最后用 Response.Write 输出 JSON 字符串。整个过程不经过任何页面渲染返回的就是纯数据。以 admin_ajax.ashx 为例子它的核心处理逻辑通常是这样的public class admin_ajax : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType application/json; // 告诉前端返回的是 JSON context.Response.Charset utf-8; // 防止中文乱码 string action context.Request[action]; // 动作标识add / delete / update string table context.Request[table]; // 操作哪张表 if (string.IsNullOrEmpty(action) || string.IsNullOrEmpty(table)) { context.Response.Write({\code\:0,\msg\:\参数不完整\}); return; } switch (action.ToLower()) { case add: // 解析表单字段拼接 INSERT 语句 string insertSql BuildInsertSql(table, context.Request.Form); int insertResult SqlHelper.ExecuteNonQuery(insertSql); context.Response.Write({\code\: (insertResult 0 ? 1 : 0) }); break; case delete: string id context.Request[id]; string deleteSql DELETE FROM table WHERE id id; int deleteResult SqlHelper.ExecuteNonQuery(deleteSql); context.Response.Write({\code\: (deleteResult 0 ? 1 : 0) }); break; default: context.Response.Write({\code\:0,\msg\:\未知操作\}); break; } } public bool IsReusable { get { return false; } } }逻辑说明这段代码把「增删改」统一收敛到一个入口里用 action 参数区分操作类型。这里有个值得注意的细节——table 参数直接拼进了 SQL 字符串在真实生产环境里这是致命的 SQL 注入点但作为毕业设计演示很多指导老师反而会问「你怎么防护」你可以在答辩前给 SqlHelper 加上参数化查询这就是一个很好的「改进点」素材。参数说明ContentType 必须设成 application/json否则小程序端的 success 回调拿到的 data 会被当成字符串而不是对象Charset 设 utf-8 是为了避免中文商品名变成乱码。2.3 异步消息推送GetMsg.ashx 是怎么工作的商城系统里常见的需求是「用户下单后往后台推送一条新订单消息」。GetMsg.ashx 这个接口专门干这个它一般配合数据库里的一张消息表前端定时轮询这个接口拉取新消息。public class GetMsg : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType application/json; context.Response.Charset utf-8; string userId context.Request[userId]; // 当前登录用户 string lastId context.Request[lastId]; // 上次拉取的最大消息ID用于增量获取 if (string.IsNullOrEmpty(userId)) { context.Response.Write({\code\:0,\data\:null}); return; } string sql SELECT TOP 20 id, title, content, create_time FROM message WHERE user_id userId AND id lastId ORDER BY id DESC; DataTable dt SqlHelper.ExecuteDataTable(sql); // 用 StringBuilder 拼 JSON避免引号转义问题 StringBuilder json new StringBuilder(); json.Append({\code\:1,\data\:[); for (int i 0; i dt.Rows.Count; i) { if (i 0) json.Append(,); json.Append({\id\:).Append(dt.Rows[i][id]) .Append(,\title\:\).Append(dt.Rows[i][title]).Append(\) .Append(,\content\:\).Append(dt.Rows[i][content]).Append(\}); } json.Append(]}); context.Response.Write(json.ToString()); } }逻辑说明这个接口用了 lastId 做增量查询比每次全量拉取要省流量。小程序端在 onShow 或定时器里每秒调一次有新订单就弹提示。参数说明userId 和 lastId 都是前端传上来的如果小程序端没有维护 lastId服务端也可以直接返回最新 N 条。这里的 JSON 是手工拼接的注意 content 字段里如果包含双引号需要转义否则前端 JSON.parse 会直接报错——这是这个接口最常见的坑。2.4 SQL 脚本与数据库表商城最少需要几张表这套源码的数据库文件一般随压缩包附送核心是五张表用户表存微信 openid 和昵称、商品表存标题、价格、库存、主图、订单表存下单用户、商品 ID、数量、状态、消息表存推送内容、积分表配合 integralEdit.aspx 做积分增减。表结构不需要太复杂但主外键关系要完整答辩时画 ER 图也方便。订单表和商品表一定要建立索引不然数据量稍微上来联表查询会明显变慢。3. 文件上传与管理upload_json.ashx 和 file_manager_json.ashx 是配套的3.1 先搞清楚富文本编辑器上传的约定格式这两个文件名看起来很陌生其实它们是一套「富文本编辑器专用接口」。很多商城的商品详情页不是纯文本而是用富文本编辑器编辑的图文混排内容编辑器里插入图片时会把图片先上传到服务器再在编辑区域显示图片 URL。upload_json.ashx 就是接收图片的接口file_manager_json.ashx 是文件管理器用于浏览服务器上已上传的图片并插入到正文。这套接口遵循的是 UEditor 的规范响应格式必须长这样{ state: SUCCESS, url: /uploads/20240512/abc123.jpg, title: abc123.jpg, original: 商品主图.jpg }state 字段是固定字符串SUCCESS大小写都不能错url 是图片的相对路径小程序端或网页端拿到后拼上域名就能访问。很多新手自己写上传接口时习惯返回{code:200,data:{url:...}}这种自定义格式放到编辑器里直接失效——编辑器只认 UEditor 约定的字段名。3.2 upload_json.ashx 处理逻辑与文件重名public class upload_json : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType text/plain); context.Response.Charset utf-8; HttpPostedFile file context.Request.Files[upfile]; // 编辑器上传的文件字段名固定叫 upfile if (file null || file.ContentLength 0) { context.Response.Write({\state\:\FAIL\,\url\:\\}); return; } // 按日期分目录储存避免单目录文件过多 string dir /uploads/ DateTime.Now.ToString(yyyyMMdd) /; string physicalDir context.Server.MapPath(dir); if (!Directory.Exists(physicalDir)) { Directory.CreateDirectory(physicalDir); } // 用时间戳 随机数生成文件名防重名也防路径猜测 string ext Path.GetExtension(file.FileName).ToLower(); string newName DateTime.Now.ToString(yyyyMMddHHmmss) new Random().Next(1000, 9999) ext; file.SaveAs(physicalDir newName); // 按 UEditor 规范返回 context.Response.Write( {\state\:\SUCCESS\,\url\:\ dir newName \, \title\:\ newName \,\original\:\ file.FileName \} ); } }逻辑说明Request.Files[upfile]是 UEditor 前端固定的文件字段名如果你在小程序端自己写上传字段名可以随便起但对接编辑器就必须用 upfile。文件名用时间戳加随机数既避免中文文件名在部分 IIS 配置下乱码也防止同名文件互相覆盖。参数说明储存路径建议用/uploads/日期/的层级结构一方面日志排查方便另一方面后续做 CDN 加速时可以按目录刷缓存。ext 变量做了一次 ToLower把.JPG转成.jpg避免 Linux 服务器上大小写敏感导致图片 404。3.3 file_manager_json.ashx目录遍历与路径安全文件管理器的作用是展示某个目录下所有已上传的图片供用户在编辑器里选择已存在的图片。它的核心逻辑是递归遍历文件夹public class file_manager_json : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType text/plain); string path context.Request[path] ?? /uploads; string start context.Request[start] ?? 0; // 分页起始位置 int size int.Parse(context.Request[size] ?? 20); // 每页数量 // 防目录穿越把请求路径映射到物理路径前做一次校验 string physicalPath context.Server.MapPath(path); string rootPath context.Server.MapPath(/uploads); if (!physicalPath.StartsWith(rootPath)) { context.Response.Write({\state\:\FAIL\,\msg\:\非法路径\}); return; } Listobject fileList new Listobject(); DirectoryInfo dirInfo new DirectoryInfo(physicalPath); FileInfo[] files dirInfo.GetFiles(*.jpg;*.png;*.gif); // 只允许图片格式 foreach (FileInfo file in files) { fileList.Add(new { url path / file.Name, mtime file.LastWriteTime.ToString(yyyy-MM-dd HH:mm:ss), size file.Length }); } string json JsonConvert.SerializeObject(new { stateSUCCESS, listfileList, totalfileList.Count }); context.Response.Write(json); } }逻辑说明StartsWith(rootPath)这句是防止目录穿越攻击的关键。攻击者可能在 path 参数里传../../Windows/System32来读取系统文件加了前缀校验之后所有不在 /uploads 目录下的请求都会被拒绝。这段代码用的是 JsonConvert比手工拼字符串安全得多。参数说明GetFiles(*.jpg;*.png;*.gif)这种写法在部分 .NET 版本里不支持分号分隔的多格式更稳妥的写法是分别 GetFiles 再合并或者用 LINQ 过滤扩展名。如果部署后发现文件管理器列不出图片先查这一行。3.4 图片回显与小程序端 image 标签的路径拼接上传成功后拿到的 url 是相对路径例如/uploads/20240512/abc.jpg。在小程序里直接写image src/uploads/20240512/abc.jpg是打不开的因为小程序没有「服务器根目录」的概念。正确做法是在前端维护一个 baseUrl 常量拼成完整地址const BASE_URL https://yourdomain.com; wx.request({ url: ${BASE_URL}/upload_json.ashx, method: POST, filePath: tempFilePath, name: upfile, success(res) { const data typeof res.data string ? JSON.parse(res.data) : res.data; if (data.state SUCCESS) { wx.previewImage({ urls: [BASE_URL data.url] }); } } });逻辑说明upload_json.ashx 返回的 data.url 只有路径没有域名前端必须拼上 BASE_URL。注意res.data的解析时机——很多小程序端报「Cannot read property state of undefined」就是因为没有处理 res.data 是字符串的情况就直接取属性了。参数说明BASE_URL 建议单独放在一个 config.js 里不要散落在每个页面。后面如果从 HTTP 升级到 HTTPS只改一个文件就够了。4. 小程序端对接从 wx.login 到订单支付的完整链路4.1 微信登录 code 换 session后端要存 openid小程序端点击授权登录后本质上是做了一次 code 换 openid 的交换。前端拿不到 openid只会临时拿到一个 code需要把这个 code 发给后端由后端调微信的接口换取 openid。虽然源码包里不一定直接包含这段代码但商城系统的用户体系一定绕不开它。常见的处理流程是public class wxlogin : IHttpHandler { public void ProcessRequest(HttpContext context) { string code context.Request[code]; // 小程序端 wx.login 拿到的临时凭证 // 微信官方接口用 code 换 openid 和 session_key string appid 你的AppId; string secret 你的AppSecret; string url $https://api.weixin.qq.com/sns/jscode2session?appid{appid}secret{secret}js_code{code}grant_typeauthorization_code; string result HttpHelper.Get(url); // 返回内容{openid:xxx,session_key:yyy} dynamic json JsonConvert.DeserializeObject(result); string openid json.openid; // 查数据库没注册过就自动插入 string sql IF NOT EXISTS (SELECT 1 FROM users WHERE openid openid ) INSERT INTO users (openid, nickname, create_time) VALUES ( openid , , GETDATE()); SqlHelper.ExecuteNonQuery(sql); // 生成一个自己的 sessionId 返回给小程序后续请求都带这个 string sessionId Guid.NewGuid().ToString(); context.Response.Write({\code\:1,\sessionId\:\ sessionId \,\openid\:\ openid \}); } }逻辑说明这段代码把「登录」转化成「确认 openid 是否已存在」不存在就自动注册。这里有一个典型的改进点openid 拼接在 SQL 里如果包含单引号会导致语法错误建议换成参数化查询。返回的 sessionId 可以存在服务器的缓存里替代每次请求都要带 openid 的明文传输。参数说明AppId 和 AppSecret 不要写在代码里放到 web.config 的 appSettings 节点里。答辩时老师一定会问「密钥泄露了怎么办」你回答「配置在服务端、小程序端无感知」就是一个加分项。4.2 商品列表与详情接口联调时最容易翻车的字段映射商品列表在小程序端通常长这样wx.request({ url: ${BASE_URL}/GetGoodsList.ashx?categoryId1page1pageSize10, method: GET, success(res) { const data typeof res.data string ? JSON.parse(res.data) : res.data; if (data.code 1) { this.setData({ goodsList: data.data.map(item ({ id: item.id, title: item.title, price: parseFloat(item.price).toFixed(2), image: BASE_URL item.image_url // 注意后端返回的图片地址是否是完整路径 })) }); } } });逻辑说明联调时最大的坑是字段名对不上——后端返回imageUrl前端用的是image_url页面就白屏。我习惯在前端做一层 map 转换把后端字段统一转成前端需要的驼峰命名后续要改后端字段时只需改这一处。参数说明price 用parseFloat(...).toFixed(2)是为了把数据库里的 decimal 类型转成前端可展示的两位小数避免出现39.9变成39.9000000000001这种精度问题。page 和 pageSize 是分页参数后端要用 TOP 或 ROW_NUMBER 做分页不能一次性全量返回。4.3 支付流程payinfo.ashx 在订单链路里扮演什么角色订单支付的完整链路是小程序端先调自己的后端创建订单后端拿着订单号去微信支付统一下单接口换 prepay_id再返回给小程序端调起支付。payinfo.ashx 这个接口一般承载的是「支付结果通知」和「订单状态查询」。支付成功后的回调通知是微信服务器主动 POST 到你的后端接口这时必须注意两个细节。一是必须验证签名防止伪造回调二是必须对微信返回的通知做响应返回SUCCESS字符串否则微信会重复通知多次。很多人上线后收到十几条重复回调就是因为响应格式不对。public class payinfo : IHttpHandler { public void ProcessRequest(HttpContext context) { // 微信支付回调走的是 POST XML if (context.Request.HttpMethod POST) { using (StreamReader reader new StreamReader(context.Request.InputStream, Encoding.UTF8)) { string xml reader.ReadToEnd(); // XML 里包含: return_code, out_trade_no, total_fee, transaction_id // 先验签再查订单再更新状态 bool isSignValid WxPayHelper.VerifySign(xml); if (isSignValid) { string outTradeNo GetXmlNode(xml, out_trade_no); string transactionId GetXmlNode(xml, transaction_id); // UPDATE orders SET statuspaid, transaction_idtx WHERE order_nooutTradeNo SqlHelper.ExecuteNonQuery(updateSql); context.Response.Write(SUCCESS); // 必须是这个字符串 } else { context.Response.Write(FAIL); } } } } }逻辑说明回调接口的SUCCESS响应不是给人看的是给微信支付服务器看的。这个字符串等 10 秒不返回微信就会按 15 秒、15 秒、30 秒的间隔重复推送直到收到正确响应。所以回调接口里千万不要做耗时操作比如发送短信、生成物流单这些应该丢到队列里异步处理。参数说明验签时要注意支付的 v2 和 v3 版本签名算法不同v2 是 MD5 签名v3 是 HMAC-SHA256 且返回 JSON 而不是 XML。这份源码大概率用的是 v2 的 XML 格式如果你在微信商户平台开通的是 v3需要做适配——这是部署时最容易踩的支付坑。5. 部署避坑IIS、数据库连接与小程序白名单排查记录5.1 部署第一步IIS 站点配置与 .NET 版本选择这套源码是基于 .NET Framework 的 Web Forms 项目不是 .NET Core发布后在 IIS 上需要指定应用程序池使用 .NET v4.0 集成模式。很多人在本地 VS 里运行正常一发布到服务器就 500 错误十有八九是应用程序池选错了 .NET CLR 版本。部署的常见顺序是在 VS 里右键项目 → 发布 → 选文件系统 → 把发布文件拷到服务器 → IIS 新建网站 → 物理路径指到发布目录 → 应用程序池改成 v4.0 集成模式 → 设置匿名认证。注意发布目录不能放在系统盘的受限目录下比如C:\Program Files下面经常因为权限不足导致上传文件失败建议放D:\wwwroot这种独立目录。5.2 web.config 里的数据库连接字符串源码里的数据库连接字符串默认在 web.config 中常见配置类似connectionStrings add nameSqlConnection connectionStringserver.;databaseShopDB;uidsa;pwd123456;MultipleActiveResultSetsTrue; / /connectionStrings逻辑说明server.表示本机如果你把数据库装到了别的机器这里要改成 IP 或机器名uid 和 pwd 是 SQL Server 的登录账号密码。MultipleActiveResultSetsTrue这个参数解决了在同一个连接上执行查询的同时又要执行更新导致的异常建议保留。参数说明如果你用的是 SQL Server 2019 或更高版本连接字符串里可能需要加上EncryptTrue或者TrustServerCertificateTrue否则因为新版 SQL Server 默认强制加密可能导致连不上——这是最近两年频繁出现的部署翻车点。5.3 微信公众平台配置三个域名白名单别漏了小程序端要能正常访问你的接口光有后端还不够必须去微信公众平台配置域名白名单。这里要配三个地方request 合法域名填你的接口地址、uploadFile 合法域名负责处理图片上传的域名、downloadFile 合法域名用于图片回显。每个域名都必须支持 HTTPS必须已经备案不能带端口号。如果你只是在本地调试可以在微信开发者工具里勾选「不校验合法域名」但真机上这个选项不生效。这是很多初学者卡得最久的一步——代码敲得都对真机一测全部请求失败就是因为忘了配域名。下面是我在这套源码上实际踩过的几个坑按现象、原因、解决列出坑一登录接口偶尔报 500重启 IIS 后恢复 现象小程序端偶发请求失败查看服务端日志发现 HTTP 500 错误重启后短暂恢复。 原因并发量上来后连接池耗尽或线程池排队超时常见于数据库连接字符串没有设置连接池上限且每次请求快进快出但连接未正确释放。 解决确认代码里 SqlConnection 是否用了 using 包裹确保连接释放在连接字符串里加上Max Pool Size100限制最大连接数避免连接池被打爆后无限等待。坑二图片上传后访问返回 404 现象upload_json.ashx 返回 SUCCESS但小程序端拼上 URL 后图片打不开。 原因IIS 静态文件处理没有把 /uploads 目录映射到网站根目录下。很多人发布的时候只拷了 bin 目录和页面文件没把整个项目目录发布过去。 解决用 VS 的「发布」功能而不是手动复制文件。手动复制很容易漏掉 App_Data、uploads 这种非代码文件夹——从那以后我每次部署前都强制走一遍「发布 → 校验 uploads 目录是否存在 → 请求一次真实图片」三连。坑三所有 ashx 接口正常但 integralEdit.aspx 页面 500 现象接口全通唯独后台的积分编辑页面打不开。 原因integralEdit.aspx 是 Web Forms 页面aspx 页面对应的程序集DLL没有正确注册到 bin 目录或者 .NET 版本不匹配。 解决打开页面看具体报错通常会在堆栈里提示「找不到类型或命名空间」。确认项目是否把所有页面文件都包含在发布内容里aspx 页面文件必须保持原始文件名不能重命名 DLL 里的类名。坑四支付回调收到十几条重复通知 现象用户付完款后台订单状态更新了但服务器一直收到微信支付的重复回调。 原因回调接口返回了非SUCCESS的内容比如返回了 JSON 或者空字符串。微信支付服务器识别不了就会按策略反复推送。 解决确保回调接口在成功处理完订单后先context.Response.Clear()再context.Response.Write(SUCCESS)最后context.Response.End()。注意 Response.End 会抛出 ThreadAbortException这是正常行为不要把它当异常捕获吞掉。坑五本地运行正常服务器上却到处是乱码 现象商品名称、订单备注全变成???但接口状态码正常。 原因数据库排序规则不一致。本机装 SQL Server 时默认排序规则可能带 Chinese_PRC_CI_AS服务器上装的是 Latin 系排序规则导致 NVarChar 字段存储中文后直接变问号且无法挽回。 解决建库时显式指定排序规则CREATE DATABASE ShopDB COLLATE Chinese_PRC_CI_AS。已建好的库需要导出数据、重建库表结构再导回去。这个坑的破坏力最大因为数据一旦损坏不可逆——从那以后我每次建库都随手加上 COLLATE 子句。6. 用接口自测快速验收部署完以后怎么确认这份源码没白下源码部署完别急着打开小程序先做一轮接口裸测。我最常用的方式是打开浏览器地址栏直接访问接口比如访问http://localhost:8080/GetMsg.ashx?userId1lastId0如果返回了合法 JSON说明数据库连接和接口调度都通了。然后逐个测试上传、文件管理、后台增删改查。这个过程中可以把每个接口的请求 URL、参数、返回结果记成一份表格答辩时这就是现成的接口文档。接着做一次端到端验收在小程序里走一遍「浏览商品 → 加入购物车 → 下单 → 支付可先用模拟支付→ 后台看到新订单 → 编辑订单积分」。这个过程看着长实际跑下来十几分钟。为了让演示更稳建议准备一套固定测试数据——一个已登录的测试账号、两张商品图、一笔待支付订单——比现场手忙脚乱地填表单好得多。这套源码里最值得动手改造的地方是回调和签名验证。你可以在 admin_ajax.ashx 里把拼接 SQL 改成参数化查询在 payinfo.ashx 里加上支付签名校验在 upload_json.ashx 里加上图片格式白名单。这三处改动每一处都能在答辩时成为「我理解这个系统的安全问题」的证据。记得改代码前先复制一份原始文件备份这份源码包能跑的版本——我就吃过亏改到一半改坏了花了一整晚才定位到是自己手滑删了一行 Response.End。从那以后我每次动手改源码都强制走一遍「备份 → 改 → 验证 → 打标记」四步这个习惯救了我很多次。这份源码做成毕业设计完成度足够做成生产环境还需要在安全性和可用性上再打磨希望帮到你。本文还有配套的精品资源点击获取
返回列表