Copilot commented on code in PR #95:
URL: 
https://github.com/apache/incubator-seata-go-samples/pull/95#discussion_r3385005886


##########
at/ecommerce/order/create.go:
##########
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "strings"
+       "time"
+
+       "github.com/gin-gonic/gin"
+       "github.com/parnurzeal/gorequest"
+       "seata.apache.org/seata-go/pkg/constant"
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+)
+
+type OrderRequest struct {
+       UserID        string `json:"userId"`
+       CommodityCode string `json:"commodityCode"`
+       Count         int    `json:"count"`
+       Money         int    `json:"money"`
+}
+
+type InventoryRequest struct {
+       CommodityCode string `json:"commodityCode"`
+       Count         int    `json:"count"`
+}
+
+type AccountRequest struct {
+       UserID string `json:"userId"`
+       Money  int    `json:"money"`
+}
+
+func createOrder(c *gin.Context) error {
+       var req OrderRequest
+       if err := c.ShouldBindJSON(&req); err != nil {
+               return err
+       }
+       if strings.TrimSpace(req.UserID) == "" {
+               return fmt.Errorf("userId is required")
+       }
+       if strings.TrimSpace(req.CommodityCode) == "" {
+               return fmt.Errorf("commodityCode is required")
+       }
+       if req.Count <= 0 {
+               return fmt.Errorf("count must be greater than 0")
+       }
+       if req.Money <= 0 {
+               return fmt.Errorf("money must be greater than 0")
+       }
+
+       return tm.WithGlobalTx(c.Request.Context(), &tm.GtxConfig{
+               Name:    "ATSampleEcommerceCreateOrder",
+               Timeout: time.Second * 30,
+       }, func(ctx context.Context) error {
+               if err := insertOrder(ctx, req); err != nil {
+                       return err
+               }
+               if err := deductInventory(ctx, req); err != nil {
+                       return err
+               }
+               if err := deductAccount(ctx, req); err != nil {
+                       return err
+               }
+               return nil
+       })
+}
+
+func insertOrder(ctx context.Context, req OrderRequest) error {
+       sql := "insert into order_tbl(user_id, commodity_code, count, money, 
status) values (?, ?, ?, ?, ?)"
+       ret, err := db.ExecContext(ctx, sql, req.UserID, req.CommodityCode, 
req.Count, req.Money, "CREATED")
+       if err != nil {
+               return err
+       }
+
+       rows, err := ret.RowsAffected()
+       if err != nil {
+               return err
+       }
+       if rows != 1 {
+               return fmt.Errorf("create order affected unexpected rows: %d", 
rows)
+       }
+       return nil
+}
+
+func deductInventory(ctx context.Context, req OrderRequest) (re error) {
+       request := gorequest.New()
+       payload, err := json.Marshal(InventoryRequest{
+               CommodityCode: req.CommodityCode,
+               Count:         req.Count,
+       })
+       if err != nil {
+               return err
+       }
+
+       log.Infof("call inventory service, xid=%s", tm.GetXID(ctx))
+       request.Post(inventoryService+"/deductInventory").
+               Set(constant.XidKey, tm.GetXID(ctx)).
+               Send(string(payload)).
+               Set("Content-Type", "application/json").
+               End(func(response gorequest.Response, body string, errs 
[]error) {
+                       if len(errs) > 0 {
+                               re = errs[0]
+                               return
+                       }
+                       if response == nil || response.StatusCode != 
http.StatusOK {
+                               re = fmt.Errorf("deduct inventory failed: %s", 
body)
+                       }
+               })
+       return
+}
+
+func deductAccount(ctx context.Context, req OrderRequest) (re error) {
+       request := gorequest.New()
+       payload, err := json.Marshal(AccountRequest{
+               UserID: req.UserID,
+               Money:  req.Money,
+       })
+       if err != nil {
+               return err
+       }
+
+       log.Infof("call account service, xid=%s", tm.GetXID(ctx))
+       request.Post(accountService+"/deductAccount").
+               Set(constant.XidKey, tm.GetXID(ctx)).
+               Send(string(payload)).
+               Set("Content-Type", "application/json").
+               End(func(response gorequest.Response, body string, errs 
[]error) {
+                       if len(errs) > 0 {
+                               re = errs[0]
+                               return
+                       }
+                       if response == nil || response.StatusCode != 
http.StatusOK {
+                               re = fmt.Errorf("deduct account failed: %s", 
body)
+                       }
+               })
+       return

Review Comment:
   Same issue as `deductInventory`: the gorequest callback form is 
asynchronous, so `deductAccount` can return before the HTTP request completes 
(returning `nil` and not failing the global transaction even when the account 
service rejects). Switch to synchronous request execution (or block until 
completion) and wire in `ctx` + timeout.



##########
at/ecommerce/inventory/deduct.go:
##########
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "fmt"
+       "strings"
+
+       "github.com/gin-gonic/gin"
+)
+
+type InventoryRequest struct {
+       CommodityCode string `json:"commodityCode"`
+       Count         int    `json:"count"`
+}
+
+func deductInventory(c *gin.Context) error {
+       var req InventoryRequest
+       if err := c.ShouldBindJSON(&req); err != nil {
+               return err
+       }
+       if strings.TrimSpace(req.CommodityCode) == "" {
+               return fmt.Errorf("commodityCode is required")
+       }
+       if req.Count <= 0 {
+               return fmt.Errorf("count must be greater than 0")
+       }
+
+       sql := "update inventory_tbl set stock = stock - ? where commodity_code 
= ? and stock >= ?"
+       ret, err := db.ExecContext(c, sql, req.Count, req.CommodityCode, 
req.Count)

Review Comment:
   `db.ExecContext` requires a `context.Context`, but `*gin.Context` does not 
implement `context.Context`, so this won’t compile. Use `c.Request.Context()` 
(or whatever context Seata Gin middleware places the XID into) when calling 
`ExecContext`.



##########
at/ecommerce/account/deduct.go:
##########
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "fmt"
+       "strings"
+
+       "github.com/gin-gonic/gin"
+)
+
+type AccountRequest struct {
+       UserID string `json:"userId"`
+       Money  int    `json:"money"`
+}
+
+func deductAccount(c *gin.Context) error {
+       var req AccountRequest
+       if err := c.ShouldBindJSON(&req); err != nil {
+               return err
+       }
+       if strings.TrimSpace(req.UserID) == "" {
+               return fmt.Errorf("userId is required")
+       }
+       if req.Money <= 0 {
+               return fmt.Errorf("money must be greater than 0")
+       }
+
+       sql := "update account_tbl set balance = balance - ? where user_id = ? 
and balance >= ?"
+       ret, err := db.ExecContext(c, sql, req.Money, req.UserID, req.Money)

Review Comment:
   Same compile-time issue as inventory: `*gin.Context` is not a 
`context.Context`. Use `c.Request.Context()` (or the Seata-propagated context) 
for `ExecContext`.



##########
at/ecommerce/order/create.go:
##########
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "strings"
+       "time"
+
+       "github.com/gin-gonic/gin"
+       "github.com/parnurzeal/gorequest"
+       "seata.apache.org/seata-go/pkg/constant"
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+)
+
+type OrderRequest struct {
+       UserID        string `json:"userId"`
+       CommodityCode string `json:"commodityCode"`
+       Count         int    `json:"count"`
+       Money         int    `json:"money"`
+}
+
+type InventoryRequest struct {
+       CommodityCode string `json:"commodityCode"`
+       Count         int    `json:"count"`
+}
+
+type AccountRequest struct {
+       UserID string `json:"userId"`
+       Money  int    `json:"money"`
+}
+
+func createOrder(c *gin.Context) error {
+       var req OrderRequest
+       if err := c.ShouldBindJSON(&req); err != nil {
+               return err
+       }
+       if strings.TrimSpace(req.UserID) == "" {
+               return fmt.Errorf("userId is required")
+       }
+       if strings.TrimSpace(req.CommodityCode) == "" {
+               return fmt.Errorf("commodityCode is required")
+       }
+       if req.Count <= 0 {
+               return fmt.Errorf("count must be greater than 0")
+       }
+       if req.Money <= 0 {
+               return fmt.Errorf("money must be greater than 0")
+       }
+
+       return tm.WithGlobalTx(c.Request.Context(), &tm.GtxConfig{
+               Name:    "ATSampleEcommerceCreateOrder",
+               Timeout: time.Second * 30,
+       }, func(ctx context.Context) error {
+               if err := insertOrder(ctx, req); err != nil {
+                       return err
+               }
+               if err := deductInventory(ctx, req); err != nil {
+                       return err
+               }
+               if err := deductAccount(ctx, req); err != nil {
+                       return err
+               }
+               return nil
+       })
+}
+
+func insertOrder(ctx context.Context, req OrderRequest) error {
+       sql := "insert into order_tbl(user_id, commodity_code, count, money, 
status) values (?, ?, ?, ?, ?)"
+       ret, err := db.ExecContext(ctx, sql, req.UserID, req.CommodityCode, 
req.Count, req.Money, "CREATED")
+       if err != nil {
+               return err
+       }
+
+       rows, err := ret.RowsAffected()
+       if err != nil {
+               return err
+       }
+       if rows != 1 {
+               return fmt.Errorf("create order affected unexpected rows: %d", 
rows)
+       }
+       return nil
+}
+
+func deductInventory(ctx context.Context, req OrderRequest) (re error) {
+       request := gorequest.New()
+       payload, err := json.Marshal(InventoryRequest{
+               CommodityCode: req.CommodityCode,
+               Count:         req.Count,
+       })
+       if err != nil {
+               return err
+       }
+
+       log.Infof("call inventory service, xid=%s", tm.GetXID(ctx))
+       request.Post(inventoryService+"/deductInventory").
+               Set(constant.XidKey, tm.GetXID(ctx)).
+               Send(string(payload)).
+               Set("Content-Type", "application/json").
+               End(func(response gorequest.Response, body string, errs 
[]error) {
+                       if len(errs) > 0 {
+                               re = errs[0]
+                               return
+                       }
+                       if response == nil || response.StatusCode != 
http.StatusOK {
+                               re = fmt.Errorf("deduct inventory failed: %s", 
body)
+                       }
+               })
+       return

Review Comment:
   Using `End(func(...))` schedules the request asynchronously in gorequest, so 
`deductInventory` can return before the HTTP call completes (often returning 
`nil`), allowing the global transaction to proceed/commit without waiting for 
inventory/account deductions. Fix by using the synchronous `End()` form 
(capture `(resp, body, errs)` directly) or otherwise block until the callback 
completes; also prefer attaching `ctx` and a timeout so the call respects the 
global transaction timeout/cancellation.



##########
at/ecommerce/sql/mysql_ecommerce.sql:
##########
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_order DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_inventory DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_account DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+USE seata_ecommerce_order;
+
+CREATE TABLE IF NOT EXISTS order_tbl (
+  id INT NOT NULL AUTO_INCREMENT,
+  user_id VARCHAR(64) NOT NULL,
+  commodity_code VARCHAR(64) NOT NULL,
+  count INT NOT NULL,
+  money INT NOT NULL,
+  status VARCHAR(32) NOT NULL,
+  PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS undo_log (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  branch_id BIGINT NOT NULL,
+  xid VARCHAR(100) NOT NULL,
+  context VARCHAR(128) NOT NULL,
+  rollback_info LONGBLOB NOT NULL,
+  log_status INT NOT NULL,
+  log_created DATETIME NOT NULL,
+  log_modified DATETIME NOT NULL,
+  ext VARCHAR(100) DEFAULT NULL,
+  PRIMARY KEY (id),
+  KEY idx_unionkey (xid, branch_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+USE seata_ecommerce_inventory;
+
+CREATE TABLE IF NOT EXISTS inventory_tbl (
+  id INT NOT NULL AUTO_INCREMENT,
+  commodity_code VARCHAR(64) NOT NULL,
+  stock INT NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_commodity_code (commodity_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO inventory_tbl (commodity_code, stock) VALUES ('C100001', 100)
+ON DUPLICATE KEY UPDATE stock = VALUES(stock);
+
+CREATE TABLE IF NOT EXISTS undo_log (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  branch_id BIGINT NOT NULL,
+  xid VARCHAR(100) NOT NULL,
+  context VARCHAR(128) NOT NULL,
+  rollback_info LONGBLOB NOT NULL,
+  log_status INT NOT NULL,
+  log_created DATETIME NOT NULL,
+  log_modified DATETIME NOT NULL,
+  ext VARCHAR(100) DEFAULT NULL,
+  PRIMARY KEY (id),
+  KEY idx_unionkey (xid, branch_id)

Review Comment:
   Seata’s `undo_log` table typically requires a UNIQUE constraint on `(xid, 
branch_id)` to prevent duplicates and support idempotency/consistency (often 
named like `ux_undo_log`). Using a non-unique index here can allow duplicate 
undo records for the same branch, which may break rollback/cleanup semantics. 
Consider changing these to `UNIQUE KEY ... (xid, branch_id)`.



##########
at/ecommerce/sql/mysql_ecommerce.sql:
##########
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_order DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_inventory DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_account DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+USE seata_ecommerce_order;
+
+CREATE TABLE IF NOT EXISTS order_tbl (
+  id INT NOT NULL AUTO_INCREMENT,
+  user_id VARCHAR(64) NOT NULL,
+  commodity_code VARCHAR(64) NOT NULL,
+  count INT NOT NULL,
+  money INT NOT NULL,
+  status VARCHAR(32) NOT NULL,
+  PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS undo_log (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  branch_id BIGINT NOT NULL,
+  xid VARCHAR(100) NOT NULL,
+  context VARCHAR(128) NOT NULL,
+  rollback_info LONGBLOB NOT NULL,
+  log_status INT NOT NULL,
+  log_created DATETIME NOT NULL,
+  log_modified DATETIME NOT NULL,
+  ext VARCHAR(100) DEFAULT NULL,
+  PRIMARY KEY (id),
+  KEY idx_unionkey (xid, branch_id)

Review Comment:
   Seata’s `undo_log` table typically requires a UNIQUE constraint on `(xid, 
branch_id)` to prevent duplicates and support idempotency/consistency (often 
named like `ux_undo_log`). Using a non-unique index here can allow duplicate 
undo records for the same branch, which may break rollback/cleanup semantics. 
Consider changing these to `UNIQUE KEY ... (xid, branch_id)`.



##########
at/ecommerce/sql/mysql_ecommerce.sql:
##########
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_order DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_inventory DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE DATABASE IF NOT EXISTS seata_ecommerce_account DEFAULT CHARACTER SET 
utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+USE seata_ecommerce_order;
+
+CREATE TABLE IF NOT EXISTS order_tbl (
+  id INT NOT NULL AUTO_INCREMENT,
+  user_id VARCHAR(64) NOT NULL,
+  commodity_code VARCHAR(64) NOT NULL,
+  count INT NOT NULL,
+  money INT NOT NULL,
+  status VARCHAR(32) NOT NULL,
+  PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS undo_log (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  branch_id BIGINT NOT NULL,
+  xid VARCHAR(100) NOT NULL,
+  context VARCHAR(128) NOT NULL,
+  rollback_info LONGBLOB NOT NULL,
+  log_status INT NOT NULL,
+  log_created DATETIME NOT NULL,
+  log_modified DATETIME NOT NULL,
+  ext VARCHAR(100) DEFAULT NULL,
+  PRIMARY KEY (id),
+  KEY idx_unionkey (xid, branch_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+USE seata_ecommerce_inventory;
+
+CREATE TABLE IF NOT EXISTS inventory_tbl (
+  id INT NOT NULL AUTO_INCREMENT,
+  commodity_code VARCHAR(64) NOT NULL,
+  stock INT NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_commodity_code (commodity_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO inventory_tbl (commodity_code, stock) VALUES ('C100001', 100)
+ON DUPLICATE KEY UPDATE stock = VALUES(stock);
+
+CREATE TABLE IF NOT EXISTS undo_log (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  branch_id BIGINT NOT NULL,
+  xid VARCHAR(100) NOT NULL,
+  context VARCHAR(128) NOT NULL,
+  rollback_info LONGBLOB NOT NULL,
+  log_status INT NOT NULL,
+  log_created DATETIME NOT NULL,
+  log_modified DATETIME NOT NULL,
+  ext VARCHAR(100) DEFAULT NULL,
+  PRIMARY KEY (id),
+  KEY idx_unionkey (xid, branch_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+USE seata_ecommerce_account;
+
+CREATE TABLE IF NOT EXISTS account_tbl (
+  id INT NOT NULL AUTO_INCREMENT,
+  user_id VARCHAR(64) NOT NULL,
+  balance INT NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO account_tbl (user_id, balance) VALUES ('U100001', 50)
+ON DUPLICATE KEY UPDATE balance = VALUES(balance);
+
+CREATE TABLE IF NOT EXISTS undo_log (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  branch_id BIGINT NOT NULL,
+  xid VARCHAR(100) NOT NULL,
+  context VARCHAR(128) NOT NULL,
+  rollback_info LONGBLOB NOT NULL,
+  log_status INT NOT NULL,
+  log_created DATETIME NOT NULL,
+  log_modified DATETIME NOT NULL,
+  ext VARCHAR(100) DEFAULT NULL,
+  PRIMARY KEY (id),
+  KEY idx_unionkey (xid, branch_id)

Review Comment:
   Seata’s `undo_log` table typically requires a UNIQUE constraint on `(xid, 
branch_id)` to prevent duplicates and support idempotency/consistency (often 
named like `ux_undo_log`). Using a non-unique index here can allow duplicate 
undo records for the same branch, which may break rollback/cleanup semantics. 
Consider changing these to `UNIQUE KEY ... (xid, branch_id)`.



##########
at/ecommerce/docker-compose.yml:
##########
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+services:
+  mysql:
+    image: mysql:8.0.32
+    container_name: ecommerce_at_mysql
+    environment:
+      MYSQL_ROOT_PASSWORD: 123456

Review Comment:
   Hard-coding the MySQL root password in the compose file is risky, even for 
samples (it gets copy-pasted into real deployments). Prefer sourcing it from an 
`.env` file / environment variable (e.g., `${MYSQL_ROOT_PASSWORD}`) and 
consider using a non-root app user in the sample to model safer defaults.



##########
at/ecommerce/docker-compose.yml:
##########
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+services:
+  mysql:
+    image: mysql:8.0.32
+    container_name: ecommerce_at_mysql
+    environment:
+      MYSQL_ROOT_PASSWORD: 123456
+    command: --default-authentication-plugin=mysql_native_password 
--default-time-zone='+08:00'
+    volumes:
+      - 
./sql/mysql_ecommerce.sql:/docker-entrypoint-initdb.d/1_ecommerce.sql:ro
+      - 
../../dockercompose/mysql/mysqld.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf:ro
+    ports:
+      - "3306:3306"
+
+  seata-server:
+    image: seataio/seata-server:1.6.1
+    container_name: ecommerce_at_seata_server
+    environment:
+      - SEATA_PORT=8091
+      - STORE_MODE=file
+    ports:
+      - "8091:8091"
+      - "7091:7091"
+    depends_on:
+      - mysql

Review Comment:
   `depends_on` only controls container start order and does not guarantee 
MySQL is ready to accept connections. This can cause flaky startup for 
`seata-server` (and any services connecting to MySQL). Add a MySQL healthcheck 
and use `depends_on: condition: service_healthy` (or implement retry/backoff in 
clients) to make the sample more reliable.



##########
at/ecommerce/order/main.go:
##########
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "database/sql"
+       "net/http"
+       "os"
+
+       "github.com/gin-gonic/gin"
+       "seata.apache.org/seata-go-samples/util"
+       "seata.apache.org/seata-go/pkg/client"
+       "seata.apache.org/seata-go/pkg/util/log"
+)
+
+var (
+       db               *sql.DB
+       inventoryService = "http://127.0.0.1:18081";
+       accountService   = "http://127.0.0.1:18082";
+)
+
+func main() {
+       client.InitPath("conf/seatago.yml")
+       setDefaultEnv("MYSQL_DB", "seata_ecommerce_order")
+       if value := os.Getenv("INVENTORY_SERVICE_URL"); value != "" {
+               inventoryService = value
+       }
+       if value := os.Getenv("ACCOUNT_SERVICE_URL"); value != "" {
+               accountService = value
+       }
+       db = util.GetAtMySqlDb()
+
+       r := gin.Default()
+       r.POST("/createOrder", createOrderHandler)
+
+       if err := r.Run(":18080"); err != nil {
+               log.Fatalf("start order service fatal: %v", err)
+       }
+}
+
+func createOrderHandler(c *gin.Context) {
+       log.Infof("receive create order request")
+       if err := createOrder(c); err != nil {
+               c.JSON(http.StatusBadRequest, err.Error())

Review Comment:
   Returning a raw string via `c.JSON` produces a JSON string (quoted) rather 
than a structured error response, which is awkward for API consumers. Consider 
returning a consistent object payload (e.g., `{ \"error\": \"...\" }`) for 
errors (and similarly for success messages).



##########
at/ecommerce/order/main.go:
##########
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "database/sql"
+       "net/http"
+       "os"
+
+       "github.com/gin-gonic/gin"
+       "seata.apache.org/seata-go-samples/util"
+       "seata.apache.org/seata-go/pkg/client"
+       "seata.apache.org/seata-go/pkg/util/log"
+)
+
+var (
+       db               *sql.DB
+       inventoryService = "http://127.0.0.1:18081";
+       accountService   = "http://127.0.0.1:18082";
+)
+
+func main() {
+       client.InitPath("conf/seatago.yml")
+       setDefaultEnv("MYSQL_DB", "seata_ecommerce_order")
+       if value := os.Getenv("INVENTORY_SERVICE_URL"); value != "" {
+               inventoryService = value
+       }
+       if value := os.Getenv("ACCOUNT_SERVICE_URL"); value != "" {
+               accountService = value
+       }
+       db = util.GetAtMySqlDb()
+
+       r := gin.Default()
+       r.POST("/createOrder", createOrderHandler)
+
+       if err := r.Run(":18080"); err != nil {
+               log.Fatalf("start order service fatal: %v", err)
+       }
+}
+
+func createOrderHandler(c *gin.Context) {
+       log.Infof("receive create order request")
+       if err := createOrder(c); err != nil {
+               c.JSON(http.StatusBadRequest, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, "create order ok")
+}
+
+func setDefaultEnv(key string, value string) {
+       if os.Getenv(key) == "" {
+               _ = os.Setenv(key, value)
+       }
+}

Review Comment:
   `setDefaultEnv` is duplicated across order/inventory/account `main.go`. To 
reduce repetition and keep behavior consistent, move this helper into a shared 
package (e.g., `util`) and reuse it from each service.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to