美味之选小结下
一.小程序端
1.1HttpClient
1.1.1 介绍
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
下载地址: http://hc.apache.org/downloads.cgi
1.1.2 HttpClient作用
- 发送HTTP请求
- 接收响应数据
1.1.3 如何使用
HttpClient的maven坐标:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
注:在之前在使用OSS时导入相关依赖,其中已经包含了HttpClient相关依赖,所以这部分可导可不导
aliyun-sdk-oss坐标:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
HttpClient的核心API:
- HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
- HttpClients:可认为是构建器,可创建HttpClient对象。
- CloseableHttpClient:实现类,实现了HttpClient接口。
- HttpGet:Get方式请求类型。
- HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求
1.1.4 GET方式请求实现
-
创建HttpClient对象
-
创建请求对象
-
发送请求,接受响应结果
-
解析结果
-
关闭资源
编写测试代码:
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.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class HttpClientTest {
/**
* 测试通过httpclient发送GET方式的请求
*/
@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();
}
}
在访问http://localhost:8080/user/shop/status请求时,需要提前启动项目。
1.1.5 POST方式请求实现
在HttpClientTest中添加POST方式请求方法,相比GET请求来说,POST请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。
实现步骤:
- 创建HttpClient对象
- 创建请求对象
- 发送请求,接收响应结果
- 解析响应结果
- 关闭资源
编写测试代码:
//测试通过httpclient发送POST方式的请求
@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);
s
//关闭资源
response.close();
httpClient.close();
}
1.2微信小程序开发
1.21介绍
小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验。
官方网址:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN
首先,在进行小程序开发时,需要先去注册一个小程序,在注册的时候,它实际上又分成了不同的注册的主体。我们可以以个人的身份来注册一个小程序,当然,也可以以企业政府、媒体或者其他组织的方式来注册小程序。那么,不同的主体注册小程序,最终开放的权限也是不一样的。比如以个人身份来注册小程序,是无法开通支付权限的。若要提供支付功能,必须是企业、政府或者其它组织等。所以,不同的主体注册小程序后,可开发的功能是不一样的。
然后,微信小程序我们提供的一些开发的支持,实际上微信的官方是提供了一系列的工具来帮助开发者快速的接入
并且完成小程序的开发,提供了完善的开发文档,并且专门提供了一个开发者工具,还提供了相应的设计指南,同时也提供了一些小程序体验DEMO,可以快速的体验小程序实现的功能。
最后,开发完一个小程序要上线,也给我们提供了详细地接入流程。
1.2.2准备工作
开发微信小程序之前需要做如下准备工作:
- 注册小程序
- 完善小程序信息
- 下载开发者工具
1)注册小程序
注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1
2)完善小程序信息
登录小程序后台:https://mp.weixin.qq.com/
3)查看小程序的 AppID
并且查看密钥(注意保存好)
4)下载开发者工具
下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html
5)熟悉开发者工具布局
设置不校验合法域名
注:开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败。
1.2.3 小程序目录结构
-
小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。主体部分由三个文件组成,必须放在项目的根目录。 文件说明: - app.js: 必须存在,主要存放小程序的逻辑代码 - app.json:必须存在,小程序配置文件,主要存放小程序的公共配置 - app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式 - js文件:必须存在,存放页面业务逻辑代码,编写的js代码。 - json文件:非必须,存放页面相关的配置。 - wxml文件:必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面。 - wxss文件:非必须,存放页面样式表,相当于CSS文件。
1.2.4编写测试用例
1)编写小程序
进入到index.wxml,编写页面布局
<view class="container">
<view>{{msg}}</view>
<view>
<button type="default" bindtap="getUserInfo">获取用户信息</button>
<image style="width: 100px;height: 100px;" src="{{avatarUrl}}"></image>
{{nickName}}
</view>
<view>
<button type="primary" bindtap="wxlogin">微信登录</button>
授权码:{{code}}
</view>
<view>
<button type="warn" bindtap="sendRequest">发送请求</button>
响应结果:{{result}}
</view>
</view>
进入到index.js,编写业务逻辑代码
Page({
data:{
msg:'hello world',
avatarUrl:'',
nickName:'',
code:'',
result:''
},
getUserInfo:function(){
wx.getUserProfile({
desc: '获取用户信息',
success:(res) => {
console.log(res.userInfo)
this.setData({
avatarUrl:res.userInfo.avatarUrl,
nickName:res.userInfo.nickName
})
}
})
},
wxlogin:function(){
wx.login({
success: (res) => {
console.log("授权码:"+res.code)
this.setData({
code:res.code
})
}
})
},
sendRequest:function(){
//通过微信小程序向项目服务器Tomcat发送一个请求
wx.request({
url: 'http://localhost:8080/user/shop/status',
method:'GET',
success:(res) => {
console.log("响应结果:" + res.data.data)
this.setData({
result:res.data.data
})
}
})
}})
点击编译按钮进行运行
为了防止小程序开发者滥用用户昵称和头像,官方停用了接口;如果想看效果需要切换到旧版基础库。
2)发布小程序
小程序的代码都已经开发完毕,要将小程序发布上线,让所有的用户都能使用到这个小程序。
点击上传按钮:
指定版本号:
上传成功:
当前小程序版本只是一个开发版本。
进到微信公众平台,打开版本管理页面。
需提交审核,变成审核版本,审核通过后,进行发布,变成线上版本。
1.2.4 导入小程序代码
导入提供的代码
AppID:使用自己的AppID
因为小程序要请求后端服务,需要修改为自己后端服务的ip地址和端口号(默认不需要修改)
common-->vendor.js-->搜索(ctrl+f)-->baseUri
- 微信登录流程
微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
1. 小程序端,调用wx.login()获取code,就是授权码。
2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
6. 小程序端,收到自定义登录态,存储storage。
7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
说明:
10. 调用https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html 获取 临时登录凭证code ,并回传到开发者服务器。
11. 调用https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 接口,
换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
1.2.5微信登录流程
说明:
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
1.2.6配置微信登录所需配置项
#定义相关配置
配置微信登录所需配置项:
application-dev.yml
在application-dev.yml配置文件中配置具体信息
#sky: 改成自己的小程序id和secret
wechat:
appid: wxffb3637a228223b8
secret: 84311df9199ecacdf4f12d27b6b9522d
在application.yml调用dev。yml中的信息(下同)
这样可以避免输入错误,修改不方便等问题
#sky:
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}
配置为微信用户生成jwt令牌时使用的配置项:
#sky:
#jwt:
# 省略......
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication
用户端拦截器
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
//==========================================
//注意HandlerMethod导包:org.springframework.web.method.HandlerMethod
if (!(handler instanceof org.springframework.web.method.HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户的id:{}", userId);
BaseContext.setCurrentId(userId); //放到ThreadLocal里边
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
在WebMvcConfiguration配置类中注册拦截器
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
//.........
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
1.3订单支付
要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。
微信支付产品:本项目选择小程序支付
参考:https://pay.weixin.qq.com/static/product/product_index.shtml
1.3.1微信小程序支付时序图:
1)微信支付相关接口:
JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)
微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)
2)完成微信支付有两个关键的步骤:
-
第一个 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。
-
第二个 就是支付成功之后微信后台会给推送消息。
解决:微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。
获取微信支付平台证书、商户私钥文件.
3)调用到商户系统
微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。
目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。
解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。
1.3.2内网穿透
1)下载安装
下载地址:https://dashboard.cpolar.com/get-started
2)cpolar指定authtoken
复制authtoken:
https://dashboard.cpolar.com/auth
--->注册登录 --->验证---->复制自己的隧道 Authtoken
3)执行命令:
命令行输入:
cpolar.exe authtoken MmJiMTBiZDAtMz333330ZWI5LTlhOTQtODE1ZjcxNmZhOGRl
4)获取临时域名
执行命令:cpolar.exe http 8080
5)验证临时域名有效性
启动项目,访问接口文档:http://localhost:8080/doc.html 使用临时域名访问,证明临时域名生效
1.3.3微信支付相关配置
application-dev.yml
sky:
wechat:
appid: wxcd2e39f677fd30ba
secret: 84fbfdf5ea288f0c432d829599083637
mchid : 1561414331
mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
privateKeyFilePath: D:\apiclient_key.pem
apiV3Key: CZBK51236435wxpay435434323FFDuv3
weChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
notifyUrl: https://www.weixin.qq.com/wxpay/pay.php
refundNotifyUrl: https://www.weixin.qq.com/wxpay/pay.php
application.yml
sky:
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}
mchid : ${sky.wechat.mchid}
mchSerialNo: ${sky.wechat.mchSerialNo}
privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
apiV3Key: ${sky.wechat.apiV3Key}
weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
notifyUrl: ${sky.wechat.notifyUrl}
refundNotifyUrl: ${sky.wechat.refundNotifyUrl}
注意:privateKeyFilePath和weChatPayCertFilePath两个路径指微信支付平台证书、商户私钥文件的存储路径
由于需要商户注册,所以这两个文件我没有,所以完成小程序支付功能我还没有实现。在后面用户统计模块,我是直接修改的数据库数据。
1.3.4 具体代码
WeChatProperties.java:读取配置
package com.sky.properties;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {
private String appid; //小程序的appid
private String secret; //小程序的秘钥
private String mchid; //商户号
private String mchSerialNo; //商户API证书的证书序列号
private String privateKeyFilePath; //商户私钥文件
private String apiV3Key; //证书解密的密钥
private String weChatPayCertFilePath; //平台证书
private String notifyUrl; //支付成功的回调地址
private String refundNotifyUrl; //退款成功的回调地址
}
Service层代码
在OrderService.java中添加payment和paySuccess两个方法定义
/**
* 订单支付
* @param ordersPaymentDTO
* @return
*/
OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;
/**
* 支付成功,修改订单状态
* @param outTradeNo
*/
void paySuccess(String outTradeNo);
在OrderServiceImpl.java中实现payment和paySuccess两个方法
@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
//调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
return vo;
}
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
}
PayNotifyController.java
/**
* 支付回调相关接口
*/
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
@Autowired
private OrderService orderService;
@Autowired
private WeChatProperties weChatProperties;
/**
* 支付成功回调
*
* @param request
*/
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
//读取数据
String body = readData(request);
log.info("支付成功回调:{}", body);
//数据解密
String plainText = decryptData(body);
log.info("解密后的文本:{}", plainText);
JSONObject jsonObject = JSON.parseObject(plainText);
String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
String transactionId = jsonObject.getString("transaction_id");//微信支付交易号
log.info("商户平台订单号:{}", outTradeNo);
log.info("微信支付交易号:{}", transactionId);
//业务处理,修改订单状态、来单提醒
orderService.paySuccess(outTradeNo);
//给微信响应
responseToWeixin(response);
}
/**
* 读取数据
*
* @param request
* @return
* @throws Exception
*/
private String readData(HttpServletRequest request) throws Exception {
BufferedReader reader = request.getReader();
StringBuilder result = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
}
/**
* 数据解密
*
* @param body
* @return
* @throws Exception
*/
private String decryptData(String body) throws Exception {
JSONObject resultObject = JSON.parseObject(body);
JSONObject resource = resultObject.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String nonce = resource.getString("nonce");
String associatedData = resource.getString("associated_data");
AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//密文解密
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
return plainText;
}
/**
* 给微信响应
* @param response
*/
private void responseToWeixin(HttpServletResponse response) throws Exception{
response.setStatus(200);
HashMap<Object, Object> map = new HashMap<>();
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
}
}
二.定时任务SpringTask
2.1介绍
Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架
作用:定时自动执行某段Java代码
应用场景:
1). 信用卡每月还款提醒
2). 银行贷款每月还款提醒
3). 火车票售票系统处理未支付订单
4). 入职纪念日为用户发送通知
强调:只要是需要定时处理的场景都可以使用Spring Task
2.2cron表达式
-
cron其实就是一个字符串,通过cron表达式可以定义任务触发的时间
-
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义:秒、分钟、小时、日、月、周、年(可选),每部分的含义如下表所示:
组成部分 | 含义 | 取值范围 |
---|---|---|
第一部分 | Seconds (秒) | 0-59 |
第二部分 | Minutes(分) | 0-59 |
第三部分 | Hours(时) | 0-23 |
第四部分 | Day-of-Month(天) | 1-31 |
第五部分 | Month(月) | 0-11或JAN-DEC |
第六部分 | Day-of-Week(星期) | 1-7(1表示星期日)或SUN-SAT |
第七部分 | Year(年) 可选 | 1970-2099 |
1)举例:
2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022
说明:一般日和周的值不同时设置,其中一个设置,另一个用?表示。
比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。
为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。
cron表达式在线生成器:https://cron.qqe2.com/
2)通配符:
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值;
3)cron表达式案例:
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
2.3使用步骤
1). 导入maven坐标 spring-context
2). 启动类添加注解 @EnableScheduling 开启任务调度
3). 自定义定时任务类
编写定时任务类:
进入sky-server模块中
/**
* 自定义定时任务类
*/
@Component
@Slf4j
public class MyTask {
/**
* 定时任务 每隔5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){
log.info("定时任务开始执行:{}",new Date());
}
}
开启任务调度:
启动类添加注解 @EnableScheduling
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
@EnableScheduling
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
三.WebSocket全双工通信
3.1介绍
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
- HTTP是短连接
- WebSocket是长连接
- HTTP通信是单向的,基于请求响应模式
- WebSocket支持双向通信
- HTTP和WebSocket底层都是TCP连接
WebSocket缺点:
服务器长期维护长连接需要一定的成本
各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
WebSocket应用场景:
1). 直播弹幕
2). 网页聊天
3). 体育实况更新
4). 股票基金报价实时更新
3.2入门案例
下面通过一个入门案例来详细了解WebSocket
- 需求:
- 实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。
实现步骤:
1). 直接使用websocket.html页面作为WebSocket客户端
2). 导入WebSocket的maven坐标
3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
5). 导入定时任务类WebSocketTask,定时向客户端推送数据
1). 定义websocket.html页面
<!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">
function abc() {
//code
}
var websocket = null;
//Math.random()返回一个0(包含)到 1(不包含)的浮点数
//toString(32): 转成字符串
//substr(): 从指定位置开始截取字符串
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); //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>
2). 导入maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
3). 定义WebSocket服务端组件
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @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);
}
/**
* 群发
*
* @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();
}
}
}
}
4). 定义配置类,注册WebSocket的服务端组件
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
5). 定义定时任务类,定时向客户端推送数据
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + LocalTime.now());
}
}
四.注册百度地图服务
4.1注册百度地图服务
1.基于百度地图开放平台实现(https://lbsyun.baidu.com/)
2.注册账号--->控制台--->我的应用-->创建应用获取AK(服务端应用)--->调用接口
创建应用时:
类型:选服务端
#IP白名单:0.0.0.0/0
3.相关接口
地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
地理编码服务(又名Geocoder)是一类Web API接口服务;
地理编码服务提供将结构化地址数据(如:北京市海淀区上地十街十号)转换为对应坐标点(经纬度)功能;
GET: https://api.map.baidu.com/geocoding/v3/?address=北京市海淀区上地十街10号&output=json&ak=您的ak
在url中传递3个参数即可,返回数据格式如下:Java中将返回的json字符串转成JSONObject
{ // JSONObject
"status": 0, // jsonObject.getIntValue("status")
"result": { //对象: jsonObject.getJSONObject("result")
"location": { //对象: jsonObject.getJSONObject("location")
"lng": 116.3076223267197, //经度 getString("lng")
"lat": 40.05682848596073 //纬度 getString("lat")
},
"precise": 1,
"confidence": 80,
"comprehension": 100,
"level": "门址"
}
}
路线规划服务:https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
轻量级路线规划服务(又名DirectionLite API )是一套REST风格的Web服务API,以HTTP/HTTPS形式提供了路线规划服务。相较于Direction API,DirectionLite API更注重服务的高性能和接口的轻便简洁,满足基础的路线规划需求,并不具备Direciton API中的驾车多路线/未来出行和公交跨城规划等高级功能。DirectionLite API支持驾车、骑行、步行、公交路线规划,支持中国大陆地区。
传递4个参数即可,返回数据如下:
{
"status": 0, //getIntValue
"message": "ok",
"result": { //对象: getJSONObject
"origin": {
"lng": 116.33929505188,
"lat": 40.011157363344
},
"destination": {
"lng": 116.45255341058,
"lat": 39.936401378723
},
"routes": [ //数组: result.getJSONArray("routes");
{
"distance": 18129, //getString 得到的两地间的距离
"duration": 6193
}
]
}
}
4.2检验配送距离
商家门店地址可以配置在配置文件中,例如:application.yml
#sky:
shop:
address: 北京市海淀区上地十街10号
baidu:
ak: sdfsdfsdfsd #百度应用id
用户下单时添加校验代码:
//两项用途过少没有必要新建一个配置类
@Value("${sky.shop.address}")//将yml文件中的属性赋值到shopAddress中
private String shopAddress;
@Value("${sky.baidu.ak}")
private String ak;
/**
* 检查客户的收货地址是否超出配送范围
* @param address 收货地址
*/
private void checkOutOfRange(String address) {
//1、调用百度地理编码服务根据店铺地址获取经纬度
String shopGeo = getGeoByAddress(shopAddress);
//2、调用地理编码服务服务根据用户配送地址获取经纬度
String userGeo = getGeoByAddress(address);
//3、调用路线规划服务对两个地址进行规划,根据返回的距离判断是否在配送范围
Integer distance = getDistance(shopGeo, userGeo);
if(distance > 5000){
//配送距离超过5000米
throw new OrderBusinessException("超出配送范围: " + distance);
}
}
编写两个私有方法:getGeoByAddress和getDistance,在方法中用HttpClientUtil.doGet()调用百度接口
/**
* 根据详细地址获取经纬度坐标
* 地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/gui
*
* @param address 详细地址(包含省市区)
* @return 经纬度,格式为:纬度,经度;小数点后不超过6位,40.056878,116.30815
*/
private String getGeoByAddress(String address) {
//1、使用map构建请求参数
Map<String, String> map = new HashMap<>();
map.put("address",address); //地址
map.put("output","json");//指定返回数据格式
map.put("ak",ak);
//2、调用百度地图服务:获取经纬度坐标
String json =
HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
log.info("地址: {},返回值:{}", address, json);
//3、解析响应结果
JSONObject jsonObject = JSON.parseObject(json);
//status=0,表示成功
if(jsonObject.getIntValue("status") != 0){
throw new OrderBusinessException("地址解析失败");
}
//数据解析
JSONObject location =
jsonObject.getJSONObject("result").getJSONObject(
String lat = location.getString("lat"); //纬度
String lng = location.getString("lng"); //经度
//返回经纬度坐标
return lat + "," + lng;
}
完善下单功能:
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
//1、下单数据校验(收货地址为空、超出配送范围、购物车为空)
Long userId = BaseContext.getCurrentId();
//1.1 收货地址为空
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//====================================================新增代码
//1.2 超出配送范围
checkOutOfRange(addressBook.getProvinceName() +
addressBook.getCityName() + addressBook.getDetail());
//1.3 购物车为空(userId)
//……省略其他代码
}
五.Apache ECharts
5.1 介绍
Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。
官网地址:https://echarts.apache.org/zh/index.html
常见效果展示:
1). 柱状图-bar
2). 饼图-pie
3). 折线图-line
总结:
不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。
5.2 实现
Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/
实现步骤:
1). 引入echarts.js 文件(当天资料已提供)
2). 为 ECharts 准备一个设置宽高的 DOM
3). 初始化echarts实例
4). 指定图表的配置项和数据
5). 使用指定的配置项和数据显示图表
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
-总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。
5.3具体业务实现 - 营业额统计
该项目数据统计用到Apache ECharts的有
营业额统计功能模块,
用户统计功能模块,
订单统计功能模块,
销量排名Top10功能模块
下面列举营业额统计模块简单分析一下其业务逻辑
5.3.1产品原型
营业额统计是基于折现图来展现,并且按照天来展示的。实际上,就是某一个时间范围之内的每一天的营业额。同时,不管光标放在哪个点上,那么它就会把具体的数值展示出来。并且还需要注意日期并不是固定写死的,是由上边时间选择器来决定。比如选择是近7天、或者是近30日,或者是本周,就会把相应这个时间段之内的每一天日期通过横坐标展示。
原型图:
业务规则:
- 营业额指订单状态为已完成的订单金额合计
- 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额
- 根据时间选择区间,展示每天的营业额数据
5.3.2 接口设计
注意:具体返回数据一般由前端来决定,前端展示图表,具体折现图对应数据是什么格式,是有固定的要求的。
所以说,后端需要去适应前端,它需要什么格式的数据,我们就给它返回什么格式的数据。
5.3.3 代码开发
根据接口定义设计对应的VO:
前端需要的返回数据有两个list集合,dateList日期和turnoverList营业额数据
在sky-pojo模块,创建TurnoverReportVO.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {
//日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
private String dateList;
//营业额,以逗号分隔,例如:406.0,1520.0,75.0
private String turnoverList;
}
管理端页面要求统计一段时间内的营业额数据,传给后端的就是一个开始日期和结束日期
在Controller层创建ReportController.java
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "统计报表相关接口")
public class ReportController {
@Autowired
private ReportService reportService;
/**
* 营业额数据统计
*
* @param begin
* @param end
* @return
*/
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额数据统计")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end) {
return Result.success(reportService.getTurnover(begin, end));
}
}
在Service层接口中声明方法,使用具体的实现类来实现
创建ReportServiceImpl实现类,实现getTurnover方法:
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
/**
* 根据时间区间统计营业额
* @param begin
* @param end
* @return
*/
public TurnoverReportVO getTurnover(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)){
begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期
dateList.add(begin);
}
List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashMap();
map.put("status", Orders.COMPLETED);
map.put("begin",beginTime);
map.put("end", endTime);
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ? 0.0 : turnover;
turnoverList.add(turnover);
}
//数据封装
return TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList,","))
.turnoverList(StringUtils.join(turnoverList,","))
.build();
}
}
在调用Mapper层查询数据时,需要给出具体的时间段,需要知道起始日期的最小时间以及结束日期的最大时间
在这里我们使用java.time包中的LocalTime类来获取,具体实现为:
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
在OrderMapper接口声明sumByMap方法:
/**
* 根据动态条件统计营业额
* @param map
*/
Double sumByMap(Map map);
在OrderMapper.xml文件中编写动态SQL:
<select id="sumByMap" resultType="java.lang.Double">
select sum(amount) from orders
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="begin != null">
and order_time >= #{begin}
</if>
<if test="end != null">
and order_time <= #{end}
</if>
</where>
</select>
因为数据库中的订单数据都是手动添加的,所以数据量有点少,看上去没有很帅气。大家测试时可以多添加一些数据。
六.Apache POI
6.1介绍
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。
Apache POI 的应用场景:
1)银行网银系统导出交易明细
2)各种业务系统导出Excel报表
3)批量导入业务数据
6.2入门案例
1)导入对应的maven坐标
Apache POI的maven坐标:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
2)将数据写入Excel文件
public class POITest {
/**
* 基于POI向Excel文件写入数据
* @throws Exception
*/
public static void write() throws Exception{
//在内存中创建一个Excel文件对象
XSSFWorkbook excel = new XSSFWorkbook();
//创建Sheet页
XSSFSheet sheet = excel.createSheet("itcast");
//在Sheet页中创建行,0表示第1行
XSSFRow row1 = sheet.createRow(0);
//创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格
row1.createCell(1).setCellValue("姓名");
row1.createCell(2).setCellValue("城市");
XSSFRow row2 = sheet.createRow(1);
row2.createCell(1).setCellValue("张三");
row2.createCell(2).setCellValue("北京");
XSSFRow row3 = sheet.createRow(2);
row3.createCell(1).setCellValue("李四");
row3.createCell(2).setCellValue("上海");
FileOutputStream out =
new FileOutputStream(new File("D:\\itcast.xlsx"));
//通过输出流将内存中的Excel文件写入到磁盘上
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();
}
public static void main(String[] args) throws Exception {
write();
}
}
3)读取Excel文件中的数据
/**
* 基于POI读取Excel文件
* @throws Exception
*/
public static void read() throws Exception{
FileInputStream in = new FileInputStream(new File("D:\\itcast.xlsx"));
//通过输入流读取指定的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取Excel文件的第1个Sheet页
XSSFSheet sheet = excel.getSheetAt(0);
//获取Sheet页中的最后一行的行号
int lastRowNum = sheet.getLastRowNum();
for (int i = 0; i <= lastRowNum; i++) {
//获取Sheet页中的行
XSSFRow titleRow = sheet.getRow(i);
//获取行的第2个单元格
XSSFCell cell1 = titleRow.getCell(1);
//获取单元格中的文本内容
String cellValue1 = cell1.getStringCellValue();
//获取行的第3个单元格
XSSFCell cell2 = titleRow.getCell(2);
//获取单元格中的文本内容
String cellValue2 = cell2.getStringCellValue();
System.out.println(cellValue1 + " " +cellValue2);
}
//关闭资源
in.close();
excel.close();
}
public static void main(String[] args) throws Exception {
read();
}
6.3具体的业务代码
导出Excel报表
在ReportServiceImpl实现类中实现导出运营数据报表的方法:
1、将资料中的运营数据报表模板.xlsx拷贝到项目的resources/template目录中
2、停止项目,删除target目录
@Autowired
private WorkspaceService workspaceService;
/**
* 导出近30天的运营数据报表
*
* @param response
**/
public void exportBusinessData(HttpServletResponse response) {
LocalDate begin = LocalDate.now().minusDays(30);
LocalDate end = LocalDate.now().minusDays(1);
//查询概览运营数据,提供给Excel模板文件
BusinessDataVO businessData = workspaceService.getBusinessData(
LocalDateTime.of(begin, LocalTime.MIN),
LocalDateTime.of(end, LocalTime.MAX));
//需要从当前运行路径下获取excel模版文件
InputStream inputStream = this.getClass().getClassLoader()
.getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于提供好的模板文件创建一个新的Excel表格对象
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//获得Excel文件中的一个Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");
//在第2行,第2列设置统计日期:2026-05-01至2026-05-30
sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
//获得第4行
XSSFRow row = sheet.getRow(3);
//在第3列设置:营业额
row.getCell(2).setCellValue(businessData.getTurnover());
//在第5列设置:订单完成率
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
//在第7列设置:新增用户数
row.getCell(6).setCellValue(businessData.getNewUsers());
//获取第5行
row = sheet.getRow(4);
//在第3列设置:有效订单
row.getCell(2).setCellValue(businessData.getValidOrderCount());
//在第5列设置:平均客单价
row.getCell(4).setCellValue(businessData.getUnitPrice());
//写入明细数据(按天显示)
for (int i = 0; i < 30; i++) {
LocalDate date = begin.plusDays(i);
//准备订单明细数据
businessData = workspaceService.getBusinessData(
LocalDateTime.of(date, LocalTime.MIN),
LocalDateTime.of(date, LocalTime.MAX));
//从第8行开始写入
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());
}
//通过输出流将文件下载到客户端浏览器中
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
这个项目所用到的技术以及其初步的使用方法大概就是这些了。
在项目编码过程中,遇到次数最多的问题大概就是导包错误了。总是导入错误的包,从而代码报错。还有好多次忘记添加注解。
在业务逻辑上,有很多解决方法时之前没有学过的,同时之前Java中的一些基础知识也有些忘记了,有空还是需要回顾之前所学的。
同时知识的深度也很重要,之前有些方法只是知道基本的,常见的用法,并没有很详细的去深挖其他用法,也没有很看过其源码等。知识的了解深度不够,这点以后学习需要注意。
通过跟学这次项目,虽然很多部分是老师直接提供的,自己真正完成的部分,现在回顾起来,也就那几部分。但是通过这次学习,仍旧学习到了很多东西。
令我最为震撼的就是作为一名程序员应有的一份严谨。有很多的常量都是事先编写好放在包内以供使用,同时防止在具体使用时程序员拼写错误,又方便修改,避免代码写死。同时看起来非常优雅!优雅!优雅!(重要的事情说三遍)
这次简述了项目用到的技术方法等,一些具体的业务逻辑可能没有说明白,这次先空着,有空再写。
注:在写这篇文章时参考了老师所提供的每日讲义
以及大佬的博客:原文链接:https://blog.csdn.net/DU1149507047/article/details/132511480
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了