目录
十一、支付
1、内网穿透
1)、简介
2)、使用场景
3)、内网穿透常用软件和安装
2、支付整合
1)、支付宝加密原理
2)、配置支付宝沙箱环境
3)、订单支付与同步通知
4)、订单列表页渲染完成
5)、异步通知内网穿透环境搭建
6)、收单
十二、秒杀服务
1、秒杀服务后台管理系统调整
2、搭建秒杀服务环境
3、定时任务
4、商品上架
1)、秒杀架构思路
2)、存储模型设计
3)、定时上架
4)、获取最近三天的秒杀信息
5)、在redis中保存秒杀场次信息
6)、在redis中保存秒杀商品信息
5、幂等性保证
6、获取当前的秒杀商品
1)、获取当前秒杀商品
2)首页获取并拼装数据
7、商品详情页获取当前商品的秒杀信息
8、秒杀系统设计
9、登录检查
10、秒杀
1)、秒杀接口
2)、创建订单
11、秒杀页面完成
内网穿透功能可以允许我们使用外网的网址来访问主机
正常的外网需要访问我们项目的流程是:
1、买服务器并且有公网固定ID
2、买域名映射到服务器的IP
3、域名需要进行备案和审核
1、开发测试(微信、支付宝)
2、智慧互联
3、远程控制
4、私有云
续断:https://www.zhexi.tech/
第一步:登录
第二步:安装客户端
第三步:安装(一定使用管理员身份安装,否则安装失败)
安装好之后,会网站会感应到我们的主机
第四步:新建隧道
隧道建立好,会给我们生成一个域名
商户私钥
加一个对应的签名,支付宝端会使用商户公钥
对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确支付宝私钥
加一个对应的签名,商户端收到支付成功数据之后也会使用支付宝公钥
延签,成功后才能确认1、导入依赖
com.alipay.sdk
alipay-sdk-java
4.9.28.ALL
抽取支付工具类并进行配置
成功调用该接口后,返回的数据就是支付页面的html,因此后续会使用@ResponseBody
添加“com.atguigu.gulimall.order.config.AlipayTemplate”类,代码如下:
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
//在支付宝创建的应用的id
private String app_id = "2016102600763190";
// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key = "MjXN6Hnj8k2GAriRFt0BS9gjihbl9Rt38VMNbBi3Vt3Cy6TOwANLLJ/DfnYjRqwCG81fkyKlDqdsamdfCiTysCa0gQKBgQDYQ45LSRxAOTyM5NliBmtev0lbpDa7FqXL0UFgBel5VgA1Ysp0+6ex2n73NBHbaVPEXgNMnTdzU3WF9uHF4Gj0mfUzbVMbj/YkkHDOZHBggAjEHCB87IKowq/uAH/++Qes2GipHHCTJlG6yejdxhOsMZXdCRnidNx5yv9+2JI37QKBgQCw0xn7ZeRBIOXxW7xFJw1WecUV7yaL9OWqKRHat3lFtf1Qo/87cLl+KeObvQjjXuUe07UkrS05h6ijWyCFlBo2V7Cdb3qjq4atUwScKfTJONnrF+fwTX0L5QgyQeDX5a4yYp4pLmt6HKh34sI5S/RSWxDm7kpj+/MjCZgp6Xc51g==";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key = "MIIBIjA74UKxt2F8VMIRKrRAAAuIMuawIsl4Ye+G12LK8P1ZLYy7ZJpgZ+Wv5nOs3DdoEazgCERj/ON8lM1KBHZOAV+TkrIcyi7cD1gfv4a1usikrUqm8/qhFvoiUfyHJFv1ymT7C4BI6aHzQ2zcUlSQPGoPl4C11tgnSkm3DlH2JZKgaIMcCOnNH+qctjNh9yIV9zat2qUiXbxmrCTtxAmiI3I+eVsUNwvwIDAQAB";
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url="http://**.natappfree.cc/payed/notify";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
private String return_url="http://order.gulimall.com/memberOrder.html";
// 签名方式
private String sign_type = "RSA2";
// 字符编码格式
private String charset = "utf-8";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
public String pay(PayVo vo) throws AlipayApiException {
//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
//付款金额,必填
String total_amount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
添加“com.atguigu.gulimall.order.vo.PayVo”类,代码如下:
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
添加配置
支付宝相关的设置
alipay.app_id=自己的APPID
修改支付页的支付宝按钮
支付宝
添加“com.atguigu.gulimall.order.web.PayWebController”类,代码如下:
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
@ResponseBody
@GetMapping("/payOrder")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
// PayVo payVo = new PayVo();
// payVo.setBody();//订单备注
// payVo.setOut_trade_no();//订单号
// payVo.setSubject();//订单主题
// payVo.setTotal_amount();//订单金额
PayVo payVo = orderService.getOrderPay(orderSn);
//返回的是一个页面。将此页面直接交给浏览器就行
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return "hello";
}
}
修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
/**
* 获取当前订单地支付信息
* @param orderSn
* @return
*/
PayVo getOrderPay(String orderSn);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public PayVo getOrderPay(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity order = this.getOrderByOrderSn(orderSn);
//支付金额设置为两位小数,否则会报错
BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(bigDecimal.toString());
payVo.setOut_trade_no(order.getOrderSn());
List order_sn = orderItemService.list(new QueryWrapper().eq("order_sn", orderSn));
OrderItemEntity entity = order_sn.get(0);
//订单名称
payVo.setSubject(entity.getSkuName());
//商品描述
payVo.setBody(entity.getSkuAttrsVals());
return payVo;
}
http://order.gulimall.com/payOrder?orderSn=202012051517520571335121191551672321
运行结果
支付宝的响应:
我们可以看出返回的结果是html 。所以我们直接修改这个接口,让他返回是html页面
@ResponseBody
@GetMapping(value = "payOrder", produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
// PayVo payVo = new PayVo();
// payVo.setBody();//订单备注
// payVo.setOut_trade_no();//订单号
// payVo.setSubject();//订单主题
// payVo.setTotal_amount();//订单金额
PayVo payVo = orderService.getOrderPay(orderSn);
//返回的是一个页面。将此页面直接交给浏览器就行
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
修改“com.atguigu.gulimall.order.config.AlipayTemplate”类,代码如下:
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
private String return_url = "http://member.gulimall.com/memberOrder";
gulimall-member
添加thymeleaf模板引擎
org.springframework.boot
spring-boot-starter-thymeleaf
添加订单页的html(orderList.html)
往虚拟机的添加订单页的静态资源(在/mydata/nginx/html/static/目录下,创建member文件夹)
修改静态资源访问路径
做登录拦截添加SpringSession依赖
org.springframework.session
spring-session-data-redis
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
添加配置
spring.session.store-type=redis
spring.redis.host=172.20.10.9
主启动类添加SpringSession自动开启
添加“com.atguigu.gulimall.member.config.GulimallSessionConfig”类,代码如下:
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer
添加登录拦截器“com.atguigu.gulimall.member.interceptor.LoginUserInterceptor”
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean status = matcher.match("/member/**", requestURI);
if (status){
return true;
}
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
把登录拦截器配置到spring里
添加“com.atguigu.gulimall.member.config.MemberWebConfig”类,代码如下:
@Configuration
public class MemberWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
在gulimall-gateway配置路由:
- id: gulimall_member_route
uri: lb://gulimall-member
predicates:
- Host=member.gulimall.com
添加域名(C:\Windows\System32\drivers\etc\hosts)
修改首页我的订单地访问路径gulimall-product (index.html)
我的订单
找到沙箱环境里面有沙箱账号
修改“com.atguigu.gulimall.member.web.MemberWebController”类,代码如下“:
@Controller
public class MemberWebController {
@Autowired
OrderFeignService orderFeignService;
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){
//查出当前登录用户的所有订单列表数据
Map page = new HashMap<>();
page.put("page",pageNum.toString());
//分页查询当前用户的所有订单及对应订单项
R r = orderFeignService.listWithItem(page);
model.addAttribute("orders",r);
return "orderList";
}
}
添加“com.atguigu.gulimall.member.feign.OrderFeignService”类,代码如下:
@FeignClient("gulimall-order")
public interface OrderFeignService {
@PostMapping("/order/order/listWithItem")
public R listWithItem(@RequestBody Map params);
}
因为订单服务做了用户登录的拦截,所以远程调用订单服务需要用户信息,我们给它共享cookies
添加“com.atguigu.gulimall.member.config.GuliFeignConfig”类,代码如下:
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
System.out.println("RequestInterceptor线程..."+Thread.currentThread().getId());
//1、RequestContextHolder拿到刚进来的请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null){
HttpServletRequest request = attributes.getRequest();//老请求
if (request != null){
//同步请求头数据。Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
System.out.println("feign远程之前先执行RequestInterceptor.apply()");
}
}
}
};
}
}
远程服务:gulimall-order
修改“com.atguigu.gulimall.order.controller.OrderController”类,代码如下:
/**
* 分页查询当前登录用户的所有订单
* @param params
* @return
*/
@PostMapping("/listWithItem")
public R listWithItem(@RequestBody Map params){
PageUtils page = orderService.queryPageWithItem(params);
return R.ok().put("page", page);
}
修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
PageUtils queryPageWithItem(Map params);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public PageUtils queryPageWithItem(Map params) {
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
IPage page = this.page(
new Query().getPage(params),
new QueryWrapper().eq("member_id",memberResponseVO.getId()).orderByDesc("id")
);
List order_sn = page.getRecords().stream().map(order -> {
List entities = orderItemService.list(new QueryWrapper().eq("order_sn", order.getOrderSn()));
order.setItemEntities(entities);
return order;
}).collect(Collectors.toList());
page.setRecords(order_sn);
return new PageUtils(page);
}
修改OrderEntity
修改orderList.html
2017-12-09 20:50:10
订单号:[[${order.orderSn}]] 70207298274
谷粒商城
[[${item.skuName}]]
找搭配
x[[${item.skuQuantity}]]
[[${order.receiverName}]]
总额 ¥[[${order.payAmount}]]
在线支付
- 待付款
- 已付款
- 已发货
- 已完成
- 已取消
- 售后中
- 售后完成
- 跟踪
普通快递 运单号:390085324974
-
[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
的快件已签收,感谢您使用韵达快递)签收
-
[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
的快件已签收,感谢您使用韵达快递)签收
-
[北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
-
[北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
- 订单详情
取消订单
催单
success
1)内网穿透设置异步通知地址
将外网映射到本地的order.gulimall.com:80
由于回调的请求头不是order.gulimall.com
,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置
将/payed/notify
异步通知转发至订单服务
设置异步通知的地址
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url = "http://8xlc1ea491.*****.tech/payed/notify";
修改内网穿透
nginx配置访问/payed/notify
异步通知转发至订单服务
配置好之后,重启nginx
http://8xlc1ea491.52http.tech/payed/notify?name=hello访问还是404,查看日志
上面日志显示默认以本地的方式访问所以直接访问静态资源/static/..,我们访问这个域名下的/payed路径,我们要添加这个域名,并把host改成order.gulimall.com服务。不然默认以本地的方式访问
再次重启niginx
修改登录拦截器给他放行
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean status = matcher.match("/order/order/status/**", requestURI);
boolean payed = matcher.match("/payed/**", requestURI);
if (status || payed)
return true;
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
2)验证签名
修改“com.atguigu.gulimall.order.listener.OrderPayedListener”类,代码如下:
@RestController
public class OrderPayedListener {
@Autowired
OrderService orderService;
@Autowired
AlipayTemplate alipayTemplate;
/**
* 支付宝成功异步通知
* @param request
* @return
*/
@PostMapping("/payed/notify")
public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException {
System.out.println("收到支付宝异步通知******************");
// 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
// 获取支付宝POST过来反馈信息
//TODO 需要验签
Map params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
if (signVerified){
System.out.println("支付宝异步通知验签成功");
//修改订单状态
orderService.handlePayResult(vo);
return "success";
}else {
System.out.println("支付宝异步通知验签失败");
return "error";
}
}
}
添加“com.atguigu.gulimall.order.vo.PayAsyncVo”类,代码如下:
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
private Date notify_time;
private String subject;
private String sign;
private String buyer_id;//支付者的id
private String body;//订单的信息
private String invoice_amount;//支付金额
private String version;
private String notify_id;//通知id
private String fund_bill_list;
private String notify_type;//通知类型; trade_status_sync
private String out_trade_no;//订单号
private String total_amount;//支付的总额
private String trade_status;//交易状态 TRADE_SUCCESS
private String trade_no;//流水号
private String auth_app_id;//
private String receipt_amount;//商家收到的款
private String point_amount;//
private String app_id;//应用id
private String buyer_pay_amount;//最终支付的金额
private String sign_type;//签名类型
private String seller_id;//商家的id
}
修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
String handlePayResult(PayAsyncVo vo);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
/**
* 处理支付成功返回结果
* @param vo
* @return
*/
@Override
public String handlePayResult(PayAsyncVo vo) {
//1、保存交易流水
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
infoEntity.setAlipayTradeNo(vo.getTrade_no());
infoEntity.setAlipayTradeNo(vo.getOut_trade_no());
infoEntity.setPaymentStatus(vo.getTrade_status());
infoEntity.setCallbackTime(vo.getNotify_time());
paymentInfoService.save(infoEntity);
//2、修改订单状态信息
if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")){
//支付成功状态
String outTradeNo = vo.getOut_trade_no();
this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
}
return "success";
}
修改“com.atguigu.gulimall.order.dao.OrderDao”类,代码如下:
@Mapper
public interface OrderDao extends BaseMapper {
void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}
OrderDao.xml
update oms_order set status = #{code} where order_sn = #{outTradeNo}
#springMVC的日期格式化
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
添加超时时间
配置网关
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?.*),/$\{segment}
修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”代码如下:
@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map params) {
QueryWrapper queryWrapper = new QueryWrapper();
//场次id不是null
String promotionSessionId = (String) params.get("promotionSessionId");
if (!StringUtils.isEmpty(promotionSessionId)){
queryWrapper.eq("promotion_session_id",promotionSessionId);
}
IPage page = this.page(
new Query().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
搭建秒杀服务环境
1)、导入pom.xml依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
com.atguigu.gulimall
gulimall-seckill
0.0.1-SNAPSHOT
gulimall-seckill
秒杀
1.8
Hoxton.SR8
org.redisson
redisson
3.13.4
com.auguigu.gulimall
gulimall-commom
0.0.1-SNAPSHOT
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
2)、添加配置
spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=172.20.10.9
3)、主启动类添加注解
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
定时任务
1、@EnableScheduling 开启定时任务
2、@Scheduled 开启一个定时任务
3、自动配置类 TaskSchedulingAutoConfiguration
异步任务
1、@EnableAsync 开启异步任务功能
2、 @Async 给希望异步执行的方法上标注
3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
* 1、Spring中6位组成,不允许7位d的年
* 2、周的位置,1-7代表周一到周日
* 3、定时任务不应该阻塞。默认是阻塞的
* 1)、可以让业务运行以异步的方式,自己提交到线程池
* 2)、支持定时任务线程池;设置TaskSchedulingProperties;
* spring.task.scheduling.pool.size=5
* 3)、让定时任务异步执行
* 异步任务
*
* 解决:使用异步任务来完成定时任务不阻塞的功能
*/
@Async
@Scheduled(cron = "*/5 * * * * ?")
public void hello() throws InterruptedException {
log.info("hello......");
Thread.sleep(3000);
}
}
配置定时任务参数
spring.task.execution.pool.core-size=20
spring.task.execution.pool.max-size=50
gulimall-seckill
redisson信号量
的形式存储在redis中添加“com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo”类,代码如下:
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @Description: SeckillSkuRedisTo
* @Author: WangTianShun
* @Date: 2020/12/13 10:27
* @Version 1.0
*/
@Data
public class SeckillSessionWithSkus {
private Long id;
/**
* 场次名称
*/
private String name;
/**
* 每日开始时间
*/
private Date startTime;
/**
* 每日结束时间
*/
private Date endTime;
/**
* 启用状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
private List relationEntities;
}
添加“com.atguigu.gulimall.seckill.vo.SkuInfoVo”类,代码如下:
package com.atguigu.gulimall.seckill.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* @Description: SkuInfoVo
* @Author: WangTianShun
* @Date: 2020/12/13 10:59
* @Version 1.0
*/
@Data
public class SkuInfoVo {
private Long skuId;
/**
* spuId
*/
private Long spuId;
/**
* sku名称
*/
private String skuName;
/**
* sku介绍描述
*/
private String skuDesc;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
* 默认图片
*/
private String skuDefaultImg;
/**
* 标题
*/
private String skuTitle;
/**
* 副标题
*/
private String skuSubtitle;
/**
* 价格
*/
private BigDecimal price;
/**
* 销量
*/
private Long saleCount;
}
添加“com.atguigu.gulimall.seckill.vo.SeckillSkuVo”类,代码如下:
package com.atguigu.gulimall.seckill.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* @Description: SeckillSkuVo
* @Author: WangTianShun
* @Date: 2020/12/13 10:30
* @Version 1.0
*/
@Data
public class SeckillSkuVo {
private Long id;
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private Integer seckillCount;
/**
* 每人限购数量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
}
添加代码如下:
package com.atguigu.gulimall.seckill.vo;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @Description: SeckillSkuRedisTo
* @Author: WangTianShun
* @Date: 2020/12/13 10:27
* @Version 1.0
*/
@Data
public class SeckillSessionWithSkus {
private Long id;
/**
* 场次名称
*/
private String name;
/**
* 每日开始时间
*/
private Date startTime;
/**
* 每日结束时间
*/
private Date endTime;
/**
* 启用状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
private List relationEntities;
}
配置定时任务 添加“com.atguigu.gulimall.seckill.config.ScheduledConfig”类,代码如下
@EnableAsync // 开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling // 开启对定时任务的支持
@Configuration
public class ScheduledConfig {
}
coupon
服务上架最近三天的秒杀商品添加“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
//TODO 幂等性处理
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days(){
//重复上架无需处理
log.info("上架秒杀的信息......");
seckillService.uploadSeckillSkuLatest3Days();
}
}
添加“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下
public interface SeckillService {
void uploadSeckillSkuLatest3Days();
}
添加“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ProductFeignService productFeignService;
@Autowired
RedissonClient redissonClient;
private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
// 1、扫描最近三天需要参与秒杀的活动
R session = couponFeignService.getLasts3DaySession();
if (session.getCode() == 0){
// 上架商品
List data = session.getData(new TypeReference>() {
});
// 缓存到redis
// 1、缓存活动信息
saveSessionInfos(data);
// 2、缓存获得关联商品信息
saveSessionSkuInfos(data);
}
}
}
添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/lasts3DaySession")
R getLasts3DaySession();
}
添加“com.atguigu.gulimall.coupon.controller.SeckillSessionController”类,代码如下:
@GetMapping("/lasts3DaySession")
public R getLasts3DaySession(){
List session = seckillSessionService.getLasts3DaySession();
return R.ok().setData(session);
}
添加“com.atguigu.gulimall.coupon.service.SeckillSessionService”类,代码如下:
List getLasts3DaySession();
添加“com.atguigu.gulimall.coupon.service.impl.SeckillSessionServiceImpl”类,代码如下:
@Override
public List getLasts3DaySession() {
//计算最近三天
LocalDate now = LocalDate.now();
LocalDate plus = now.plusDays(3);
List list = this.list(new QueryWrapper().between("start_time", startTime(), endTime()));
if (null != list && list.size() >0){
List collect = list.stream().map(session -> {
Long id = session.getId();
List relationEntities = seckillSkuRelationService.list(new QueryWrapper().eq("promotion_session_id", id));
session.setRelationEntities(relationEntities);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
/**
* 起始时间
* @return
*/
private String startTime(){
LocalDate now = LocalDate.now();
LocalTime time = LocalTime.MIN;
LocalDateTime start = LocalDateTime.of(now, time);
String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
/**
* 结束时间
* @return
*/
private String endTime(){
LocalDate now = LocalDate.now();
LocalDate localDate = now.plusDays(2);
LocalTime time = LocalTime.MIN;
LocalDateTime end = LocalDateTime.of(localDate, time);
String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”类,代码如下:
@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map params) {
QueryWrapper queryWrapper = new QueryWrapper();
//场次id不是null
String promotionSessionId = (String) params.get("promotionSessionId");
if (!StringUtils.isEmpty(promotionSessionId)){
queryWrapper.eq("promotion_session_id",promotionSessionId);
}
IPage page = this.page(
new Query().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
private void saveSessionInfos(List sessions){
if (!CollectionUtils.isEmpty(sessions)){
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey){
List collect = session.getRelationEntities()
.stream()
.map(item -> item.getPromotionId().toString() +"_"+ item.getSkuId().toString())
.collect(Collectors.toList());
// 缓存活动信息
redisTemplate.opsForList().leftPushAll(key, collect);
}
});
}
}
private void saveSessionSkuInfos(List sessions){
// 准备hash操作
BoundHashOperations ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
sessions.stream().forEach(session -> {
session.getRelationEntities().stream().forEach(seckillSkuVo -> {
// 缓存商品
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
// 1、sku的基本信息
R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (0 == r.getCode()){
SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference() {
});
redisTo.setSkuInfo(skuInfo);
}
// 2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3、设置当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 4、随机码
String token = UUID.randomUUID().toString().replace("_", "");
redisTo.setRandomCode(token);
// 5、使用库存作为分布式信号量 限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getSkuId(), jsonString);
});
});
}
添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}
分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态
修改“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,打码如下
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
// TODO 幂等性处理
@Scheduled(cron = "*/3 0 0 * * ?")
public void uploadSeckillSkuLatest3Days(){
// 重复上架无需处理
log.info("上架秒杀的信息......");
// 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try{
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
}
修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下
private void saveSessionInfos(List sessions){
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey){
List collect = session.getRelationEntities().stream().map(item -> item.getPromotionId().toString() +"_"+ item.getSkuId().toString()).collect(Collectors.toList());
// 缓存活动信息
redisTemplate.opsForList().leftPushAll(key, collect);
}
});
}
private void saveSessionSkuInfos(List sessions){
// 准备hash操作
BoundHashOperations ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
sessions.stream().forEach(session -> {
// 随机码
String token = UUID.randomUUID().toString().replace("_", "");
session.getRelationEntities().stream().forEach(seckillSkuVo -> {
if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())){
// 缓存商品
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
// 1、sku的基本信息
R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (0 == r.getCode()){
SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference() {
});
redisTo.setSkuInfo(skuInfo);
}
// 2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3、设置当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 4、随机码
redisTo.setRandomCode(token);
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
// 如果当前这个场次的商品的库存信息已经上架就不需要上架
// 5、使用库存作为分布式信号量 限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与的秒杀商品信息
* @return
*/
@GetMapping(value = "/getCurrentSeckillSkus")
public R getCurrentSeckillSkus() {
//获取到当前可以参加秒杀商品的信息
List vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}
添加“com.atguigu.gulimall.seckill.service.SeckillService”类:代码如下:
/**
* 返回当前时间可以参与的秒杀商品信息
* @return
*/
List getCurrentSeckillSkus();
修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:
@Override
public List getCurrentSeckillSkus() {
Set keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
long currentTime = System.currentTimeMillis();
for (String key : keys) {
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] split = replace.split("_");
long startTime = Long.parseLong(split[0]);
long endTime = Long.parseLong(split[1]);
// 当前秒杀活动处于有效期内
if (currentTime > startTime && currentTime < endTime) {
// 获取这个秒杀场次的所有商品信息
List range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
assert range != null;
List strings = hashOps.multiGet(range);
if (!CollectionUtils.isEmpty(strings)) {
return strings.stream().map(item -> JSON.parseObject(item, SeckillSkuRedisTo.class))
.collect(Collectors.toList());
}
break;
}
}
return null;
}
配置网关
- id: gulimall_seckill_route
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.com
配置域名