一 .【項目設計】
①【角色划分】
買家(手機端) 賣家(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; }
⑩微信接口技術
-
-
- 微信支付
- 微信掃碼登錄
- 微信模板消息推送
-