手擼golang GO與微服務 Saga模式之8 集成測試git
最近閱讀<<Go微服務實戰>> (劉金亮, 2021.1)
本系列筆記擬採用golang練習之github
Saga由一系列的子事務「Ti」組成, 每一個Ti都有對應的補償「Ci」, 當Ti出現問題時Ci用於處理Ti執行帶來的問題。 能夠經過下面的兩個公式理解Saga模式。 T = T1 T2 … Tn T = TCT Saga模式的核心理念是避免使用長期持有鎖(如14.2.2節介紹的兩階段提交)的長事務, 而應該將事務切分爲一組按序依次提交的短事務, Saga模式知足ACD(原子性、一致性、持久性)特徵。 摘自 <<Go微服務實戰>> 劉金亮, 2021.1
事務消息隊列服務的功能性要求golang
建立虛擬的庫存服務sql
order_test.gojson
package saga import ( "github.com/jmoiron/sqlx" "learning/gooop/saga/mqs/cmd" "learning/gooop/saga/mqs/database" "learning/gooop/saga/mqs/logger" "learning/gooop/saga/order" "learning/gooop/saga/stock" "sync" "testing" "time" ) var gRunOnce sync.Once func fnBootMQS() { gRunOnce.Do(func() { // boot mqs go cmd.BootMQS() // wait for mqs up time.Sleep(1 * time.Second) }) } func fnAssertTrue (t *testing.T, b bool, msg string) { if !b { t.Fatal(msg) } } func Test_SagaSaleOrder(t *testing.T) { // prepare mqs fnClearDB(t) fnBootMQS() // 1 create prod stock prodID := "test-prod-1" err := stock.MockStockService.AddStock(prodID, 10) if err != nil { t.Fatal(err) } // create order 1 o1 := &order.SaleOrder{ OrderID: "test-order-1", ProductID: prodID, CustomerID: "test-customer-1", Quantity: 1, Price: 100, Amount: 100, CreateTime: time.Now().UnixNano(), StatusFlag: order.StatusNotDelivered, } err = order.MockSaleOrderService.Create(o1) if err != nil { t.Fatal(err) } // create order 2 time.Sleep(10*time.Millisecond) o2 := &order.SaleOrder{ OrderID: "test-order-2", ProductID: prodID, CustomerID: "test-customer-2", Quantity: 10, Price: 100, Amount: 1000, CreateTime: time.Now().UnixNano(), StatusFlag: order.StatusNotDelivered, } err = order.MockSaleOrderService.Create(o2) if err != nil { t.Fatal(err) } time.Sleep(1 * time.Second) logger.Logf("============================================") log := "tSaleOrderService.beginSubscribeMQ, done" fnAssertTrue(t, logger.Count(log)==1, "expecting log: " + log) log = "tSaleOrderService.publishMQ, done, order=test-order-1" fnAssertTrue(t, logger.Count(log)==1, "expecting log: " + log) log = "tSaleOrderService.publishMQ, done, order=test-order-2" fnAssertTrue(t, logger.Count(log)==1, "expecting log: " + log) log = "stock.NotifySaleOrderCreated, order=test-order-1" fnAssertTrue(t, logger.Count(log)==1, "expecting log: " + log) log = "stock.NotifySaleOrderCreated, order=test-order-2" fnAssertTrue(t, logger.Count(log)==1, "expecting log: " + log) o1 = order.MockSaleOrderService.Get(o1.OrderID) fnAssertTrue(t, o1.StatusFlag == order.StatusStockOutboundDone, "expecting o1 done") o2 = order.MockSaleOrderService.Get(o2.OrderID) fnAssertTrue(t, o2.StatusFlag == order.StatusStockOutboundFailed, "expecting o2 failed") logger.Logf("test passed") } func fnClearDB(t *testing.T) { fnDBExec(t, "delete from subscriber") fnDBExec(t, "delete from tx_msg") fnDBExec(t, "delete from delivery_queue") fnDBExec(t, "delete from success_queue") } func fnDBExec(t *testing.T, sql string, args... interface{}) int { rows := []int64{ 0 } err := database.DB(func(db *sqlx.DB) error { r,e := db.Exec(sql, args...) if e != nil { return e } rows[0], e = r.RowsAffected() if e != nil { return e } return nil }) if err != nil { t.Fatal(err) } return int(rows[0]) }
$ go test -v order_test.go === RUN Test_SagaSaleOrder 23:55:54.292132442 eventbus.Pub, event=system.boot, handler=gDeliveryService.handleBootEvent [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /ping --> learning/gooop/saga/mqs/handlers.Ping (4 handlers) [GIN-debug] POST /subscribe --> learning/gooop/saga/mqs/handlers.Subscribe (4 handlers) [GIN-debug] POST /publish --> learning/gooop/saga/mqs/handlers.Publish (4 handlers) [GIN-debug] POST /notify --> learning/gooop/saga/mqs/handlers.Notify (4 handlers) [GIN-debug] POST /notify/sale-order.stock.outbound --> learning/gooop/saga/order.NotifyStockOutbound (4 handlers) [GIN-debug] POST /notify/sale-order.created --> learning/gooop/saga/stock.NotifySaleOrderCreated (4 handlers) [GIN-debug] Listening and serving HTTP on :3333 23:55:54.292287032 tDeliveryService.beginCleanExpiredWorkers 23:55:54.292345845 tDeliveryService.beginCreatingWorkers 23:55:54.356542981 handlers.Subscribe, msg=&{sale-order-service sale-order.stock.outbound http://localhost:3333/notify/sale-order.stock.outbound 1616086554355593476} 23:55:54.356524325 handlers.Subscribe, msg=&{stock-service sale-order.created http://localhost:3333/notify/sale-order.created 1616086554355598830} 23:55:54.365256441 handlers.Subscribe, event=subscriber.registered, msg=&{sale-order-service sale-order.stock.outbound http://localhost:3333/notify/sale-order.stock.outbound 1616086554355593476} 23:55:54.365271105 eventbus.Pub, event=subscriber.registered, handler=gDeliveryService.handleSubscriberRegistered [GIN] 2021/03/18 - 23:55:54 | 200 | 8.865173ms | ::1 | POST "/subscribe" [GIN] 2021/03/18 - 23:55:54 | 200 | 8.882138ms | ::1 | POST "/subscribe" 23:55:54.365488163 tSaleOrderService.beginSubscribeMQ, done 23:55:54.365861542 database.DB, err=empty rows 23:55:54.366239244 tDeliveryWorker.afterInitialLoad, clientID=sale-order-service, rows=0 23:55:54.373588493 handlers.Subscribe, event=subscriber.registered, msg=&{stock-service sale-order.created http://localhost:3333/notify/sale-order.created 1616086554355598830} 23:55:54.373605972 eventbus.Pub, event=subscriber.registered, handler=gDeliveryService.handleSubscriberRegistered [GIN] 2021/03/18 - 23:55:54 | 200 | 17.189632ms | ::1 | POST "/subscribe" [GIN] 2021/03/18 - 23:55:54 | 200 | 17.205549ms | ::1 | POST "/subscribe" 23:55:54.373843032 tStockService.beginSubscribeMQ, done 23:55:54.3743926 database.DB, err=empty rows 23:55:54.374499757 tDeliveryWorker.afterInitialLoad, clientID=stock-service, rows=0 23:55:55.292336699 tStockService.AddStock, done, prodId=test-prod-1, stock=0, delta=0, after=10 23:55:55.323746568 handlers.Publish, msg=test-order-1/test-order-1/sale-order.created, msgId=112 [GIN] 2021/03/18 - 23:55:55 | 200 | 31.112478ms | ::1 | POST "/publish" [GIN] 2021/03/18 - 23:55:55 | 200 | 31.125855ms | ::1 | POST "/publish" 23:55:55.323811205 handlers.Publish, pubLiveMsg 112 23:55:55.323910377 tSaleOrderService.publishMQ, done, order=test-order-1/&{test-order-1 test-customer-1 test-prod-1 1 100 100 1616082955292352151 0} 23:55:55.324227736 handlers.Publish, pubLiveMsg, msgId=112, rows=1 23:55:55.324273573 handlers.Publish, event=msg.published, clientID=stock-service, msg=test-order-1/test-order-1/http://localhost:3333/notify/sale-order.created 23:55:55.32428051 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.sale-order-service 23:55:55.324285512 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.stock-service 23:55:55.324292286 tLiveMsgSource.handleMsgPublished, clientID=stock-service, msg=test-order-1/test-order-1/sale-order.created 23:55:55.324346678 tDeliveryWorker.beginPollAndDeliver, msg from live=&{98 stock-service http://localhost:3333/notify/sale-order.created 112 test-order-1 test-order-1 sale-order-service 1616082955292352151 sale-order.created {"OrderID":"test-order-1","CustomerID":"test-customer-1","ProductID":"test-prod-1","Quantity":1,"Price":100,"Amount":100,"CreateTime":1616082955292352151,"StatusFlag":0} 0 0} 23:55:55.33925766 handlers.Publish, msg=test-order-2/test-order-2/sale-order.created, msgId=113 [GIN] 2021/03/18 - 23:55:55 | 200 | 15.264561ms | ::1 | POST "/publish" [GIN] 2021/03/18 - 23:55:55 | 200 | 15.280884ms | ::1 | POST "/publish" 23:55:55.339353768 handlers.Publish, pubLiveMsg 113 23:55:55.339446893 tSaleOrderService.publishMQ, done, order=test-order-2/&{test-order-2 test-customer-2 test-prod-1 10 100 1000 1616082955302734821 0} 23:55:55.339909493 handlers.Publish, pubLiveMsg, msgId=113, rows=1 23:55:55.339919874 handlers.Publish, event=msg.published, clientID=stock-service, msg=test-order-2/test-order-2/http://localhost:3333/notify/sale-order.created 23:55:55.339925049 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.sale-order-service 23:55:55.339929964 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.stock-service 23:55:55.339935935 tLiveMsgSource.handleMsgPublished, clientID=stock-service, msg=test-order-2/test-order-2/sale-order.created 23:55:55.350117186 tDeliveryWorker.deliver, begin, id=stock-service, msg=test-order-1/test-order-1 23:55:55.35041833 stock.NotifySaleOrderCreated, order=test-order-1/&{test-order-1 test-customer-1 test-prod-1 1 100 100 1616082955292352151 0} 23:55:55.350429178 tStockService.AddStock, done, prodId=test-prod-1, stock=10, delta=-1, after=9 [GIN] 2021/03/18 - 23:55:55 | 200 | 88.872µs | ::1 | POST "/notify/sale-order.created" [GIN] 2021/03/18 - 23:55:55 | 200 | 133.617µs | ::1 | POST "/notify/sale-order.created" 23:55:55.350592351 tDeliveryWorker.deliver, OK, id=stock-service, msg=test-order-1/test-order-1 23:55:55.367336707 tDeliveryWorker.afterDeliverySuccess, done, id=stock-service, msg=test-order-1/test-order-1 23:55:55.36738322 tDeliveryWorker.beginPollAndDeliver, msg from live=&{99 stock-service http://localhost:3333/notify/sale-order.created 113 test-order-2 test-order-2 sale-order-service 1616082955302734821 sale-order.created {"OrderID":"test-order-2","CustomerID":"test-customer-2","ProductID":"test-prod-1","Quantity":10,"Price":100,"Amount":1000,"CreateTime":1616082955302734821,"StatusFlag":0} 0 0} 23:55:55.367530495 database.DB, err=empty rows 23:55:55.374978535 tDeliveryWorker.deliver, begin, id=stock-service, msg=test-order-2/test-order-2 23:55:55.375201115 stock.NotifySaleOrderCreated, order=test-order-2/&{test-order-2 test-customer-2 test-prod-1 10 100 1000 1616082955302734821 0} 23:55:55.375211216 tStockService.AddStock, failed, prodId=test-prod-1, stock=9, delta=-10 23:55:55.375219558 tStockService.HandleSaleOrderCreated, err=insufficient stock, order=&{test-order-2 test-customer-2 test-prod-1 10 100 1000 1616082955302734821 0} [GIN] 2021/03/18 - 23:55:55 | 200 | 102.52µs | ::1 | POST "/notify/sale-order.created" [GIN] 2021/03/18 - 23:55:55 | 200 | 116.933µs | ::1 | POST "/notify/sale-order.created" 23:55:55.375354895 tDeliveryWorker.deliver, OK, id=stock-service, msg=test-order-2/test-order-2 23:55:55.389901711 tDeliveryWorker.afterDeliverySuccess, done, id=stock-service, msg=test-order-2/test-order-2 23:55:55.38993077 tDeliveryWorker.beginPollAndDeliver, msg from db=&{99 stock-service http://localhost:3333/notify/sale-order.created 113 test-order-2 test-order-2 sale-order-service 1616082955302734821 sale-order.created {"OrderID":"test-order-2","CustomerID":"test-customer-2","ProductID":"test-prod-1","Quantity":10,"Price":100,"Amount":1000,"CreateTime":1616082955302734821,"StatusFlag":0} 1 1616082955367401386} 23:55:55.420121681 handlers.Publish, msg=test-order-1/test-order-1.outbound/sale-order.stock.outbound, msgId=114 [GIN] 2021/03/18 - 23:55:55 | 200 | 69.507171ms | ::1 | POST "/publish" [GIN] 2021/03/18 - 23:55:55 | 200 | 69.520805ms | ::1 | POST "/publish" 23:55:55.420220719 handlers.Publish, pubLiveMsg 114 23:55:55.420321792 tStockService.publishMQ, done, msg=&{test-order-1 test-order-1.outbound stock-service 1616082955350432496 sale-order.stock.outbound 1} 23:55:55.42071623 handlers.Publish, pubLiveMsg, msgId=114, rows=1 23:55:55.420731889 handlers.Publish, event=msg.published, clientID=sale-order-service, msg=test-order-1/test-order-1.outbound/http://localhost:3333/notify/sale-order.stock.outbound 23:55:55.420741935 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.sale-order-service 23:55:55.420746401 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.stock-service 23:55:55.420755367 tLiveMsgSource.handleMsgPublished, clientID=sale-order-service, msg=test-order-1/test-order-1.outbound/sale-order.stock.outbound 23:55:55.42079505 tDeliveryWorker.beginPollAndDeliver, msg from live=&{100 sale-order-service http://localhost:3333/notify/sale-order.stock.outbound 114 test-order-1 test-order-1.outbound stock-service 1616082955350432496 sale-order.stock.outbound 1 0 0} 23:55:55.435844021 handlers.Publish, msg=test-order-2/test-order-2.outbound/sale-order.stock.outbound, msgId=115 [GIN] 2021/03/18 - 23:55:55 | 200 | 15.407267ms | ::1 | POST "/publish" [GIN] 2021/03/18 - 23:55:55 | 200 | 15.420327ms | ::1 | POST "/publish" 23:55:55.4359058 handlers.Publish, pubLiveMsg 115 23:55:55.436026025 tStockService.publishMQ, done, msg=&{test-order-2 test-order-2.outbound stock-service 1616082955375214295 sale-order.stock.outbound 0} 23:55:55.436398324 handlers.Publish, pubLiveMsg, msgId=115, rows=1 23:55:55.436409937 handlers.Publish, event=msg.published, clientID=sale-order-service, msg=test-order-2/test-order-2.outbound/http://localhost:3333/notify/sale-order.stock.outbound 23:55:55.43642793 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.sale-order-service 23:55:55.436433697 eventbus.Pub, event=msg.published, handler=tLiveMsgSource.stock-service 23:55:55.43644379 tLiveMsgSource.handleMsgPublished, clientID=sale-order-service, msg=test-order-2/test-order-2.outbound/sale-order.stock.outbound 23:55:55.446599314 tDeliveryWorker.deliver, begin, id=sale-order-service, msg=test-order-1/test-order-1.outbound 23:55:55.446809726 order.NotifyStockOutbound, orderID=test-order-1, succeeded=true [GIN] 2021/03/18 - 23:55:55 | 200 | 61.898µs | ::1 | POST "/notify/sale-order.stock.outbound" [GIN] 2021/03/18 - 23:55:55 | 200 | 81.911µs | ::1 | POST "/notify/sale-order.stock.outbound" 23:55:55.446951354 tDeliveryWorker.deliver, OK, id=sale-order-service, msg=test-order-1/test-order-1.outbound 23:55:55.462584405 tDeliveryWorker.afterDeliverySuccess, done, id=sale-order-service, msg=test-order-1/test-order-1.outbound 23:55:55.462615131 tDeliveryWorker.beginPollAndDeliver, msg from live=&{101 sale-order-service http://localhost:3333/notify/sale-order.stock.outbound 115 test-order-2 test-order-2.outbound stock-service 1616082955375214295 sale-order.stock.outbound 0 0 0} 23:55:55.469999185 tDeliveryWorker.deliver, begin, id=sale-order-service, msg=test-order-2/test-order-2.outbound 23:55:55.470163043 order.NotifyStockOutbound, orderID=test-order-2, succeeded=false [GIN] 2021/03/18 - 23:55:55 | 200 | 85.14µs | ::1 | POST "/notify/sale-order.stock.outbound" [GIN] 2021/03/18 - 23:55:55 | 200 | 105.638µs | ::1 | POST "/notify/sale-order.stock.outbound" 23:55:55.470369408 tDeliveryWorker.deliver, OK, id=sale-order-service, msg=test-order-2/test-order-2.outbound 23:55:55.486229145 tDeliveryWorker.afterDeliverySuccess, done, id=sale-order-service, msg=test-order-2/test-order-2.outbound 23:55:56.302885199 ============================================ 23:55:56.303470422 test passed --- PASS: Test_SagaSaleOrder (2.05s) PASS ok command-line-arguments 2.057s
模擬的庫存服務接口app
package stock; import "learning/gooop/saga/order" type IStockService interface { GetStock(prodId string) int AddStock(prodId string, delta int) error HandleSaleOrderCreated(it *order.SaleOrder) error }
虛擬庫存服務, 實現IStockService接口分佈式
package stock import ( "bytes" "encoding/json" "errors" "io/ioutil" "learning/gooop/saga/mqs/logger" "learning/gooop/saga/mqs/models" "learning/gooop/saga/order" "net/http" "sync" "time" ) type tStockService struct { rwmutex *sync.RWMutex stock map[string]int bMQReady bool publishQueue chan *models.TxMsg } func newStockService() IStockService { it := new(tStockService) it.init() return it } func (me *tStockService) init() { me.rwmutex = new(sync.RWMutex) me.stock = make(map[string]int) me.bMQReady = false me.publishQueue = make(chan *models.TxMsg, gMQMaxQueuedMsg) go func() { time.Sleep(100*time.Millisecond) go me.beginSubscribeMQ() go me.beginPublishMQ() }() } func (me *tStockService) GetStock(prodId string) int { me.rwmutex.RLock() defer me.rwmutex.RUnlock() it,ok := me.stock[prodId] if ok { return it } else { return 0 } } func (me *tStockService) AddStock(prodId string, delta int) error { me.rwmutex.RLock() defer me.rwmutex.RUnlock() it,ok := me.stock[prodId] if ok { n := it + delta if n < 0 { logger.Logf("tStockService.AddStock, failed, prodId=%s, stock=%d, delta=%d", prodId, it, delta) return gInsufficientStockError } else { logger.Logf("tStockService.AddStock, done, prodId=%s, stock=%d, delta=%d, after=%d", prodId, it, delta, n) me.stock[prodId] = n } } else { if delta < 0 { logger.Logf("tStockService.AddStock, failed, prodId=%s, stock=0, delta=%d", prodId, delta) return gInsufficientStockError } else { logger.Logf("tStockService.AddStock, done, prodId=%s, stock=0, delta=%d, after=%d", prodId, it, delta) me.stock[prodId] = delta } } return nil } func (me *tStockService) beginSubscribeMQ() { expireDuration := int64(1 * time.Hour) subscribeDuration := 20 * time.Minute pauseDuration := 3*time.Second lastSubscribeTime := int64(0) for { now := time.Now().UnixNano() if now - lastSubscribeTime >= int64(subscribeDuration) { expireTime := now + expireDuration err := fnSubscribeMQ(expireTime) if err != nil { me.bMQReady = false logger.Logf("tStockService.beginSubscribeMQ, failed, err=%v", err) } else { lastSubscribeTime = now me.bMQReady = true logger.Logf("tStockService.beginSubscribeMQ, done") } } time.Sleep(pauseDuration) } } func fnSubscribeMQ(expireTime int64) error { msg := &models.SubscribeMsg{ ClientID: gMQClientID, Topic: gMQSubscribeTopic, NotifyUrl: gMQServerURL + PathOfNotifySaleOrderCreated, ExpireTime: expireTime, } url := gMQServerURL + "/subscribe" return fnPost(msg, url) } func fnPost(msg interface{}, url string) error { body,_ := json.Marshal(msg) rsp, err := http.Post(url, "application/json;charset=utf-8", bytes.NewReader(body)) if err != nil { return err } defer rsp.Body.Close() j, err := ioutil.ReadAll(rsp.Body) if err != nil { return err } ok := &models.OkMsg{} err = json.Unmarshal(j, ok) if err != nil { return err } if !ok.OK { return gMQReplyFalse } return nil } func (me *tStockService) beginPublishMQ() { for { select { case msg := <- me.publishQueue : me.publishMQ(msg) break } } } func (me *tStockService) publishMQ(msg *models.TxMsg) { url := gMQServerURL + "/publish" for i := 0;i < gMQMaxPublishRetry;i++ { err := fnPost(msg, url) if err != nil { logger.Logf("tStockService.publishMQ, failed, err=%v, msg=%v", err, msg) time.Sleep(gMQPublishInterval) } else { logger.Logf("tStockService.publishMQ, done, msg=%v", msg) return } } // publish failed logger.Logf("tStockService.publishMQ, failed max retries, msg=%v", msg) } func (me *tStockService) HandleSaleOrderCreated(it *order.SaleOrder) error { msg := &models.TxMsg{} msg.GlobalID = it.OrderID msg.SubID = it.OrderID + ".outbound" msg.SenderID = gMQClientID msg.Topic = gMQPublishTopic err := me.AddStock(it.ProductID, -it.Quantity) msg.CreateTime = time.Now().UnixNano() if err != nil { logger.Logf("tStockService.HandleSaleOrderCreated, err=%s, order=%v", err.Error(), it) msg.Content = "0" } else { msg.Content = "1" } if len(me.publishQueue) >= gMQMaxQueuedMsg { logger.Logf("tStockService.HandleSaleOrderCreated, err=%s, order=%v", gMQBlocked.Error(), it) return gMQBlocked } else { me.publishQueue <- msg return err } } var gInsufficientStockError = errors.New("insufficient stock") var gMQReplyFalse = errors.New("mq reply false") var gMQBlocked = errors.New("mq blocked") var gMQMaxPublishRetry = 10 var gMQPublishInterval = 1*time.Second var gMQSubscribeTopic = "sale-order.created" var gMQPublishTopic = "sale-order.stock.outbound" var gMQClientID = "stock-service" var gMQServerURL = "http://localhost:3333" var gMQMaxQueuedMsg = 1024 var MockStockService = newStockService()
用於監聽訂單建立消息的http回調處理器微服務
package stock import ( "encoding/json" "github.com/gin-gonic/gin" "io/ioutil" "learning/gooop/saga/mqs/logger" "learning/gooop/saga/mqs/models" "learning/gooop/saga/order" "net/http" ) func NotifySaleOrderCreated(c *gin.Context) { body := c.Request.Body defer body.Close() j, e := ioutil.ReadAll(body) if e != nil { logger.Logf("stock.NotifySaleOrderCreated, failed ioutil.ReadAll") c.JSON(http.StatusBadRequest, gin.H { "ok": false, "error": e.Error()}) return } msg := &models.TxMsg{} e = json.Unmarshal(j, msg) if e != nil { logger.Logf("stock.NotifySaleOrderCreated, failed json.Unmarshal msg") c.JSON(http.StatusBadRequest, gin.H { "ok": false, "error": e.Error()}) return } order := &order.SaleOrder{} e = json.Unmarshal([]byte(msg.Content), order) if e != nil { logger.Logf("stock.NotifySaleOrderCreated, failed json.Unmarshal order") c.JSON(http.StatusBadRequest, gin.H { "ok": false, "error": e.Error()}) return } logger.Logf("stock.NotifySaleOrderCreated, order=%s/%v", order.OrderID, order) // notify stock service _ = MockStockService.HandleSaleOrderCreated(order) c.JSON(http.StatusOK, gin.H{ "ok": true }) } var PathOfNotifySaleOrderCreated = "/notify/sale-order.created"
(未完待續)oop