導(dǎo)語: 現(xiàn)在可以開始項目實(shí)戰(zhàn)開發(fā)了,關(guān)于項目搭建就是普通的springboot項目搭建,數(shù)據(jù)庫需要注意的問題就是字段屬性要設(shè)置正確,比如金額就要用BigDecimal而字段值就要用32、64、128的倍數(shù)設(shè)置更好。 項目目錄: 還是按照傳統(tǒng)的三層結(jié)構(gòu):Controller、Service、Dao(repository)編寫代碼,我們這里按照從底往上的順序進(jìn)行編寫并以O(shè)rder類的操作為例,首先開發(fā)Dao層 Dao層 我們這里采用的是JPA的方式操作數(shù)據(jù)庫 /** * 查找訂單詳情 * Created by KHM * 2017/7/27 9:59 */public interface OrderDetailRepository extends JpaRepository{ List findByOrderId(String orderId);}1 2 3 4 5 6 7 8 9 10 Service層接口和Impl /** * 訂單service層 * Created by KHM * 2017/7/27 11:04 */public interface OrderService { //創(chuàng)建訂單 OrderDTO create(OrderDTO orderDTO); //查詢單個訂單詳情 OrderDTO findOne(String orderId); //查詢訂單總列表(買家用) Page findList(String buyerOpenid, Pageable pageable); //取消訂單 OrderDTO cancel(OrderDTO orderDTO); //完結(jié)訂單 OrderDTO finish(OrderDTO orderDTO); //支付訂單 OrderDTO paid(OrderDTO orderDTO); //查詢訂單列表(賣家管理系統(tǒng)用的) Page findList(Pageable pageable);}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * Created by KHM * 2017/7/27 11:28 */@Service@Slf4jpublic class OrderServiceImpl implements OrderService { @Autowired private ProductService productService; @Autowired private OrderDetailRepository orderDetailRepository; @Autowired private OrderMasterRepository orderMasterRepository; @Autowired private PayService payService; @Autowired private WebSocket webSocket; @Override @Transactional//事務(wù)管理,一旦失敗就回滾 public OrderDTO create(OrderDTO orderDTO) { //設(shè)置下訂單id(是個隨機(jī),這里調(diào)用了根據(jù)時間產(chǎn)生6位隨機(jī)數(shù)的方法) String orderId = KeyUtil.genUniqueKey(); //給總價賦值 BigDecimal orderAmount = new BigDecimal(BigInteger.ZERO); //List cartDTOList = new ArrayList<>(); //1.查詢商品(數(shù)量,價格) for (OrderDetail orderDetail : orderDTO.getOrderDetailList()){ ProductInfo productInfo = productService.findOne(orderDetail.getProductId()); if(productInfo == null){ throw new SellException(ResultEnum.PRODUCT_NOT_EXIST); } //2.計算總價=單價*數(shù)量 orderAmount orderAmount = productInfo.getProductPrice() .multiply(new BigDecimal(orderDetail.getProductQuantity())) .add(orderAmount); //3.訂單詳情入庫(OrderMaster和orderDetail) //利用BeanUtils方法把前端查找出來的productInfo商品信息復(fù)制給訂單詳情 BeanUtils.copyProperties(productInfo, orderDetail);//先復(fù)制,再賦值 orderDetail.setDetailId(KeyUtil.genUniqueKey()); orderDetail.setOrderId(orderId); orderDetailRepository.save(orderDetail); /* CartDTO cartDTO = new CartDTO(orderDetail.getProductId(), orderDetail.getProductQuantity()); cartDTOList.add(cartDTO);*/ } //3.訂單總表入庫(OrderMaster和orderDetail) OrderMaster orderMaster = new OrderMaster(); orderDTO.setOrderId(orderId); BeanUtils.copyProperties(orderDTO, orderMaster); orderMaster.setOrderAmount(orderAmount);//是一個整個訂單的總價,所以在foe循環(huán)之外設(shè)置 orderMaster.setOrderStatus(OrderStatusEnum.New.getCode()); orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode()); orderMasterRepository.save(orderMaster); //4.扣庫存 List cartDTOList = orderDTO.getOrderDetailList().stream().map(e -> new CartDTO(e.getProductId(), e.getProductQuantity()) ).collect(Collectors.toList()); productService.decreaseStock(cartDTOList); //發(fā)送websocket消息 webSocket.sendMessage(orderDTO.getOrderId()); return orderDTO; }1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 這里需要注意的問題主要是Impl層邏輯的編寫,我們需要用到的注解有: @Service @Slf4j日志需要 @Transactional//事務(wù)管理,一旦失敗就回滾(主要用在要對數(shù)據(jù)庫進(jìn)行操作的方法上) 然后是一些技巧:善于使用工具類做開發(fā),當(dāng)需要例如隨機(jī)數(shù)或者特殊字段的時候最好聲明在方法開始,方便之后的調(diào)用 多多關(guān)注java的新特性,對代碼的優(yōu)化很有幫助 最重要的,寫一個方法前先對這個方法的邏輯做個列舉,第一步是什么、第二步是什么,再去編寫具體的代碼 Controller層 /** * 購買操作 * Created by KHM * 2017/7/30 16:48 */@RestController@RequestMapping('/buyer/order')@Slf4jpublic class BuyerOrderController { @Autowired private OrderService orderService; @Autowired private BuyerService buyerService; //創(chuàng)建訂單 @PostMapping(value = '/create') public ResultVO> creat(@Valid OrderForm orderForm, BindingResult bindingResult){ if(bindingResult.hasErrors()){ log.error('【創(chuàng)建訂單】 參數(shù)不正確, orderForm={}', orderForm); throw new SellException(ResultEnum.PARAM_ERROR.getCode(), bindingResult.getFieldError().getDefaultMessage()); } OrderDTO orderDTO = OrderFormZOrderDTOConverter.convert(orderForm); if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){ log.error('【創(chuàng)建訂單】 購物車不能為空'); throw new SellException(ResultEnum.CART_EMPTY); } OrderDTO createResult = orderService.create(orderDTO); Map map = new HashMap<>(); map.put('orderId', createResult.getOrderId()); return ResultVOUtil.success(map); } //訂單列表 @GetMapping(value = '/list') public ResultVO> list(@RequestParam('openid') String openid, @RequestParam(value = 'page', defaultValue = '0') Integer page, @RequestParam(value = 'size', defaultValue = '10') Integer size){ if(StringUtils.isNullOrEmpty(openid)){ log.error('【查詢訂單列表】 openid為空'); throw new SellException(ResultEnum.PARAM_ERROR); } PageRequest request = new PageRequest(page, size); Page orderDTOPage = orderService.findList(openid, request); //只用返回當(dāng)前頁面的數(shù)據(jù)集合就行了,因為前端傳過來的就是第幾頁和每一頁的size(一般都會定好) return ResultVOUtil.success(orderDTOPage.getContent()); } //訂單詳情 @GetMapping('/detail') public ResultVO detail(@RequestParam('openid') String openid, @RequestParam('orderId') String orderId){ /* //TODO 不安全的做法,改進(jìn) OrderDTO orderDTO = orderService.findOne(orderId);*/ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtil.success(orderDTO); } //取消訂單 @PostMapping('/cancel') public ResultVO cancel(@RequestParam('openid') String openid, @RequestParam('orderId') String orderId){ /* //TODO 不安全的做法,改進(jìn) OrderDTO orderDTO = orderService.findOne(orderId); orderService.cancle(orderDTO);*/ buyerService.cancelOrder(openid, orderId); return ResultVOUtil.success(); }}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 Controller需要注意的問題是: 請求方式一定要寫清楚Post和Get不要弄錯了 需要前端做表單驗證的時候就要用@Valid 前端表單類 做好幾乎所有的出現(xiàn)null或者錯誤的if判斷去拋出異常 返回要用包裝類VO去返回 關(guān)于Exception、幾種包裝類、工具類的介紹 一般情況下有兩種注冊異常類的方法: /** * Created by KHM * 2017/7/27 17:34 */@Getterpublic class SellException extends RuntimeException { private Integer code; public SellException(ResultEnum resultEnum) { //把枚舉中自己定義的message傳到父類的構(gòu)造方法里,相當(dāng)于覆蓋message super(resultEnum.getMessage()); this.code = resultEnum.getCode(); } //而這個是需要自己去填寫code的新的meg,不一定是枚舉中的模糊的說法,可以把具體的錯誤信息信使出來 public SellException(Integer code, String message) { super(message); this.code = code; }}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 善用工具類,反正只要是能單獨(dú)拆分出來使代碼看上去更優(yōu)雅的代碼都可以單獨(dú)寫出來當(dāng)做工具類 包裝類:但凡是需要把原dataobject(數(shù)據(jù)庫對應(yīng)的實(shí)體類)組合或者拆分的新類我們都用包裝類來代替,可大致分為以下幾種包裝類: 返回給前端的VO對象,主要按照前端API開發(fā) /** * 需要返回的商品詳情 * Created by KHM * 2017/7/26 17:54 */@Datapublic class ProductInfoVO implements Serializable { private static final long serialVersionUID = -3013889380494680036L; //為了防止多個name造成混淆,所以要細(xì)起名,但為了和返回對象名一致,所以用這個注解 //其實(shí)也不是造成混淆,主要原因還是為了和原productId對象中屬性名一致并且為了和前端API一致,才要在這里起別名,讓他在返回時實(shí)例化成別的名字 @JsonProperty('id') private String productId; @JsonProperty('name') private String productName; @JsonProperty('price') private BigDecimal productPrice; @JsonProperty('description') private String productDescription; @JsonProperty('icon') private String productIcon;}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 前端和后端都包裝的DTO對象,主要也是按照前端API開發(fā),但是這個和VO的區(qū)別是DTO包含了整個原始對象,而VO縮減了一些屬性 /** * DTO類用來關(guān)聯(lián)dataobject中有聯(lián)系的類,比如創(chuàng)建訂單就需要訂單總表和訂單詳情表兩種數(shù)據(jù), * 所以就需要一種包含了這兩種實(shí)體的包裝類把他們聯(lián)系起來 * 因為用dataobject來關(guān)聯(lián)的話會破壞映射的數(shù)據(jù)庫的關(guān)系 * Created by KHM * 2017/7/27 11:10 */@Data//@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)//@JsonInclude(JsonInclude.Include.NON_NULL)//可以讓為null屬性不返回public class OrderDTO { //訂單id //@Id,不需要此注解了,因為這不是關(guān)聯(lián)數(shù)據(jù)庫的類 private String orderId; //買家姓名 private String buyerName; //買家手機(jī)號 private String buyerPhone; //買家地址 private String buyerAddress; //買家微信openid private String buyerOpenid; //訂單總金額 private BigDecimal orderAmount; //訂單狀態(tài),默認(rèn)為0新下單 private Integer orderStatus; //支付狀態(tài),默認(rèn)為0未支付 private Integer payStatus; //創(chuàng)建時間 @JsonSerialize(using = Date2LongSerializer.class) private Date createTime; //更新時間 @JsonSerialize(using = Date2LongSerializer.class) private Date updateTime; //@Transient//為了方便關(guān)聯(lián)訂單總表和詳情表,把此字段加在這.用此注解就可以讓程序在與數(shù)據(jù)庫關(guān)聯(lián)時忽略此字段,但是更規(guī)范的寫法就是創(chuàng)建新的DTO private List orderDetailList; //= new ArrayList<>();(配置中配置了如果為null就不返回) @JsonIgnore//在返回json的時候回忽略這個屬性 public OrderStatusEnum getOrderStatusEnum() { return EnumUtil.getByCode(orderStatus, OrderStatusEnum.class); } @JsonIgnore public PayStatusEnum getPayStatusEnum() { return EnumUtil.getByCode(payStatus, PayStatusEnum.class); }#配置了這個就不會返回為NULL的參數(shù) jackson: default-property-inclusion: non_null}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 總結(jié):這些常規(guī)的后臺開發(fā)最主要要的問題在于邏輯,開發(fā)每一個方法前要先想清楚這個方法的步奏,怎樣寫最方便,什么時候該用到工具類,返回時要不要用包裝對象,時刻在可能出現(xiàn)錯誤的地方打上日志和拋出異常,單元測試一定要貫穿三層,Dao和Service層用Slf4j,Controller用Postman。這樣才能保證開發(fā)不會出問題找不到。 微信支付篇:http://blog.csdn.net/qq_31783173/article/details/77618374 關(guān)于FreeMarker ibootstrap ModelAndView 假如你是個后端人員又需要自己寫后臺管理界面的時候,這套組合可謂是快速開發(fā)的神器了,用ibootstrap完成html代碼的實(shí)現(xiàn),放在FreeMarker中,不用AJAX即可完成數(shù)據(jù)的交互,詳情請查看FreeMarker:? 用分布式Session完成用戶信息的登錄判斷 什么是分布式系統(tǒng): 旨在支持應(yīng)用程序和服務(wù)的開發(fā),可以利用物理架構(gòu)由多個自治的處理元素,不共享主內(nèi)存,但通過網(wǎng)絡(luò)發(fā)送消息合作。 --Leslie Lamport 三個特點(diǎn)和三個概念 Session 廣義的session:會話控制,不是普通的Http的Session 可以理解為一種Key-value的機(jī)制 它的關(guān)鍵點(diǎn)在于怎么設(shè)置Key和獲取對應(yīng)的value 第一種:SessionId客戶端在請求服務(wù)端的時候,服務(wù)端會在Http的Header里面設(shè)置key和value,而客戶端的cookie會把這個保存,后續(xù)的請求里面會自動的帶上 第二種:token,我們需要手動在Http的Header或Url里設(shè)置token這個字段,服務(wù)器獲得請求后在從Url或者Header里取出token進(jìn)行驗證,安全要求比較嚴(yán)格的時候需要配合簽名一起使用 共同點(diǎn):區(qū)局唯一 分布式系統(tǒng)中的session問題 當(dāng)我們使用分布式系統(tǒng)運(yùn)行時,會有多臺服務(wù)器,怎么放呢?有兩種方式 水平擴(kuò)展:就是在多臺服務(wù)器上部署一樣的程序,就是集群 垂直擴(kuò)展:其實(shí)就是拆分服務(wù),不同Url負(fù)載均衡到不同的服務(wù)器上去 然后,當(dāng)用戶進(jìn)行登錄時,第一次可能在A服務(wù)器上,第二次可能就跑到B服務(wù)器上了,B服務(wù)器沒有用戶的Session,就以為沒有登錄,IPHash的解決方案還是優(yōu)缺點(diǎn)不適用,真正的解決方案是什么呢? 加一臺服務(wù)器裝上Redis來專門保存用戶的session信息,當(dāng)其他服務(wù)器需要session信息的時候都去找他要 我們知道常規(guī)的登錄、登錄就是驗證信息,存儲瀏覽狀態(tài)和讓瀏覽狀態(tài)失效,我們這里使用的第二種:token的方式,自己設(shè)置一個token字段,然后手動添加到cookie中,還有失效時間;登出的時候先清除redis的token,之后我們在訪問其他頁面的時候就可以通過cookie和redis的驗證了,但這里似乎沒有做關(guān)閉瀏覽器清除session的設(shè)置,具體代碼實(shí)現(xiàn): /** * 賣家用戶登錄管理 * Created by Akk_Mac * Date: 2017/8/30 上午9:31 */@Controller@RequestMapping('/seller')public class SellerUserController { @Autowired private SellerService sellerService; //redis的service,這里主要用stringredis @Autowired private StringRedisTemplate redisTemplate; @Autowired private ProjectUrlConfig projectUrlConfig; @RequestMapping('/login') public ModelAndView login(@RequestParam(value = 'username', required=false ) String username, @RequestParam(value = 'password', required = false) String password, HttpServletResponse response, Map map) { //1. 由于我們這里沒有申請微信開放平臺,所以就不用掃碼登錄了 if(username == null && password == null){ return new ModelAndView('common/login'); } SellerInfo sellerInfo = sellerService.findSellerInfoByUsername(username); if(sellerInfo == null && !sellerInfo.getPassword().equals(password)) { map.put('msg', ResultEnum.LGOIN_FAIL.getMessage()); //map.put('url', '/sell/seller/order/list'); return new ModelAndView('common/login', map); } //2. 設(shè)置token至redis(用什么UUID設(shè)置) String token = UUID.randomUUID().toString(); Integer expire = RedisConstant.EXPIRE;//token過期時間 //(key:token_ 為開頭的格式String.format是格式設(shè)置方法, value=這里先設(shè)置為username, 過期時間, 時間單位) redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), username, expire, TimeUnit.SECONDS); //3. 設(shè)置token至cookie CookieUtil.set(response, CookieConstant.TOKEN, token, expire); //這里不是跳轉(zhuǎn)到模板而是地址所以要用redirect,而且跳轉(zhuǎn)最好用絕對地址 return new ModelAndView('redirect:' projectUrlConfig.getSell() '/sell/seller/order/list'); } @RequestMapping('/logout') public ModelAndView logout(HttpServletRequest request, HttpServletResponse response, Map map) { //1. 從cookie中查詢 Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN); if(cookie != null) { //2. 清除redis redisTemplate.opsForValue().getOperations().delete(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue())); //3. 清除cookie CookieUtil.set(response, CookieConstant.TOKEN, null, 0); } map.put('msg', ResultEnum.LOGOUT_SUCCESS.getMessage()); map.put('url', '/sell/seller/login'); return new ModelAndView('common/success', map); }}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 AOP切面編程 /** * AOP切面編程驗證登錄 * Created by Akk_Mac * Date: 2017/8/30 上午11:05 */@Aspect@Component@Slf4jpublic class SellerAuthorizeAspect { @Autowired private StringRedisTemplate redisTemplate; //攔截除了登錄登出之外的操作,這是設(shè)置攔截范圍 @Pointcut('execution(public * com.akk.controller.Seller*.*(..))' '&& !execution(public * com.akk.controller.SellerUserController.*(..))') public void verify(){} @Before('verify()') public void doVerify() { ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //1. 查詢Cookie Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN); if(cookie == null) { log.warn('【登錄校驗】Cookie中查不到token'); throw new SellerAuthorizeException(); } //2. 根據(jù)cookie查redis String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue())); if(StringUtils.isEmpty(tokenValue)) { log.warn('【登錄校驗】Redis中查不到token'); throw new SellerAuthorizeException(); } }}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 項目部署 原本SpringBoot的部署是最好把項目打包成war包放在tomcat上,但我發(fā)現(xiàn),我這里用打war的方法,要去掉spring-boot-starter-web包中的內(nèi)置tomcat容器,這樣會使websocket類出現(xiàn)問題,檢測不到j(luò)avax包,試了幾次沒有辦法,就用了另一種方法,打成Jar包的形式,先用控制臺運(yùn)行的方式在服務(wù)器運(yùn)行,這樣是有點(diǎn)隱患的,但這個沖突還沒有解決。打jar包詳情見:http://blog.csdn.net/xiao__gui/article/details/47341385
|