一 .【项目设计】
①【角色划分】
买家(手机端) 卖家(PC端)
②【功能模块划分】
③【部署架构】
买家端(手机)和卖家端(PC)都会发出请求到Nginx服务器(搭建在虚拟机上,服务器上包括买家端的前端资源),
如果请求的是后端接口,Nginx会转发到Tomcat服务器(java项目所在),如果接口做了缓冲就会访问Redis服务,没有
缓冲则会访问到mysql数据库
④【数据库设计】
1》 表与表之间的关系
2》 建表sql
# 微信点餐数据库 ```sql -- 类目 create table `product_category` ( `category_id` int not null auto_increment, `category_name` varchar(64) not null comment '类目名字', `category_type` int not null comment '类目编号', `create_time` timestamp not null default current_timestamp comment '创建时间', `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间', primary key (`category_id`), UNIQUE KEY `uqe_category_type` (`category_type`) ); -- 商品 create table `product_info` ( `product_id` varchar(32) not null, `product_name` varchar(64) not null comment '商品名称', `product_price` decimal(8,2) not null comment '单价', `product_stock` int not null comment '库存', `product_description` varchar(64) comment '描述', `product_icon` varchar(512) comment '小图', `product_status` tinyint(3) DEFAULT '0' COMMENT '商品状态,0正常1下架', `category_type` int not null comment '类目编号', `create_time` timestamp not null default current_timestamp comment '创建时间', `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间', primary key (`product_id`) ); -- 订单 create table `order_master` ( `order_id` varchar(32) not null, `buyer_name` varchar(32) not null comment '买家名字', `buyer_phone` varchar(32) not null comment '买家电话', `buyer_address` varchar(128) not null comment '买家地址', `buyer_openid` varchar(64) not null comment '买家微信openid', `order_amount` decimal(8,2) not null comment '订单总金额', `order_status` tinyint(3) not null default '0' comment '订单状态, 默认为新下单', `pay_status` tinyint(3) not null default '0' comment '支付状态, 默认未支付', `create_time` timestamp not null default current_timestamp comment '创建时间', `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间', primary key (`order_id`), key `idx_buyer_openid` (`buyer_openid`) ); -- 订单商品 create table `order_detail` ( `detail_id` varchar(32) not null, `order_id` varchar(32) not null, `product_id` varchar(32) not null, `product_name` varchar(64) not null comment '商品名称', `product_price` decimal(8,2) not null comment '当前价格,单位分', `product_quantity` int not null comment '数量', `product_icon` varchar(512) comment '小图', `create_time` timestamp not null default current_timestamp comment '创建时间', `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间', primary key (`detail_id`), key `idx_order_id` (`order_id`) ); -- 卖家(登录后台使用, 卖家登录之后可能直接采用微信扫码登录,不使用账号密码) create table `seller_info` ( `seller_id` varchar(32) not null, `username` varchar(32) not null, `password` varchar(32) not null, `openid` varchar(64) not null comment '微信openid', `create_time` timestamp not null default current_timestamp comment '创建时间', `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间', primary key (`seller_id`) ) comment '卖家信息表'; ```
二 .【开发环境搭建】
①虚拟机centos7.3 ---工具:VirtualBox
jdk 1.8.0_111
nginx 1.11.7
mysql 5.7.17
redis 3.2.8
②IDE:Intellij IDEA 2018.1.3
jdk: 1.8.0_171
maven: 3.3.9
springboot-version: 1.5.21.RELEASE
三 .【开发涉及软件】
Postman 测试接口
Intellij IDEA 代码开发
Oracle VM VirtualBox 使用虚拟机
Fiddler 4 抓包
natapp 内网穿透工具
RedisDesktopManager 查看管理redis数据
四 .【项目目录结构】
五 .【Maven依赖】
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.21.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.imoooc</groupId>
<artifactId>sell</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sell</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>cn.springboot</groupId>
<artifactId>best-pay-sdk</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<finalName>sell</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
六 .【application.yml】
spring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 url: jdbc:mysql://192.168.11.106/sell?characterEncoding=utf-8&useSSL=false jpa: show-sql: true jackson: default-property-inclusion: non_null redis: host: 192.168.11.106 port: 6379 server: context-path: /sell wechat: # 测试公众账号 ,授权 mpAppId: wx9f*******01398a6 mpAppSecret: d9db8b20**********f0a2f0eed4fc53 #师兄openId appid openIdPro: oTgZpw**********4upolmjp7Q1c mpAppIdPro: wxd******01713c658 # 开放平台,卖家扫码登录用 openAppId: wx6ad******af67d87 openAppSecret: 91a2************fb7e9f9079108e2e #支付/商户号 mchId: 1*****9312 mchKey: C52***********964D494B0735025 # 发起支付不需要证书,退款需要 keyPath: /weChatSystem/weixin_cert/h5.p12 notifyUrl : http://lcb666.natapp1.cc/sell/pay/notify templateId: orderStatus: 17d4xlTJDUQ27yERFtS2KDeaZ6OImf_7aqtOMJ9AKe4 projectUrl: wechatMpAuthorize: http://lcb666.natapp1.cc wechatOpenAuthorize: http://sell.springboot.cn sell: http://lcb666.natapp1.cc logging: level: com.imoooc.dataobject.mapper: trace mybatis: mapper-locations: classpath:mapper/*.xml
七 .【开发相关】
①SpringBoot+Hibernate+JPA
https://www.cnblogs.com/slimshady/p/11309017.html
②枚举的使用(自定义异常)
@Getter public class SellException extends RuntimeException { private Integer code; public SellException(ResultEnum resultEnum) { super(resultEnum.getMessage()); this.code = resultEnum.getCode(); } public SellException(Integer code,String message){ super(message); this.code=code; } }
@Getter public enum ResultEnum { SUCCESS(0,"成功"), PARAM_ERROR(1,"参数不正确"), PRODUCT_NOT_EXIST(10,"商品不存在"), PRODUCT_STOCK_ERROR(11,"商品库存不正确"), ORDER_NOT_EXIST(12,"订单不存在"), ORDERDETAIL_NOT_EXIST(13,"订单详情不存在"), ORDER_STATUS_ERROR(14,"订单状态不正确"), ORDER_UPDATE_FAIL(15,"订单更新失败"), ORDER_DETAIL_EMPTY(16,"订单详情为空"), ORDER_PAY_STATUS_ERROR(17,"订单支付状态不正确"), CART_EMPRT(18,"购物车为空"), ORDER_OWENER_ERROR(19,"该订单不属于当前用户"), WECHAT_MP_ERROR(20,"微信公众账号方面错误"), WXPAY_NOTIFY_MONEY_VERIFY_ERROR(21,"微信支付异步通知金额校验不通过"), ODERR_CANCEL_SUCCESS(22,"订单取消成功"), ODERR_FINSH_SUCCESS(23,"订单完结成功"), PRODUCT_STATUS_error(24,"商品状态不正确"), LOGIN_FAIL(25,"登录失败,登录信息不正确"), LOGOUT_SUCCESS(26,"登出成功"), ; private Integer code; private String message; ResultEnum(Integer code, String message) { this.code = code; this.message = message; }
③断言
Assert.assertTrue("查询所有订单表",orderDTOPage.getTotalElements()<0);
Assert.assertNotEquals(0,orderDTOPage.getTotalElements());
④lambda表达式(java8)
//1.查询类目(一次性查询) //传统方法 List<Integer> categoryTypeList=new ArrayList<>(); for (ProductInfo productInfo:productInfoList) { categoryTypeList.add(productInfo.getCategoryType()); } //精简方法(java8 , lambda) List<Integer> categoryTypeList= productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());
⑤Cookies,Redis使用
//2.设置token至redis String token = UUID.randomUUID().toString(); Integer expire = RedisConstant.EXPIRE; redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX,token),openid,expire,TimeUnit.SECONDS); //3.设置token至cookie CookieUtil.set(response,CookieConstant.TOKEN,token,expire);
//2.清除redis redisTemplate.opsForValue().getOperations().delete(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue())); //3.清除cookie CookieUtil.set(response,CookieConstant.TOKEN,null,0);
⑥JavaBean读取配置文件
application.yml
projectUrl: wechatMpAuthorize: http://lcb666.natapp1.cc wechatOpenAuthorize: http://sell.springboot.cn sell: http://lcb666.natapp1.cc
@Component @Data @ConfigurationProperties(prefix = "projectUrl") public class ProjectUrlConfig { /** * 微信公众平台授权url */ public String wechatMpAuthorize; /** * 微信开放平台授权url */ public String wechatOpenAuthorize; /** * 项目url */ public String sell; }
⑦拦截登录异常
@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用
到所有@RequestMapping中,该注解很多时候是应用在全局异常处理中。
通过 @ExceptionHandler(value= xxxException.class)实现对异常的捕获,并获取异常的详细信息和错误码。
@ControllerAdvice public class SellerExceptionHandler { @Autowired private ProjectUrlConfig projectUrlConfig; //拦截登录异常并返回指定界面 @ExceptionHandler(value=SellerAuthorizeException.class) public ModelAndView handlerAuthorizeException(){ return new ModelAndView("redirect:" .concat(projectUrlConfig.getWechatOpenAuthorize()) .concat("/sell/wechat/qrAuthorize") .concat("?returnUrl=") .concat(projectUrlConfig.getSell()) .concat("/sell/seller/login")); } @ExceptionHandler(value=SellException.class) @ResponseBody public ResultVO handlerSellException(SellException e){ return ResultVOUtil.error(e.getCode(),e.getMessage()); } }
⑧Aspect切面 AOP实现身份验证
AOP是spring框架中的一个重要内容,AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的
问题,比如日志,事务,权限等待。 使用@Aspect声明一个切面类。 使用注解完成切点表达式、环绕通知、前置
通知、后置通知的声明。 通过切点表达式,实现对访问请求的拦截,并进行切面中逻辑的处理,这里进行的是登录
校验。
@Aspect @Component @Slf4j public class SellerAuthorizeAspect { @Autowired private StringRedisTemplate redisTemplate; //切入点 @Pointcut("execution(public * com.imoooc.controller.Seller*.*(..))"+ "&& !execution(public * com.imoooc.controller.SellerUserController.*(..))") public void verify(){ };
//前置通知 @Before("verify()") public void doVerify(){ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //查询cookie Cookie cookie = CookieUtil.get(request,CookieConstant.TOKEN); if(cookie == null){ log.warn("【登录校验】Cookie中查不到token"); throw new SellerAuthorizeException(); } //去redis里查询 String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX,cookie.getValue())) ; if(StringUtils.isEmpty(tokenValue)){ log.warn("登录校验】Redis中查不到token"); throw new SellerAuthorizeException(); } } }
⑨HTML5 webSocket 卖家端消息推送
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
前段代码 (list.ftl)
<#--弹窗--> <div class="modal fade" id="myModal" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel"> 提醒 </h4> </div> <div class="modal-body"> 你有新的订单 </div> <div class="modal-footer"> <button onclick="javascript:document.getElementById('notice').pause()" type="button" class="btn btn-default" data-dismiss="modal">关闭</button> <button onclick="location.reload()" type="button" class="btn btn-primary">查看新的订单</button> </div> </div> </div> </div> <#--播放音乐--> <audio id="notice" loop="loop"> <source src="/sell/mp3/song.mp3" type="audio/mpeg" /> </audio> <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script src="https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script> var websocket = null; if('WebSocket' in window){ websocket = new WebSocket('ws://lcb666.natapp1.cc/sell/webSocket'); }else{ alert('该浏览器不支持websocket'); } websocket.onopen = function (event) { console.log('建立连接'); } websocket.onclose = function (event) { console.log('连接关闭'); } websocket.onmessage = function (event) { console.log('收到消息:' + event.data) //弹窗提醒,播放音乐 $('#myModal').modal('show'); document.getElementById('notice').play(); } websocket.onerror = function (event) { alert('websocket通信发生错误!'); } window.onbeforeunload = function () { websocket.close(); } </script>
后端代码
@Component public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
@Component @ServerEndpoint("/webSocket") @Slf4j public class WebSocket { private Session session; private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>(); @OnOpen public void onOpen(Session session){ this.session = session; webSocketSet.add(this); log.info("【websocket消息】 有新的连接,总数:{}",webSocketSet.size()); } @OnClose public void onClose(){ webSocketSet.remove(this); log.info("【websocket消息】 连接断开,总数:{}",webSocketSet.size()); } @OnMessage public void onMessage(String message){ log.info("【websocket消息】 收到客户端发来的消息:{}",message); } public void sendMesage(String message){ for (WebSocket webSocket:webSocketSet){ log.info("【websocket消息】 广播消息,message={}",message); try{ webSocket.session.getBasicRemote().sendText(message); }catch (Exception e){ e.printStackTrace(); } } } }
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; @Autowired private OrderDetailRepository orderDetailRepository; @Autowired private OrdreMasterRepository ordreMasterRepository; @Autowired private PayService payService; @Autowired private PushMessageService pushMessageService; @Autowired private WebSocket webSocket; @Override @Transactional public OrderDTO create(OrderDTO orderDTO) { String orderId=KeyUtil.getUniqueKey(); BigDecimal orderAmount=new BigDecimal(BigInteger.ZERO); //1.查询商品(数量,价格) for(OrderDetail orderDetail:orderDTO.getOrderDetailList()){ ProductInfo productInfo=productService.finOne(orderDetail.getProductId()); if(productInfo==null){ throw new SellException(ResultEnum.PRODUCT_NOT_EXIST); } //2.计算订单总价 orderAmount=productInfo.getProductPrice() .multiply(new BigDecimal(orderDetail.getProductQuantity())) .add(orderAmount); //订单详情入库(OrderDetail) orderDetail.setDetailId(KeyUtil.getUniqueKey()); orderDetail.setOrderId(orderId); BeanUtils.copyProperties(productInfo,orderDetail); orderDetailRepository.save(orderDetail); } //3.写入订单数据库(OrderMaster) OrderMaster orderMaster=new OrderMaster(); orderDTO.setOrderId(orderId); BeanUtils.copyProperties(orderDTO,orderMaster); orderMaster.setOrderAmount(orderAmount); orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode()); orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode()); ordreMasterRepository.save(orderMaster); //4.扣库存 List<CartDTO> cartDTOList=orderDTO.getOrderDetailList().stream().map(e -> new CartDTO(e.getProductId(),e.getProductQuantity())) .collect(Collectors.toList()); productService.decreaseStock(cartDTOList); //发送websocket消息 webSocket.sendMesage(orderDTO.getOrderId()); return orderDTO; }
⑩微信接口技术
-
-
- 微信支付
- 微信扫码登录
- 微信模板消息推送
-