go-zero微服務實戰系列(三、API定義和表結構設計)

語言: CN / TW / HK

前兩篇文章分別介紹了本系列文章的背景以及根據業務職能對商城系統做了服務的拆分,其中每個服務又可分為如下三類:

  • api服務 - BFF層,對外提供HTTP介面
  • rpc服務 - 內部依賴的微服務,實現單一的業務功能
  • rmq服務 - 負責流式任務的處理,如消費kafka等等
  • admin服務 - 對內部管理後臺提供HTTP介面,通常資料操作許可權比較高

如果沒看過前兩篇文章可通過如下傳送門檢視

go-zero 微服務實戰系列(一、開篇)

go-zero微服務實戰系列(二、服務拆分)

前兩篇文章比較偏理論,以至於文章發出去後有些同學覺得寫得比較水,非常理解大家迫切想要寫程式碼的心情,我也進行了深刻的反思哈哈哈。所以從本篇開始就要進入萬眾期待的程式碼環節了。但是,所謂磨刀不誤砍柴工,在真正的生產開發過程中,我們一般都會花大量的時間在需求的理解和協議的設計上,如果需求理解的不透徹或者協議設計的不合理就會大大增加我們專案返工的可能,甚至還沒上線就得重構。所以前期多投入一些時間也完全是值得的。當我們把需求理解透徹,專案結構和協議定義清晰後,其實寫程式碼就是順水推舟的事情,速度那是大大滴快。閒言少敘,我們開始今天的內容。

API定義

可能大家在工作中都遇到過這樣的場景,就是程式碼更新了但是文件沒有更新,從而產生一些問題導致一些扯皮事情的發生。這個問題的本質是服務和文件是割裂的。我們期望的是文件即協議,協議即服務,這個理念與go-zero的api定義不謀而合。

我們定義了BFF層,BFF是對外提供HTTP介面的統一出口,所以我們這裡API的定義主要是針對BFF服務的API的定義。

API的相容性

我們定義或修改API的時候一定要考慮向前相容,如下幾種情況是向前相容的:

  • 增加新的API介面協議
  • 請求引數新增欄位,需要保證新老客戶端對該欄位的處理方式不同
  • 響應結果新增欄位,該欄位資訊只會在新版本客戶端中展示

如下幾種情況是向前不相容的:

  • 刪除或重新命名服務、欄位、方法等,從本質上說,如果客戶端程式碼可以引用某些內容,那麼刪除或者重新命名它都是不相容的變化,這時必須修改major版本號
  • 修改欄位型別,這會導致客戶端庫生成的程式碼發生變化,因此必須增加major版本號,對於編譯型靜態語言來說,可能會編譯錯誤
  • 修改現有請求的可見行為,客戶端通常依賴於API行為和語義,即使這樣的行為沒有被明確支援或記錄。因此,在大多數情況下,修改API資料的行為或語義將被消費者視為是破壞性的
  • 給資源訊息新增 讀取/寫入 欄位
首頁API定義

首頁功能主要分為四個部分,搜尋、Banner圖、限時搶購和推薦商品列表,點選搜尋框會跳轉到搜尋頁,推薦部分是分頁展示的,使用者通過不斷地往上滑動可以載入下一頁。通過分析首頁我們大致需要提供三個介面,分別是Banner介面,限時搶購介面和推薦介面。

這裡需要注意的是推薦介面,推薦介面返回的資料是需要支援分頁的,這裡分頁採用遊標的方式,Ps引數為每頁返回資料條數,預設一頁返回20條資料,注意在服務端一定需要再次校驗Ps值,防止Ps惡意值導致的效能問題,比如Ps傳了10000,當為非法值的時候需要把Ps置為預設值,Cursor為遊標值,遊標為每頁最後一條資料的RecommendTime。

返回值中Products定義了返回的商品列表,IsEnd表示是否是最後一頁,客戶端通過判斷IsEnd是否為true決定是否終止請求,RecommendTime為本頁返回資料最後一條資料的推薦時間,推進列表按照推薦時間倒序返回。

RecommendRequest {
		Cursor int64 `json:"cursor"`
		Ps     int64 `form:"ps,default=20"` // 每頁大小
}

RecommendResponse {
		Products      []*Product `json:"products"`
		IsEnd         bool       `json:"is_end"`         // 是否最後一頁
		RecommendTime int64      `json:"recommend_time"` // 商品列表最後一個商品的推薦時間
}

Product {
		ID          int64   `json:"id"`          // 商品ID
		Name        string  `json:"name"`        // 產品名稱
		Description string  `json:"description"` // 商品描述
		Price       float64 `json:"price"`       // 商品價格
		Stock       int64   `json:"stock"`       // 庫存
		Category    string  `json:"category"`    // 分類
		Status      int64   `json:"status"`      // 狀態:1-正常,2-下架
		CreateTime  int64   `json:"create_time"` // 建立時間
		UpdateTime  int64   `json:"update_time"` // 更新時間
}

搶購有一個倒計時的功能,我們這裡返回搶購開始時間,客戶端計算剩餘時間進行倒計時。

FlashSaleResponse {
		StartTime int64      `json:"start_time"` // 搶購開始時間
		Products  []*Product `json:"products"`
}
分類API定義

分類列表中可以切換不同的tab來選擇不同的分類,同時在每一種分類下面又可以按照不同的維度進行排序,且支援分頁。

分類商品列表和推薦介面的分頁方式一樣,都是採用遊標的方式,同時分類商品列表需要根據不同的分類和排序屬性進行排序,此類需要排序的列表我們一般會通過redis的sorted set來實現,score為需要排序的屬性,比如銷量,member為對應商品的id。

CategoryListRequest {
		Cursor   int64  `form:"cursor"`        // 分頁遊標
		Ps       int64  `form:"ps,default=20"` // 每頁大小
		Category string `form:"category"`      // 分類
		Sort     string `form:"sort"`          // 排序
}

CategoryListResponse {
		Products []*Product `json:"products"`
		IsEnd    bool       `json:"is_end"`
		LastVal  int64      `json:"last_val"`
}

提到sorted set在這裡說一個筆者使用sorted set曾經踩過的一個坑。我們使用快取的常用姿勢是cache aside模式,即先讀快取,如果快取命中則直接從快取中返回資料,如果讀取快取miss了,則回源到DB中讀資料,且為了後面更快的讀取資料,從DB中讀取的資料會回塞到快取中,且會給快取設定一個過期時間。

而為了保證快取和資料庫資料的一致性,當我們新增資料的時候需要把這條資料也寫到快取中從而保證快取和資料庫資料一致,一般程式碼會這麼寫,先通過Exists判斷快取對應的key是否存在,如果存在就往sorted set中增加一條資料,如果不存在則不處理,等待下次來讀取列表的時候重新載入列表資料到快取中。我們發現有時候快取中列表資料會變成一條,但是資料其實是有多條的,當時感覺是很詭異的,通過排查最終定位到問題,原來是Exists操作和Zadd兩個操作不是原子的操作導致的,也就是在Exists的時候快取的Key還沒有過期,但是在Exists後和進行Zadd前這個key過期了,然後再執行Zadd就導致快取列表中就只有本次新增的這條資料了。解決這個問題的辦法也很簡單,不使用Exists判斷key是否存在,而是通過Expire給這個key續期,如果key不存在則Expire返回0,key存在則Expire返回1,續期成功。快取的使用我們還踩過很多坑,特別是在高併發的場景下,這個後續文章再詳細介紹。

購物車API定義

在這裡我們對購物車的數量做一下限制,我們限制購物車最多隻能加200個商品,這樣做是為了在全選的時候下單不會導致過高的寫放大,由於加了200條的限制,所以購物車列表不需要分頁。

購物車列表請求和返回定義如下:

CartListRequest {
		UID int64 `form:"uid"`
	}

	CartListResponse {
		Products []*CartProduct `json:"products"`
	}

	CartProduct {
		Product *Product `json:"product"`
		Count   int64    `json:"count"` // 購買數量
	}
商品評價API定義

商品評價的功能同樣也是需要支援分頁的,採用遊標的方式進行分頁,同時按照評論時間進行倒序

評論列表定義如下:

ProductCommentRequest {
		ProductID int64 `form:"product_id"`
		Cursor    int64 `form:"cursor"`
		Ps        int64 `form:"ps,default=20"`
	}

	ProductCommentResponse {
		Comments    []*Comment `json:"comments"`
		IsEnd       bool       `json:"is_end"`       // 是否最後一頁
		CommentTime int64      `json:"comment_time"` // 評論列表最後一個評論的時間
	}

	Comment {
		ID         int64    `json:"id"`          // 評論ID
		ProductID  int64    `json:"product_id"`  // 商品ID
		Content    string   `json:"content"`     // 評論內容
		Images     []*Image `json:"images"`      // 評論圖片
		User       *User    `json:"user"`        // 使用者資訊
		CreateTime int64    `json:"create_time"` // 評論時間
		UpdateTime int64    `json:"update_time"` // 更新時間
	}

	User {
		ID     int64  `json:"id"`     // 使用者ID
		Name   string `json:"name"`   // 使用者名稱
		Avatar string `json:"avatar"` // 頭像
	}

	Image {
		ID  int64  `json:"id"`
		URL string `json:"url"`
	}

以上列出了一些核心的API的定義,商城的功能點非常多,很難短時間內全部定義完,筆者會在工作之餘不斷的完善。定義介面返回資料的時候我們要儘量的收斂只返回必要的資料。

定義好api後,我們使用如下命令重新生成專案程式碼,輸出如下資訊表明生成成功

$ goctl api go -api api.api -dir .

etc/api-api.yaml exists, ignored generation
internal/config/config.go exists, ignored generation
api.go exists, ignored generation
internal/svc/servicecontext.go exists, ignored generation
internal/handler/homebannerhandler.go exists, ignored generation
internal/handler/flashsalehandler.go exists, ignored generation
internal/handler/recommendhandler.go exists, ignored generation
internal/handler/categorylisthandler.go exists, ignored generation
internal/handler/cartlisthandler.go exists, ignored generation
internal/handler/productcommenthandler.go exists, ignored generation
internal/logic/homebannerlogic.go exists, ignored generation
internal/logic/flashsalelogic.go exists, ignored generation
internal/logic/recommendlogic.go exists, ignored generation
internal/logic/categorylistlogic.go exists, ignored generation
internal/logic/cartlistlogic.go exists, ignored generation
internal/logic/productcommentlogic.go exists, ignored generation
Done.
RPC定義

因為BFF只負責資料的組裝工作,資料真正的來源是各個微服務通過RPC介面提供,接下來我們來定義各個微服務的proto。如下展示的訂單列表頁面由兩部分資料組成,分別是訂單資料和商品資料,也就是我們的BFF需要依賴order-rpc和product-rpc來完成該頁面資料的組裝,下面我們分別來定義order-rpc和product-rpc

order.proto定義如下,service名字為Order,添加了Orders獲取訂單列表rpc介面。

syntax = "proto3";

package order;
option go_package="./order";


service Order {
  rpc Orders(OrdersRequest) returns(OrdersResponse);
}

message OrdersRequest {
  int64 user_id = 1;
  int32 status = 2;
  int64 cursor = 3;
  int32 ps = 4;
}

message OrdersResponse {
  repeated OrderItem orders = 1;
  bool is_end = 2;
  string create_time = 3;
}

message OrderItem {
  string order_id = 1;
  int64 quantity = 2;
  float payment = 3;
  int64 product_id = 4;
  int64 user_id = 5;
  int64 create_time = 6;
}

使用如下命令重新生成程式碼,注意這裡需要依賴protoc-gen-goprotoc-gen-go-grpc兩個外掛,木有安裝的話執行下面命令會報錯

$ goctl rpc protoc order.proto --go_out=. --go-grpc_out=. --zrpc_out=.

生成好後然後啟動order-rpc服務,輸出如下:

$ go run order.go

Starting rpc server at 127.0.0.1:8080...
{"level":"warn","ts":"2022-06-09T15:42:21.680+0800","logger":"etcd-client","caller":"[email protected]/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0xc000029c00/127.0.0.1:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: last connection error: connection error: desc = \"transport: Error while dialing dial tcp 127.0.0.1:2379: connect: connection refused\""}
{"@timestamp":"2022-06-09T15:42:21.682+08:00","caller":"zrpc/server.go:90","content":"context deadline exceeded","level":"error"}
panic: context deadline exceeded

什麼情況?竟然報錯了,還好日誌輸出的比較詳細,通過日誌可以看出來好像是本地的etcd沒有啟動,那我們就把本地的etcd啟動,啟動後再次執行order rpc服務,已經偵聽在預設的8080埠上

$ go run order.go

Starting rpc server at 127.0.0.1:8080...

product.proto定義如下

syntax = "proto3";

package product;
option go_package="./product";

service Product {
  rpc Products(ProductRequest) returns(ProductResponse);
}

message ProductRequest {
  string product_ids = 1;
}

message ProductResponse {
  repeated ProductItem products = 1;
}

message ProductItem {
  int64 product_id = 1;
  string name = 2;
  string description = 3;
  string image_url = 4;
}

執行如下命令生成product rpc的程式碼

$ goctl rpc protoc product.proto --go_out=. --go-grpc_out=. --zrpc_out=.

注意,goctl生成的rpc服務預設偵聽在8080埠,因為我們現在是在本地測試,所以把product rpc預設的埠改為8081,然後啟動服務。

Name: product.rpc
ListenOn: 127.0.0.1:8081
Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: product.rpc

$ go run product.go

Starting rpc server at 127.0.0.1:8081...

因為我們的BFF需要依賴order.rpc和product.rpc,我們需要先新增配置檔案,如下:

Name: api-api
Host: 0.0.0.0
Port: 8888
OrderRPC:
    Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: order.rpc
ProductRPC:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: product.rpc

然後在ServiceContext中新增RPC的客戶端,如下:

type ServiceContext struct {
	Config config.Config
	OrderRPC order.Order
	ProductRPC product.Product
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		OrderRPC: order.NewOrder(zrpc.MustNewClient(c.OrderRPC)),
		ProductRPC: product.NewProduct(zrpc.MustNewClient(c.ProductRPC)),
	}
}

最後只要在訂單介面的logic方法中新增邏輯就可以啦,這裡只是演示,所以會比較簡單:

func (l *OrderListLogic) OrderList(req *types.OrderListRequest) (resp *types.OrderListResponse, err error) {
	orderRet, err := l.svcCtx.OrderRPC.Orders(l.ctx, &order.OrdersRequest{UserId: req.UID})
	if err != nil {
		return nil, err
	}
	var pids []string
	for _, o := range orderRet.Orders {
		pids = append(pids, strconv.Itoa(int(o.ProductId)))
	}
	productRet, err := l.svcCtx.ProductRPC.Products(l.ctx, &product.ProductRequest{ProductIds: strings.Join(pids, ",")})
	if err != nil {
		return nil, err
	}
	var orders []*types.Order
	for _, o := range orderRet.Orders {
		if p, ok := productRet.Products[o.ProductId]; ok {
			orders = append(orders, &types.Order{
				OrderID: o.OrderId,
				ProductName: p.Name,
			})
		}
	}
	return &types.OrderListResponse{Orders: orders}, nil
}

然後在瀏覽器中請求訂單介面,就可以看到輸出瞭如下的資料,說明從BFF到RPC的鏈路已經打通:

http://127.0.0.1:8888/v1/order/list?uid=123

{
  "orders": [
    {
      "order_id": "20220609123456",
      "status": 0,
      "quantity": 0,
      "payment": 0,
      "total_price": 0,
      "create_time": 0,
      "product_id": 0,
      "product_name": "測試商品名稱",
      "product_image": "",
      "product_description": ""
    }
  ],
  "is_end": false,
  "order_time": 0
}

表結構定義

不同的微服務間需要做資料的隔離,每個微服務獨佔資料庫資源,通過RPC呼叫來獲取資料依賴,整體架構如下圖所示:

通過以上對API的定義我們大致瞭解了需要哪些資料欄位,下面開始進行資料表的設計,建表語句放在專案根目錄下data.sql檔案中,該檔案會不斷更新,主要涉及的庫和表定義如下:

使用者表主要儲存使用者資訊,在user庫中後續可能還會擴充套件比如使用者積分,使用者等級等功能

CREATE DATABASE user;
USE user;

CREATE TABLE `user` (
    `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '使用者ID',
    `username` varchar(50) NOT NULL DEFAULT '' COMMENT '使用者名稱',
    `password` varchar(50) NOT NULL DEFAULT '' COMMENT '使用者密碼,MD5加密',
    `phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手機號',
    `question` varchar(100) NOT NULL DEFAULT '' COMMENT '找回密碼問題',
    `answer` varchar(100) NOT NULL DEFAULT '' 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 (`id`),
    KEY `ix_update_time` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='使用者表';

商品庫中主要涉及商品表和商品分類表:

CREATE DATABASE product;
USE product;

CREATE TABLE `product` (
    `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商品id',
    `cateid` smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT '類別Id',
    `name` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名稱',
    `subtitle` varchar(200) DEFAULT NULL DEFAULT '' COMMENT '商品副標題',
    `images` text COMMENT '圖片地址,json格式,擴充套件用',
    `detail` text COMMENT '商品詳情',
    `price` decimal(20,2) NOT NULL DEFAULT 0 COMMENT '價格,單位-元保留兩位小數',
    `stock` int(11) NOT NULL DEFAULT 0 COMMENT '庫存數量',
    `status` int(6) NOT NULL DEFAULT 1 COMMENT '商品狀態.1-在售 2-下架 3-刪除',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
    PRIMARY KEY (`id`),
    KEY `ix_cateid` (`cateid`),
    KEY `ix_update_time` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';


CREATE TABLE `category` (
    `id` smallint(6) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分類id',
    `parentid` smallint(6) NOT NULL DEFAULT 0 COMMENT '父類別id當id=0時說明是根節點,一級類別',
    `name` varchar(50) NOT NULL DEFAULT '' COMMENT '類別名稱',
    `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '類別狀態1-正常,2-已廢棄',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品類別表';

購物車

CREATE DATABASE cart;
USE cart;

CREATE TABLE `cart` (
    `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '購物車id',
    `userid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者id',
    `proid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '商品id',
    `quantity` int(11) NOT NULL DEFAULT 0 COMMENT '數量',
    `checked` int(11) NOT NULL DEFAULT 0 COMMENT '是否選擇,1=已勾選,0=未勾選',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
    PRIMARY KEY (`id`),
    KEY `ix_userid` (`userid`),
    KEY `ix_proid` (`proid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='購物車表';

訂單相關:

CREATE DATABASE order;
USE order;

CREATE TABLE `orders` (
    `id` varchar(64) NOT NULL DEFAULT '' COMMENT '訂單id',
    `userid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者id',
    `shoppingid` bigint(20) NOT NUMBER DEFAULT 0 COMMENT '收貨資訊表id',
    `payment` decimal(20,2) DEFAULT NULL DEFAULT 0 COMMENT '實際付款金額,單位是元,保留兩位小數',
    `paymenttype` tinyint(4) NOT NULL DEFAULT 1 COMMENT '支付型別,1-線上支付',
    `postage` int(10)  NOT NULL DEFAULT 0 COMMENT '運費,單位是元',
    `status` smallint(6) NOT NULL DEFAULT 10 COMMENT '訂單狀態:0-已取消-10-未付款,20-已付款,30-待發貨 40-待收貨,50-交易成功,60-交易關閉',
    `payment_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '支付時間',
    `send_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '發貨時間',
    `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '交易完成時間',
    `close_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' 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 (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='訂單表';

CREATE TABLE `orderitem` (
     `id` bigint(20) UNSIGNED NOT NULL COMMENT '訂單子表id',
     `orderid` varchar(64) NOT NULL DEFAULT '' COMMENT '訂單id',
     `userid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者id',
     `proid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '商品id',
     `proname` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名稱',
     `proimage` varchar(500) NOT NULL DEFAULT '' COMMENT '商品圖片地址',
     `currentunitprice` decimal(20,2) NOT NULL DEFAULT 0 COMMENT '生成訂單時的商品單價,單位是元,保留兩位小數',
     `quantity` int(10) NOT NULL DEFAULT 0 COMMENT '商品數量',
     `totalprice` decimal(20,2) 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 (`id`),
     KEY `ix_orderid` (`orderid`),
     KEY `ix_userid` (`userid`),
     KEY `ix_proid` (`proid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='訂單明細表';

CREATE TABLE `shopping` (
    `id` bigint(20) UNSIGNED NOT NULL COMMENT '收貨資訊表id',
    `orderid` varchar(64) NOT NULL DEFAULT '' COMMENT '訂單id',
    `userid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者id',
    `receiver_name` varchar(20) NOT NULL DEFAULT '' COMMENT '收貨姓名',
    `receiver_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '收貨固定電話',
    `receiver_mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '收貨行動電話',
    `receiver_province` varchar(20) NOT NULL DEFAULT '' COMMENT '省份',
    `receiver_city` varchar(20) NOT NULL DEFAULT '' COMMENT '城市',
    `receiver_district` varchar(20) NOT NULL DEFAULT '' COMMENT '區/縣',
    `receiver_address` varchar(200) NOT NULL DEFAULT '' 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 (`id`),
    KEY `ix_orderid` (`orderid`),
    KEY `ix_userid` (`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收貨資訊表';

支付相關:

CREATE DATABASE pay;
USE pay;

CREATE TABLE `payinfo` (
    `id` bigint(20) UNSIGNED NOT NULL COMMENT '支付資訊表id',
    `orderid` varchar(64) NOT NULL DEFAULT '' COMMENT '訂單id',
    `userid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者id',
    `payplatform` tinyint(4) NOT NULL DEFAULT 0 COMMENT '支付平臺:1-支付寶,2-微信',
    `platformnumber` varchar(200) NOT NULL DEFAULT '' COMMENT '支付流水號',
    `platformstatus` varchar(20) NOT NULL DEFAULT '' 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 (`id`),
    KEY `ix_orderid` (`orderid`),
    KEY `ix_userid` (`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付資訊表';

結束語

本篇文章介紹瞭如何定義API,並根據定義好的api檔案通過 goctl 生成服務程式碼,整個專案涉及的api非常多,沒辦法一次性定義完,後續還會不斷的補充。

接著演示瞭如何在BFF服務中呼叫RPC服務,把整個呼叫鏈路打通,這裡只是為了演示所以寫死了程式碼,後面RPC返回的資料會從快取或者資料庫中獲取。

最後定義了整個專案主要涉及的庫和表,我們採用了微服務的架構,服務間資料做了隔離,每個服務獨享了資料庫。

到這裡前期的準備工作基本完成了,後面主要就是按照需求完成業務功能,和應對高併發來做優化。

由於筆者水平有限,難免會出現理解有誤的地方,如果你發現有可以改進的地方,希望能夠得到你寶貴的意見。

另外,如果你感興趣,非常歡迎你加入,我們一起來完成這個專案,為社群獻出自己的一份力。

希望本篇文章對你有所幫助,謝謝。

每週一、週四更新

程式碼倉庫 https://github.com/zhoushuguang/lebron

專案地址

https://github.com/zeromicro/go-zero

https://gitee.com/kevwan/go-zero

歡迎使用 go-zerostar 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。