This is an automated email from the ASF dual-hosted git repository.

thunguo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go-samples.git


The following commit(s) were added to refs/heads/main by this push:
     new 7c50b3a  feat(tcc): add ride-order TCC sample with 5 services + 
initiator (#104)
7c50b3a is described below

commit 7c50b3a9151be0e4504ae0de1d90c175eaba8d1a
Author: Gust wang <[email protected]>
AuthorDate: Thu Aug 27 06:06:36 2026 -0400

    feat(tcc): add ride-order TCC sample with 5 services + initiator (#104)
    
    * feat(tcc): add ride-order TCC sample with 5 services + initiator
    
    Implements a ride-hailing order scenario demonstrating the TCC distributed
    transaction pattern. Five independent Gin HTTP services (order, dispatch,
    pricing, coupon, capacity) each implement Try/Confirm/Cancel with real
    MySQL operations and idempotent state transitions. The initiator coordinates
    the full TCC lifecycle via seata-go with XID propagation over HTTP.
    
    Includes failure simulation where dispatch-service fails at Try phase,
    triggering Cancel on all other services to restore resources.
    
    * fix(tcc): use Request.Context() for XID propagation, add per-service 
seatago.yml
    
    - Fix critical bug: proxy.Prepare(c, req) → 
proxy.Prepare(c.Request.Context(), req)
      Gin middleware stores XID in Request.Context, not gin.Context values.
      Without this fix, TCC branch registration silently fails and TC cannot
      call Commit/Rollback on services.
    
    - Add independent seatago.yml per service with unique application-id so
      Seata TC can correctly route branch callbacks to each service process.
    
    - Update README with per-service startup instructions.
    
    * fix(tcc): make ride-order sample re-runnable and data-correct
    
    - coupon-service: select an available coupon dynamically instead of a
      hardcoded id, so repeated runs work (Confirm permanently consumes a
      coupon). Seed data widened to 6 coupons + 6 drivers.
    - order-service: return the generated order_id; the initiator now threads
      the real id into pricing/dispatch instead of hardcoding order_id=1.
    - initiator: drop the infinite `<-make(chan struct{})` block so it exits
      after both scenarios; callService returns the response body.
    - all services: defensive comma-ok type assertion on the TCC params.
    - seatago.yml: disable enable-auto-data-source-proxy (AT-only flag, unused
      by this pure-TCC sample).
    - README: document the in-memory idempotency limitation and TCC fence
      pointer, plus how to reset resources between runs.
    
    Verified end-to-end against MySQL + Seata server: scenario 1 confirms all
    five branches; scenario 2 (dispatch Try fails) cancels order/pricing/
    coupon/capacity and empty-rollbacks dispatch, restoring all resources.
    
    * fix(tcc): use US spelling "canceled" in the ride-order sample
    
    golangci-lint runs misspell with locale: US, which rejects "cancelled".
    It was not surfacing yet because the linter aborts before analysing any
    package while tcc/rocketmq fails to type-check. Renamed in the SQL comment
    and README too so the sample stays self-consistent.
    
    * fix(tcc): address review feedback on the ride-order sample
    
    - dispatch-service: check RowsAffected after reserving a driver. A
      concurrent transaction can take the same driver between the SELECT and
      the UPDATE, which left the branch recording a driver it never reserved
      and reporting success. Mirrors the guard coupon-service already has.
    - initiator: bound every /prepare call with a 10s timeout, so a hung
      service cannot block the global transaction indefinitely.
    - docker-compose: pin seataio/seata-server to 1.6.1 instead of latest,
      matching the other per-sample compose files in the repo.
    
    Re-verified end to end against seata-server 1.6.1 and mysql 8.0.32.
    Scenario 1 commits all five branches; scenario 2 rolls back order,
    pricing, coupon and capacity and empty-rollbacks dispatch. Final state:
    1 order confirmed and 1 canceled, 1 driver busy and 5 available,
    1 fare lock confirmed and 1 released, 1 coupon used and 5 available,
    reserved_slots 1 (scenario 1 kept, scenario 2 released).
---
 tcc/ride-order/README.md                    | 184 ++++++++++++++++++++++++++++
 tcc/ride-order/capacity-service/main.go     |  61 +++++++++
 tcc/ride-order/capacity-service/seatago.yml | 158 ++++++++++++++++++++++++
 tcc/ride-order/capacity-service/service.go  |  94 ++++++++++++++
 tcc/ride-order/common/db.go                 |  55 +++++++++
 tcc/ride-order/coupon-service/main.go       |  61 +++++++++
 tcc/ride-order/coupon-service/seatago.yml   | 158 ++++++++++++++++++++++++
 tcc/ride-order/coupon-service/service.go    | 104 ++++++++++++++++
 tcc/ride-order/dispatch-service/main.go     |  61 +++++++++
 tcc/ride-order/dispatch-service/seatago.yml | 158 ++++++++++++++++++++++++
 tcc/ride-order/dispatch-service/service.go  | 109 ++++++++++++++++
 tcc/ride-order/docker-compose.yml           |  38 ++++++
 tcc/ride-order/initiator/main.go            | 149 ++++++++++++++++++++++
 tcc/ride-order/initiator/seatago.yml        | 158 ++++++++++++++++++++++++
 tcc/ride-order/order-service/main.go        |  69 +++++++++++
 tcc/ride-order/order-service/seatago.yml    | 158 ++++++++++++++++++++++++
 tcc/ride-order/order-service/service.go     |  92 ++++++++++++++
 tcc/ride-order/pricing-service/main.go      |  61 +++++++++
 tcc/ride-order/pricing-service/seatago.yml  | 158 ++++++++++++++++++++++++
 tcc/ride-order/pricing-service/service.go   |  93 ++++++++++++++
 tcc/ride-order/sql/ride_order.sql           |  78 ++++++++++++
 21 files changed, 2257 insertions(+)

diff --git a/tcc/ride-order/README.md b/tcc/ride-order/README.md
new file mode 100644
index 0000000..106f815
--- /dev/null
+++ b/tcc/ride-order/README.md
@@ -0,0 +1,184 @@
+<!--
+    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.
+-->
+
+# TCC Ride Order Sample
+
+A ride-hailing order scenario demonstrating the TCC (Try-Confirm-Cancel) 
distributed
+transaction pattern with five independent services coordinated by seata-go.
+
+## Scenario
+
+A passenger requests a ride. Creating a valid order requires five services to 
each
+reserve their own resources first. Only when all reservations succeed does the 
order
+get confirmed across all services. If any one fails, all already-reserved 
resources
+must be explicitly released.
+
+### Services
+
+| Service | Port | Try (Reserve) | Confirm (Commit) | Cancel (Release) |
+|---------|------|--------------|-------------------|------------------|
+| **order-service** | 8001 | Create pending order | Mark order confirmed | 
Mark order canceled |
+| **dispatch-service** | 8002 | Reserve available driver | Mark driver busy | 
Release driver back to pool |
+| **pricing-service** | 8003 | Lock estimated fare | Confirm fare lock | 
Release fare lock |
+| **coupon-service** | 8004 | Freeze an available coupon | Mark coupon used | 
Unfreeze coupon |
+| **capacity-service** | 8005 | Reserve vehicle slot (+1) | Keep reservation | 
Release slot (-1) |
+
+The **initiator** (ride-order-service) acts as the Transaction Manager (TM) and
+coordinates the full TCC lifecycle via HTTP calls to each service.
+
+## Architecture
+
+```
+                         Seata TC (8091)
+                        ╱  │  │  │  │  ╲
+                       ╱   │  │  │  │   ╲
+  initiator ──HTTP──► order dispatch pricing coupon capacity
+  (TM)                :8001 :8002   :8003  :8004  :8005
+                       │     │       │      │      │
+                       └─────┴───────┴──────┴──────┘
+                                  MySQL
+```
+
+- **Initiator** starts a global transaction and calls each service's 
`/prepare` endpoint via HTTP,
+  passing the XID in the request header.
+- Each **service** registers its TCC branch with Seata TC during Prepare.
+- **Seata TC** calls Commit or Rollback on each service directly via the seata 
protocol (TCP).
+- Each service has its own `seatago.yml` with a unique `application-id` so 
that TC can correctly
+  route branch callbacks to the right service process.
+
+## TCC Lifecycle
+
+### Success Flow
+
+```
+initiator (TM)
+    │
+    ├── POST order-service/prepare       ✓ pending order created
+    ├── POST pricing-service/prepare     ✓ fare locked
+    ├── POST coupon-service/prepare      ✓ coupon frozen
+    ├── POST capacity-service/prepare    ✓ slot reserved
+    └── POST dispatch-service/prepare    ✓ driver reserved
+    
+    All Try succeeded → Seata TC triggers Confirm on all branches:
+    
+    ├── order-service.Confirm()      → order confirmed
+    ├── pricing-service.Confirm()    → fare confirmed
+    ├── coupon-service.Confirm()     → coupon used
+    ├── capacity-service.Confirm()   → slot confirmed
+    └── dispatch-service.Confirm()   → driver assigned
+```
+
+### Failure Flow (dispatch fails)
+
+```
+initiator (TM)
+    │
+    ├── POST order-service/prepare       ✓ pending order created
+    ├── POST pricing-service/prepare     ✓ fare locked
+    ├── POST coupon-service/prepare      ✓ coupon frozen
+    ├── POST capacity-service/prepare    ✓ slot reserved
+    └── POST dispatch-service/prepare    ✗ NO AVAILABLE DRIVER
+    
+    Try failed → Seata TC triggers Cancel on all registered branches:
+    
+    ├── order-service.Cancel()      → order canceled
+    ├── pricing-service.Cancel()    → fare released
+    ├── coupon-service.Cancel()     → coupon unfrozen
+    └── capacity-service.Cancel()   → slot released
+    
+    All resources restored to original state.
+```
+
+## Idempotency
+
+All Confirm and Cancel methods are idempotent:
+
+- **Status-based SQL conditions**: Each UPDATE uses `WHERE status=<expected>`, 
so repeated
+  calls on an already-committed/canceled record affect zero rows and return 
success.
+- **In-memory deduplication**: Each service tracks active transactions in a 
`sync.Map` keyed
+  by XID. Once a Confirm/Cancel completes, the entry is removed. Subsequent 
calls for the
+  same XID find no entry and return success immediately.
+
+> **Limitation (by design, to keep the sample small):** the XID→resource 
mapping lives in
+> process memory. It makes Confirm/Cancel idempotent against TC retries and 
handles *empty
+> rollback* (a branch whose Try failed), but it is **not crash-durable** and 
does **not** guard
+> against TCC *suspension* (a delayed Try arriving after Cancel). If a service 
restarts between
+> Try and the TC callback, the mapping is lost and the reserved resource is 
left stranded.
+> Production code should use the seata-go **TCC fence** (`tcc_fence_log` 
table) instead — see the
+> [`tcc/fence`](../fence) sample.
+
+## How to Run
+
+### 1. Start infrastructure
+
+```bash
+cd tcc/ride-order
+docker-compose up -d
+```
+
+This starts MySQL (with auto-initialized `seata_ride` database) and Seata 
Server.
+
+### 2. Start all five services (each in a separate terminal, from its own 
directory)
+
+```bash
+cd tcc/ride-order/order-service    && go run .   # :8001
+cd tcc/ride-order/dispatch-service && go run .   # :8002
+cd tcc/ride-order/pricing-service  && go run .   # :8003
+cd tcc/ride-order/coupon-service   && go run .   # :8004
+cd tcc/ride-order/capacity-service && go run .   # :8005
+```
+
+Each service must be started from its own directory so it can find its local 
`seatago.yml`.
+
+### 3. Run the initiator
+
+```bash
+cd tcc/ride-order/initiator
+go run .
+```
+
+### 4. Expected output
+
+**Scenario 1** (success): The initiator logs five successful Prepare calls. 
Each
+service logs `[*-Try]` followed by `[*-Confirm]`.
+
+**Scenario 2** (dispatch failure): The initiator logs four successful Prepare 
calls,
+then `dispatch-service prepare failed`. Each of the four services logs 
`[*-Cancel]`,
+restoring all resources.
+
+### Reset between runs
+
+Each successful ride consumes real resources (a coupon becomes *used*, a 
driver becomes
+*busy*) — that is the correct business outcome, so those rows are **not** 
restored on Confirm.
+The seed data (6 coupons, 6 drivers) is enough for several runs. To start 
completely fresh,
+recreate the database volume:
+
+```bash
+docker-compose down -v && docker-compose up -d
+```
+
+### Customize MySQL connection
+
+Override via environment variables (set on each service):
+
+```bash
+export MYSQL_HOST=127.0.0.1
+export MYSQL_PORT=3306
+export MYSQL_USERNAME=root
+export MYSQL_PASSWORD=12345678
+export MYSQL_DB=seata_ride
+```
diff --git a/tcc/ride-order/capacity-service/main.go 
b/tcc/ride-order/capacity-service/main.go
new file mode 100644
index 0000000..2552721
--- /dev/null
+++ b/tcc/ride-order/capacity-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * 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 (
+       "net/http"
+
+       "github.com/gin-gonic/gin"
+
+       "seata.apache.org/seata-go/pkg/client"
+       ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+       "seata.apache.org/seata-go/pkg/rm/tcc"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+       client.InitPath("seatago.yml")
+       common.InitDB()
+
+       r := gin.Default()
+       r.Use(ginmiddleware.TransactionMiddleware())
+
+       proxy, err := tcc.NewTCCServiceProxy(&CapacityService{})
+       if err != nil {
+               log.Fatalf("create CapacityService proxy error: %v", err)
+       }
+
+       r.POST("/prepare", func(c *gin.Context) {
+               var req CapacityRequest
+               if err := c.ShouldBindJSON(&req); err != nil {
+                       c.JSON(http.StatusBadRequest, gin.H{"error": 
err.Error()})
+                       return
+               }
+               if _, err := proxy.Prepare(c.Request.Context(), req); err != 
nil {
+                       c.JSON(http.StatusInternalServerError, gin.H{"error": 
err.Error()})
+                       return
+               }
+               c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+       })
+
+       if err := r.Run(":8005"); err != nil {
+               log.Fatalf("start capacity-service error: %v", err)
+       }
+}
diff --git a/tcc/ride-order/capacity-service/seatago.yml 
b/tcc/ride-order/capacity-service/seatago.yml
new file mode 100644
index 0000000..4476d43
--- /dev/null
+++ b/tcc/ride-order/capacity-service/seatago.yml
@@ -0,0 +1,158 @@
+# 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+  enabled: true
+  # application id
+  application-id: capacity-service
+  # service group
+  tx-service-group: default_tx_group
+  access-key: aliyunAccessKey
+  secret-key: aliyunSecretKey
+  enable-auto-data-source-proxy: false
+  data-source-proxy-mode: AT
+  client:
+    rm:
+      # Maximum cache length of asynchronous queue
+      async-commit-buffer-limit: 10000
+      # The maximum number of retries when report reports the status
+      report-retry-count: 5
+      # The interval for regularly checking the metadata of the db(AT)
+      table-meta-check-enable: false
+      # Whether to report the status if the transaction is successfully 
executed(AT)
+      report-success-enable: false
+      # Whether to allow regular check of db metadata(AT)
+      saga-branch-register-enable: false
+      saga-json-parser: fastjson
+      saga-retry-persist-mode-update: false
+      saga-compensate-persist-mode-update: false
+      #Ordered.HIGHEST_PRECEDENCE + 1000  #
+      tcc-action-interceptor-order: -2147482648
+      # Parse SQL parser selection
+      sql-parser-type: druid
+      lock:
+        retry-interval: 30
+        retry-times: 10
+        retry-policy-branch-rollback-on-conflict: true
+    tm:
+      commit-retry-count: 5
+      rollback-retry-count: 5
+      default-global-transaction-timeout: 60s
+      degrade-check: false
+      degrade-check-period: 2000
+      degrade-check-allow-times: 10s
+      interceptor-order: -2147482648
+    undo:
+      # Judge whether the before image and after image are the same,If it is 
the same, undo will not be recorded
+      data-validation: true
+      # Serialization method
+      log-serialization: json
+      # undo log table name
+      log-table: undo_log
+      # Only store modified fields
+      only-care-update-columns: true
+      compress:
+        # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2, 
Lz4, Zstd, Deflate
+        type: None
+        #  Compression threshold Unit: k
+        threshold: 64k
+    load-balance:
+      type: RandomLoadBalance
+      virtual-nodes: 10
+  service:
+    vgroup-mapping:
+      # Prefix for Print Log
+      default_tx_group: default
+    grouplist:
+      default: 127.0.0.1:8091
+    enable-degrade: false
+    # close the transaction
+    disable-global-transaction: false
+  transport:
+    shutdown:
+      wait: 3s
+    # Netty related configurations
+    # type
+    type: TCP
+    server: NIO
+    heartbeat: true
+    # Encoding and decoding mode
+    serialization: seata
+    # Message compression mode
+    compressor: none
+    # Allow batch sending of requests (TM)
+    enable-tm-client-batch-send-request: false
+    # Allow batch sending of requests (RM)
+    enable-rm-client-batch-send-request: true
+    # RM send request timeout
+    rpc-rm-request-timeout: 30s
+    # TM send request timeout
+    rpc-tm-request-timeout: 30s
+  # Configuration Center
+  config:
+    type: file
+    file:
+      name: config.conf
+    nacos:
+      namespace: ""
+      server-addr: 127.0.0.1:8848
+      group: SEATA_GROUP
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute
+      #access-key: ""
+      #secret-key: ""
+      data-id: seata.properties
+  # Registration Center
+  registry:
+    type: file
+    file:
+      name: registry.conf
+    nacos:
+      application: seata-server
+      server-addr: 127.0.0.1:8848
+      group: "SEATA_GROUP"
+      namespace: ""
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute  #
+      #access-key: ""  #
+      #secret-key: ""  #
+  log:
+    exception-rate: 100
+  tcc:
+    fence:
+      # Anti suspension table name
+      log-table-name: tcc_fence_log_test
+      clean-period: 60s
+  # getty configuration
+  getty:
+    reconnect-interval: 0
+    # temporary not supported connection-num
+    connection-num: 1
+    session:
+      compress-encoding: false
+      tcp-no-delay: true
+      tcp-keep-alive: true
+      keep-alive-period: 120s
+      tcp-r-buf-size: 262144
+      tcp-w-buf-size: 65536
+      tcp-read-timeout: 1s
+      tcp-write-timeout: 5s
+      wait-timeout: 1s
+      max-msg-len: 16498688
+      session-name: client_test
+      cron-period: 1s
diff --git a/tcc/ride-order/capacity-service/service.go 
b/tcc/ride-order/capacity-service/service.go
new file mode 100644
index 0000000..d1968b9
--- /dev/null
+++ b/tcc/ride-order/capacity-service/service.go
@@ -0,0 +1,94 @@
+/*
+ * 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"
+       "fmt"
+       "sync"
+
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type CapacityRequest struct {
+       CapacityID int64 `json:"capacity_id"`
+}
+
+type CapacityService struct{}
+
+// capacityRecords tracks xid -> capacityID for Commit/Rollback lookup.
+var capacityRecords sync.Map
+
+func (s *CapacityService) GetActionName() string {
+       return "CapacityService"
+}
+
+// Prepare reserves a vehicle slot: reserved_slots + 1 (Try phase).
+func (s *CapacityService) Prepare(ctx context.Context, params interface{}) 
(bool, error) {
+       req, ok := params.(CapacityRequest)
+       if !ok {
+               return false, fmt.Errorf("invalid params type %T, want 
CapacityRequest", params)
+       }
+       result, err := common.DB.ExecContext(ctx,
+               "UPDATE vehicle_capacity SET reserved_slots=reserved_slots+1 
WHERE id=? AND reserved_slots<total_slots",
+               req.CapacityID)
+       if err != nil {
+               return false, fmt.Errorf("capacity prepare failed: %v", err)
+       }
+       rows, _ := result.RowsAffected()
+       if rows == 0 {
+               return false, fmt.Errorf("vehicle capacity %d full, no slots 
available", req.CapacityID)
+       }
+       xid := tm.GetXID(ctx)
+       capacityRecords.Store(xid, req.CapacityID)
+       log.Infof("[Capacity-Try] reserved 1 slot on vehicle capacity %d, 
xid=%s", req.CapacityID, xid)
+       return true, nil
+}
+
+// Commit keeps the reservation as-is (reservation is the final state).
+func (s *CapacityService) Commit(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       capacityID, ok := capacityRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Capacity-Confirm] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       capacityRecords.Delete(bac.Xid)
+       log.Infof("[Capacity-Confirm] slot reservation confirmed on capacity 
%d, xid=%s", capacityID, bac.Xid)
+       return true, nil
+}
+
+// Rollback releases the vehicle slot: reserved_slots - 1. Idempotent via 
reserved_slots>0 check.
+func (s *CapacityService) Rollback(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       capacityID, ok := capacityRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Capacity-Cancel] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec(
+               "UPDATE vehicle_capacity SET reserved_slots=reserved_slots-1 
WHERE id=? AND reserved_slots>0",
+               capacityID)
+       if err != nil {
+               return false, fmt.Errorf("capacity cancel failed: %v", err)
+       }
+       capacityRecords.Delete(bac.Xid)
+       log.Infof("[Capacity-Cancel] slot released on capacity %d, xid=%s", 
capacityID, bac.Xid)
+       return true, nil
+}
diff --git a/tcc/ride-order/common/db.go b/tcc/ride-order/common/db.go
new file mode 100644
index 0000000..a767ba5
--- /dev/null
+++ b/tcc/ride-order/common/db.go
@@ -0,0 +1,55 @@
+/*
+ * 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 common
+
+import (
+       "database/sql"
+       "os"
+
+       _ "github.com/go-sql-driver/mysql"
+)
+
+var DB *sql.DB
+
+func InitDB() {
+       defaultEnv()
+       dsn := 
os.ExpandEnv("${MYSQL_USERNAME}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DB}?parseTime=true")
+       var err error
+       DB, err = sql.Open("mysql", dsn)
+       if err != nil {
+               panic("open mysql failed: " + err.Error())
+       }
+       if err = DB.Ping(); err != nil {
+               panic("ping mysql failed: " + err.Error())
+       }
+}
+
+func defaultEnv() {
+       envDefaults := map[string]string{
+               "MYSQL_HOST":     "127.0.0.1",
+               "MYSQL_PORT":     "3306",
+               "MYSQL_USERNAME": "root",
+               "MYSQL_PASSWORD": "12345678",
+               "MYSQL_DB":       "seata_ride",
+       }
+       for k, v := range envDefaults {
+               if os.Getenv(k) == "" {
+                       os.Setenv(k, v)
+               }
+       }
+}
diff --git a/tcc/ride-order/coupon-service/main.go 
b/tcc/ride-order/coupon-service/main.go
new file mode 100644
index 0000000..a9e24fe
--- /dev/null
+++ b/tcc/ride-order/coupon-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * 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 (
+       "net/http"
+
+       "github.com/gin-gonic/gin"
+
+       "seata.apache.org/seata-go/pkg/client"
+       ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+       "seata.apache.org/seata-go/pkg/rm/tcc"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+       client.InitPath("seatago.yml")
+       common.InitDB()
+
+       r := gin.Default()
+       r.Use(ginmiddleware.TransactionMiddleware())
+
+       proxy, err := tcc.NewTCCServiceProxy(&CouponService{})
+       if err != nil {
+               log.Fatalf("create CouponService proxy error: %v", err)
+       }
+
+       r.POST("/prepare", func(c *gin.Context) {
+               var req CouponRequest
+               if err := c.ShouldBindJSON(&req); err != nil {
+                       c.JSON(http.StatusBadRequest, gin.H{"error": 
err.Error()})
+                       return
+               }
+               if _, err := proxy.Prepare(c.Request.Context(), req); err != 
nil {
+                       c.JSON(http.StatusInternalServerError, gin.H{"error": 
err.Error()})
+                       return
+               }
+               c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+       })
+
+       if err := r.Run(":8004"); err != nil {
+               log.Fatalf("start coupon-service error: %v", err)
+       }
+}
diff --git a/tcc/ride-order/coupon-service/seatago.yml 
b/tcc/ride-order/coupon-service/seatago.yml
new file mode 100644
index 0000000..53161af
--- /dev/null
+++ b/tcc/ride-order/coupon-service/seatago.yml
@@ -0,0 +1,158 @@
+# 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+  enabled: true
+  # application id
+  application-id: coupon-service
+  # service group
+  tx-service-group: default_tx_group
+  access-key: aliyunAccessKey
+  secret-key: aliyunSecretKey
+  enable-auto-data-source-proxy: false
+  data-source-proxy-mode: AT
+  client:
+    rm:
+      # Maximum cache length of asynchronous queue
+      async-commit-buffer-limit: 10000
+      # The maximum number of retries when report reports the status
+      report-retry-count: 5
+      # The interval for regularly checking the metadata of the db(AT)
+      table-meta-check-enable: false
+      # Whether to report the status if the transaction is successfully 
executed(AT)
+      report-success-enable: false
+      # Whether to allow regular check of db metadata(AT)
+      saga-branch-register-enable: false
+      saga-json-parser: fastjson
+      saga-retry-persist-mode-update: false
+      saga-compensate-persist-mode-update: false
+      #Ordered.HIGHEST_PRECEDENCE + 1000  #
+      tcc-action-interceptor-order: -2147482648
+      # Parse SQL parser selection
+      sql-parser-type: druid
+      lock:
+        retry-interval: 30
+        retry-times: 10
+        retry-policy-branch-rollback-on-conflict: true
+    tm:
+      commit-retry-count: 5
+      rollback-retry-count: 5
+      default-global-transaction-timeout: 60s
+      degrade-check: false
+      degrade-check-period: 2000
+      degrade-check-allow-times: 10s
+      interceptor-order: -2147482648
+    undo:
+      # Judge whether the before image and after image are the same,If it is 
the same, undo will not be recorded
+      data-validation: true
+      # Serialization method
+      log-serialization: json
+      # undo log table name
+      log-table: undo_log
+      # Only store modified fields
+      only-care-update-columns: true
+      compress:
+        # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2, 
Lz4, Zstd, Deflate
+        type: None
+        #  Compression threshold Unit: k
+        threshold: 64k
+    load-balance:
+      type: RandomLoadBalance
+      virtual-nodes: 10
+  service:
+    vgroup-mapping:
+      # Prefix for Print Log
+      default_tx_group: default
+    grouplist:
+      default: 127.0.0.1:8091
+    enable-degrade: false
+    # close the transaction
+    disable-global-transaction: false
+  transport:
+    shutdown:
+      wait: 3s
+    # Netty related configurations
+    # type
+    type: TCP
+    server: NIO
+    heartbeat: true
+    # Encoding and decoding mode
+    serialization: seata
+    # Message compression mode
+    compressor: none
+    # Allow batch sending of requests (TM)
+    enable-tm-client-batch-send-request: false
+    # Allow batch sending of requests (RM)
+    enable-rm-client-batch-send-request: true
+    # RM send request timeout
+    rpc-rm-request-timeout: 30s
+    # TM send request timeout
+    rpc-tm-request-timeout: 30s
+  # Configuration Center
+  config:
+    type: file
+    file:
+      name: config.conf
+    nacos:
+      namespace: ""
+      server-addr: 127.0.0.1:8848
+      group: SEATA_GROUP
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute
+      #access-key: ""
+      #secret-key: ""
+      data-id: seata.properties
+  # Registration Center
+  registry:
+    type: file
+    file:
+      name: registry.conf
+    nacos:
+      application: seata-server
+      server-addr: 127.0.0.1:8848
+      group: "SEATA_GROUP"
+      namespace: ""
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute  #
+      #access-key: ""  #
+      #secret-key: ""  #
+  log:
+    exception-rate: 100
+  tcc:
+    fence:
+      # Anti suspension table name
+      log-table-name: tcc_fence_log_test
+      clean-period: 60s
+  # getty configuration
+  getty:
+    reconnect-interval: 0
+    # temporary not supported connection-num
+    connection-num: 1
+    session:
+      compress-encoding: false
+      tcp-no-delay: true
+      tcp-keep-alive: true
+      keep-alive-period: 120s
+      tcp-r-buf-size: 262144
+      tcp-w-buf-size: 65536
+      tcp-read-timeout: 1s
+      tcp-write-timeout: 5s
+      wait-timeout: 1s
+      max-msg-len: 16498688
+      session-name: client_test
+      cron-period: 1s
diff --git a/tcc/ride-order/coupon-service/service.go 
b/tcc/ride-order/coupon-service/service.go
new file mode 100644
index 0000000..2cc1c02
--- /dev/null
+++ b/tcc/ride-order/coupon-service/service.go
@@ -0,0 +1,104 @@
+/*
+ * 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"
+       "fmt"
+       "sync"
+
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type CouponRequest struct {
+       PassengerID string `json:"passenger_id"`
+}
+
+type CouponService struct{}
+
+// couponRecords tracks xid -> couponID for Commit/Rollback lookup.
+var couponRecords sync.Map
+
+func (s *CouponService) GetActionName() string {
+       return "CouponService"
+}
+
+// Prepare freezes an available coupon for the passenger: available -> frozen 
(Try phase).
+// The coupon is selected dynamically so the sample stays re-runnable (no 
hardcoded id
+// that gets permanently consumed after the first Confirm).
+func (s *CouponService) Prepare(ctx context.Context, params interface{}) 
(bool, error) {
+       req, ok := params.(CouponRequest)
+       if !ok {
+               return false, fmt.Errorf("invalid params type %T, want 
CouponRequest", params)
+       }
+
+       var couponID int64
+       err := common.DB.QueryRowContext(ctx,
+               "SELECT id FROM coupons WHERE user_id=? AND status=0 ORDER BY 
id LIMIT 1", req.PassengerID).Scan(&couponID)
+       if err != nil {
+               return false, fmt.Errorf("no available coupon for %s: %v", 
req.PassengerID, err)
+       }
+       // Guard against a concurrent transaction grabbing the same coupon.
+       result, err := common.DB.ExecContext(ctx,
+               "UPDATE coupons SET status=1 WHERE id=? AND status=0", couponID)
+       if err != nil {
+               return false, fmt.Errorf("coupon prepare failed: %v", err)
+       }
+       if rows, _ := result.RowsAffected(); rows == 0 {
+               return false, fmt.Errorf("coupon %d no longer available", 
couponID)
+       }
+       xid := tm.GetXID(ctx)
+       couponRecords.Store(xid, couponID)
+       log.Infof("[Coupon-Try] froze coupon %d for %s, xid=%s", couponID, 
req.PassengerID, xid)
+       return true, nil
+}
+
+// Commit uses the coupon: frozen -> used. Idempotent via status check.
+func (s *CouponService) Commit(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       couponID, ok := couponRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Coupon-Confirm] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE coupons SET status=2 WHERE id=? AND 
status=1", couponID)
+       if err != nil {
+               return false, fmt.Errorf("coupon confirm failed: %v", err)
+       }
+       couponRecords.Delete(bac.Xid)
+       log.Infof("[Coupon-Confirm] coupon %d used, xid=%s", couponID, bac.Xid)
+       return true, nil
+}
+
+// Rollback unfreezes the coupon: frozen -> available. Idempotent via status 
check.
+func (s *CouponService) Rollback(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       couponID, ok := couponRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Coupon-Cancel] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE coupons SET status=0 WHERE id=? AND 
status=1", couponID)
+       if err != nil {
+               return false, fmt.Errorf("coupon cancel failed: %v", err)
+       }
+       couponRecords.Delete(bac.Xid)
+       log.Infof("[Coupon-Cancel] coupon %d unfrozen, xid=%s", couponID, 
bac.Xid)
+       return true, nil
+}
diff --git a/tcc/ride-order/dispatch-service/main.go 
b/tcc/ride-order/dispatch-service/main.go
new file mode 100644
index 0000000..2cce5b8
--- /dev/null
+++ b/tcc/ride-order/dispatch-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * 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 (
+       "net/http"
+
+       "github.com/gin-gonic/gin"
+
+       "seata.apache.org/seata-go/pkg/client"
+       ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+       "seata.apache.org/seata-go/pkg/rm/tcc"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+       client.InitPath("seatago.yml")
+       common.InitDB()
+
+       r := gin.Default()
+       r.Use(ginmiddleware.TransactionMiddleware())
+
+       proxy, err := tcc.NewTCCServiceProxy(&DispatchService{})
+       if err != nil {
+               log.Fatalf("create DispatchService proxy error: %v", err)
+       }
+
+       r.POST("/prepare", func(c *gin.Context) {
+               var req DispatchRequest
+               if err := c.ShouldBindJSON(&req); err != nil {
+                       c.JSON(http.StatusBadRequest, gin.H{"error": 
err.Error()})
+                       return
+               }
+               if _, err := proxy.Prepare(c.Request.Context(), req); err != 
nil {
+                       c.JSON(http.StatusInternalServerError, gin.H{"error": 
err.Error()})
+                       return
+               }
+               c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+       })
+
+       if err := r.Run(":8002"); err != nil {
+               log.Fatalf("start dispatch-service error: %v", err)
+       }
+}
diff --git a/tcc/ride-order/dispatch-service/seatago.yml 
b/tcc/ride-order/dispatch-service/seatago.yml
new file mode 100644
index 0000000..122d4e3
--- /dev/null
+++ b/tcc/ride-order/dispatch-service/seatago.yml
@@ -0,0 +1,158 @@
+# 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+  enabled: true
+  # application id
+  application-id: dispatch-service
+  # service group
+  tx-service-group: default_tx_group
+  access-key: aliyunAccessKey
+  secret-key: aliyunSecretKey
+  enable-auto-data-source-proxy: false
+  data-source-proxy-mode: AT
+  client:
+    rm:
+      # Maximum cache length of asynchronous queue
+      async-commit-buffer-limit: 10000
+      # The maximum number of retries when report reports the status
+      report-retry-count: 5
+      # The interval for regularly checking the metadata of the db(AT)
+      table-meta-check-enable: false
+      # Whether to report the status if the transaction is successfully 
executed(AT)
+      report-success-enable: false
+      # Whether to allow regular check of db metadata(AT)
+      saga-branch-register-enable: false
+      saga-json-parser: fastjson
+      saga-retry-persist-mode-update: false
+      saga-compensate-persist-mode-update: false
+      #Ordered.HIGHEST_PRECEDENCE + 1000  #
+      tcc-action-interceptor-order: -2147482648
+      # Parse SQL parser selection
+      sql-parser-type: druid
+      lock:
+        retry-interval: 30
+        retry-times: 10
+        retry-policy-branch-rollback-on-conflict: true
+    tm:
+      commit-retry-count: 5
+      rollback-retry-count: 5
+      default-global-transaction-timeout: 60s
+      degrade-check: false
+      degrade-check-period: 2000
+      degrade-check-allow-times: 10s
+      interceptor-order: -2147482648
+    undo:
+      # Judge whether the before image and after image are the same,If it is 
the same, undo will not be recorded
+      data-validation: true
+      # Serialization method
+      log-serialization: json
+      # undo log table name
+      log-table: undo_log
+      # Only store modified fields
+      only-care-update-columns: true
+      compress:
+        # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2, 
Lz4, Zstd, Deflate
+        type: None
+        #  Compression threshold Unit: k
+        threshold: 64k
+    load-balance:
+      type: RandomLoadBalance
+      virtual-nodes: 10
+  service:
+    vgroup-mapping:
+      # Prefix for Print Log
+      default_tx_group: default
+    grouplist:
+      default: 127.0.0.1:8091
+    enable-degrade: false
+    # close the transaction
+    disable-global-transaction: false
+  transport:
+    shutdown:
+      wait: 3s
+    # Netty related configurations
+    # type
+    type: TCP
+    server: NIO
+    heartbeat: true
+    # Encoding and decoding mode
+    serialization: seata
+    # Message compression mode
+    compressor: none
+    # Allow batch sending of requests (TM)
+    enable-tm-client-batch-send-request: false
+    # Allow batch sending of requests (RM)
+    enable-rm-client-batch-send-request: true
+    # RM send request timeout
+    rpc-rm-request-timeout: 30s
+    # TM send request timeout
+    rpc-tm-request-timeout: 30s
+  # Configuration Center
+  config:
+    type: file
+    file:
+      name: config.conf
+    nacos:
+      namespace: ""
+      server-addr: 127.0.0.1:8848
+      group: SEATA_GROUP
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute
+      #access-key: ""
+      #secret-key: ""
+      data-id: seata.properties
+  # Registration Center
+  registry:
+    type: file
+    file:
+      name: registry.conf
+    nacos:
+      application: seata-server
+      server-addr: 127.0.0.1:8848
+      group: "SEATA_GROUP"
+      namespace: ""
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute  #
+      #access-key: ""  #
+      #secret-key: ""  #
+  log:
+    exception-rate: 100
+  tcc:
+    fence:
+      # Anti suspension table name
+      log-table-name: tcc_fence_log_test
+      clean-period: 60s
+  # getty configuration
+  getty:
+    reconnect-interval: 0
+    # temporary not supported connection-num
+    connection-num: 1
+    session:
+      compress-encoding: false
+      tcp-no-delay: true
+      tcp-keep-alive: true
+      keep-alive-period: 120s
+      tcp-r-buf-size: 262144
+      tcp-w-buf-size: 65536
+      tcp-read-timeout: 1s
+      tcp-write-timeout: 5s
+      wait-timeout: 1s
+      max-msg-len: 16498688
+      session-name: client_test
+      cron-period: 1s
diff --git a/tcc/ride-order/dispatch-service/service.go 
b/tcc/ride-order/dispatch-service/service.go
new file mode 100644
index 0000000..2cb489d
--- /dev/null
+++ b/tcc/ride-order/dispatch-service/service.go
@@ -0,0 +1,109 @@
+/*
+ * 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"
+       "fmt"
+       "sync"
+
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type DispatchRequest struct {
+       OrderID      int64 `json:"order_id"`
+       SimulateFail bool  `json:"simulate_fail"`
+}
+
+type DispatchService struct{}
+
+// dispatchRecords tracks xid -> driverID for Commit/Rollback lookup.
+var dispatchRecords sync.Map
+
+func (s *DispatchService) GetActionName() string {
+       return "DispatchService"
+}
+
+// Prepare reserves an available driver (Try phase).
+// When SimulateFail is true, returns an error to trigger global rollback.
+func (s *DispatchService) Prepare(ctx context.Context, params interface{}) 
(bool, error) {
+       req, ok := params.(DispatchRequest)
+       if !ok {
+               return false, fmt.Errorf("invalid params type %T, want 
DispatchRequest", params)
+       }
+       if req.SimulateFail {
+               log.Infof("[Dispatch-Try] SIMULATED FAILURE: no available 
driver, xid=%s", tm.GetXID(ctx))
+               return false, fmt.Errorf("no available driver found (simulated 
failure)")
+       }
+
+       var driverID int64
+       err := common.DB.QueryRowContext(ctx,
+               "SELECT id FROM drivers WHERE status=0 LIMIT 1").Scan(&driverID)
+       if err != nil {
+               return false, fmt.Errorf("no available driver: %v", err)
+       }
+       // Guard against a concurrent transaction grabbing the same driver.
+       result, err := common.DB.ExecContext(ctx,
+               "UPDATE drivers SET status=1, reserved_order_id=? WHERE id=? 
AND status=0",
+               req.OrderID, driverID)
+       if err != nil {
+               return false, fmt.Errorf("dispatch prepare failed: %v", err)
+       }
+       if rows, _ := result.RowsAffected(); rows == 0 {
+               return false, fmt.Errorf("driver %d no longer available", 
driverID)
+       }
+       xid := tm.GetXID(ctx)
+       dispatchRecords.Store(xid, driverID)
+       log.Infof("[Dispatch-Try] reserved driver %d for order %d, xid=%s", 
driverID, req.OrderID, xid)
+       return true, nil
+}
+
+// Commit confirms the driver: reserved -> busy. Idempotent via status check.
+func (s *DispatchService) Commit(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       driverID, ok := dispatchRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Dispatch-Confirm] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE drivers SET status=2 WHERE id=? AND 
status=1", driverID)
+       if err != nil {
+               return false, fmt.Errorf("dispatch confirm failed: %v", err)
+       }
+       dispatchRecords.Delete(bac.Xid)
+       log.Infof("[Dispatch-Confirm] driver %d now busy, xid=%s", driverID, 
bac.Xid)
+       return true, nil
+}
+
+// Rollback releases the driver: reserved -> available. Idempotent via status 
check.
+func (s *DispatchService) Rollback(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       driverID, ok := dispatchRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Dispatch-Cancel] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE drivers SET status=0, 
reserved_order_id=NULL WHERE id=? AND status=1", driverID)
+       if err != nil {
+               return false, fmt.Errorf("dispatch cancel failed: %v", err)
+       }
+       dispatchRecords.Delete(bac.Xid)
+       log.Infof("[Dispatch-Cancel] driver %d released, xid=%s", driverID, 
bac.Xid)
+       return true, nil
+}
diff --git a/tcc/ride-order/docker-compose.yml 
b/tcc/ride-order/docker-compose.yml
new file mode 100644
index 0000000..7c197db
--- /dev/null
+++ b/tcc/ride-order/docker-compose.yml
@@ -0,0 +1,38 @@
+#
+# 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.
+#
+
+version: '3'
+services:
+  seata-server:
+    image: seataio/seata-server:1.6.1
+    ports:
+      - "8091:8091"
+      - "7091:7091"
+    environment:
+      - SEATA_PORT=8091
+      - STORE_MODE=file
+
+  mysql:
+    image: mysql:8.0.32
+    container_name: ride-order-mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=12345678
+    command: --default-authentication-plugin=mysql_native_password 
--default-time-zone='+08:00'
+    volumes:
+      - ./sql:/docker-entrypoint-initdb.d
+    ports:
+      - "3306:3306"
diff --git a/tcc/ride-order/initiator/main.go b/tcc/ride-order/initiator/main.go
new file mode 100644
index 0000000..b66aa2f
--- /dev/null
+++ b/tcc/ride-order/initiator/main.go
@@ -0,0 +1,149 @@
+/*
+ * 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"
+       "time"
+
+       "github.com/parnurzeal/gorequest"
+
+       "seata.apache.org/seata-go/pkg/client"
+       "seata.apache.org/seata-go/pkg/constant"
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+)
+
+const (
+       orderServiceURL    = "http://127.0.0.1:8001";
+       dispatchServiceURL = "http://127.0.0.1:8002";
+       pricingServiceURL  = "http://127.0.0.1:8003";
+       couponServiceURL   = "http://127.0.0.1:8004";
+       capacityServiceURL = "http://127.0.0.1:8005";
+
+       // Bound each Try call so a hung service cannot block the global 
transaction.
+       prepareTimeout = 10 * time.Second
+)
+
+func main() {
+       client.InitPath("seatago.yml")
+       ctx := context.Background()
+
+       // ========== Scenario 1: All services succeed ==========
+       fmt.Println("\n========== Scenario 1: All services succeed ==========")
+
+       err := tm.WithGlobalTx(ctx, &tm.GtxConfig{
+               Name:    "RideOrderSuccess",
+               Timeout: 60 * time.Second,
+       }, func(ctx context.Context) error {
+               return rideOrderBusiness(ctx, false)
+       })
+       if err != nil {
+               log.Errorf("Scenario 1 FAILED: %v", err)
+       } else {
+               log.Infof("Scenario 1 SUCCESS: ride order confirmed")
+       }
+
+       // Wait for Seata async Confirm callbacks
+       time.Sleep(3 * time.Second)
+
+       // ========== Scenario 2: Dispatch fails, triggers global rollback 
==========
+       fmt.Println("\n========== Scenario 2: Dispatch fails, triggers global 
rollback ==========")
+
+       err = tm.WithGlobalTx(ctx, &tm.GtxConfig{
+               Name:    "RideOrderDispatchFail",
+               Timeout: 60 * time.Second,
+       }, func(ctx context.Context) error {
+               return rideOrderBusiness(ctx, true)
+       })
+       if err != nil {
+               log.Infof("Scenario 2 EXPECTED FAILURE: %v", err)
+               log.Infof("All resources should be released via Cancel methods")
+       } else {
+               log.Errorf("Scenario 2 should have failed but succeeded")
+       }
+
+       // Wait for Cancel callbacks
+       time.Sleep(3 * time.Second)
+       fmt.Println("\n========== Done ==========")
+}
+
+func rideOrderBusiness(ctx context.Context, simulateDispatchFail bool) error {
+       xid := tm.GetXID(ctx)
+
+       // 1. Create pending order — order-service returns the generated order 
id.
+       body, err := callService(ctx, orderServiceURL, 
`{"passenger_id":"passenger-001"}`)
+       if err != nil {
+               return fmt.Errorf("order-service prepare failed: %v", err)
+       }
+       var order struct {
+               OrderID int64 `json:"order_id"`
+       }
+       if err := json.Unmarshal([]byte(body), &order); err != nil || 
order.OrderID == 0 {
+               return fmt.Errorf("order-service returned no order id: body=%s 
err=%v", body, err)
+       }
+       log.Infof("[Initiator] order-service prepared, order_id=%d, xid=%s", 
order.OrderID, xid)
+
+       // 2. Lock estimated fare for the real order id.
+       if _, err := callService(ctx, pricingServiceURL, fmt.Sprintf(
+               `{"order_id":%d,"amount":2800}`, order.OrderID)); err != nil {
+               return fmt.Errorf("pricing-service prepare failed: %v", err)
+       }
+       log.Infof("[Initiator] pricing-service prepared, xid=%s", xid)
+
+       // 3. Freeze an available coupon for the passenger (selected 
dynamically by the service).
+       if _, err := callService(ctx, couponServiceURL, 
`{"passenger_id":"passenger-001"}`); err != nil {
+               return fmt.Errorf("coupon-service prepare failed: %v", err)
+       }
+       log.Infof("[Initiator] coupon-service prepared, xid=%s", xid)
+
+       // 4. Reserve vehicle slot.
+       if _, err := callService(ctx, capacityServiceURL, `{"capacity_id":1}`); 
err != nil {
+               return fmt.Errorf("capacity-service prepare failed: %v", err)
+       }
+       log.Infof("[Initiator] capacity-service prepared, xid=%s", xid)
+
+       // 5. Reserve driver — THIS WILL FAIL in scenario 2.
+       if _, err := callService(ctx, dispatchServiceURL, fmt.Sprintf(
+               `{"order_id":%d,"simulate_fail":%t}`, order.OrderID, 
simulateDispatchFail)); err != nil {
+               return fmt.Errorf("dispatch-service prepare failed: %v", err)
+       }
+       log.Infof("[Initiator] dispatch-service prepared, xid=%s", xid)
+
+       return nil
+}
+
+func callService(ctx context.Context, serviceURL string, jsonBody string) 
(string, error) {
+       resp, body, errs := gorequest.New().
+               Timeout(prepareTimeout).
+               Post(serviceURL+"/prepare").
+               Set(constant.XidKey, tm.GetXID(ctx)).
+               Type("json").
+               Send(jsonBody).
+               End()
+       if len(errs) != 0 {
+               return "", errs[0]
+       }
+       if resp.StatusCode != http.StatusOK {
+               return "", fmt.Errorf("service returned %d: %s", 
resp.StatusCode, body)
+       }
+       return body, nil
+}
diff --git a/tcc/ride-order/initiator/seatago.yml 
b/tcc/ride-order/initiator/seatago.yml
new file mode 100644
index 0000000..ba1a62e
--- /dev/null
+++ b/tcc/ride-order/initiator/seatago.yml
@@ -0,0 +1,158 @@
+# 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+  enabled: true
+  # application id
+  application-id: ride-order-initiator
+  # service group
+  tx-service-group: default_tx_group
+  access-key: aliyunAccessKey
+  secret-key: aliyunSecretKey
+  enable-auto-data-source-proxy: false
+  data-source-proxy-mode: AT
+  client:
+    rm:
+      # Maximum cache length of asynchronous queue
+      async-commit-buffer-limit: 10000
+      # The maximum number of retries when report reports the status
+      report-retry-count: 5
+      # The interval for regularly checking the metadata of the db(AT)
+      table-meta-check-enable: false
+      # Whether to report the status if the transaction is successfully 
executed(AT)
+      report-success-enable: false
+      # Whether to allow regular check of db metadata(AT)
+      saga-branch-register-enable: false
+      saga-json-parser: fastjson
+      saga-retry-persist-mode-update: false
+      saga-compensate-persist-mode-update: false
+      #Ordered.HIGHEST_PRECEDENCE + 1000  #
+      tcc-action-interceptor-order: -2147482648
+      # Parse SQL parser selection
+      sql-parser-type: druid
+      lock:
+        retry-interval: 30
+        retry-times: 10
+        retry-policy-branch-rollback-on-conflict: true
+    tm:
+      commit-retry-count: 5
+      rollback-retry-count: 5
+      default-global-transaction-timeout: 60s
+      degrade-check: false
+      degrade-check-period: 2000
+      degrade-check-allow-times: 10s
+      interceptor-order: -2147482648
+    undo:
+      # Judge whether the before image and after image are the same,If it is 
the same, undo will not be recorded
+      data-validation: true
+      # Serialization method
+      log-serialization: json
+      # undo log table name
+      log-table: undo_log
+      # Only store modified fields
+      only-care-update-columns: true
+      compress:
+        # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2, 
Lz4, Zstd, Deflate
+        type: None
+        #  Compression threshold Unit: k
+        threshold: 64k
+    load-balance:
+      type: RandomLoadBalance
+      virtual-nodes: 10
+  service:
+    vgroup-mapping:
+      # Prefix for Print Log
+      default_tx_group: default
+    grouplist:
+      default: 127.0.0.1:8091
+    enable-degrade: false
+    # close the transaction
+    disable-global-transaction: false
+  transport:
+    shutdown:
+      wait: 3s
+    # Netty related configurations
+    # type
+    type: TCP
+    server: NIO
+    heartbeat: true
+    # Encoding and decoding mode
+    serialization: seata
+    # Message compression mode
+    compressor: none
+    # Allow batch sending of requests (TM)
+    enable-tm-client-batch-send-request: false
+    # Allow batch sending of requests (RM)
+    enable-rm-client-batch-send-request: true
+    # RM send request timeout
+    rpc-rm-request-timeout: 30s
+    # TM send request timeout
+    rpc-tm-request-timeout: 30s
+  # Configuration Center
+  config:
+    type: file
+    file:
+      name: config.conf
+    nacos:
+      namespace: ""
+      server-addr: 127.0.0.1:8848
+      group: SEATA_GROUP
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute
+      #access-key: ""
+      #secret-key: ""
+      data-id: seata.properties
+  # Registration Center
+  registry:
+    type: file
+    file:
+      name: registry.conf
+    nacos:
+      application: seata-server
+      server-addr: 127.0.0.1:8848
+      group: "SEATA_GROUP"
+      namespace: ""
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute  #
+      #access-key: ""  #
+      #secret-key: ""  #
+  log:
+    exception-rate: 100
+  tcc:
+    fence:
+      # Anti suspension table name
+      log-table-name: tcc_fence_log_test
+      clean-period: 60s
+  # getty configuration
+  getty:
+    reconnect-interval: 0
+    # temporary not supported connection-num
+    connection-num: 1
+    session:
+      compress-encoding: false
+      tcp-no-delay: true
+      tcp-keep-alive: true
+      keep-alive-period: 120s
+      tcp-r-buf-size: 262144
+      tcp-w-buf-size: 65536
+      tcp-read-timeout: 1s
+      tcp-write-timeout: 5s
+      wait-timeout: 1s
+      max-msg-len: 16498688
+      session-name: client_test
+      cron-period: 1s
diff --git a/tcc/ride-order/order-service/main.go 
b/tcc/ride-order/order-service/main.go
new file mode 100644
index 0000000..f188aa1
--- /dev/null
+++ b/tcc/ride-order/order-service/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 (
+       "net/http"
+
+       "github.com/gin-gonic/gin"
+
+       "seata.apache.org/seata-go/pkg/client"
+       ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+       "seata.apache.org/seata-go/pkg/rm/tcc"
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+       client.InitPath("seatago.yml")
+       common.InitDB()
+
+       r := gin.Default()
+       r.Use(ginmiddleware.TransactionMiddleware())
+
+       proxy, err := tcc.NewTCCServiceProxy(&OrderService{})
+       if err != nil {
+               log.Fatalf("create OrderService proxy error: %v", err)
+       }
+
+       r.POST("/prepare", func(c *gin.Context) {
+               var req OrderRequest
+               if err := c.ShouldBindJSON(&req); err != nil {
+                       c.JSON(http.StatusBadRequest, gin.H{"error": 
err.Error()})
+                       return
+               }
+               ctx := c.Request.Context()
+               if _, err := proxy.Prepare(ctx, req); err != nil {
+                       c.JSON(http.StatusInternalServerError, gin.H{"error": 
err.Error()})
+                       return
+               }
+               // Return the generated order id so the initiator can thread it 
through
+               // the downstream service calls (pricing, dispatch) instead of 
hardcoding it.
+               var orderID int64
+               if v, ok := orderRecords.Load(tm.GetXID(ctx)); ok {
+                       orderID, _ = v.(int64)
+               }
+               c.JSON(http.StatusOK, gin.H{"message": "prepare ok", 
"order_id": orderID})
+       })
+
+       if err := r.Run(":8001"); err != nil {
+               log.Fatalf("start order-service error: %v", err)
+       }
+}
diff --git a/tcc/ride-order/order-service/seatago.yml 
b/tcc/ride-order/order-service/seatago.yml
new file mode 100644
index 0000000..71ef1bf
--- /dev/null
+++ b/tcc/ride-order/order-service/seatago.yml
@@ -0,0 +1,158 @@
+# 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+  enabled: true
+  # application id
+  application-id: order-service
+  # service group
+  tx-service-group: default_tx_group
+  access-key: aliyunAccessKey
+  secret-key: aliyunSecretKey
+  enable-auto-data-source-proxy: false
+  data-source-proxy-mode: AT
+  client:
+    rm:
+      # Maximum cache length of asynchronous queue
+      async-commit-buffer-limit: 10000
+      # The maximum number of retries when report reports the status
+      report-retry-count: 5
+      # The interval for regularly checking the metadata of the db(AT)
+      table-meta-check-enable: false
+      # Whether to report the status if the transaction is successfully 
executed(AT)
+      report-success-enable: false
+      # Whether to allow regular check of db metadata(AT)
+      saga-branch-register-enable: false
+      saga-json-parser: fastjson
+      saga-retry-persist-mode-update: false
+      saga-compensate-persist-mode-update: false
+      #Ordered.HIGHEST_PRECEDENCE + 1000  #
+      tcc-action-interceptor-order: -2147482648
+      # Parse SQL parser selection
+      sql-parser-type: druid
+      lock:
+        retry-interval: 30
+        retry-times: 10
+        retry-policy-branch-rollback-on-conflict: true
+    tm:
+      commit-retry-count: 5
+      rollback-retry-count: 5
+      default-global-transaction-timeout: 60s
+      degrade-check: false
+      degrade-check-period: 2000
+      degrade-check-allow-times: 10s
+      interceptor-order: -2147482648
+    undo:
+      # Judge whether the before image and after image are the same,If it is 
the same, undo will not be recorded
+      data-validation: true
+      # Serialization method
+      log-serialization: json
+      # undo log table name
+      log-table: undo_log
+      # Only store modified fields
+      only-care-update-columns: true
+      compress:
+        # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2, 
Lz4, Zstd, Deflate
+        type: None
+        #  Compression threshold Unit: k
+        threshold: 64k
+    load-balance:
+      type: RandomLoadBalance
+      virtual-nodes: 10
+  service:
+    vgroup-mapping:
+      # Prefix for Print Log
+      default_tx_group: default
+    grouplist:
+      default: 127.0.0.1:8091
+    enable-degrade: false
+    # close the transaction
+    disable-global-transaction: false
+  transport:
+    shutdown:
+      wait: 3s
+    # Netty related configurations
+    # type
+    type: TCP
+    server: NIO
+    heartbeat: true
+    # Encoding and decoding mode
+    serialization: seata
+    # Message compression mode
+    compressor: none
+    # Allow batch sending of requests (TM)
+    enable-tm-client-batch-send-request: false
+    # Allow batch sending of requests (RM)
+    enable-rm-client-batch-send-request: true
+    # RM send request timeout
+    rpc-rm-request-timeout: 30s
+    # TM send request timeout
+    rpc-tm-request-timeout: 30s
+  # Configuration Center
+  config:
+    type: file
+    file:
+      name: config.conf
+    nacos:
+      namespace: ""
+      server-addr: 127.0.0.1:8848
+      group: SEATA_GROUP
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute
+      #access-key: ""
+      #secret-key: ""
+      data-id: seata.properties
+  # Registration Center
+  registry:
+    type: file
+    file:
+      name: registry.conf
+    nacos:
+      application: seata-server
+      server-addr: 127.0.0.1:8848
+      group: "SEATA_GROUP"
+      namespace: ""
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute  #
+      #access-key: ""  #
+      #secret-key: ""  #
+  log:
+    exception-rate: 100
+  tcc:
+    fence:
+      # Anti suspension table name
+      log-table-name: tcc_fence_log_test
+      clean-period: 60s
+  # getty configuration
+  getty:
+    reconnect-interval: 0
+    # temporary not supported connection-num
+    connection-num: 1
+    session:
+      compress-encoding: false
+      tcp-no-delay: true
+      tcp-keep-alive: true
+      keep-alive-period: 120s
+      tcp-r-buf-size: 262144
+      tcp-w-buf-size: 65536
+      tcp-read-timeout: 1s
+      tcp-write-timeout: 5s
+      wait-timeout: 1s
+      max-msg-len: 16498688
+      session-name: client_test
+      cron-period: 1s
diff --git a/tcc/ride-order/order-service/service.go 
b/tcc/ride-order/order-service/service.go
new file mode 100644
index 0000000..75420dc
--- /dev/null
+++ b/tcc/ride-order/order-service/service.go
@@ -0,0 +1,92 @@
+/*
+ * 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"
+       "fmt"
+       "sync"
+
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type OrderRequest struct {
+       PassengerID string `json:"passenger_id"`
+}
+
+type OrderService struct{}
+
+// orderRecords tracks xid -> orderID for Commit/Rollback lookup.
+var orderRecords sync.Map
+
+func (s *OrderService) GetActionName() string {
+       return "OrderService"
+}
+
+// Prepare creates a pending ride order (Try phase).
+func (s *OrderService) Prepare(ctx context.Context, params interface{}) (bool, 
error) {
+       req, ok := params.(OrderRequest)
+       if !ok {
+               return false, fmt.Errorf("invalid params type %T, want 
OrderRequest", params)
+       }
+       result, err := common.DB.ExecContext(ctx,
+               "INSERT INTO ride_orders (passenger_id, status) VALUES (?, 0)", 
req.PassengerID)
+       if err != nil {
+               return false, fmt.Errorf("order prepare failed: %v", err)
+       }
+       orderID, _ := result.LastInsertId()
+       xid := tm.GetXID(ctx)
+       orderRecords.Store(xid, orderID)
+       log.Infof("[Order-Try] created pending order %d for passenger %s, 
xid=%s", orderID, req.PassengerID, xid)
+       return true, nil
+}
+
+// Commit confirms the order: pending -> confirmed. Idempotent via status 
check.
+func (s *OrderService) Commit(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       orderID, ok := orderRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Order-Confirm] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE ride_orders SET status=1 WHERE id=? 
AND status=0", orderID)
+       if err != nil {
+               return false, fmt.Errorf("order confirm failed: %v", err)
+       }
+       orderRecords.Delete(bac.Xid)
+       log.Infof("[Order-Confirm] order %d confirmed, xid=%s", orderID, 
bac.Xid)
+       return true, nil
+}
+
+// Rollback cancels the order: pending -> canceled. Idempotent via status 
check.
+func (s *OrderService) Rollback(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       orderID, ok := orderRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Order-Cancel] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE ride_orders SET status=2 WHERE id=? 
AND status=0", orderID)
+       if err != nil {
+               return false, fmt.Errorf("order cancel failed: %v", err)
+       }
+       orderRecords.Delete(bac.Xid)
+       log.Infof("[Order-Cancel] order %d canceled, xid=%s", orderID, bac.Xid)
+       return true, nil
+}
diff --git a/tcc/ride-order/pricing-service/main.go 
b/tcc/ride-order/pricing-service/main.go
new file mode 100644
index 0000000..32bbbfd
--- /dev/null
+++ b/tcc/ride-order/pricing-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * 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 (
+       "net/http"
+
+       "github.com/gin-gonic/gin"
+
+       "seata.apache.org/seata-go/pkg/client"
+       ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+       "seata.apache.org/seata-go/pkg/rm/tcc"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+       client.InitPath("seatago.yml")
+       common.InitDB()
+
+       r := gin.Default()
+       r.Use(ginmiddleware.TransactionMiddleware())
+
+       proxy, err := tcc.NewTCCServiceProxy(&PricingService{})
+       if err != nil {
+               log.Fatalf("create PricingService proxy error: %v", err)
+       }
+
+       r.POST("/prepare", func(c *gin.Context) {
+               var req PricingRequest
+               if err := c.ShouldBindJSON(&req); err != nil {
+                       c.JSON(http.StatusBadRequest, gin.H{"error": 
err.Error()})
+                       return
+               }
+               if _, err := proxy.Prepare(c.Request.Context(), req); err != 
nil {
+                       c.JSON(http.StatusInternalServerError, gin.H{"error": 
err.Error()})
+                       return
+               }
+               c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+       })
+
+       if err := r.Run(":8003"); err != nil {
+               log.Fatalf("start pricing-service error: %v", err)
+       }
+}
diff --git a/tcc/ride-order/pricing-service/seatago.yml 
b/tcc/ride-order/pricing-service/seatago.yml
new file mode 100644
index 0000000..0f43513
--- /dev/null
+++ b/tcc/ride-order/pricing-service/seatago.yml
@@ -0,0 +1,158 @@
+# 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+  enabled: true
+  # application id
+  application-id: pricing-service
+  # service group
+  tx-service-group: default_tx_group
+  access-key: aliyunAccessKey
+  secret-key: aliyunSecretKey
+  enable-auto-data-source-proxy: false
+  data-source-proxy-mode: AT
+  client:
+    rm:
+      # Maximum cache length of asynchronous queue
+      async-commit-buffer-limit: 10000
+      # The maximum number of retries when report reports the status
+      report-retry-count: 5
+      # The interval for regularly checking the metadata of the db(AT)
+      table-meta-check-enable: false
+      # Whether to report the status if the transaction is successfully 
executed(AT)
+      report-success-enable: false
+      # Whether to allow regular check of db metadata(AT)
+      saga-branch-register-enable: false
+      saga-json-parser: fastjson
+      saga-retry-persist-mode-update: false
+      saga-compensate-persist-mode-update: false
+      #Ordered.HIGHEST_PRECEDENCE + 1000  #
+      tcc-action-interceptor-order: -2147482648
+      # Parse SQL parser selection
+      sql-parser-type: druid
+      lock:
+        retry-interval: 30
+        retry-times: 10
+        retry-policy-branch-rollback-on-conflict: true
+    tm:
+      commit-retry-count: 5
+      rollback-retry-count: 5
+      default-global-transaction-timeout: 60s
+      degrade-check: false
+      degrade-check-period: 2000
+      degrade-check-allow-times: 10s
+      interceptor-order: -2147482648
+    undo:
+      # Judge whether the before image and after image are the same,If it is 
the same, undo will not be recorded
+      data-validation: true
+      # Serialization method
+      log-serialization: json
+      # undo log table name
+      log-table: undo_log
+      # Only store modified fields
+      only-care-update-columns: true
+      compress:
+        # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2, 
Lz4, Zstd, Deflate
+        type: None
+        #  Compression threshold Unit: k
+        threshold: 64k
+    load-balance:
+      type: RandomLoadBalance
+      virtual-nodes: 10
+  service:
+    vgroup-mapping:
+      # Prefix for Print Log
+      default_tx_group: default
+    grouplist:
+      default: 127.0.0.1:8091
+    enable-degrade: false
+    # close the transaction
+    disable-global-transaction: false
+  transport:
+    shutdown:
+      wait: 3s
+    # Netty related configurations
+    # type
+    type: TCP
+    server: NIO
+    heartbeat: true
+    # Encoding and decoding mode
+    serialization: seata
+    # Message compression mode
+    compressor: none
+    # Allow batch sending of requests (TM)
+    enable-tm-client-batch-send-request: false
+    # Allow batch sending of requests (RM)
+    enable-rm-client-batch-send-request: true
+    # RM send request timeout
+    rpc-rm-request-timeout: 30s
+    # TM send request timeout
+    rpc-tm-request-timeout: 30s
+  # Configuration Center
+  config:
+    type: file
+    file:
+      name: config.conf
+    nacos:
+      namespace: ""
+      server-addr: 127.0.0.1:8848
+      group: SEATA_GROUP
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute
+      #access-key: ""
+      #secret-key: ""
+      data-id: seata.properties
+  # Registration Center
+  registry:
+    type: file
+    file:
+      name: registry.conf
+    nacos:
+      application: seata-server
+      server-addr: 127.0.0.1:8848
+      group: "SEATA_GROUP"
+      namespace: ""
+      username: ""
+      password: ""
+      ##if use MSE Nacos with auth, mutex with username/password attribute  #
+      #access-key: ""  #
+      #secret-key: ""  #
+  log:
+    exception-rate: 100
+  tcc:
+    fence:
+      # Anti suspension table name
+      log-table-name: tcc_fence_log_test
+      clean-period: 60s
+  # getty configuration
+  getty:
+    reconnect-interval: 0
+    # temporary not supported connection-num
+    connection-num: 1
+    session:
+      compress-encoding: false
+      tcp-no-delay: true
+      tcp-keep-alive: true
+      keep-alive-period: 120s
+      tcp-r-buf-size: 262144
+      tcp-w-buf-size: 65536
+      tcp-read-timeout: 1s
+      tcp-write-timeout: 5s
+      wait-timeout: 1s
+      max-msg-len: 16498688
+      session-name: client_test
+      cron-period: 1s
diff --git a/tcc/ride-order/pricing-service/service.go 
b/tcc/ride-order/pricing-service/service.go
new file mode 100644
index 0000000..817c6ff
--- /dev/null
+++ b/tcc/ride-order/pricing-service/service.go
@@ -0,0 +1,93 @@
+/*
+ * 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"
+       "fmt"
+       "sync"
+
+       "seata.apache.org/seata-go/pkg/tm"
+       "seata.apache.org/seata-go/pkg/util/log"
+
+       "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type PricingRequest struct {
+       OrderID int64 `json:"order_id"`
+       Amount  int   `json:"amount"`
+}
+
+type PricingService struct{}
+
+// pricingRecords tracks xid -> priceLockID for Commit/Rollback lookup.
+var pricingRecords sync.Map
+
+func (s *PricingService) GetActionName() string {
+       return "PricingService"
+}
+
+// Prepare locks the estimated fare (Try phase).
+func (s *PricingService) Prepare(ctx context.Context, params interface{}) 
(bool, error) {
+       req, ok := params.(PricingRequest)
+       if !ok {
+               return false, fmt.Errorf("invalid params type %T, want 
PricingRequest", params)
+       }
+       result, err := common.DB.ExecContext(ctx,
+               "INSERT INTO price_locks (order_id, amount, status) VALUES (?, 
?, 0)", req.OrderID, req.Amount)
+       if err != nil {
+               return false, fmt.Errorf("pricing prepare failed: %v", err)
+       }
+       lockID, _ := result.LastInsertId()
+       xid := tm.GetXID(ctx)
+       pricingRecords.Store(xid, lockID)
+       log.Infof("[Pricing-Try] locked fare %d cents for order %d, lockID=%d, 
xid=%s", req.Amount, req.OrderID, lockID, xid)
+       return true, nil
+}
+
+// Commit confirms the fare lock: locked -> confirmed. Idempotent via status 
check.
+func (s *PricingService) Commit(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       lockID, ok := pricingRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Pricing-Confirm] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE price_locks SET status=1 WHERE id=? 
AND status=0", lockID)
+       if err != nil {
+               return false, fmt.Errorf("pricing confirm failed: %v", err)
+       }
+       pricingRecords.Delete(bac.Xid)
+       log.Infof("[Pricing-Confirm] fare lock %d confirmed, xid=%s", lockID, 
bac.Xid)
+       return true, nil
+}
+
+// Rollback releases the fare lock: locked -> released. Idempotent via status 
check.
+func (s *PricingService) Rollback(ctx context.Context, bac 
*tm.BusinessActionContext) (bool, error) {
+       lockID, ok := pricingRecords.Load(bac.Xid)
+       if !ok {
+               log.Infof("[Pricing-Cancel] no record for xid=%s, idempotent 
skip", bac.Xid)
+               return true, nil
+       }
+       _, err := common.DB.Exec("UPDATE price_locks SET status=2 WHERE id=? 
AND status=0", lockID)
+       if err != nil {
+               return false, fmt.Errorf("pricing cancel failed: %v", err)
+       }
+       pricingRecords.Delete(bac.Xid)
+       log.Infof("[Pricing-Cancel] fare lock %d released, xid=%s", lockID, 
bac.Xid)
+       return true, nil
+}
diff --git a/tcc/ride-order/sql/ride_order.sql 
b/tcc/ride-order/sql/ride_order.sql
new file mode 100644
index 0000000..25aea61
--- /dev/null
+++ b/tcc/ride-order/sql/ride_order.sql
@@ -0,0 +1,78 @@
+/*
+ * 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_ride DEFAULT CHARACTER SET utf8mb4 COLLATE 
utf8mb4_unicode_ci;
+USE seata_ride;
+
+-- ride orders
+CREATE TABLE IF NOT EXISTS ride_orders (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    passenger_id VARCHAR(64) NOT NULL,
+    status TINYINT NOT NULL DEFAULT 0 COMMENT '0=pending, 1=confirmed, 
2=canceled',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- drivers
+CREATE TABLE IF NOT EXISTS drivers (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    name VARCHAR(64) NOT NULL,
+    status TINYINT NOT NULL DEFAULT 0 COMMENT '0=available, 1=reserved, 
2=busy',
+    reserved_order_id BIGINT DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO drivers (name, status) VALUES
+    ('Driver-A', 0),
+    ('Driver-B', 0),
+    ('Driver-C', 0),
+    ('Driver-D', 0),
+    ('Driver-E', 0),
+    ('Driver-F', 0);
+
+-- price locks
+CREATE TABLE IF NOT EXISTS price_locks (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    order_id BIGINT NOT NULL,
+    amount INT NOT NULL COMMENT 'fare in cents',
+    status TINYINT NOT NULL DEFAULT 0 COMMENT '0=locked, 1=confirmed, 
2=released'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- coupons
+CREATE TABLE IF NOT EXISTS coupons (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    user_id VARCHAR(64) NOT NULL,
+    discount INT NOT NULL COMMENT 'discount in cents',
+    status TINYINT NOT NULL DEFAULT 0 COMMENT '0=available, 1=frozen, 2=used'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO coupons (user_id, discount, status) VALUES
+    ('passenger-001', 500, 0),
+    ('passenger-001', 300, 0),
+    ('passenger-001', 500, 0),
+    ('passenger-001', 300, 0),
+    ('passenger-001', 500, 0),
+    ('passenger-001', 300, 0);
+
+-- vehicle capacity
+CREATE TABLE IF NOT EXISTS vehicle_capacity (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    vehicle_type VARCHAR(32) NOT NULL,
+    total_slots INT NOT NULL,
+    reserved_slots INT NOT NULL DEFAULT 0
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO vehicle_capacity (vehicle_type, total_slots, reserved_slots) VALUES
+    ('standard', 10, 0);


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

Reply via email to