简介:这是一套面向计算机专业本科生的毕业设计完整交付包,聚焦微信生态下的数字阅读服务开发,适用于Java全栈与小程序双端实践学习。资源基于微信小程序前端+SSM后端+MySQL数据库技术栈构建,实现图书上架、订单支付、章节阅读、留言互动与后台管理等核心功能,助力学生快速完成毕设选题、开题、开发、答辩全流程。压缩包共1082个文件,涵盖144个JavaScript逻辑文件、111个Vue组件、101个Java业务类、66个WXML页面结构及64个WXSS样式文件,辅以SQL建表脚本、开题报告、毕业论文、答辩PPT和实操演示视频,整体大小15.46MB。已有177人下载学习,内容组织清晰,含build/run/install三类批处理脚本及大量.bak备份文件,便于理解开发迭代过程与工程结构演进,特别适合需要真实项目练手与文档齐备的初学者与毕设开发者。
1. 微信阅读小程序毕业设计:为什么选 SSM + MySQL 而不是 Spring Boot?
你手头有一份标着「微信阅读小程序-毕业设计」的压缩包,解压后看到wx-read-miniprogram/(前端)、ssm-server/(后端 Java 工程)、db/wx_read.sql(建表脚本),还有开题报告、答辩PPT、论文Word和一段3分钟演示视频——这不是一个线上运营项目,而是一套面向高校计算机类专业本科生、可闭环交付、能过答辩、能跑通核心链路的最小可行毕业系统。它不追求高并发或微服务治理,但必须清晰体现「小程序端发起请求 → Java 后端处理业务逻辑 → MySQL 持久化数据」这条主干路径。很多同学卡在「为什么不用更火的 Spring Boot?」——答案很实在:SSM(Spring + SpringMVC + MyBatis)结构清晰、分层明确、配置显式、调试直观,对初学者来说,每个 XML 文件、每个 Controller 方法、每条 SQL 都是可追踪、可打断点、可截图进答辩PPT的实体证据;而 Spring Boot 的自动装配像黑匣子,学生常在@SpringBootApplication启动失败时抓瞎,答辩老师问「这个 Mapper 是怎么被扫描到的?」反而答不上来。这套方案真正解决的是:如何在 8 周内,让一个刚学完 JDBC 的学生,独立完成从登录页到图书列表再到阅读记录保存的全链路闭环,并把每一环节的代码、SQL、接口响应截图放进论文附录。
2. 小程序端:从app.js到book-list.wxml,微信原生开发的最小必要路径
微信阅读小程序不是用 UniApp 或 Taro 编译的,而是纯原生开发——这意味着所有页面结构、样式、逻辑都严格遵循微信官方文档定义的.wxml、.wxss、.js、.json四件套。毕业设计场景下,不追求复杂动画或自定义组件库,只保核心三页:登录页(获取手机号)、图书列表页(分页加载)、图书详情页(阅读进度同步)。下面以「用户登录并绑定手机号」这一高频答辩必讲环节为例,拆解真实可复现的最小实现。
2.1 登录态打通:wx.login()+wx.getPhoneNumber()的双步验证
小程序不能直接获取用户手机号,必须走微信官方提供的getPhoneNumber组件回调。但该组件触发前,必须先完成wx.login()获取临时登录凭证code,再由后端调用微信auth.code2Session接口换取openid。这是微信生态的硬性安全要求,也是答辩老师最爱问的「为什么不能前端直接传手机号?」的答案所在。
// pages/login/login.js Page({ data: { hasLogin: false, userInfo: {} }, // 第一步:获取 code 并传给后端 onGetCode() { wx.login({ success: res => { // 将 code 发送给后端,换取 session_key 和 openid wx.request({ url: 'https://your-domain.com/api/user/login', method: 'POST', data: { code: res.code }, success: resp => { if (resp.data.code === 200) { wx.setStorageSync('token', resp.data.data.token); this.setData({ hasLogin: true }); } } }); } }); }, // 第二步:点击按钮触发手机号授权(需用户主动点击) onGetPhoneNumber(e) { const { encryptedData, iv } = e.detail; if (!encryptedData) return; // 将加密数据发给后端解密 wx.request({ url: 'https://your-domain.com/api/user/bindPhone', method: 'POST', data: { encryptedData, iv }, header: { 'Authorization': wx.getStorageSync('token') }, success: resp => { if (resp.data.code === 200) { wx.showToast({ title: '绑定成功', icon: 'success' }); wx.switchTab({ url: '/pages/book-list/book-list' }); } } }); } });注意:
wx.getPhoneNumber必须绑定在<button open-type="getPhoneNumber">上,且该 button 不能加bindtap,否则无法触发授权弹窗;encryptedData和iv由微信客户端生成,绝对不可在前端解密——这是微信安全红线,也是答辩中「数据安全性」得分点。后端需用session_key+encryptedData+iv调用微信decryptPhoneNumber接口(见下文 SSM 章节)。
2.2 图书列表页:分页加载 + 下拉刷新的实操写法
毕业设计不要求无限滚动或骨架屏,但必须体现「数据驱动视图」的基本思想。book-list页面使用wx:for渲染列表,通过data中的bookList数组绑定,而分页逻辑靠pageNo和pageSize控制。
<!-- pages/book-list/book-list.wxml --> <view class="container"> <block wx:for="{{bookList}}" wx:key="id"> <navigator url="/pages/book-detail/book-detail?id={{item.id}}" class="book-item"> <image src="{{item.coverUrl}}" mode="aspectFill" class="cover" /> <view class="info"> <text class="title">{{item.title}}</text> <text class="author">作者:{{item.author}}</text> <text class="progress">已读 {{item.readProgress}}%</text> </view> </navigator> </block> <view wx:if="{{loading}}" class="loading">加载中...</view> <view wx:if="{{noMore}}" class="no-more">没有更多了</view> </view>对应 JS 中的加载逻辑:
// pages/book-list/book-list.js Page({ data: { bookList: [], pageNo: 1, pageSize: 10, loading: false, noMore: false }, onLoad() { this.loadBooks(); }, loadBooks() { if (this.data.loading || this.data.noMore) return; this.setData({ loading: true }); wx.request({ url: 'https://your-domain.com/api/book/list', data: { pageNo: this.data.pageNo, pageSize: this.data.pageSize }, header: { 'Authorization': wx.getStorageSync('token') }, success: res => { const { data, code } = res.data; if (code === 200) { const newList = this.data.pageNo === 1 ? data.list : [...this.data.bookList, ...data.list]; this.setData({ bookList: newList, pageNo: this.data.pageNo + 1, loading: false, noMore: data.list.length < this.data.pageSize }); } }, fail: () => { this.setData({ loading: false }); } }); }, onReachBottom() { this.loadBooks(); } });关键参数说明:
wx:for="{{bookList}}":WXML 中的列表渲染指令,bookList是 Page.data 中的数组;wx:if="{{loading}}":条件渲染,避免重复请求时 UI 闪烁;onReachBottom:微信原生下拉触底事件,无需额外监听页面滚动;noMore判断逻辑:后端返回的list.length < pageSize即代表无更多数据——这是比total <= pageNo * pageSize更可靠的判断方式,避免因数据删除导致总数不准。
3. SSM 后端:Controller → Service → Mapper 的三层落地与微信解密实战
SSM 架构在毕业设计中不是摆设,而是每一层都必须有对应文件、有可截图的代码段、有可调试的断点位置。我们以「绑定手机号」接口为例,从 Controller 接收请求,到 Service 调用微信解密 API,再到 Mapper 写入数据库,全程可追踪、可解释、可答辩。
3.1 Controller 层:接收前端加密数据,校验 token 并转发
UserController.java中定义/api/user/bindPhone接口,使用@RequestBody接收 JSON 数据,并通过拦截器校验Authorization头中的 token(此处 token 是后端生成的 JWT,非微信 token)。
// com.example.controller.UserController.java @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/bindPhone") public Result bindPhone(@RequestBody BindPhoneRequest request, @RequestHeader("Authorization") String token) { // 此处 token 由 LoginInterceptor 校验,确保用户已登录 String openid = JwtUtil.getOpenid(token); // 从 JWT 中解析出 openid if (openid == null) { return Result.fail("登录失效"); } boolean success = userService.bindPhone(openid, request.getEncryptedData(), request.getIv()); return success ? Result.success("绑定成功") : Result.fail("绑定失败"); } }参数说明:
BindPhoneRequest是自定义 DTO,含encryptedData和iv字段,对应小程序端e.detail.encryptedData;@RequestHeader("Authorization")强制要求前端携带 token,体现权限控制意识;JwtUtil.getOpenid(token)是自定义工具类,从 JWT payload 中提取openid字段——这是毕业设计中「前后端分离鉴权」的最小实现,比 Session 更易说明原理。
3.2 Service 层:调用微信接口解密手机号,并更新用户信息
UserServiceImpl.java中实现核心逻辑:先用code2Session换取session_key(若缓存中无),再用session_key+encryptedData+iv解密手机号。
// com.example.service.impl.UserServiceImpl.java @Service public class UserServiceImpl implements UserService { @Value("${wechat.appid}") private String appId; @Value("${wechat.secret}") private String secret; @Value("${wechat.sessionKeyCacheTime}") private long sessionKeyCacheTime; @Autowired private UserMapper userMapper; @Override public boolean bindPhone(String openid, String encryptedData, String iv) { // 1. 查询用户是否存在(根据 openid) User user = userMapper.selectByOpenid(openid); if (user == null) return false; // 2. 获取 session_key(优先查 Redis 缓存,未命中则调用微信接口) String sessionKey = getSessionKey(openid); if (sessionKey == null) return false; // 3. 解密手机号 String phone = decryptPhoneNumber(sessionKey, encryptedData, iv); if (phone == null) return false; // 4. 更新用户手机号 user.setPhone(phone); user.setBindTime(new Date()); return userMapper.updateById(user) > 0; } private String getSessionKey(String openid) { // 实际项目中应使用 Redis 缓存 session_key,此处简化为本地 Map(仅演示) String cacheKey = "session_key:" + openid; String sessionKey = (String) CacheUtil.get(cacheKey); if (sessionKey != null) return sessionKey; // 调用微信 code2Session 接口(实际需用 openid 换取 code,此处省略) // 正确流程:前端 login → 后端用 code 换 session_key → 存缓存 → 供 bindPhone 复用 // 毕业设计中可简化为:在 login 接口里已存好 session_key,此处直接取 return (String) CacheUtil.get("session_key:" + openid); } private String decryptPhoneNumber(String sessionKey, String encryptedData, String iv) { try { // AES-128-CBC 解密(微信官方算法) AlgorithmParameters params = AlgorithmParameters.getInstance("AES"); params.init(new IvParameterSpec(Base64.getDecoder().decode(iv))); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(Base64.getDecoder().decode(sessionKey), "AES"), params); byte[] result = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); return new String(result, StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); return null; } } }关键细节:
decryptPhoneNumber方法必须使用AES/CBC/PKCS5Padding算法,iv和session_key都需 Base64 解码;- 微信
encryptedData是 Base64 编码字符串,解密后是 JSON 字符串,格式为{ "phoneNumber": "138****1234", "purePhoneNumber": "138****1234", ... },需进一步JSON.parse()提取phoneNumber;- 毕业设计中可不实现 Redis 缓存,但必须在论文中说明「此处应使用 Redis 缓存 session_key,避免频繁调用微信接口」,并画出缓存流程图——这是架构设计得分点。
3.3 Mapper 层:MyBatis XML 显式 SQL 与动态 SQL 实战
UserMapper.java定义接口方法,UserMapper.xml编写对应 SQL。毕业设计强调「看得见的 SQL」,所以不用@Select注解,全部写在 XML 中。
<!-- mapper/UserMapper.xml --> <mapper namespace="com.example.mapper.UserMapper"> <resultMap id="UserResultMap" type="com.example.entity.User"> <id property="id" column="id"/> <result property="openid" column="openid"/> <result property="phone" column="phone"/> <result property="bindTime" column="bind_time"/> </resultMap> <select id="selectByOpenid" resultMap="UserResultMap"> SELECT * FROM t_user WHERE openid = #{openid} </select> <update id="updateById" parameterType="com.example.entity.User"> UPDATE t_user SET phone = #{phone}, bind_time = #{bindTime} WHERE id = #{id} </update> </mapper>为什么用 XML 不用注解?
- XML 可独立查看 SQL,方便答辩时截图说明「这条 SQL 如何防止 SQL 注入」(
#{}预编译);- 动态 SQL(如
<if test="phone != null">)可扩展性强,论文中可对比「静态 SQL vs 动态 SQL」优劣;resultMap显式映射字段,体现 ORM 思想,比@Results注解更直观。
4. MySQL 数据库:建表规范、索引设计与wx_read.sql的真实结构还原
毕业设计的数据库不是随便CREATE TABLE出来的,它必须体现「范式设计」「索引优化」「字符集统一」三个基本素养。wx_read.sql文件虽小(通常 2–3 KB),但包含 4–5 张核心表:t_user(用户)、t_book(图书)、t_chapter(章节)、t_read_record(阅读记录)、t_category(分类)。我们以t_read_record表为例,还原其设计逻辑与常见踩坑。
4.1t_read_record表:为什么用联合索引(user_id, book_id)?
阅读记录表存储用户对某本书的阅读进度,核心查询场景是「查某个用户最近读过的 5 本书」或「查某本书被多少人读过」。若只对user_id加索引,查「用户 A 最近 5 条记录」仍需filesort;若只对book_id加索引,查「书 B 的阅读人数」效率低。因此,联合索引(user_id, book_id)能同时覆盖两个高频查询:
-- wx_read.sql 片段 CREATE TABLE `t_read_record` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint NOT NULL COMMENT '用户ID', `book_id` bigint NOT NULL COMMENT '图书ID', `chapter_id` bigint DEFAULT NULL COMMENT '当前章节ID', `read_progress` int DEFAULT '0' COMMENT '阅读进度(百分比)', `last_read_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后阅读时间', PRIMARY KEY (`id`), KEY `idx_user_book` (`user_id`,`book_id`) COMMENT '联合索引:支持按用户查记录、按书查读者' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;索引选择依据:
user_id在WHERE条件中出现频率远高于book_id,所以放前面;last_read_time用ON UPDATE CURRENT_TIMESTAMP自动更新,避免 Java 层手动 set;utf8mb4是微信昵称、书名含 emoji 的刚需,utf8会丢数据——这是答辩时「为什么用 utf8mb4?」的标准答案。
4.2t_book表:封面 URL 字段为何用VARCHAR(512)?
图书表中cover_url存储微信云存储返回的 HTTPS 地址,如https://xxxxxx.png。有人用TEXT,但毕业设计中更推荐VARCHAR(512):
-- wx_read.sql 片段 CREATE TABLE `t_book` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) DEFAULT NULL COMMENT '作者', `cover_url` varchar(512) DEFAULT NULL COMMENT '封面图URL(微信云存储地址)', `category_id` bigint DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段长度选择逻辑:
- 微信云存储 URL 最长约 320 字符(含签名参数),512 是安全冗余;
VARCHAR比TEXT在ORDER BY和GROUP BY时性能更好,且支持前缀索引(如KEY idx_cover (cover_url(100)));- 若用
TEXT,MySQL 8.0+ 要求innodb_large_prefix=ON,增加部署复杂度——毕业设计应规避非常规配置。
4.3 初始化数据:INSERT INTO t_category的分类设计哲学
wx_read.sql开头通常有几条INSERT语句初始化分类,如「文学」「科幻」「历史」「儿童」。这不是随便写的,而是体现「业务领域建模能力」:
INSERT INTO `t_category` (`id`, `name`, `sort_order`, `status`) VALUES (1, '文学', 1, 1), (2, '科幻', 2, 1), (3, '历史', 3, 1), (4, '儿童', 4, 1);设计意图说明:
sort_order字段支持后台管理排序,答辩时可演示「拖拽调整分类顺序」;status字段预留上下架功能(1=上架,0=下架),体现「可扩展性思维」;- 分类数控制在 4–6 个,避免答辩时被问「为什么不分得更细?」——回答「符合 MVP 原则,后续可按用户行为聚类新增」即得分。
5. 避坑指南:SSM + 微信小程序毕业设计的 5 个血泪现场
这 5 条全是我在带毕设时,学生反复翻车、重装环境、熬夜改论文的真实记录。不是理论推测,是拿git commit -m "fix login bug"换来的教训。
5.1 现象:小程序wx.request报request:fail ssl hand shake error
原因:后端域名未备案,或 HTTPS 证书不被微信信任(如用自签名证书、Let's Encrypt 泛域名证书未覆盖子域)。微信小程序强制要求https且证书链完整。
解决:
- 买阿里云/腾讯云基础型 DV 证书(百元内),绑定
api.yourdomain.com; - Nginx 配置中
ssl_certificate和ssl_certificate_key路径必须绝对正确,且 key 文件不能有密码; - 用
curl -v https://api.yourdomain.com验证证书是否生效,看* SSL connection using TLSv1.3是否出现。
5.2 现象:SSM 项目启动报ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet
原因:Maven 依赖 scope 错误。spring-webmvc默认 scope 是compile,但若误设为provided(常见于 Tomcat 依赖冲突),运行时找不到类。
解决:
- 检查
pom.xml中<scope>provided</scope>是否误加在spring-webmvc上; - 执行
mvn dependency:tree | grep spring-webmvc,确认该 jar 包出现在compile节点下; - Eclipse 用户右键项目 → Maven → Update Project,勾选
Force Updates。
5.3 现象:MySQL 插入中文乱码,日志显示???
原因:MySQL 服务端、数据库、表、连接四层字符集不一致。最常见是my.cnf中character-set-server=utf8mb4缺失,或建库时没指定CHARSET=utf8mb4。
解决:
- 查看当前配置:
SHOW VARIABLES LIKE 'character_set%';,确保character_set_server和collation_server都是utf8mb4; - 修改
my.cnf(Linux 在/etc/my.cnf,Windows 在my.ini):[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci - 重启 MySQL 后,重建数据库:
CREATE DATABASE wx_read DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
5.4 现象:小程序wx.getPhoneNumber回调e.detail.errMsg为getPhoneNumber:fail cancel
原因:用户点击了「取消」按钮,而非拒绝授权。微信将「用户主动取消」和「用户拒绝授权」统一返回fail cancel,但业务逻辑需区分对待。
解决:
- 前端加提示:
if (e.detail.errMsg.includes('cancel')) { wx.showToast({ title: '请授权手机号以继续使用', icon: 'none' }); }; - 后端不记录错误日志,避免刷屏;
- 论文中写明「该状态属正常交互流程,非系统异常」,体现用户体验设计意识。
5.5 现象:MyBatisselectByOpenid返回null,但数据库明明有数据
原因:t_user.openid字段类型为VARCHAR(64),但插入时用了''(空字符串)而非NULL,而SELECT * FROM t_user WHERE openid = ''与SELECT * FROM t_user WHERE openid IS NULL结果不同。
解决:
- 建表时
openid字段加NOT NULL DEFAULT '',并确保所有插入逻辑都赋值; - Mapper XML 中用
WHERE openid = #{openid},而非WHERE openid = #{openid,jdbcType=VARCHAR}(后者在空字符串时可能出错); - 调试时用
System.out.println("openid=" + user.getOpenid())打印实际值,比猜更可靠。
6. 毕业答辩通关技巧:用「三页 PPT + 一段视频」讲清技术深度
答辩不是代码朗诵会,而是用有限时间证明「我懂,且我能」。我带过的 37 个毕设学生里,过线率最高的,都是把「技术决策理由」讲得比「功能演示」更扎实的人。下面是我压箱底的三页 PPT 结构,不讲废话,只放干货。
6.1 第一页:架构图 + 选型对比表(答辩开场 60 秒定调)
用 PlantUML 或 draw.io 画一张极简架构图:小程序图标 → HTTP 箭头 → SSM 服务器(标注 Spring/SpringMVC/MyBatis 三层)→ MySQL 图标。旁边放一张 3×4 对比表:
| 维度 | SSM | Spring Boot | Node.js(Express) |
|---|---|---|---|
| 学习成本 | ⭐⭐⭐⭐(概念显式,XML 可读) | ⭐⭐(自动装配难溯源) | ⭐⭐⭐(异步回调易混乱) |
| 调试难度 | ⭐⭐(断点直达 Controller) | ⭐⭐⭐⭐(需理解 Starter 原理) | ⭐⭐⭐(堆栈深) |
| 论文支撑点 | ✅ 每层代码可截图、可解释 | ❌ 自动配置难写进论文附录 | ❌ JS 逻辑分散难归总 |
| 答辩优势 | ✅ 每个 XML 文件、每个 SQL 都是得分证据 | — | — |
这页 PPT 的价值在于:用对比代替自夸,用可验证的特征(XML、SQL、断点)替代空泛描述(“轻量”“高效”)。老师一眼就明白你选 SSM 不是偷懒,而是精准匹配毕业设计目标。
6.2 第二页:核心接口时序图 + 关键代码片段(证明「我真写了」)
聚焦「绑定手机号」这一最高频答辩问题,画时序图(小程序 → 后端 → 微信服务器 → 后端 → 数据库),并在图下方贴两段代码:
- 小程序端
onGetPhoneNumber的 8 行 JS(突出encryptedData和iv传递); - SSM 端
decryptPhoneNumber方法的 15 行 Java(突出AES/CBC/PKCS5Padding和Base64.decode)。
为什么只贴这两段?
- 它们是微信生态特有的安全链路,其他框架无法替代;
- 每行都有可解释的技术点(如
Cipher.getInstance("AES/CBC/PKCS5Padding")中的算法名必须准确);- 老师若追问「为什么用 CBC 而不用 GCM?」,你能答「微信官方 SDK 固定使用 CBC 模式,GCM 需要额外 IV 长度校验,毕业设计以官方为准」——这就是深度。
6.3 第三页:数据库 ER 图 + 索引执行计划截图(展示工程素养)
用 MySQL Workbench 导出t_user、t_book、t_read_record三张表的 ER 图,标注外键关系(t_read_record.user_id → t_user.id)。再贴一张EXPLAIN SELECT * FROM t_read_record WHERE user_id = 123的执行结果截图,箭头指向type=ref和key=idx_user_book。
这个细节的价值:
- ER 图证明你理解「一对多」关系建模;
EXPLAIN截图证明你不止会写 SQL,还会看执行计划——这是企业级开发的入门门槛;- 如果老师问「为什么不用 MongoDB?」,你可以答:「阅读记录需强一致性事务(如更新进度+记录时间必须原子),MySQL 的 ACID 更稳妥;MongoDB 适合日志类非结构化数据,不符合本系统核心诉求」。
最后,别忘了那段 3 分钟演示视频。我要求学生必须包含:
- 0:00–0:30:小程序首页 → 点击「微信登录」→ 弹出手机号授权 → 输入验证码 → 进入图书列表;
- 0:30–1:30:下滑加载更多图书 → 点击某本 → 进入详情页 → 滑动阅读 → 进度自动保存;
- 1:30–2:30:后台管理系统(可选)展示
t_read_record表中新增记录; - 2:30–3:00:终端窗口
tail -f catalina.out显示UserServiceImpl.bindPhone success日志。
这段视频不是炫技,而是用 180 秒证明:从用户点击到数据库落盘,整条链路真实存在、可复现、可验证。
带毕设这些年,我越来越确信:毕业设计的价值,不在于代码多酷,而在于每个技术选择背后,都有你能说清楚的理由;不在于功能多全,而在于每个 Bug 的修复过程,都成了你简历上可展开的故事。希望帮到你。
本文还有配套的精品资源,点击获取