一、引言
1、软件开发整体介绍
(1)软件开发流程
(2)角色分工
(3)软件环境
2、技术选型
该项目所用到的技术框架及中间件如下:
3、前后端分离开发流程
4、小程序接入流程
5、小程序支付
微信小程序支付时序图
① 调用过程如何保证数据安全?
获取微信支付平台证书、商户私钥文件:
② 微信后台如何调用到商户系统?
获取临时域名:支付成功后微信服务通过该域名回调我们的程序
JSAPI下单
商户系统调用该接口在微信支付服务后台生成预支付交易单
文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
请求地址:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
微信小程序调起支付
通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付
文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
请求:调用wx.requestPayment(OBJECT)发起微信支付
二、项目开发
1、后端开发
(1)Nginx 反向代理
实际上是由nginx做了一个反向代理:
nginx 反向代理的好处:
nginx 反向代理的配置方式:
nginx 负载均衡的配置方式:
nginx 负载均衡策略:
(2)Swagger
有些接口需要的参数很多,若使用postman进行测试,就需要构造非常多的参数,测试的效率就会较低。此时,我们就可以通过swagger来帮助我们后端生成接口文档,并且可以进行后端的接口测试。
介绍
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
Knife4j 是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
官网:https://swagger.io/
使用方式
- 导入 knife4j 的maven坐标
- 在配置类中加入 knife4j 相关配置
- 设置静态资源映射,否则接口文档页面无法访问
设置静态资源映射时,其方法名必须为addResourceHandlers,因为当前这个类继承自WebMvcConfigurationSupport(spring提供的一个类),addResourceHandlers其实是重写的父类WebMvcConfigurationSupport中的方法
常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
通过 Swagger 就可以生成接口文档,那么我们就不需要 Yapi 了?
1、Yapi 是设计阶段使用的工具,管理和维护接口;
2、Swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
(3)Redis
Redis入门
-
Redis简介
-
Redis下载与安装
Redis安装包分为 Windows 版和 Linux 版:
Windows版下载地址:https://github.com/microsoftarchive/redis/releases
(Redis的Windows版属于绿色软件,直接解压即可使用,解压后目录结构如下:)
Linux版下载地址: https://download.redis.io/releases/ -
Redis服务启动与停止
① 服务启动命令:redis-server.exe redis.windows.conf
Redis服务默认端口号为 6379 ,通过快捷键 Ctrl + C 即可停止Redis服务
② 客户端连接命令:redis-cli.exe
通过redis-cli.exe命令默认连接的是本地的redis服务,并且使用默认6379端口。也可以通过指定如下参数连接:
-h ip地址
-p 端口号
-a 密码(如果需要)
③ 设置Redis服务密码,修改redis.windows.conf
注意:
修改密码后需要重启Redis服务才能生效
Redis配置文件中 # 表示注释
Redis数据类型
- 5种常用数据类型介绍
- 各种数据类型的特点
Redis常用命令
- 字符串操作命令
- 哈希操作命令
- 列表操作命令
- 集合操作命令
- 有序集合操作命令
- 通用命令
RANGE命令中stop参数给-1表示想要获取所有元素
在Java中操作Redis
- Redis的Java客户端
- Spring Data Redis使用方式
在本项目(苍穹外卖)中,redis主要用于实现店铺营业状态设置业务(由于本项目的后台管理系统只针对一个商户,即商户-状态对该项目而言只有一条记录,故使用redis去代替mysql来存储这条数据)及缓存菜品和套餐业务(为了避免“出现由于在短时间内大量用户使用小程序时触发大量请求从而给数据库造成压力导致查询性能下降最终降低用户使用体验”的问题,所以这里通过redis来缓存菜品和套餐数据,从而减少数据库查询操作)
(4)HttpClient
通过HttpClient发送get和post请求的示例代码:
package com.sky.test;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @version 1.8
* @Author: Kim
* @Project: sky-take-out
* @Package: PACKAGE_NAME
* @Name: HttpClientTest
* @Date: 2024/5/25 20:56
*/
@SpringBootTest
public class HttpClientTest {
/**
* 测试通过HttpClient发送get方式的请求
*
* @throws Exception
*/
@Test
public void testGET() throws Exception {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
// 发送请求,接受响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);
// 获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码为:" + statusCode);
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:" + body);
// 关闭资源
response.close();
httpClient.close();
}
/**
* 测试通过HttpClient发送post方式的请求
*
* @throws Exception
*/
@Test
public void testPOST() throws Exception {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
// 封装请求体
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", "admin");
jsonObject.put("password", "123456");
StringEntity entity = new StringEntity(jsonObject.toString());
// 指定请求的编码方式
entity.setContentEncoding("utf-8");
// 指定请求的数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
// 发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
// 解析返回结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:" + statusCode);
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为:" + body);
// 关闭资源
response.close();
httpClient.close();
}
}
封装的操作HttpClient的工具类:
package com.sky.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
(5)SpringCache
使用Spring Cache实际上就是使用其提供的一系列注解:
- @EnableCaching:
package com.itheima;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@Slf4j
@SpringBootApplication
@EnableCaching // 开启缓存注解功能
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
log.info("项目启动成功...");
}
}
- @CachePut
@PostMapping
// 建议使用第一种,保持与参数名一致,最为直观
// . 作用:对象导航
@CachePut(cacheNames = "userCache", key = "#user.id") // 若使用Spring Cache缓存数据, key的生成:userCache::1(1是通过 #user.id 动态获取到的)
// @CachePut(cacheNames = "userCache", key = "#result.id") // result 为关键字,代表当前方法的返回值
// @CachePut(cacheNames = "userCache", key = "#p0.id") // 多个参数下,编号从0开始
// @CachePut(cacheNames = "userCache", key = "#a0.id")
// @CachePut(cacheNames = "userCache", key = "#root.args[0].id") // 表明的仍然是当前方法的第一个参数
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
- @Cacheable
@GetMapping
@Cacheable(cacheNames = "userCache", key = "#id") // key的生成:userCache::1(1是通过 #id 动态获取到的)
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
Spring Cache底层基于代理技术。一旦加入Spring Cache提供的注解后,Spring Cache就会为当前类创建一个代理对象。在请求对应方法前,实际上请求会先进入到代理对象,在代理对象中先查询redis等缓存
- @CacheEvict
@DeleteMapping
@CacheEvict(cacheNames = "userCache", key = "#id") // key的生成:userCache::1(1是通过 #id 动态获取到的)
public void deleteById(Long id){
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache", allEntries = true) // 会匹配到userCache下所有键值对儿
public void deleteAll(){
userMapper.deleteAll();
}
这里仍然是通过代理对象来实现的删除redis中的数据
(6)Spring Task
介绍
Spring Task 是Spring框架提供的任务调度工具(Spring Task 本身也属于Spring家族当中的一个框架),可以按照约定的时间自动执行某个代码逻辑。
- 定位:定时任务框架
- 作用:定时自动执行某段Java代码
- 应用场景:只要是需要定时处理的场景都可以使用Spring Task,如:信用卡每月还款提醒、银行贷款每月还款提醒火车票售票系统处理未支付订单及入职纪念日为用户发送通知等
cron表达式(通过cron表达式就可以去定义任务的触发时间)
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
- 构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
- 每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选,如果不包含年份,那么任务将会在每年的指定时间执行)
示例:
日与周两个域往往是不能同时出现的:如果指定了日,那么在周的位置就需要用 ? 填充;同理,若指定了周,那么就需要在日的位置用 ? 填充。也就是说,日与周的位置往往只能定义一个,另外一方设置成 ? 即可
对于一些表达式,若直接手动填写还是会有一些难度。因为表达式中除了数字外,还会有一些特殊的字符。如,若想表达2月份的最后一天,光靠数字是不能实现的,此时我们就需要使用一些特殊的字符来表达。我们可以借助cron表达式在线生成器(https://cron.qqe2.com/)来生成我们想要的cron表达式,如下图:
入门案例
Spring Task是一个非常小的框架,小到都没有自己单独的jar包,都是集成在了spring-context包中
实现步骤:
① 导入maven坐标 spring-context(已存在)
② 启动类添加注解 @EnableScheduling 开启任务调度
package com.sky;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement // 开启注解方式的事务管理
@EnableCaching // 开启缓存注解功能
@EnableScheduling // 开启任务调度
@Slf4j
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
③ 自定义定时任务类✨
/**
* @Author: Kim
* @Project: sky-take-out
* @Package: com.sky.task
* @Name: MyTask
* @Date: 2024/5/27 15:08
* @version 1.8
*/
package com.sky.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 自定义定时任务类
*/
@Component // 当前这个类也需要实例化并交给spring容器管理
@Slf4j
public class MyTask {
/**
* 定时任务 每隔5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?") // @Scheduled源自spring-context的jar包中,通过该注解的cron属性指定任务何时触发
public void executeTask() { // 方法名任意 无返回值 用于存放定时任务具体处理的业务逻辑
log.info("定时任务开始执行:{}", new Date());
}
}
使用Spring Task实际上只需要关注以下两个点即可:
① 表达式如何编写->指定何时触发
② 具体业务逻辑
Spring Task是Spring框架提供的一个定时任务工具。通过Spring Task可以定时处理一些任务。如,定时处理订单状态等
(7)WebSocket
介绍
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信(主要体现在浏览器和服务器可以实现双向数据传输。说的更直白些,就是浏览器可以向服务器来传输数据;同时,服务器也可以主动向浏览器来传输数据,他们之间是双向的)——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
- 应用场景:视频弹幕、网页聊天、体育实况更新及股票基金报价实时更新等(总结:页面没有刷新,但数据进行了实时更新的地方大多都用到了WebSocket。也就是说,并不需要页面发送请求去获取数据,而是服务器主动给浏览器推送数据)
入门案例
实现步骤:
① 直接使用websocket.html页面作为WebSocket客户端
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
② 导入WebSocket的maven坐标
③ 导入WebSocket服务端组件WebSocketServer(名称随意),用于和客户端通信
package com.sky.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* WebSocket服务
*/
@Component // 这个类最终交给spring容器去管理
@ServerEndpoint("/ws/{sid}") // 类似于Controller,通过路径去匹配,只不过在这里用的注解是@ServerEndpoint(该注解源于javax.websocket.server包)
public class WebSocketServer {
/**
* 存放会话对象
* Session是javax.websocket包下的类,代表一个会话。客户端与服务端要建立一个连接,实际本质上就是一个会话
* 建立好会话后,客户端与服务端就可以进行双向通信
*/
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
* 加上@OnOpen注解后,onOpen方法就变成了一个回调方法
* 握手成功后,客户端与服务端连接就建立好了。建立好连接后,服务端就会去调用由@OnOpen注解修饰的onOpen方法
* 这个过程是由websocket框架自动调的
*/
@OnOpen // @OnOpen也是来源于javax.websocket包
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session); // 将sid作为key,并将对应session(会话)存入map中
}
/**
* 收到客户端消息后调用的方法
* 类似于Controller中的接口。websocket协议下发消息相当于http协议下发请求:http协议下会调用相应方法,而在websocket则会调由@onMessage注解修饰的onMessage方法
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid); // 连接断开后清理相应会话
}
/**
* 群发
* 这个方法上就没有注解了,也就是说我们需要主动去调用该方法
* 取出map并遍历得到所有session(会话)
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
// 固定方法:服务器向客户端发送消息👈
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
④ 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
package com.sky.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
⑤ 导入定时任务类WebSocketTask,定时向客户端推送数据(方便测试)
package com.sky.task;
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer; // 服务端websocket组件
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
WebSocket是一个新型的协议。通过这个协议可以做到我们客户端浏览器与我们的服务端进行双向的数据传输。基于WebSocket就可以向我们客户端浏览器来推送消息,从而实现来单提醒、客户催单等功能
补充
既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?
WebSocket缺点:① 服务器长期维护长连接需要一定的成本;② 各个浏览器支持程度不一;③ WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
(8)Apache ECharts
Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。
官网地址:https://echarts.apache.org/zh/index.html
Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/
总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。
(9)Apache POI
介绍
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。
为什么要在Java程序中操作Excel文件呢?
答:Apache POI 的应用场景:① 银行网银系统导出交易明细(写);② 各种业务系统导出Excel报表(写);③ 批量导入业务数据(读)(这里的读写均针对文件而言)
入门案例
Apache POI的maven坐标:
示例代码:
package com.sky.test;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
/**
* 使用POI操作Excel文件
*/
public class POITest {
/**
* 通过POI创建Excel文件并且写入文件内容
*/
public static void write() throws Exception{
//在内存中创建一个Excel文件
XSSFWorkbook excel = new XSSFWorkbook();
//在Excel文件中创建一个Sheet页,使用代码方式创建Excel文件时默认不会自动创建Sheet页,所以需要手动添加一个Sheet页
XSSFSheet sheet = excel.createSheet("info");
//在Sheet中创建行对象,rownum编号从0开始
XSSFRow row = sheet.createRow(1);
//创建单元格并且写入文件内容
row.createCell(1).setCellValue("姓名");
row.createCell(2).setCellValue("城市");
//创建一个新行
row = sheet.createRow(2);
row.createCell(1).setCellValue("张三");
row.createCell(2).setCellValue("北京");
row = sheet.createRow(3);
row.createCell(1).setCellValue("李四");
row.createCell(2).setCellValue("南京");
//通过输出流将内存中的Excel文件写入到磁盘
FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
excel.write(out);
//关闭资源
out.close();
excel.close();
}
/**
* 通过POI读取Excel文件中的内容
* @throws Exception
*/
public static void read() throws Exception{
// 获取要读取的Excel文件的输入流对象
InputStream in = new FileInputStream(new File("D:\\info.xlsx"));
//读取磁盘上已经存在的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//读取Excel文件中的第一个Sheet页
XSSFSheet sheet = excel.getSheetAt(0);
//获取Sheet中最后一行有内容的行号
int lastRowNum = sheet.getLastRowNum();
for (int i = 1; i <= lastRowNum ; i++) {
//获得某一行
XSSFRow row = sheet.getRow(i);
//获得单元格对象
String cellValue1 = row.getCell(1).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1 + " " + cellValue2);
}
//关闭资源
in.close();
excel.close();
}
public static void main(String[] args) throws Exception {
// 测试写方法
write();
// 测试读方法
read();
}
}
三、积累
① 使用spring提供的BeanUtils进行对象属性的拷贝,如:Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTO, employee);(源->目标)(前提:属性名必须一致)
② 获取当前登录用户id
员工登录成功后会生成JWT令牌并响应给前端:
后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:
解析出登录员工id后,借助ThreadLocal传递给Controller、Service及Mapper
封装操作 ThreadLocal 的工具类:
在拦截器中解析出当前登录员工id,并放入线程局部变量中:
可以在Service中获取线程局部变量中的值:
③ 基于SpringBoot+Mybatis+PageHelper实现的分页查询示例:如示例代码1所示
④ PageHelper底层其实是基于Mybatis拦截器实现,即,根据接收到的页码和每页数量动态的计算出limit后的两个参数并拼接到后面的sql语句中
⑤ 在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
⑥ 公共字段自动填充(技术点:枚举、注解、AOP、反射)
自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法:
自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值:
在 Mapper 的方法上加入 AutoFill 注解:
⑦ 想要使用注解方式的事务控制时,要在启动类上开启注解方式的事务管理:即,添加注解@EnableTransactionManagement
⑧ 为了当接口中使用日期类型参数时能够正确接收到前端发送来的数据,在后端接口参数前需要加上@DateTimeFormat注解,并将该注解中的pattern属性设置为与前端发送过来的日期数据格式一致的形式,如下图所示:
⑨ 将参数封装成map传给mapper接口示例(保证动态sql拼接时使用的名称与map中提供的key名称一致即可):
ServiceImpl.java:
Mapper.xml:
⑩ stream流使用技巧:如示例代码2所示
⑪ 导出Excel报表功能实现步骤:
(1) 设计Excel模板文件(若生成的excel表格形式复杂则建议提前设计好Excel模板文件,否则也可以借助POI生成一个新的Excel文件);
(2) 查询相应数据;
(3) 将查询到的数据写入模板文件(或创建的文件)中;
(4) 通过输出流将Excel文件下载到客户端浏览器,如示例代码3所示
- 示例代码1:
package com.sky.controller.admin;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 分类管理
*/
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 分类分页查询
* @param categoryPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分类分页查询")
public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
log.info("分页查询:{}", categoryPageQueryDTO);
PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
return Result.success(pageResult);
}
}
package com.sky.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* 分类业务层
*/
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
//下一条sql进行分页,自动加入limit关键字分页
Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.CategoryMapper">
<select id="pageQuery" resultType="com.sky.entity.Category">
select * from category
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
<if test="type != null">
and type = #{type}
</if>
</where>
order by sort asc , create_time desc
</select>
</mapper>
- 示例代码2:
/**
* @Author: Kim
* @Project: sky-take-out
* @Package: com.sky.test
* @Name: StreamSkill
* @Date: 2024/5/28 5:23
* @version 1.8
*/
package com.sky.test;
import com.sky.dto.GoodsSalesDTO;
import com.sky.vo.SalesTop10ReportVO;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class StreamSkill {
/**
* 利用stream流实现累加集合中元素
*/
public void streamSkill1() {
ArrayList<Integer> list = new ArrayList<>();
// ...
Integer sum = list // 集合
.stream() // 转换成流
.reduce(Integer::sum) // 对流中的元素进行聚合操作(在这里时累加)
.get(); // 获取结果
log.info("累加结果为:{}", sum);
}
/**
* 利用stream流实现对集合的拆分及重组
*/
public void streamSkill2() {
/**
* @Data
* @Builder
* @NoArgsConstructor
* @AllArgsConstructor
* public class GoodsSalesDTO {
* // 商品名称
* private String name;
* // 销售
* private Integer number;
* }
*/
/**
* @Data
* @Builder
* @NoArgsConstructor
* @AllArgsConstructor
* public class SalesTop10ReportVO {
* // 商品名称列表,以逗号分隔
* private String nameList;
* // 销售列表,以逗号分隔
* private String numberList;
* }
*/
List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
List<String> names = salesTop10 // 集合
.stream() // 转换成流
.map(GoodsSalesDTO::getName) // 获取商品名称
.collect(Collectors.toList()); // 收集
String nameList = StringUtils.join(names, ",");
List<Integer> numbers = salesTop10 // 集合
.stream() // 转换成流
.map(GoodsSalesDTO::getNumber) // 获取销售数量
.collect(Collectors.toList()); // 收集
String numberList = StringUtils.join(numbers, ",");
SalesTop10ReportVO vo = SalesTop10ReportVO // 利用Builder(构建者)模式创建一个SalesTop10ReportVO实例
.builder()
.nameList(nameList)
.numberList(numberList)
.build();
}
/**
* 利用stream流实现查询功能
*/
public void streamSkill3() {
// Redis文章中 点赞排行榜 部分代码
// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
List<UserDTO> userDTOS = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list() // 保证用in的同时是按in中给的元素顺序进行查询的;由于mp中的orderBy不支持field功能,所以使用last(表示最后一条sql语句,会在原有sql语句后拼接)来添加要执行的语句
.stream() // 转换成流
.map(user -> BeanUtil.copyProperties(user, UserDTO.class)) // 映射
.collect(Collectors.toList()); // 收集
}
}
- 示例代码3:
package com.sky.controller.admin;
import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.OrderReportVO;
import com.sky.vo.SalesTop10ReportVO;
import com.sky.vo.TurnoverReportVO;
import com.sky.vo.UserReportVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDate;
/**
* 导出运营数据报表
* @param response 通过response对象获得输出流,再通过输出流将Excel文件下载到客户端浏览器
*/
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessData(response);
}
}
package com.sky.service.impl;
import com.sky.service.ReportService;
import com.sky.dto.GoodsSalesDTO;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.WorkspaceService;
import com.sky.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private WorkspaceService workspaceService;
/**
* 导出运营数据报表
* @param response
*/
public void exportBusinessData(HttpServletResponse response) {
//1. 查询数据库,获取营业数据---查询最近30天的运营数据
LocalDate dateBegin = LocalDate.now().minusDays(30);
LocalDate dateEnd = LocalDate.now().minusDays(1);
//查询概览数据
BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
//2. 通过POI将数据写入到Excel文件中
// 从类路径下读取资源并返回一个输入流对象
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
ServletOutputStream out = null;
XSSFWorkbook excel = null;
try {
//传入一个输入流,实现基于模板文件创建一个新的Excel文件
excel = new XSSFWorkbook(in);
//获取表格文件的Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充数据--时间
sheet.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);
//获得第4行
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessDataVO.getTurnover());
row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
row.getCell(6).setCellValue(businessDataVO.getNewUsers());
//获得第5行
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
//填充明细数据
for (int i = 0; i < 30; i++) {
LocalDate date = dateBegin.plusDays(i);
//查询某一天的营业数据
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
//获得某一行
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(3).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData.getUnitPrice());
row.getCell(6).setCellValue(businessData.getNewUsers());
}
//3. 通过输出流将Excel文件下载到客户端浏览器
out = response.getOutputStream();
// 将Excel写到HttpServletResponse对象(代表服务器的响应)的输出流中,实际上就是客户端浏览器
// 而此时客户端浏览器就会弹出一个对话框并实现文件下载
excel.write(out);
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
try {
out.close();
excel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package com.sky.service.impl;
import com.sky.constant.StatusConstant;
import com.sky.entity.Orders;
import com.sky.mapper.DishMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WorkspaceServiceImpl implements WorkspaceService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
/**
* 根据时间段统计营业数据
* @param begin
* @param end
* @return
*/
public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {
/**
* 营业额:当日已完成订单的总金额
* 有效订单:当日已完成订单的数量
* 订单完成率:有效订单数 / 总订单数
* 平均客单价:营业额 / 有效订单数
* 新增用户:当日新增用户的数量
*/
Map map = new HashMap();
map.put("begin",begin);
map.put("end",end);
//查询总订单数
Integer totalOrderCount = orderMapper.countByMap(map);
map.put("status", Orders.COMPLETED);
//营业额
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null? 0.0 : turnover;
//有效订单数
Integer validOrderCount = orderMapper.countByMap(map);
Double unitPrice = 0.0;
Double orderCompletionRate = 0.0;
if(totalOrderCount != 0 && validOrderCount != 0){
//订单完成率
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
//平均客单价
unitPrice = turnover / validOrderCount;
}
//新增用户数
Integer newUsers = userMapper.countByMap(map);
return BusinessDataVO.builder()
.turnover(turnover)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.unitPrice(unitPrice)
.newUsers(newUsers)
.build();
}
}
四、注意
① 采用MD5对用户密码加密:需要注意的是,MD5加密是不可逆的。也就是说我们只能由一个明文进行MD5加密然后得到一个密文,而不能通过一个密文算出来原本的明文是什么。即,这个过程是单向的,只能从左到右,而不能从右到左。此时想要进行密码比对只能将一个明文进行加密处理再跟另一个密文去进行比对
五、其他
YApi
YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
使用Google Chrome浏览器打开YApi官网:http://api.doc.jiyou-tech.com/
阿里云存储OSS文件上传
- 在阿里云进行注册,创建一个Bucket(Bucket(存储桶),是用户用来管理所存储对象的存储空间):登录OSS管理控制台 (aliyun.com)
- 各个版本的操作文档:https://promotion.aliyun.com/ntms/act/ossdoclist.html?spm=5176.8465980.entries.1.4e651450Z1gT44
- 导入依赖
- 生成子账户,用于后台登陆阿里云OSS
- 测试demo
public static void main(String[] args) throws FileNotFoundException {
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
String endpoint = "oss-cn-shanghai.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "LTAI5tD8ACw25Nd3Ppq5U1Fq";
String accessKeySecret = "30niIcJE6qcRYjrc9cqFeFeKcV8IEt";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
InputStream inputStream = new FileInputStream("C:\\Users\\liaoz\\Desktop\\1630417064.jpg");
// 依次填写Bucket名称(例如examplebucket)和Object完整路径(例如exampledir/exampleobject.txt)。Object完整路径中不能包含Bucket名称。
ossClient.putObject("gulimail-liaozk", "oss测试.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传完成。。。。。");
}
-
验证
详细教程:https://blog.csdn.net/liao3399084/article/details/135640647
微信小程序
微信公众平台官网:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN
注册小程序地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1
登录小程序后台:https://mp.weixin.qq.com/
微信开发者工具下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html
微信服务端登录接口:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
参考:https://pay.weixin.qq.com/static/product/product_index.shtml
JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单
cpolar
cpolar是一款安全的内网穿透工具,适合微信公众号开发调试,Web开发,OpenAPI开发,webhook开发和调试工具。只需一行命令,就可以将内网站点发布至公网,方便给客户演示。高效调试微信公众号、小程序、对接支付宝网关等云端服务,提高编程效率。
cpolar官网:https://dashboard.cpolar.com/login
在本项目中,使用cpolar工具获取临时域名,从而实现支付成功后微信服务通过该域名能够成功回调我们的程序:
cron表达式
cron表达式在线生成器:https://cron.qqe2.com/