0.學習目标
會調用訂單系統接口
實作訂單結算功能
實作微信支付功能
1.訂單系統接口
我們不做開發,隻講解
1.1.導入訂單服務
把課前資料提供的leyou-order複制到D:\heima\code\leyou目錄。
然後在工程内導入:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLxUFRNh3aU5keNpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2kTM5UzMwAjM4IzMwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
然後導入module:
選擇導入module:
選擇目錄中的 ly-order:
打開父工程leyou的pom檔案,添加ly-order子產品:
1.2.Swagger-UI
1.2.1.什麼是OpenAPI
随着網際網路技術的發展,現在的網站架構基本都由原來的後端渲染,變成了:前端渲染、前後端分離的形态,而且前端技術和後端技術在各自的道路上越走越遠。? 前端和後端的唯一聯系,變成了API接口;API文檔變成了前後端開發人員聯系的紐帶,變得越來越重要。
沒有API文檔工具之前,大家都是手寫API文檔的,在什麼地方書寫的都有,而且API文檔沒有統一規範和格式,每個公司都不一樣。這無疑給開發帶來了災難。
OpenAPI規範(OpenAPI Specification 簡稱OAS)是Linux基金會的一個項目,試圖通過定義一種用來描述API格式或API定義的語言,來規範RESTful服務開發過程。目前V3.0版本的OpenAPI規範已經釋出并開源在github上 。
官網:https://github.com/OAI/OpenAPI-Specification
1.2.2.什麼是swagger?
OpenAPI是一個編寫API文檔的規範,然而如果手動去編寫OpenAPI規範的文檔,是非常麻煩的。而Swagger就是一個實作了OpenAPI規範的工具集。
官網:https://swagger.io/
看官方的說明:
Swagger包含的工具集:
Swagger編輯器: Swagger Editor允許您在浏覽器中編輯YAML中的OpenAPI規範并實時預覽文檔。
Swagger UI: Swagger UI是HTML,Javascript和CSS資産的集合,可以從符合OAS标準的API動态生成漂亮的文檔。
Swagger Codegen:允許根據OpenAPI規範自動生成API用戶端庫(SDK生成),伺服器存根和文檔。
Swagger Parser:用于解析來自Java的OpenAPI定義的獨立庫
Swagger Core:與Java相關的庫,用于建立,使用和使用OpenAPI定義
Swagger Inspector(免費): API測試工具,可讓您驗證您的API并從現有API生成OpenAPI定義
SwaggerHub(免費和商業): API設計和文檔,為使用OpenAPI的團隊建構。
1.2.3.快速入門
SpringBoot已經內建了Swagger,使用簡單注解即可生成swagger的API文檔。
1)引入依賴
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
2)編寫配置
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.host("http://order.leyou.com")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.leyou.order.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("樂優商城訂單系統")
.description("樂優商城訂單系統接口文檔")
.version("1.0")
.build();
}
}
3)接口聲明
在controller的每個handler上添加接口說明注解:
@RestController
@RequestMapping("order")
@Api("訂單服務接口")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private PayHelper payHelper;
/**
* 建立訂單
*
* @param order 訂單對象
* @return 訂單編号
*/
@PostMapping
@ApiOperation(value = "建立訂單接口,傳回訂單編号", notes = "建立訂單")
@ApiImplicitParam(name = "order", required = true, value = "訂單的json對象,包含訂單條目和物流資訊")
public ResponseEntity<Long> createOrder(@RequestBody @Valid Order order) {
Long id = this.orderService.createOrder(order);
return new ResponseEntity<>(id, HttpStatus.CREATED);
}
/**
* 分頁查詢目前使用者訂單
*
* @param status 訂單狀态
* @return 分頁訂單資料
*/
@GetMapping("list")
@ApiOperation(value = "分頁查詢目前使用者訂單,并且可以根據訂單狀态過濾",
notes = "分頁查詢目前使用者訂單")
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "目前頁",
defaultValue = "1", type = "Integer"),
@ApiImplicitParam(name = "rows", value = "每頁大小",
defaultValue = "5", type = "Integer"),
@ApiImplicitParam(
name = "status",
value = "訂單狀态:1未付款,2已付款未發貨,3已發貨未确認,4已确認未評價,5交易關閉,6交易成功,已評價", type = "Integer"),
})
public ResponseEntity<PageResult<Order>> queryUserOrderList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "rows", defaultValue = "5") Integer rows,
@RequestParam(value = "status", required = false) Integer status) {
PageResult<Order> result = this.orderService.queryUserOrderList(page, rows, status);
if (result == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(result);
}
}
常用注解說明:
/**
@Api:修飾整個類,描述Controller的作用
@ApiOperation:描述一個類的一個方法,或者說一個接口
@ApiParam:單個參數描述
@ApiModel:用對象來接收參數
@ApiProperty:用對象接收參數時,描述對象的一個字段
@ApiResponse:HTTP響應其中1個描述
@ApiResponses:HTTP響應整體描述
@ApiIgnore:使用該注解忽略這個API
@ApiError :發生錯誤傳回的資訊
@ApiImplicitParam:一個請求參數
@ApiImplicitParams:多個請求參數
*/
4)啟動測試
啟動服務,然後通路:http://localhost:8089/swagger-ui.html
點選order-controller,檢視接口資訊:
點選任意一個接口,即可看到詳細資訊:
1.3.測試接口
1.3.1.建立訂單接口
可以通過頁面看到接口資訊:
請求方式:POST
請求路徑:/order
請求參數:包含訂單、訂單詳情等資料的json對象。
傳回結果:訂單編号
點選Try It Out來測試:
輸入資料:
{
"totalPay": 236800,
"postFee": 0,
"paymentType": 2,
"actualPay": 236800,
"buyerMessage": null,
"buyerNick": "huge",
"orderDetails": [
{
"skuId": 3893493,
"num": 1,
"title": "蘋果(Apple)iPhone 6 (A1586) 16GB 金色 移動聯通電信4G手機3",
"price": 236800,
"ownSpec": "{\"機身顔色\":\"鑽雕藍\",\"記憶體\":\"4GB\",\"機身存儲\":\"64GB\"}",
"image": "http://image.leyou.com/images/9/4/1524297342728.jpg"
}
],
"receiver": "鋒哥",
"receiverMobile": "15800000000",
"receiverState": "上海",
"receiverCity": "上海",
"receiverDistrict": "浦東新簽",
"receiverAddress": "航頭鎮航頭路18号傳智播客3号樓",
"receiverZip": "210000",
"invoiceType": 0,
"sourceType":2
}
然後點選execute:
結果:
下單需要登入,通過登入生成token:
把token的值手動加入到浏覽器的cookie中:
添加成功,響應訂單編号。但是和資料庫儲存的訂單編号不太一樣(後幾位不一樣,浏覽器展示長整型會出現精度損失)
1.3.2.生成ID的方式
訂單id的特殊性
訂單資料非常龐大,将來一定會做分庫分表。那麼這種情況下, 要保證id的唯一,就不能靠資料庫自增,而是自己來實作算法,生成唯一id。
雪花算法
這裡的訂單id是通過一個工具類生成的:
而工具類所采用的生成id算法,是由Twitter公司開源的snowflake(雪花)算法。
簡單原理
雪花算法會生成一個64位的二進制資料,為一個Long型。(轉換成字元串後長度最多19位) ,其基本結構:
第一位:為未使用
第二部分:41位為毫秒級時間(41位的長度可以使用69年)
第三部分:5位datacenterId和5位workerId(10位的長度最多支援部署1024個節點)
第四部分:最後12位是毫秒内的計數(12位的計數順序号支援每個節點每毫秒産生4096個ID序号)
snowflake生成的ID整體上按照時間自增排序,并且整個分布式系統内不會産生ID碰撞(由datacenter和workerId作區分),并且效率較高。經測試snowflake每秒能夠産生26萬個ID。
配置
為了保證不重複,我們給每個部署的節點都配置機器id:
leyou:
worker:
workerId: 1
datacenterId: 1
加載屬性:
@ConfigurationProperties(prefix = "leyou.worker")
public class IdWorkerProperties {
private long workerId;// 目前機器id
private long datacenterId;// 序列号
public long getWorkerId() {
return workerId;
}
public void setWorkerId(long workerId) {
this.workerId = workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public void setDatacenterId(long datacenterId) {
this.datacenterId = datacenterId;
}
}
編寫配置類:
@Configuration
@EnableConfigurationProperties(IdWorkerProperties.class)
public class IdWorkerConfig {
@Bean
public IdWorker idWorker(IdWorkerProperties prop) {
return new IdWorker(prop.getWorkerId(), prop.getDatacenterId());
}
}
使用:
1.3.2.查詢訂單接口
接口說明:
請求方式:GET
請求路徑:/order/{id}
請求參數:id,訂單編号
傳回結果:Order,訂單的json對象
測試:
結果:
1.3.3.更新訂單狀态
接口說明:
請求參數:PUT
請求路徑:/order/{id}/{status}
請求參數:
id:訂單編号,String類型,不能為空
status:訂單狀态,不能為空
傳回結果:null
測試:
結果:
資料庫中也發生了改變:
1.3.4.分頁查詢訂單
接口說明:
請求方式:Get
請求路徑:/order/list
請求參數:
page:目前頁,Integer類型,預設為1
rows:每頁大小,Integer類型,預設為5
status:訂單狀态,String類型,預設查詢全部狀态訂單
傳回結果:PageResult 對象,包含下面屬性:
total:總條數
items:目前頁訂單數組
訂單對象
測試:
結果:
1.3.5.生成微信付款連結
接口說明:
請求方式:Get
請求路徑:/order/url/{id}
請求參數:id,訂單編号
傳回結果:String類型,生成的微信支付連結
測試:
結果:
微信支付工具
PayHelper
@Component
public class PayHelper {
private WXPay wxPay;
private static final Logger logger = LoggerFactory.getLogger(PayHelper.class);
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderService orderService;
public PayHelper(PayConfig payConfig) {
// 真實開發時
wxPay = new WXPay(payConfig);
// 測試時
// wxPay = new WXPay(payConfig, WXPayConstants.SignType.MD5, true);
}
public String createPayUrl(Long orderId) {
String key = "ly.pay.url." + orderId;
try {
String url = this.redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(url)) {
return url;
}
} catch (Exception e) {
logger.error("查詢緩存付款連結異常,訂單編号:{}", orderId, e);
}
try {
Map<String, String> data = new HashMap<>();
// 商品描述
data.put("body", "樂優商城測試");
// 訂單号
data.put("out_trade_no", orderId.toString());
//貨币
data.put("fee_type", "CNY");
//金額,機關是分
data.put("total_fee", "1");
//調用微信支付的終端IP(estore商城的IP)
data.put("spbill_create_ip", "127.0.0.1");
//回調位址
data.put("notify_url", "http://test.leyou.com/wxpay/notify");
// 交易類型為掃碼支付
data.put("trade_type", "NATIVE");
//商品id,使用假資料
data.put("product_id", "1234567");
Map<String, String> result = this.wxPay.unifiedOrder(data);
if ("SUCCESS".equals(result.get("return_code"))) {
String url = result.get("code_url");
// 将付款位址緩存,時間為10分鐘
try {
this.redisTemplate.opsForValue().set(key, url, 10, TimeUnit.MINUTES);
} catch (Exception e) {
logger.error("緩存付款連結異常,訂單編号:{}", orderId, e);
}
return url;
} else {
logger.error("建立預交易訂單失敗,錯誤資訊:{}", result.get("return_msg"));
return null;
}
} catch (Exception e) {
logger.error("建立預交易訂單異常", e);
return null;
}
}
/**
* 查詢訂單狀态
*
* @param orderId
* @return
*/
public PayState queryOrder(Long orderId) {
Map<String, String> data = new HashMap<>();
// 訂單号
data.put("out_trade_no", orderId.toString());
try {
Map<String, String> result = this.wxPay.orderQuery(data);
if (result == null) {
// 未查詢到結果,認為是未付款
return PayState.NOT_PAY;
}
String state = result.get("trade_state");
if ("SUCCESS".equals(state)) {
// success,則認為付款成功
// 修改訂單狀态
this.orderService.updateStatus(orderId, 2);
return PayState.SUCCESS;
} else if (StringUtils.equals("USERPAYING", state)
|| StringUtils.equals("NOTPAY", state)) {
// 未付款或正在付款,都認為是未付款
return PayState.NOT_PAY;
} else {
// 其它狀态認為是付款失敗
return PayState.FAIL;
}
} catch (Exception e) {
logger.error("查詢訂單狀态異常", e);
return PayState.NOT_PAY;
}
}
}
跟支付相關的其它幾個類:
1.3.6.查詢支付狀态
接口說明:
請求方式: Get
請求路徑: /state/{id}
請求參數: id,訂單編号
傳回結果:0, 未查詢到支付資訊 1,支付成功 2,支付失敗(查詢失敗,或者訂單過期)
1.3.6.1.未付款
未付款時查詢,測試:
結果:
因為尚未付款,是以查詢傳回0。
1.3.6.2.付款
通過JS把連結變成二維碼。
找到課前資料提供的JS頁面:
進入,并輸入剛剛生成的位址:
1.3.6.3.已付款
掃碼支付,然後再次查詢:
狀态碼為1,代表支付成功了!
2.訂單結算頁
2.1.頁面跳轉
在購物車頁面的最下方,有一個去結算按鈕:
當點選結算,我們應該跳轉到訂單結算頁,即:getOrderInfo.html
檢視購物車的結算按鈕:
可以看到,位址是正确的。但是隻有登入使用者才可以去結算付款,是以我們不能直接跳轉,而是在跳轉前校驗使用者的登入狀态,如果發現是未登入,應該重定向到登入頁!
我們給這個按鈕綁定點選事件:
事件中判斷登入狀态,進行頁面跳轉:
toOrderInfo() {
// 判斷是否登入
ly.verifyUser().then(() => {
// 已登入
window.location.href = "/getOrderInfo.html"
}).catch(() => {
// 未登入
window.location.href = "/login.html?returnUrl=" + window.location.href;
})
}
登入後測試:
此處頁面需要渲染的内容主要包含3部分:
收貨人資訊
支付方式
商品資訊
2.2.收貨人資訊(作業)
這裡的收貨人資訊肯定是目前登入使用者的收貨位址。是以需要根據目前登入使用者去查詢,目前我們在頁面是寫的假資料:
大家可以在在背景提供位址的增删改查接口,然後頁面加載時根據目前登入使用者查詢,而後指派給addresses即可。
2.3.支付方式
支付方式有2種:
微信支付
貨到付款
與我們訂單資料中的paymentType關聯:
是以我們可以在Vue執行個體中定義一個屬性來記錄支付方式:
然後在頁面渲染時與這個變量關聯:
2.4.商品清單
效果圖:
這裡的送貨清單,其實就是購物車中使用者選擇的要付款的商品
是以,我們需要在購物車跳轉過來的同時,攜帶選中的購物車的資訊
2.4.1.購物車資訊擷取
我們修改cart.html中的頁面跳轉邏輯,把使用者選中的購物車資訊傳遞過來:
然後在created鈎子函數中擷取購物車資料,儲存到本地屬性,要注意的是,我們應該在擷取資料前校驗使用者登入狀态,如果發現未登入,則直接重定向到登入頁:
然後重新加載頁面,檢視控制台:
2.4.2.頁面渲染
要修改的頁面位置:每一個li就是一件商品
我們修改為:
<ul class="send-detail">
<li v-for="(cart,index) in carts" :key="index">
<div class="sendGoods">
<ul class="yui3-g">
<li class="yui3-u-1-6">
<span><img width="70px" height="70px" :src="cart.image"/></span>
</li>
<li class="yui3-u-7-12">
<div class="desc">{{cart.title}}</div>
<div class="seven">
<span v-for="(v) in JSON.parse(cart.ownSpec)">{{v + " "}} </span>
</div>
</li>
<li class="yui3-u-1-12">
<div class="price">¥{{ly.formatPrice(cart.price * cart.num)}}</div>
</li>
<li class="yui3-u-1-12">
<div class="num">{{cart.num}}</div>
</li>
<li class="yui3-u-1-12">
<div class="exit">有貨</div>
</li>
</ul>
</div>
</li>
</ul>
2.5.總金額
另外在商品清單下面,還有一個總金額的計算:
可以看出這裡主要有4個資料:
總金額:totalPay
優惠返現:discount
運費:postFee
實付金額:actualPay
不過我們沒有做優惠活動,另外運費需要結合物流系統來計算,暫時我們都設定為0,在order屬性中寫死:
我們通過計算屬性來得到totalPay和actualPay值:
computed: {
totalNum(){
return this.carts.reduce((c1, c2) => c1 + c2.num, 0)
},
totalPay(){
return this.carts.reduce((c1, c2) => c1 + c2.price * c2.num, 0);
},
actualPay(){
return this.totalPay + this.order.postFee - this.order.discount;
}
},
然後在頁面渲染:
效果:
2.6.送出訂單
2.6.1.頁面送出
來看下訂單接口所需要的資料:
分為3部分,分别是
訂單本身的基本資訊
總金額
實付金額
付款類型
買家資訊就是目前使用者
訂單詳情
就是購物車中的商品,不過購物車資料會多出一個userId,我們去除即可:
物流資訊
目前使用者選中的物流位址資訊
給送出按鈕綁定事件:
然後編寫方法,組織資料并送出:
methods: {
submit() {
// 把購物車資料處理成訂單詳情
const orderDetails = this.carts.map(({userId, ...rest}) => rest);
// 處理物流資訊
const addr = this.addresses[this.selectedAddress];
const obj = {
receiver: addr.name,
receiverState: addr.state,
receiverCity: addr.city,
receiverAddress: addr.address,
receiverDistrict: addr.district,
receiverMobile: addr.phone,
receiverZip: addr.zipCode
};
// 複制到訂單對象
Object.assign(this.order, obj, {
orderDetails,
totalPay: this.totalPay,
actualPay: this.actualPay,
});
// 送出訂單
ly.http.post("/order", this.order).then(({data}) => {
// 線上支付,需要到付款頁
window.location = "pay.html?orderId=" + data;
}).catch((resp) => {
alert("訂單送出失敗,可能是缺貨!")
})
}
},
2.6.2.精度損失問題
在頁面點選送出測試:
成功生成訂單!
然後看頁面跳轉:
好像有什麼不對?訂單号的最後2位不正确啊!
這其實是因為JS的長整數精度有限,java的Long類型資料超出了範圍,是以出現了精度損失。
我們背景傳回的是Json的字元串,在axios内部會自動調用 JSON.parse()方法把json字元串轉為JS資料,就會出現進度損失。如果不進行轉換,依然當做字元串來使用,就不會有問題了。
是以,我們重寫axios對響應的處理回調函數:
再次測試,就OK了。
接下來就輪到支付了。
3.微信支付
3.1.介紹
微信支付官方文檔:https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F
我們選擇開發文檔,而後進入選擇頁面:
選擇掃碼支付:
此處我們使用模式二來開發:
3.2.開發流程
模式二與模式一相比,流程更為簡單,不依賴設定的回調支付URL。
商戶背景系統先調用微信支付的統一下單接口,微信背景系統傳回連結參數code_url;
商戶背景系統将code_url值生成二維碼圖檔,使用者使用微信用戶端掃碼後發起支付。
注意:code_url有效期為2小時,過期後掃碼不能再發起支付。
流程圖:
這裡我們把商戶(我們)要做的事情總結一下:
1、商戶生成訂單
2、商戶調用微信下單接口,擷取預交易的連結
3、商戶将連結生成二維碼圖檔,展示給使用者;
4、使用者支付并确認
5、支付結果通知:
微信異步通知商戶支付結果,商戶告知微信支付接收情況
商戶如果沒有收到通知,可以調用接口,查詢支付狀态
6、如果支付成功,發貨,修改訂單狀态
在前面的業務中,我們已經完成了:
1、生成訂單
接下來,我們需要做的是:
2、調用微信接口,生成連結。
3、并且生成二維碼圖檔
3.3.生成二維碼
3.3.1.生成預交易連結
我們先根據訂單的編号,調用背景服務,生成交易連結,而後才能根據連結生成二維碼。
在頁面發起請求:
var payVm = new Vue({
el:"#payVm",
data:{
ly,
orderId:0,// 訂單編号
},
created(){
// 判斷登入狀态
ly.http.get("/auth/verify").then(() => {
// 擷取訂單編号
this.orderId = ly.getUrlParam("orderId");
// 擷取請求連結
ly.http.get("/order/url/" + this.orderId)
.then(resp => {
console.log(resp.data);
})
}.catch(() => {
// 未登入,跳轉至登入頁
location.href = "/login.html?returnUrl=" + location.href;
})
},
components: {
shortcut: () => import("./js/pages/shortcut.js")
}
});
背景已經定義好生成付款位址的接口。
重新整理頁面檢視:
3.3.2.生成二維碼
這裡我們使用一個生成二維碼的JS插件:qrcode,官網:https://github.com/davidshimjs/qrcodejs
我們把課這個js腳本引入到項目中:
官方使用案例:
然後在頁面引用:
頁面定義一個div,用于展示二維碼:
然後擷取到付款連結後,根據連結生成二維碼:
// 判斷登入狀态
ly.http.get("/auth/verify").then(() => {
// 擷取訂單編号
this.orderId = ly.getUrlParam("orderId");
// 擷取請求連結
ly.http.get("/order/url/" + this.orderId)
.then(resp => {
new QRCode(document.getElementById("qrImage"), {
text: resp.data,
width: 250,
height: 250,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
})
}).catch(() => {
// 未登入,跳轉至登入頁
location.href = "/login.html?returnUrl=" + location.href;
})
重新整理頁面,檢視效果:
此時,客戶用手機掃描二維碼,可以看到付款頁面。
3.4.付款狀态查詢
跳轉到支付頁面後,我們等待使用者付款,付款完成則跳轉到付款成功頁面。
3.4.1.頁面循環查詢支付狀态
不過,因為不清楚使用者何時會付款,是以這裡采用循環的方式,不斷請求判斷是否支付成功。
// 開啟定時任務,查詢付款狀态
const taskId = setInterval(() => {
ly.http.get("/order/state/" + this.orderId)
.then(resp => {
let i = resp.data;
if (i === 1) {
// 付款成功
clearInterval(taskId);
// 跳轉到付款成功頁
location.href = "/paysuccess.html?orderId=" + this.orderId;
} else if (i === 2) {
// 付款失敗
clearInterval(taskId);
// 跳轉到付款失敗頁
location.href = "/payfail.html";
}
})
}, 3000);
3.4.2.付款成功頁面
當付款成功後,自動跳轉到付款成功頁面: