总结
项目总结
如何把需求抽象成为代码实现,这是一种需要持续精进的能力
——2021.6.16记录
所认为最重要的几个点:
底层原理:从底层视角去看编程会容易理解很多,从数据的组织管理到运用,其实都是循循渐进的过程。从组织数据的数据结构,衍生出相应的算法,去解决不同场景的需求,将这些不同的小规模的应用集合起来,进而可能构建大规模的应用,此时 就需要更大的管理资源的操作系统,根据不同的类别划分,譬如进程管理,内存管理,文件管理,网络管理,磁盘管理,外设管理这些复杂的管理体系,以及对应于不同管理体系下所应用的不同的数据结构和算法。当然这些仅仅是从软件层面去审视计算机的内核,只是庞大计算机体系的冰山一角,但我们如果没有这些基础,所谓的编程就如同空中楼阁。抽象能力:动手能力:关想不做,这是大忌,必须有熟悉的语言工具,再辅之以沉淀深厚的思想,才能做出成就融会贯通:从最前沿借鉴经验为自己所用,这是最快的学习方式
之后准备冲刺的几个点:
在拿到一个项目的时候,最重要的有这么几个点:
- 需求是什么,数据库该怎么设计
- 后端服务怎么安排,才能以最合适的技术满足需求
- 前台怎么展示,用户交互设计
1、项目管理
架构:

后端:
技术栈
-springboot
-mybatisplus
-swagger+knife4j
-redis
-springsecurity+jwt
-springcloud (Nacos,Feign,Hystrix,Gateway)
-cannal
-git
前端:
技术栈
-VUE
-Element-ui
-nuxt服务端渲染(前台展示)
-echarts
-nginx
2、数据库设计
admin:
- 用户管理=>RBAC
- 讲师管理=>CRUD
- 课程管理=>课程、章节、小节——生成树状图
- 订单管理=>订单+流水
- 其他:轮播图、课程评论、视频、统计表、会员
RBAC

讲师

课程

订单

其他

查表实现思路
单表CRUD->mybatisplus
联表查询->join或者子查询
3、后台功能模块

guli-parent:在线教学根目录(父工程),管理四个子模块:
-canal-client:canal数据库表同步模块(统计同步数据)
-common:公共模块父节点
common-util:工具类模块,所有模块都可以依赖于它
service-base:service服务的base包,包含service服务的公共配置类,所有service模块依赖于它
spring-security:认证与授权模块,需要认证授权的service服务依赖于它
-infrastructure:基础服务模块父节点
api-gateway:api网关服务
-service:api接口服务父节点
service-acl:用户权限管理api接口服务(用户管理、角色管理和权限管理等)
service-cms:cms api接口服务
service-edu:教学相关api接口服务
service-msm:短信api接口服务
service-order:订单相关api接口服务
service-oss:阿里云oss api接口服务
service-statistics:统计报表api接口服务
service-ucenter:会员api接口服务
service-vod:视频点播api接口服务
其实整个项目下来,死代码很多,最关键的是掌握整个开发逻辑,以及业务需求和实现方式,一般调API,写 配置文件,这些代码理解就行,关键的是设计思想
难点:
- 课程章节联表查询
- 订单处理
- 后台权限管理
开发问题:
- 多模块开发环境下->启动类扫包
- 跨域问题->解决方案:注解、配置类、nginx、gateway网关(和注解不兼容)
- 统一配置->logback日志、knife4j+swagger接口文档、统一返回类Result、统一日志处理及自定义异常
- 静态数据和视频放oss
- 注意!->阿里云的部分oss相关api未开源,要手动下载安装
讲师管理
思路:
主要是单表CRUD,mp就能解决,条件分页查询加wrapper
课程管理
一二级树状图数据:
public List<OneSubject> getAllOneTwoSubject() {
//1 查询所有一级分类 parentid = 0
QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();
wrapperOne.eq("parent_id","0");
List<EduSubject> oneSubjectList = baseMapper.selectList(wrapperOne);
//2 查询所有二级分类 parentid != 0
QueryWrapper<EduSubject> wrapperTwo = new QueryWrapper<>();
wrapperTwo.ne("parent_id","0");
List<EduSubject> twoSubjectList = baseMapper.selectList(wrapperTwo);
//创建list集合,用于存储最终封装数据
List<OneSubject> finalSubjectList = new ArrayList<>();
//3 封装一级分类
//查询出来所有的一级分类list集合遍历,得到每个一级分类对象,获取每个一级分类对象值,
//封装到要求的list集合里面 List<OneSubject> finalSubjectList
for (int i = 0; i < oneSubjectList.size(); i++) { //遍历oneSubjectList集合
//得到oneSubjectList每个eduSubject对象
EduSubject eduSubject = oneSubjectList.get(i);
//把eduSubject里面值获取出来,放到OneSubject对象里面
OneSubject oneSubject = new OneSubject();
// oneSubject.setId(eduSubject.getId());
// oneSubject.setTitle(eduSubject.getTitle());
//eduSubject值复制到对应oneSubject对象里面
BeanUtils.copyProperties(eduSubject,oneSubject);
//多个OneSubject放到finalSubjectList里面
finalSubjectList.add(oneSubject);
//在一级分类循环遍历查询所有的二级分类
//创建list集合封装每个一级分类的二级分类
List<TwoSubject> twoFinalSubjectList = new ArrayList<>();
//遍历二级分类list集合
for (int m = 0; m < twoSubjectList.size(); m++) {
//获取每个二级分类
EduSubject tSubject = twoSubjectList.get(m);
//判断二级分类parentid和一级分类id是否一样
if(tSubject.getParentId().equals(eduSubject.getId())) {
//把tSubject值复制到TwoSubject里面,放到twoFinalSubjectList里面
TwoSubject twoSubject = new TwoSubject();
BeanUtils.copyProperties(tSubject,twoSubject);
twoFinalSubjectList.add(twoSubject);
}
}
//把一级下面所有二级分类放到一级分类里面
oneSubject.setChildren(twoFinalSubjectList);
}
return finalSubjectList;
}
}
EXCEL
思路
API:阿里的easyexcel
自己写实体类封装,调easyexcel读写方法,读方法要listener
@Data
public class SubjectData {
@ExcelProperty(index = 0)
private String oneSubjectName;
@ExcelProperty(index = 1)
private String twoSubjectName;
}
读操作:
public class ExcelListener extends AnalysisEventListener<DemoData> {
//一行一行读取excel内容
@Override
public void invoke(DemoData data, AnalysisContext analysisContext) {
System.out.println("****"+data);
}
//读取表头内容
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头:"+headMap);
}
//读取完成之后
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) { }
}
public class TestEasyExcel {
public static void main(String[] args) {
//实现excel写的操作
//1 设置写入文件夹地址和excel文件名称
// String filename = "F:\\write.xlsx";
//2 调用easyexcel里面的方法实现写操作
//write方法两个参数:第一个参数文件路径名称,第二个参数实体类class
// EasyExcel.write(filename,DemoData.class).sheet("学生列表").doWrite(getData());
//实现excel读操作
String filename = "F:\\write.xlsx";
EasyExcel.read(filename,DemoData.class,new ExcelListener()).sheet().doRead();
}
//创建方法返回list集合
private static List<DemoData> getData() {
List<DemoData> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setSno(i);
data.setSname("lucy"+i);
list.add(data);
}
return list;
}
}
实际开发:
SubjectService
//添加课程分类
@Override
public void saveSubject(MultipartFile file,EduSubjectService subjectService) {
try {
//文件输入流
InputStream in = file.getInputStream();
//调用方法进行读取
EasyExcel.read(in, SubjectData.class,new SubjectExcelListener(subjectService)).sheet().doRead();
}catch(Exception e){
e.printStackTrace();
}
}
listener->invoke执行方法拿到的是自己封装的对象
public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {
//因为SubjectExcelListener不交给spring进行管理,需要自己new,不能注入其他对象
//不能实现数据库操作
public EduSubjectService subjectService;
public SubjectExcelListener() {}
public SubjectExcelListener(EduSubjectService subjectService) {
this.subjectService = subjectService;
}
//读取excel内容,一行一行进行读取
@Override
public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
if(subjectData == null) {
throw new GuliException(20001,"文件数据为空");
}
//一行一行读取,每次读取有两个值,第一个值一级分类,第二个值二级分类
//判断一级分类是否重复
EduSubject existOneSubject = this.existOneSubject(subjectService, subjectData.getOneSubjectName());
if(existOneSubject == null) { //没有相同一级分类,进行添加
existOneSubject = new EduSubject();
existOneSubject.setParentId("0");
existOneSubject.setTitle(subjectData.getOneSubjectName());//一级分类名称
subjectService.save(existOneSubject);
}
//获取一级分类id值
String pid = existOneSubject.getId();
//添加二级分类
//判断二级分类是否重复
EduSubject existTwoSubject = this.existTwoSubject(subjectService, subjectData.getTwoSubjectName(), pid);
if(existTwoSubject == null) {
existTwoSubject = new EduSubject();
existTwoSubject.setParentId(pid);
existTwoSubject.setTitle(subjectData.getTwoSubjectName());//二级分类名称
subjectService.save(existTwoSubject);
}
}
//判断一级分类不能重复添加
private EduSubject existOneSubject(EduSubjectService subjectService,String name) {
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title",name);
wrapper.eq("parent_id","0");
EduSubject oneSubject = subjectService.getOne(wrapper);
return oneSubject;
}
//判断二级分类不能重复添加
private EduSubject existTwoSubject(EduSubjectService subjectService,String name,String pid) {
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title",name);
wrapper.eq("parent_id",pid);
EduSubject twoSubject = subjectService.getOne(wrapper);
return twoSubject;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
轮播图
核心:
/**
* <p>
* 首页banner表 服务实现类
* </p>
*
* @author testjava
* @since 2020-03-07
*/
@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {
//查询所有banner
// @Cacheable(value = "banner",key = "'selectIndexList'")
@Override
public List<CrmBanner> selectAllBanner() {
//根据id进行降序排列,显示排列之后前两条记录
QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
//last方法,拼接sql语句
wrapper.last("limit 2");
List<CrmBanner> list = baseMapper.selectList(null);
return list;
}
}
视频点播
核心代码:
1、oss服务
@Service
public class OssServiceImpl implements OssService {
//上传头像到oss
@Override
public String uploadFileAvatar(MultipartFile file) {
// 工具类获取值
String endpoint = ConstantPropertiesUtils.END_POIND;
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiesUtils.BUCKET_NAME;
try {
// 创建OSS实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
//获取上传文件输入流
InputStream inputStream = file.getInputStream();
//获取文件名称
String fileName = file.getOriginalFilename();
//1 在文件名称里面添加随机唯一的值
String uuid = UUID.randomUUID().toString().replaceAll("-","");
// yuy76t5rew01.jpg
fileName = uuid+fileName;
//2 把文件按照日期进行分类
//获取当前日期
// 2019/11/12
String datePath = new DateTime().toString("yyyy/MM/dd");
//拼接
// 2019/11/12/ewtqr313401.jpg
fileName = datePath+"/"+fileName;
//调用oss方法实现上传
//第一个参数 Bucket名称
//第二个参数 上传到oss文件路径和文件名称 aa/bb/1.jpg
//第三个参数 上传文件输入流
ossClient.putObject(bucketName,fileName , inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//把上传之后文件路径返回
//需要把上传到阿里云oss路径手动拼接出来
// https://edu-guli-1010.oss-cn-beijing.aliyuncs.com/01.jpg
String url = "https://"+bucketName+"."+endpoint+"/"+fileName;
return url;
}catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
2、视频
@Service
public class VodServiceImpl implements VodService {
@Override
public String uploadVideoAly(MultipartFile file) {
try {
//accessKeyId, accessKeySecret
//fileName:上传文件原始名称
// 01.03.09.mp4
String fileName = file.getOriginalFilename();
//title:上传之后显示名称
String title = fileName.substring(0, fileName.lastIndexOf("."));
//inputStream:上传文件输入流
InputStream inputStream = file.getInputStream();
UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID,ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
String videoId = null;
if (response.isSuccess()) {
videoId = response.getVideoId();
} else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
videoId = response.getVideoId();
}
return videoId;
}catch(Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public void removeMoreAlyVideo(List videoIdList) {
try {
//初始化对象
DefaultAcsClient client = InitVodCilent.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
//创建删除视频request对象
DeleteVideoRequest request = new DeleteVideoRequest();
//videoIdList值转换成 1,2,3
String videoIds = StringUtils.join(videoIdList.toArray(), ",");
//向request设置视频id
request.setVideoIds(videoIds);
//调用初始化对象的方法实现删除
client.getAcsResponse(request);
}catch(Exception e) {
e.printStackTrace();
throw new GuliException(20001,"删除视频失败");
}
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("11");
list.add("22");
list.add("33");
// 11,22,33
String join = StringUtils.join(list.toArray(), ",");
System.out.println(join);
}
}
短信服务
需要提前在阿里云申请短信服务,拿到签名名称、模板code
思路:自己生成一个四位或者六位随机数发给阿里云,拿到响应结果
核心代码:
@Service
public class MsmServiceImpl implements MsmService {
//发送短信的方法
@Override
public boolean send(Map<String, Object> param, String phone) {
if(StringUtils.isEmpty(phone)) return false;
DefaultProfile profile =
DefaultProfile.getProfile("default", "LTAI4FvvVEWiTJ3GNJJqJnk7", "9st82dv7EvFk9mTjYO1XXbM632fRbG");
IAcsClient client = new DefaultAcsClient(profile);
//设置相关固定的参数
CommonRequest request = new CommonRequest();
//request.setProtocol(ProtocolType.HTTPS);
request.setMethod(MethodType.POST);
request.setDomain("dysmsapi.aliyuncs.com");
request.setVersion("2017-05-25");
request.setAction("SendSms");
//设置发送相关的参数
request.putQueryParameter("PhoneNumbers",phone); //手机号
request.putQueryParameter("SignName","我的谷粒在线教育网站"); //申请阿里云 签名名称
request.putQueryParameter("TemplateCode","SMS_180051135"); //申请阿里云 模板code
request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param)); //验证码数据,转换json数据传递
try {
//最终发送
CommonResponse response = client.getCommonResponse(request);
boolean success = response.getHttpResponse().isSuccess();
return success;
}catch(Exception e) {
e.printStackTrace();
return false;
}
}
}
随机数工具类
/**
* 获取随机数
*
* @author qianyi
*
*/
public class RandomUtil {
private static final Random random = new Random();
private static final DecimalFormat fourdf = new DecimalFormat("0000");
private static final DecimalFormat sixdf = new DecimalFormat("000000");
public static String getFourBitRandom() {
return fourdf.format(random.nextInt(10000));
}
public static String getSixBitRandom() {
return sixdf.format(random.nextInt(1000000));
}
/**
* 给定数组,抽取n个数据
* @param list
* @param n
* @return
*/
public static ArrayList getRandom(List list, int n) {
Random random = new Random();
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
// 生成随机数字并存入HashMap
for (int i = 0; i < list.size(); i++) {
int number = random.nextInt(100) + 1;
hashMap.put(number, i);
}
// 从HashMap导入数组
Object[] robjs = hashMap.values().toArray();
ArrayList r = new ArrayList();
// 遍历数组并打印数据
for (int i = 0; i < n; i++) {
r.add(list.get((int) robjs[i]));
System.out.print(list.get((int) robjs[i]) + "\t");
}
System.out.print("\n");
return r;
}
}
微信登录

申请->appid、appsecret、redirect_url
用户登录->返回二维码->扫码登录->重定向到网站,拿到用户数据
# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
wx.open.redirect_url=http://guli.shop/api/ucenter/wx/callback
初始化(implements InitializingBean)
@Component
//@PropertySource("classpath:application.properties")
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wx.open.app_id}")
private String appId;
@Value("${wx.open.app_secret}")
private String appSecret;
@Value("${wx.open.redirect_url}")
private String redirectUrl;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}
wx控制
@CrossOrigin
@Controller //只是请求地址,不需要返回数据
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
@Autowired
private UcenterMemberService memberService;
//2 获取扫描人信息,添加数据
@GetMapping("callback")
public String callback(String code, String state) {
try {
//1 获取code值,临时票据,类似于验证码
//2 拿着code请求 微信固定的地址,得到两个值 accsess_token 和 openid
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//拼接三个参数 :id 秘钥 和 code值
String accessTokenUrl = String.format(
baseAccessTokenUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
ConstantWxUtils.WX_OPEN_APP_SECRET,
code
);
//请求这个拼接好的地址,得到返回两个值 accsess_token 和 openid
//使用httpclient发送请求,得到返回结果
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
//从accessTokenInfo字符串获取出来两个值 accsess_token 和 openid
//把accessTokenInfo字符串转换map集合,根据map里面key获取对应值
//使用json转换工具 Gson
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String access_token = (String)mapAccessToken.get("access_token");
String openid = (String)mapAccessToken.get("openid");
//把扫描人信息添加数据库里面
//判断数据表里面是否存在相同微信信息,根据openid判断
UcenterMember member = memberService.getOpenIdMember(openid);
if(member == null) {//memeber是空,表没有相同微信数据,进行添加
//3 拿着得到accsess_token 和 openid,再去请求微信提供固定的地址,获取到扫描人信息
//访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//拼接两个参数
String userInfoUrl = String.format(
baseUserInfoUrl,
access_token,
openid
);
//发送请求
String userInfo = HttpClientUtils.get(userInfoUrl);
//获取返回userinfo字符串扫描人信息
HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
String nickname = (String)userInfoMap.get("nickname");//昵称
String headimgurl = (String)userInfoMap.get("headimgurl");//头像
member = new UcenterMember();
member.setOpenid(openid);
member.setNickname(nickname);
member.setAvatar(headimgurl);
memberService.save(member);
}
//使用jwt根据member对象生成token字符串
String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
//最后:返回首页面,通过路径传递token字符串
return "redirect:http://localhost:3000?token="+jwtToken;
}catch(Exception e) {
throw new GuliException(20001,"登录失败");
}
}
//1 生成微信扫描二维码
@GetMapping("login")
public String getWxCode() {
//固定地址,后面拼接参数
// String url = "https://open.weixin.qq.com/" +
// "connect/qrconnect?appid="+ ConstantWxUtils.WX_OPEN_APP_ID+"&response_type=code";
// 微信开放平台授权baseUrl %s相当于?代表占位符
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
//对redirect_url进行URLEncoder编码
String redirectUrl = ConstantWxUtils.WX_OPEN_REDIRECT_URL;
try {
redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");
}catch(Exception e) {
}
//设置%s里面值
String url = String.format(
baseUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
redirectUrl,
"atguigu"
);
//重定向到请求微信地址里面
return "redirect:"+url;
}
}
微信支付
订单号工具类
/**
* 订单号工具类
*
* @author qy
* @since 1.0
*/
public class OrderNoUtil {
/**
* 获取订单号
* @return
*/
public static String getOrderNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
String result = "";
Random random = new Random();
for (int i = 0; i < 3; i++) {
result += random.nextInt(10);
}
return newDate + result;
}
}
http请求
/**
* http请求客户端
*
* @author qy
*
*/
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst)
url.append("?");
else
url.append("&");
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
订单服务实现类
/**
* <p>
* 订单 服务实现类
* </p>
*
* @author testjava
* @since 2020-03-13
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Autowired
private EduClient eduClient;
@Autowired
private UcenterClient ucenterClient;
//1 生成订单的方法
@Override
public String createOrders(String courseId, String memberId) {
//通过远程调用根据用户id获取用户信息
UcenterMemberOrder userInfoOrder = ucenterClient.getUserInfoOrder(memberId);
//通过远程调用根据课程id获取课信息
CourseWebVoOrder courseInfoOrder = eduClient.getCourseInfoOrder(courseId);
//创建Order对象,向order对象里面设置需要数据
Order order = new Order();
order.setOrderNo(OrderNoUtil.getOrderNo());//订单号
order.setCourseId(courseId); //课程id
order.setCourseTitle(courseInfoOrder.getTitle());
order.setCourseCover(courseInfoOrder.getCover());
order.setTeacherName(courseInfoOrder.getTeacherName());
order.setTotalFee(courseInfoOrder.getPrice());
order.setMemberId(memberId);
order.setMobile(userInfoOrder.getMobile());
order.setNickname(userInfoOrder.getNickname());
order.setStatus(0); //订单状态(0:未支付 1:已支付)
order.setPayType(1); //支付类型 ,微信1
baseMapper.insert(order);
//返回订单号
return order.getOrderNo();
}
}
支付日志表服务实现类
/**
* <p>
* 支付日志表 服务实现类
* </p>
*
* @author testjava
* @since 2020-03-13
*/
@Service
public class PayLogServiceImpl extends ServiceImpl<PayLogMapper, PayLog> implements PayLogService {
@Autowired
private OrderService orderService;
//生成微信支付二维码接口
@Override
public Map createNatvie(String orderNo) {
try {
//1 根据订单号查询订单信息
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no",orderNo);
Order order = orderService.getOne(wrapper);
//2 使用map设置生成二维码需要参数
Map m = new HashMap();
m.put("appid","wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("nonce_str", WXPayUtil.generateNonceStr());
m.put("body", order.getCourseTitle()); //课程标题
m.put("out_trade_no", orderNo); //订单号
m.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue()+"");
m.put("spbill_create_ip", "127.0.0.1");
m.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n");
m.put("trade_type", "NATIVE");
//3 发送httpclient请求,传递参数xml格式,微信支付提供的固定的地址
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
//设置xml格式的参数
client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
//执行post请求发送
client.post();
//4 得到发送请求返回结果
//返回内容,是使用xml格式返回
String xml = client.getContent();
//把xml格式转换map集合,把map集合返回
Map<String,String> resultMap = WXPayUtil.xmlToMap(xml);
//最终返回数据 的封装
Map map = new HashMap();
map.put("out_trade_no", orderNo);
map.put("course_id", order.getCourseId());
map.put("total_fee", order.getTotalFee());
map.put("result_code", resultMap.get("result_code")); //返回二维码操作状态码
map.put("code_url", resultMap.get("code_url")); //二维码地址
return map;
}catch(Exception e) {
throw new GuliException(20001,"生成二维码失败");
}
}
//查询订单支付状态
@Override
public Map<String, String> queryPayStatus(String orderNo) {
try {
//1、封装参数
Map m = new HashMap<>();
m.put("appid", "wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("out_trade_no", orderNo);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//2 发送httpclient
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
client.post();
//3 得到请求返回内容
String xml = client.getContent();
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
//6、转成Map再返回
return resultMap;
}catch(Exception e) {
return null;
}
}
//添加支付记录和更新订单状态
@Override
public void updateOrdersStatus(Map<String, String> map) {
//从map获取订单号
String orderNo = map.get("out_trade_no");
//根据订单号查询订单信息
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no",orderNo);
Order order = orderService.getOne(wrapper);
//更新订单表订单状态
if(order.getStatus().intValue() == 1) { return; }
order.setStatus(1);//1代表已经支付
orderService.updateById(order);
//向支付表添加支付记录
PayLog payLog = new PayLog();
payLog.setOrderNo(orderNo); //订单号
payLog.setPayTime(new Date()); //订单完成时间
payLog.setPayType(1);//支付类型 1微信
payLog.setTotalFee(order.getTotalFee());//总金额(分)
payLog.setTradeState(map.get("trade_state"));//支付状态
payLog.setTransactionId(map.get("transaction_id")); //流水号
payLog.setAttr(JSONObject.toJSONString(map));
baseMapper.insert(payLog);
}
}
认证鉴权

1、Security配置类:
/**
* <p>
* Security配置类
* </p>
*
* @author qy
* @since 2019-11-18
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private TokenManager tokenManager;
private DefaultPasswordEncoder defaultPasswordEncoder;
private RedisTemplate redisTemplate;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 配置设置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}
/**
* 密码处理
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
/**
* 配置哪些请求不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**",
"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
);
}
}
2、安全认证用户详情信息
/**
* <p>
* 安全认证用户详情信息
* </p>
*
* @author qy
* @since 2019-11-08
*/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3、用户实体类
/**
* <p>
* 用户实体类
* </p>
*
* @author qy
* @since 2019-11-08
*/
@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "用户头像")
private String salt;
@ApiModelProperty(value = "用户签名")
private String token;
}
4、登录过滤器
/**
* <p>
* 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
* </p>
*
* @author qy
* @since 2019-11-08
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功
* @param req
* @param res
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(res, R.ok().data("token", token));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
5、访问过滤器
/**
* <p>
* 访问过滤器
* </p>
*
* @author qy
* @since 2019-11-08
*/
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("================="+req.getRequestURI());
if(req.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
ResponseUtil.out(res, R.error());
}
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
if (token != null && !"".equals(token.trim())) {
String userName = tokenManager.getUserFromToken(token);
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
return null;
}
}
6、密码加密
/**
* <p>
* t密码的处理方法类型
* </p>
*
* @author qy
* @since 2019-11-08
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
/**
* @param strength
* the log rounds to use, between 4 and 31
*/
public DefaultPasswordEncoder(int strength) {
}
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
8、登出业务逻辑
/**
* <p>
* 登出业务逻辑类
* </p>
*
* @author qy
* @since 2019-11-08
*/
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
if (token != null) {
tokenManager.removeToken(token);
//清空当前用户缓存中的权限数据
String userName = tokenManager.getUserFromToken(token);
redisTemplate.delete(userName);
}
ResponseUtil.out(response, R.ok());
}
}
9、token管理(工具类)
/**
* <p>
* token管理
* </p>
*
* @author qy
* @since 2019-11-08
*/
@Component
public class TokenManager {
private long tokenExpiration = 24*60*60*1000;
private String tokenSignKey = "123456";
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
public String getUserFromToken(String token) {
String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return user;
}
public void removeToken(String token) {
//jwttoken无需删除,客户端扔掉即可。
}
}
10、 未授权的统一处理
/**
* <p>
* 未授权的统一处理方式
* </p>
*
* @author qy
* @since 2019-11-08
*/
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
数据统计
实现:
按前端Echarts需求封装数据为数据即可
/**
* <p>
* 网站统计日数据 服务实现类
* </p>
*
* @author testjava
* @since 2020-03-14
*/
@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {
@Autowired
private UcenterClient ucenterClient;
@Override
public void registerCount(String day) {
//添加记录之前删除表相同日期的数据
QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
wrapper.eq("date_calculated",day);
baseMapper.delete(wrapper);
//远程调用得到某一天注册人数
R registerR = ucenterClient.countRegister(day);
Integer countRegister = (Integer)registerR.getData().get("countRegister");
//把获取数据添加数据库,统计分析表里面
StatisticsDaily sta = new StatisticsDaily();
sta.setRegisterNum(countRegister); //注册人数
sta.setDateCalculated(day);//统计日期
sta.setVideoViewNum(RandomUtils.nextInt(100,200));
sta.setLoginNum(RandomUtils.nextInt(100,200));
sta.setCourseNum(RandomUtils.nextInt(100,200));
baseMapper.insert(sta);
}
//图表显示,返回两部分数据,日期json数组,数量json数组
@Override
public Map<String, Object> getShowData(String type, String begin, String end) {
//根据条件查询对应数据
QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
wrapper.between("date_calculated",begin,end);
wrapper.select("date_calculated",type);
List<StatisticsDaily> staList = baseMapper.selectList(wrapper);
//因为返回有两部分数据:日期 和 日期对应数量
//前端要求数组json结构,对应后端java代码是list集合
//创建两个list集合,一个日期list,一个数量list
List<String> date_calculatedList = new ArrayList<>();
List<Integer> numDataList = new ArrayList<>();
//遍历查询所有数据list集合,进行封装
for (int i = 0; i < staList.size(); i++) {
StatisticsDaily daily = staList.get(i);
//封装日期list集合
date_calculatedList.add(daily.getDateCalculated());
//封装对应数量
switch (type) {
case "login_num":
numDataList.add(daily.getLoginNum());
break;
case "register_num":
numDataList.add(daily.getRegisterNum());
break;
case "video_view_num":
numDataList.add(daily.getVideoViewNum());
break;
case "course_num":
numDataList.add(daily.getCourseNum());
break;
default:
break;
}
}
//把封装之后两个list集合放到map集合,进行返回
Map<String, Object> map = new HashMap<>();
map.put("date_calculatedList",date_calculatedList);
map.put("numDataList",numDataList);
return map;
}
}
微服务
nacos:注册微服务
先安装跑起来
->配置文件spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
->启动类加@EnableDiscoveryClient
feign:调用
启动类@EnableFeignClients
->调用接口
package com.guli.edu.client;
@FeignClient("service-vod")
@Component
public interface VodClient {
@DeleteMapping(value = "/eduvod/vod/video/{videoId}")
public R removeVideo(@PathVariable("videoId") String videoId);
}
hystrix:熔断
配置文件
#开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=600
->熔断实现类
@Component
public class VodFileDegradeFeignClient implements VodClient {
@Override
public R removeVideo(String videoId) {
return R.error().message("time out");
}
@Override
public R removeVideoList(List videoIdList) {
return R.error().message("time out");
}
}
->feign client改注解
@FeignClient(name = "service-vod", fallback = VodFileDegradeFeignClient.class)
@Component
public interface VodClient {
@DeleteMapping(value = "/eduvod/vod/{videoId}")
public R removeVideo(@PathVariable("videoId") String videoId);
@DeleteMapping(value = "/eduvod/vod/delete-batch")
public R removeVideoList(@RequestParam("videoIdList") List videoIdList);
}
gateway:网关
# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#设置路由id
spring.cloud.gateway.routes[0].id=service-acl
#设置路由的uri lb://nacos注册服务名称
spring.cloud.gateway.routes[0].uri=lb://service-acl
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**
#配置service-edu服务
spring.cloud.gateway.routes[1].id=service-edu
spring.cloud.gateway.routes[1].uri=lb://service-edu
spring.cloud.gateway.routes[1].predicates= Path=/eduservice/**
#配置service-edu服务
spring.cloud.gateway.routes[2].id=service-msm
spring.cloud.gateway.routes[2].uri=lb://service-msm
spring.cloud.gateway.routes[2].predicates= Path=/edumsm/**
->gateway跨域处理
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
->网关拦截
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
//谷粒学院api接口,校验用户必须登录
if(antPathMatcher.match("/api/**/auth/**", path)) {
List<String> tokenList = request.getHeaders().get("token");
if(null == tokenList) {
ServerHttpResponse response = exchange.getResponse();
return out(response);
} else {
// Boolean isCheck = JwtUtils.checkToken(tokenList.get(0));
// if(!isCheck) {
ServerHttpResponse response = exchange.getResponse();
return out(response);
// }
}
}
//内部服务接口,不允许外部访问
if(antPathMatcher.match("/**/inner/**", path)) {
ServerHttpResponse response = exchange.getResponse();
return out(response);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
private Mono<Void> out(ServerHttpResponse response) {
JsonObject message = new JsonObject();
message.addProperty("success", false);
message.addProperty("code", 28004);
message.addProperty("data", "鉴权失败");
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
4、后台管理模块
列表展示
头像上传组件
富文本编辑器
Tinymce
Echarts
5、前台展示模块
NUXT
Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎
生成静态站点应用,具有优雅的代码结构分层和热加载等特性。
6、部署
nginx
监听9001端口请求,对应到后台各个服务
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
.......
server {
listen 9001;
server_name localhost;
location ~ /eduservice/ {
proxy_pass http://localhost:8001;
}
location ~ /eduoss/ {
proxy_pass http://localhost:8002;
}
.......
}
docker
脚本:

浙公网安备 33010602011771号