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

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

  • 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

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

微信交流羣

關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章