学成在线 第1天 讲义-项目概述 CMS接口开发
1 项目的功能构架
1.1 项目背景
受互联网+概念的催化,当今中国在线教育市场的发展可谓是百花齐放、如火如荼。 按照市场领域细分为:学前教 育、K12教育、高等教育、留学教育、职业教育、语言教育、兴趣教育以及综合平台,其中,职业教育和语言教育 的市场优势突出。 根据Analysys易观发布的数据显示,预计2019年中国互联网教育市场交易规模将达到3718亿元 人民币,未来三年互联网教育市场规模保持高速增长。
学成在线借鉴了MOOC(大型开放式网络课程,即MOOC(massive open online courses))的设计思想,是一 个提供IT职业课程在线学习的平台,它为即将和已经加入IT领域的技术人才提供在线学习服务,用户通过在线学 习、在线练习、在线考试等学习内容,最终掌握所学的IT技能,并能在工作中熟练应用
1.2 功能模块
当前市场的在线教育模式多种多样,包括:B2C、C2C、B2B2C等业务模式,学成在线采用B2B2C业务模式,即向 企业或个人提供在线教育平台提供教学服务,老师和学生通过平台完成整个教学和学习的过程,市场上类 似的平台 有:网易云课堂、腾讯课堂等,学成在线的特点是IT职业课程在线教学。
学成在线包括门户、学习中心、教学管理中、社交系统、系统管理等功能模块。


1.3 项目原型
通过项目原型进一步了解项目的功能,包括:门户首页、课程搜索页、在线学习页面、个人中心等
参考“项目原型”
2 项目的技术架构
2.1 技术架构
学成在线采用当前流行的前后端分离架构开发,由用户层、UI层、微服务层、数据层等部分组成,为PC、App、 H5等客户端用户提供服务。下图是系统的技术架构图:

业务流程举例:
- 用户可以通过pc、手机等客户端访问系统进行在线学习。
- 系统应用CDN技术,对一些图片、CSS、视频等资源从CDN调度访问。
- 所有的请求全部经过负载均衡器。
- 对于PC、H5等客户端请求,首先请求UI层,渲染用户界面。
- 客户端UI请求服务层获取进行具体的业务操作。
- 服务层将数据持久化到数据库
各模块说明如下:

2.2 技术栈
下图是项目技术架构的简图,通过简图了解项目所使用的技术栈

重点了解微服务技术栈:
学成在线服务端基于Spring Boot构建,采用Spring Cloud微服务框架。
持久层:MySQL、MongoDB、Redis、ElasticSearch 数据访问层:使用Spring Data JPA 、Mybatis、Spring Data Mongodb等
业务层:Spring IOC、Aop事务控制、Spring Task任务调度、Feign、Ribbon、Spring AMQP、Spring Data Redis 等。
控制层:Spring MVC、FastJSON、RestTemplate、Spring Security Oauth2+JWT等 微服务治理:Eureka、Zuul、Hystrix、Spring Cloud Config等
2.3 开发步骤
项目是基于前后端分离的架构进行开发,前后端分离架构总体上包括前端和服务端,通常是多人协作并行开发,开 发步骤如下:
1、需求分析
梳理用户的需求,分析业务流程
2、接口定义
根据需求分析定义接口
3、服务端和前端并行开发
依据接口进行服务端接口开发。
前端开发用户操作界面,并请求服务端接口完成业务处理。
4、前后端集成测试
最终前端调用服务端接口完成业务
3 CMS需求分析
3.1 什么是CMS
1、CMS是什么 ? CMS (Content Management System)即内容管理系统,不同的项目对CMS的定位不同,比如:一个在线教育网 站,有些公司认为CMS系统是对所有的课程资源进行管理,而在早期网站刚开始盛行时很多公司的业务是网站制 作,当时对CMS的定位是创建网站,即对网站的页面、图片等静态资源进行管理。
2、CMS有哪些类型?
上边也谈到每个公司对每个项目的CMS定位不同,CMS基本上分为:针对后台数据内容的管理、针对前端页面的 管理、针对样式风格的管理等 。比如:一个给企业做网站的公司,其CMS系统主要是网站页面管理及样式风格的 管理
3、本项目CMS的定位是什么?
本项目作为一个大型的在线教育平台,对CMS系统的定位是对各各网站(子站点)页面的管理,主要管理由于运营 需要而经常变动的页面,从而实现根据运营需要快速进行页面开发、上线的需求。
3.2 静态门户工程搭建
本项目CMS是对页面进行管理,对页面如何进行管理呢?我们首先搭建学成网的静态门户工程,根据门户的页面结 构来分析页面的管理方案。
门户,是一个网站的入口,一般网站都有一个对外的门户,学成在线门户效果图如下

3.2.1 导入门户工程
配置好Nginx ,讲前端项目导入到webStorm中,并配置好Server得路径


3.2.2 配置虚拟主机
在nginx中配置虚拟主机
server{
listen 80;
server_name www.xuecheng.com;
ssi on;
ssi_silent_errors on;
location / {
alias D:/IDEA_work/xc_onine/xc-ui-pc-static-portal/;
index index.html;
}
}
D:/IDEA_work/xc_onine/xc-ui-pc-static-portal 本目录即为门户的主目录。
PS :这个里面又 个严重的问题就是Ngix可以重复启动多个,启动的时候一定要把之前的给停掉在启动新的,不然一直会加载不出来,这个NGINX配置这个别名的时候最后末尾一定要又反斜杠,从windows直接复制的路径没有反斜杠!!!!!


3.3 SSI服务端包含技术
本节分析首页的管理方案。
1、页面内容多如何管理? 将页面拆分成一个一个的小页面,通过cms去管理这些小页面,当要更改部分页面内容时只需要更改具体某个小页 面即可。
2、页面拆出来怎么样通过web服务浏览呢? 使用web服务(例如nginx)的SSI技术,将多个子页面合并渲染输出。
3、SSI是什么?



ssi的配置参数如下: ssi on: 开启ssi支持 ssi_silent_errors on:默认为off,设置为on则在处理SSI文件出错时不 输出错误信息 ssi_types:默认为 ssi_types text/html,如果需要支持shtml(服务器执行脚本,类似于jsp)

则需 要设置为ssi_types text/shtml
测试 去掉某个#include查看页面效果

3.3 CMS页面管理需求
1、这些页面的管理流程是什么?
1)创建站点:
一个网站有很多子站点,比如:学成在线有主门户、学习中心、问答系统等子站点。具体的哪个页面是归属于具体 的站点,所以要管理页面,先要管理页面所属的站点。
2)创建模板:
页面如何创建呢?比如电商网站的商品详情页面,每个页面的内容布局、板式是相同的,不同的只是内容,这个页 面的布局、板式就是页面模板,模板+数据就组成一个完整的页面,最终要创建一个页面文件需要先定义此页面的 模板,最终拿到页面的数据再结合模板就拼装成一个完整的页面
3)创建页面: 创建页面是指填写页面的基本信息,如:页面的名称、页面的url地址等。
4)页面预览:
页面预览是页面发布前的一项工作,页面预览使用静态化技术根据页面模板和数据生成页面内容,并通过浏览器预 览页面。页面发布前进行页面预览的目是为了保证页面发布后的正确性。
5)页面发布:
使用计算机技术将页面发送到页面所在站点的服务器,页面发布成功就可以通过浏览器来访问了。 2、本项目要实现什么样的功能?
1)页面管理
管理员在后台添加、修改、删除页面信息
2)页面预览
管理员通过页面预览功能预览页面发布后的效果。
3)页面发布
管理员通过页面发布功能将页面发布到远程门户服务器。
页面发布成功,用户即可在浏览器浏览到最新发布的页面,整个页面添加、发布的过程由于软件自动执行,无需人 工登录服务器操作。
4 CMS服务端工程搭建
4.1 开发工具配置
服务端工程使用IntellijIDEA开发。
1、创建工程代码目录 XcEduCode(本教程创建XcEduCode01目录),并且IDEA打开。
2、配置maven 环境 拷贝老师提供的maven仓库,setting.xml文件中配置maven仓库,maven仓库的目录位置不要去使用中文。
配置Maven

配置编码格式

配置JDK

配置快捷建

配置自动导包

配置自定义代码模板

4.2 导入基础工程
4.2.1 工程结构
CMS及其它服务端工程基于maven进行构建,首先需要创建如下基础工程: parent工程:父工程,提供依赖管理。 common工程:通用工程,提供各层封装
model工程:模型工程,提供统一的模型类管理 utils工程:工具类工程,提供本项目所使用的工具类
Api工程:接口工程,统一管理本项目的服务接口。

基础工程代码及pom.xml配置参考课程资料“基础工程”。 4.2.2导入父工程
1、将课程资料中的parent工程拷贝到代码目录
2、点击Import Model,选择parent工程目录

4.3 MongoDB
CMS 采用 MongoDB 数据库存储 CMS 页面信息, CMS 选用 Mongodb 的原因如下:
- Mongodb是非关系型数据库,存储Json格式数据 ,数据格式灵活。
- 相比课程管理等核心数据CMS数据不重要,且没有事务管理要求。

下载
MongoDB 提供了可用于 32 位和 64 位系统的预编译二进制包,你可以从 MongoDB 官网下载安装。
官方地址: https://www.mongodb.com/
本教程下载 3.4 版本:
Download MongoDB Community Server | MongoDB
安装
- 下载zip文件解压后,如果里头没有data 和 logs文件目录 如下 新建这两个目录 data目录下再新建一个db目录 logs 目录下新建一个 mongo.log 文件

- 在data文件夹中新建db文件夹,
- 在logs中创建mongo.log文件
- 在mongo.config 新建的配置文件中添加如下配置信息
dbpath=D:\environment\mongodb\data\db #数据库路径
logpath=D:\environment\mongodb\logs\mongo.log #日志输出文件路径
logappend=true #错误日志采用追加模式
journal=true #启用日志文件,默认启用
quiet=true #这个选项可以过滤掉一些无用的日志信息,若需要调试使用请设置为false
port=27017 #端口号默认为27017
使用管理员什么打开在bin目录cmd
.\mongod --dbpath D:\Mongodb\mongodb\mongodb\data\db

然后打开浏览器输入 localhost:27017 看看就会有下图

安装windows服务
切换到bin目录下,执行
mongod --config "D:\Mongodb\mongodb\mongodb\mongo.config" --install --serviceName "mongodb"
- 配置环境变量
添加path路径到系统变量D:\environment\mongodb\bin - 每次使用前需要先开启服务
启动客户端
双击bin目录里的mongo.exe文件
链接:https://pan.baidu.com/s/1uu66wV_JYeIBefJzSSm5xw
提取码:2015
基础概念
在 mongodb 中是通过数据库、集合、文档的方式来管理数据,下边是 mongodb 与关系数据库的一些概念对比:


- 一个mongodb实例可以创建多个数据库
- 一个数据库可以创建多个集合
- 一个集合可以包括多个文档
连接mongodb
- 双击bin目录中的mongo.exe
- 使用studio3t连接
- 使用cmd输入mongo.exe
- 使用java程序连接
参考文档:
http://mongodb.github.io/mongo-java-driver/3.4/driver/tutorials/connect-to-mongodb/
添加依赖
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo‐java‐driver</artifactId>
<version>3.4.3</version>
</dependency>
测试程序
@Test public void testConnection(){
//创建mongodb 客户端
MongoClient mongoClient = new MongoClient( "localhost" , 27017 );
//或者采用连接字符串
//MongoClientURI connectionString = new MongoClientURI("mongodb://root:root@localhost:27017"); //MongoClient mongoClient = new MongoClient(connectionString);
//连接数据库
MongoDatabase database = mongoClient.getDatabase("test");
// 连接collection MongoCollection<Document> collection = database.getCollection("student");
//查询第一个文档
Document myDoc = collection.find().first();
//得到文件内容 json串
String json = myDoc.toJson();
System.out.println(json);
}
数据库
查询数据库
show dbs 查询全部数据库
db 显示当前数据库
创建数据库
use DATABASE_NAME
例子:
use test02
有 test02 数据库则切换到此数据库,没有则创建。
注意:
新创建的数据库不显示,需要至少包括一个集合。
删除数据库
db.dropDatabase()
删除 test02 数据库
先切换数据库: use test02
再执行删除: db.dropDatabase()
集合
集合相当于关系数据库中的表,一个数据库可以创建多个集合,一个集合是将相同类型的文档管理起来。
创建集合
db.createCollection(name, options)
name: 新创建的集合名称
options: 创建参数
例子
db.createCollection("NAME")
查询集合
show collections
删除集合
db.collection.drop()
例子: db.student.drop() 删除student集合
文档
插入文档
mongodb 中文档的格式是 json 格式,下边就是一个文档,包括两个 key :_id主键和 name
{
"_id" : ObjectId("5b2cc4bfa6a44812707739b5"), "name" : "黑马程序员"
}
插入命令:
db.COLLECTION_NAME.insert(document)
每个文档默认以_id作为主键,主键默认类型为ObjectId(对象类型),mongodb会自动生成主键值。
例子:
db.student.insert({"name":"黑马程序员","age":10})
注意:同一个集合中的文档的 key 可以不相同!但是建议设置为相同的。
更新文档
命令格式
db.collection.update(
<query>,
<update>,
<options>
)
query:查询条件,相当于sql语句的where
update:更新文档内容
options:选项
替换文档
将符合条件 "name":" 北京黑马程序 " 的第一个文档替换为 {"name":" 北京黑马程序员 ","age":10}
db.student.update({"name":"黑马程序员"},{"name":"北京黑马程序员","age":10})
$set修改器
使用 $set 修改器指定要更新的 key , key 不存在则创建,存在则更新。
将符合条件 "name":" 北京黑马程序 " 的所有文档更新 name 和 age 的值。
db.student.update({"name":"黑马程序员"},{$set:{"name":"北京黑马程序员","age":10}},{multi:true})
multi :
false 表示更新第一个匹配的文档,
true 表示更新所有匹配的文档。
删除文档
命令格式:
db.student.remove(<query>)
query:删除条件,相当于sql语句中的where
删除所有文档
db.student.remove({})
删除符合条件的文档
db.student.remove({"name":"黑马程序员"})
查询文档
命令格式
db.collection.find(query, projection)
query:查询条件,可不填
projection:投影查询key,可不填
查询全部
db.student.find()
查询符合条件的记录
查询name等为"黑马程序员"的文档。
db.student.find({"name":"黑马程序员"})
投影查询
只显示name和age两个key,_id主键不显示。
db.student.find({"name":"黑马程序员"},{name:1,age:1,_id:0})
用户
创建用户
mongo>db.createUser(
{ user: "<name>",
pwd: "<cleartext password>",
customData: { <any information> },
roles: [
{ role: "<role>", db: "<database>" } | "<role>",
...
]}
)
例子:
创建 root 用户,角色为 root
use admin
db.createUser(
{
user:"root",
pwd:"root",
roles:[{role:"root",db:"admin"}]
}
)
内置角色如下:
- 数据库用户角色:read、readWrite;
- 数据库管理角色:dbAdmin、dbOwner、userAdmin;
- 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager;
- 备份恢复角色:backup、restore;
- 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、 dbAdminAnyDatabase
- 超级用户角色:root
在admin数据库下创建的用户是可以访问其他数据库的
查询用户
查询当前库下的所有用户:
show users
认证登录
为了安全需要,Mongodb要打开认证开关,即用户连接Mongodb要进行认证,其中就可以通过账号密码方式进行认证
- 在mono.conf中添加设置auth=true
- 重启Mongodb
- 使用账号和密码连接数据库
1) mongo.exe连接
mongo.exe -u root -p root --authenticationDatabase admin
2)studio3t连接

删除用户
语法格式:
db.dropUser(" 用户名 ")
例子:
删除 root1 用户
db.dropUser("root1")
修改用户
语法格式
db.updateUser(
"<username>",
{
customData : { <any information> },
roles : [
{ role: "<role>", db: "<database>" } | "<role>",
...
],
pwd: "<cleartext password>"
},
writeConcern: { <write concern> }
)
例子:
修改root用户的角色为readWriteAnyDatabase
use admin
db.updateUser("root",{roles:[{role:"readWriteAnyDatabase",db:"admin"}]})
修改密码
语法格式
db.changeUserPassword("username","newPasswd")
例如
修改 root 用户的密码为 123
use admin
db.changeUserPassword("root","123")
4.4 导入CMS数据库
导入cms数据库:
使用Studio 3T软件导入cms数据库
1、创建xc_cms数据库
2、导入 cms数据库
右键数据库,点击导入数据库
打开窗口,选择第一个 json。
下一步,选择要导入的数据文件(json文件
下一步操作即可完成。
导入成功

5 页面查询接口定义
5.1 定义模型
5.1.1 需求分析
在梳理完用户需求后就要去定义前后端的接口,接口定义后前端和后端就可以依据接口去开发功能了。
本次定义页面查询接口,本接口供前端请求查询页面列表,支持分页及自定义条件查询方式。
具体需求如下:
1、分页查询CmsPage 集合下的数据 2、根据站点Id、模板Id、页面别名查询页面信息
3、接口基于Http Get请求,响应Json数据 5.1.2 模型类介绍
接口的定义离不开数据模型,根据前边对需求的分析,整个页面管理模块的数据模型如下:

CmsSite:站点模型 CmsTemplate:页面模板 CmsPage:页面信息
页面信息如下
5.2定义接口
5.2.1 定义请求及响应类型
1、定义请求模型QueryPageRequest,此模型作为查询条件类型 为后期扩展需求,请求类型统一继承RequestData类型。

2.定义接口

6 页面查询服务端开发
6.1 创建CMS服务工程
6.1.1 创建CMS工程
搭建完成

编写测试控制器出现空指针异常

修改,这个里面又个问题请求的参数是QueryPageRequest,码字码的快很容易写成QueryPageResponse,而根据正常的流程,Repsonse怎么会出现在请求参数里面的,明显是错的,所以这个错误很不好发现,建议一定要把参数写正确!!!非常重要

完成测试,Bug修复

页面端开发分页查询
可以看到查询失败

这个里面犹豫用的是MongoDb所以URL写的不对造成的查询失败,这个还是YML配置文件写的又问题,这个地址和密码一定要写对

完成BUG修复

代码总结:
@SpringBootTest
@RunWith(SpringRunner.class)
public class CmsPageRepositoryTest {
@Autowired
private CmsPageRepository cmsPageRepository;
//查询全部测试
public void test(){
List<CmsPage> list = cmsPageRepository.findAll();
for (CmsPage cmsPage : list) {
System.out.println("查询出来的数据----》"+cmsPage);
}
}
//分页测试
@Test
public void testquery(){
int Page=0; //起始页
int size=5; //每页显示的个数
Pageable pageable= PageRequest.of(Page,size);
org.springframework.data.domain.Page<CmsPage> all = cmsPageRepository.findAll(pageable);
System.out.println(all);
}
}
分页查询查询正常

开始进行修改更新的测试
//修改测试
@Test
public void updateTest(){
//先根据ID查询到这个对象
Optional<CmsPage> cmsPage = cmsPageRepository.findById("5abefd525b05aa293098fca6");
if (cmsPage.isPresent()){
//在得到这个对象
CmsPage cms = cmsPage.get();
//在进行修改
cms.setPageAliase("testUpdate");
//在进行保存
CmsPage save = cmsPageRepository.save(cms);
System.out.println(save);
}
}
测试完成这个是先根据ID查询,查询到了之后在进行保存的

自定义方法根据,别名来查询对象
@Test
public void TestName(){
//根据自定义方法pageNam查询
CmsPage cmsPage = cmsPageRepository.findByPageAliase("testUpdate");
System.out.println(cmsPage);
}
修改完成:

开始整合Service:
@Service
public class PageService implements CmsPageControllerApi {
@Autowired
private CmsPageRepository cmsPageRepository;
@Override
public QueryResponseResult findList(int page, int size, QueryPageRequest queryPageRequest) {
QueryResult queryResult=new QueryResult();
if (page<=0){
page=1;
}
if (size<=0){
size=10;
}
Pageable pageable= PageRequest.of(page,size);
Page<CmsPage> cmsPages = cmsPageRepository.findAll(pageable);
List<CmsPage> cmsPageList = cmsPages.getContent();
long totalElements = cmsPages.getTotalElements();
queryResult.setTotal(totalElements);
queryResult.setList(cmsPageList);
return new QueryResponseResult(CommonCode.SUCCESS,queryResult);
}
}
优化Controller:
@Override
@GetMapping("/list/{page}/{size}")
public QueryResponseResult findList(@PathVariable("page") int page,@PathVariable("size") int size, QueryPageRequest queryPageRequest) {
return pageService.findList(page,size,queryPageRequest);
}
Service整合成功:

6.6 接口开发规范
6.6.1 Api请求及响应规范
为了严格按照接口进行开发,提高效率,对请求及响应格式进行规范化。
1、get 请求时,采用key/value格式请求,SpringMVC可采用基本类型的变量接收,也可以采用对象接收。
2、Post请求时,可以提交form表单数据(application/x-www-form-urlencoded)和Json数据(ContentType=application/json),文件等多部件类型(multipart/form-data)三种数据格式,SpringMVC接收Json数据 使用@RequestBody注解解析请求的json数据。 4、响应结果统一信息为:是否成功、操作代码、提示信息及自定义数据。
5、响应结果统一格式为json
6.6.2 Api定义约束
Api定义使用SpringMVC来完成,由于此接口后期将作为微服务远程调用使用,在定义接口时有如下限制: 1、@PathVariable 统一指定参数名称,如:@PathVariable("id") 2、@RequestParam统一指定参数名称,如: @RequestParam("id")
7 页面查询接口测试
上边的代码是基于服务端编写接口,如果前端人员等待服务端人员将接口开发完毕再去开发前端内容这样做效率是 非常低下的,所以当接口定义完成,可以使用工具生成接口文档,前端人员查看接口文档即可进行前端开发,这样 前端和服务人员并行开发,大大提高了生产效率。
本章节介绍两种接口开发工具,Swagger和Postman。 7.1 Swagger
7.1.1 Swagger介绍
OpenAPI规范(OpenAPI Specification 简称OAS)是Linux基金会的一个项目,试图通过定义一种用来描述API格 式或API定义的语言,来规范RESTful服务开发过程,目前版本是V3.0,并且已经发布并开源在github上。
(https://github.com/OAI/OpenAPI-Specification) Swagger是全球最大的OpenAPI规范(OAS)API开发工具框架,支持从设计和文档到测试和部署的整个API生命周 期的开发。 (https://swagger.io/)
Spring Boot 可以集成Swagger,生成Swagger接口,Spring Boot是Java领域的神器,它是Spring项目下快速构建 项目的框架。
7.1.2 Swagger常用注解
在Java类中添加Swagger的注解即可生成Swagger接口,常用Swagger注解如下:
@Api:修饰整个类,描述Controller的作用 @ApiOperation:描述一个类的一个方法,或者说一个接口 @ApiParam:单个参数描述 @ApiModel:用对象来接收参数 @ApiModelProperty:用对象接收参数时,描述对 象的一个字段 @ApiResponse:HTTP响应其中1个描述 @ApiResponses:HTTP响应整体描述 @ApiIgnore:使用 该注解忽略这个API @ApiError :发生错误返回的信息 @ApiImplicitParam:一个请求参数 @ApiImplicitParams:多个请求参数
@ApiImplicitParam属性:

7.1.3 Swagger接口定义
修改接口工程中页面查询接口,添加Swagger注解。
@Api(value="cms页面管理接口",description="cms页面管理接口,提供页面的增、删、改、查")
public interface CmsPageControllerApi {
@ApiOperation("分页查询页面列表")
@ApiImplicitParams({@ApiImplicitParam(name="page",value="页 码",required=true,paramType="path",dataType="int"),
@ApiImplicitParam(name="size",value="每页记录 数",required=true,paramType="path",dataType="int")})
//此接口编写后会在CMS服务工程编写Controller类实现此接口。页面查询
public QueryResponseResult findList(int page, int size, QueryPageRequest queryPageRequest);
}
public class QueryPageRequest {
//定义请求模型QueryPageRequest,此模型作为查询条件类型
@ApiModelProperty("站点id")
private String siteId; //站点id
@ApiModelProperty("页面ID")
private String pageId; //页面ID
@ApiModelProperty("页面名称")
private String pageName; //页面名称
@ApiModelProperty("别名")
private String pageAliase; //别名
@ApiModelProperty("模版id")
private String templateId; //模版id
}
完成测试

POSTMan测试

今日总结:
总的来说这个学成在线的项目和之前的乐友相比,更加接近基础,其实个人感觉做项目就是不断夯实基础的过程,推翻原来的认知,Get到新的知识点,之前用的新技术很多,但是总部知道为什么用这个技术,所以还是的需要不断的去尝试,不断的总结,基础还是非常重要的,只有基础打实了,学技术才会学的更加的的心应手

182

被折叠的 条评论
为什么被折叠?



