實施訂單處理系統:建立基礎部分

1. 簡介和目標
歡迎來到我們關於使用 Temporal 進行微服務編排實現複雜訂單處理系統的綜合部落格系列的第一部分。在本系列中,我們將探索建立一個強大、可擴展且可維護的系統的複雜性,該系統可以處理複雜、長時間運行的工作流程。
我們的旅程從為我們的專案奠定基礎開始。在本文結束時,您將擁有一個在 Golang 中實現的功能齊全的 CRUD REST API,與 Temporal 整合以進行工作流程編排,並由 Postgres 資料庫提供支援。我們將使用現代工具和最佳實踐來確保我們的程式碼庫乾淨、高效且易於維護。
這篇文章的目標:
- 使用 Go 模組建立一個結構良好的專案
- 使用 Gin 和 oapi-codegen 實作基本的 CRUD API
- 設定 Postgres 資料庫並實作遷移
- 透過資料庫互動建立簡單的時態工作流程
- 實現依賴注入以獲得更好的可測試性和可維護性
- 使用 Docker 將我們的應用程式容器化
- 使用docker-compose提供完整的本機開發環境
讓我們深入了解並開始建立我們的訂單處理系統!
2 理論背景與概念
在開始實施之前,讓我們先簡單回顧一下我們將使用的關鍵技術和概念:
戈蘭
Go 是一種靜態類型的編譯語言,以其簡單、高效和對並發程式設計的出色支援而聞名。其標準庫和強大的生態系統使其成為建立微服務的絕佳選擇。
顳
Temporal 是一個微服務編排平台,可簡化分散式應用程式的開發。它允許我們將複雜、長時間運行的工作流程編寫為簡單的程式碼,自動處理失敗和重試。
琴酒網路框架
Gin 是一個用 Go 寫的高效能 HTTP Web 框架。它提供了類似 martini 的 API,具有更好的效能和更低的記憶體使用量。
OpenAPI 和 oapi-codegen
OpenAPI(以前稱為 Swagger)是一種機器可讀介面檔案規範,用於描述、產生、使用和視覺化 RESTful Web 服務。 oapi-codegen 是一個根據 OpenAPI 3.0 規格產生 Go 程式碼的工具,允許我們先定義 API 合約並產生伺服器存根和用戶端程式碼。
sqlc
sqlc 從 SQL 產生型別安全的 Go 程式碼。它允許我們編寫簡單的 SQL 查詢並產生完全類型安全的 Go 程式碼來與我們的資料庫交互,從而減少運行時錯誤的可能性並提高可維護性。
Postgres
PostgreSQL 是一個強大的開源物件關聯式資料庫系統,以其可靠性、功能穩健性和效能而聞名。
Docker 和 docker-compose
Docker 允許我們將應用程式及其相依性打包到容器中,確保不同環境之間的一致性。 docker-compose 是一個用於定義和運行多容器 Docker 應用程式的工具,我們將使用它來設定本機開發環境。
現在我們已經了解了基礎知識,讓我們開始實現我們的系統。
3. 逐步實施指南
3.1 設定項目結構
首先,讓我們建立專案目錄並設定基本結構:
mkdir order-processing-system
cd order-processing-system
# Create directory structure
mkdir -p cmd/api \
internal/api \
internal/db \
internal/models \
internal/service \
internal/workflow \
migrations \
pkg/logger \
scripts
# Initialize Go module
go mod init github.com/yourusername/order-processing-system
# Create main.go file
touch cmd/api/main.go
此結構遵循標準 Go 專案佈局:
- cmd/api:包含主應用程式入口點
- 內部:包含特定於該專案且不打算由其他專案匯入的套件
- migrations:儲存資料庫遷移檔案
- pkg:包含可以被其他項目匯入的套件
- 腳本:保存用於開發和部署的實用腳本
3.2 建立Makefile
讓我們建立一個 Makefile 來簡化常見任務:
touch Makefile
將以下內容加入Makefile:
.PHONY: generate build run test clean
generate:
@echo "Generating code..."
go generate ./...
build:
@echo "Building..."
go build -o bin/api cmd/api/main.go
run:
@echo "Running..."
go run cmd/api/main.go
test:
@echo "Running tests..."
go test -v ./...
clean:
@echo "Cleaning..."
rm -rf bin
.DEFAULT_GOAL := build
此 Makefile 提供產生程式碼、建置應用程式、執行應用程式、執行測試和清理建置工件的目標。
3.3 實作基本CRUD API
3.3.1 定義OpenAPI規範
建立一個名為 api/openapi.yaml 的檔案並定義我們的 API 規格:
openapi: 3.0.0
info:
title: Order Processing API
version: 1.0.0
description: API for managing orders in our processing system
paths:
/orders:
get:
summary: List all orders
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Order'
post:
summary: Create a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
/orders/{id}:
get:
summary: Get an order by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Order not found
put:
summary: Update an order
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateOrderRequest'
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Order not found
delete:
summary: Delete an order
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: Successful response
'404':
description: Order not found
components:
schemas:
Order:
type: object
properties:
id:
type: integer
customer_id:
type: integer
status:
type: string
enum: [pending, processing, completed, cancelled]
total_amount:
type: number
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
CreateOrderRequest:
type: object
required:
- customer_id
- total_amount
properties:
customer_id:
type: integer
total_amount:
type: number
UpdateOrderRequest:
type: object
properties:
status:
type: string
enum: [pending, processing, completed, cancelled]
total_amount:
type: number
該規範定義了我們對訂單的基本 CRUD 操作。
3.3.2 產生API程式碼
安裝oapi-codegen:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
Generate the server code:
oapi-codegen -package api -generate types,server,spec api/openapi.yaml > internal/api/api.gen.go
This command generates the Go code for our API, including types, server interfaces, and the OpenAPI specification.
3.3.3 Implement the API Handler
Create a new file internal/api/handler.go:
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Handler struct {
// We'll add dependencies here later
}
func NewHandler() *Handler {
return &Handler{}
}
func (h *Handler) RegisterRoutes(r *gin.Engine) {
RegisterHandlers(r, h)
}
// Implement the ServerInterface methods
func (h *Handler) GetOrders(c *gin.Context) {
// TODO: Implement
c.JSON(http.StatusOK, []Order{})
}
func (h *Handler) CreateOrder(c *gin.Context) {
var req CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// TODO: Implement order creation logic
order := Order{
Id: 1,
CustomerId: req.CustomerId,
Status: "pending",
TotalAmount: req.TotalAmount,
}
c.JSON(http.StatusCreated, order)
}
func (h *Handler) GetOrder(c *gin.Context, id int) {
// TODO: Implement
c.JSON(http.StatusOK, Order{Id: id})
}
func (h *Handler) UpdateOrder(c *gin.Context, id int) {
var req UpdateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// TODO: Implement order update logic
order := Order{
Id: id,
Status: *req.Status,
}
c.JSON(http.StatusOK, order)
}
func (h *Handler) DeleteOrder(c *gin.Context, id int) {
// TODO: Implement
c.Status(http.StatusNoContent)
}
This implementation provides a basic structure for our API handlers. We’ll flesh out the actual logic when we integrate with the database and Temporal workflows.
3.4 Setting Up the Postgres Database
3.4.1 Create a docker-compose file
Create a docker-compose.yml file in the project root:
version: '3.8'
services:
postgres:
image: postgres:13
environment:
POSTGRES_USER: orderuser
POSTGRES_PASSWORD: orderpass
POSTGRES_DB: orderdb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
This sets up a Postgres container for our local development environment.
3.4.2 Implement Database Migrations
Install golang-migrate:
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
Create our first migration:
migrate create -ext sql -dir migrations -seq create_orders_table
Edit the migrations/000001_create_orders_table.up.sql file:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);
Edit the migrations/000001_create_orders_table.down.sql file:
DROP TABLE IF EXISTS orders;
3.4.3 Run Migrations
Add a new target to our Makefile:
migrate-up:
@echo "Running migrations..."
migrate -path migrations -database "postgresql://orderuser:orderpass@localhost:5432/orderdb?sslmode=disable" up
migrate-down:
@echo "Reverting migrations..."
migrate -path migrations -database "postgresql://orderuser:orderpass@localhost:5432/orderdb?sslmode=disable" down
Now we can run migrations with:
make migrate-up
3.5 Implementing Database Operations with sqlc
3.5.1 Install sqlc
go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
3.5.2 Configure sqlc
Create a sqlc.yaml file in the project root:
version: "2"
sql:
- engine: "postgresql"
queries: "internal/db/queries.sql"
schema: "migrations"
gen:
go:
package: "db"
out: "internal/db"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false
3.5.3 Write SQL Queries
Create a file internal/db/queries.sql:
-- name: GetOrder :one SELECT * FROM orders WHERE id = $1 LIMIT 1; -- name: ListOrders :many SELECT * FROM orders ORDER BY id; -- name: CreateOrder :one INSERT INTO orders ( customer_id, status, total_amount ) VALUES ( $1, $2, $3 ) RETURNING *; -- name: UpdateOrder :one UPDATE orders SET status = $2, total_amount = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *; -- name: DeleteOrder :exec DELETE FROM orders WHERE id = $1;
3.5.4 Generate Go Code
Add a new target to our Makefile:
generate-sqlc:
@echo "Generating sqlc code..."
sqlc generate
Run the code generation:
make generate-sqlc
This will generate Go code for interacting with our database in the internal/db directory.
3.6 Integrating Temporal
3.6.1 Set Up Temporal Server
Add Temporal to our docker-compose.yml:
temporal:
image: temporalio/auto-setup:1.13.0
ports:
- "7233:7233"
environment:
- DB=postgresql
- DB_PORT=5432
- POSTGRES_USER=orderuser
- POSTGRES_PWD=orderpass
- POSTGRES_SEEDS=postgres
depends_on:
- postgres
temporal-admin-tools:
image: temporalio/admin-tools:1.13.0
depends_on:
- temporal
3.6.2 Implement a Basic Workflow
Create a file internal/workflow/order_workflow.go:
package workflow
import (
"time"
"go.temporal.io/sdk/workflow"
"github.com/yourusername/order-processing-system/internal/db"
)
func OrderWorkflow(ctx workflow.Context, order db.Order) error {
logger := workflow.GetLogger(ctx)
logger.Info("OrderWorkflow started", "OrderID", order.ID)
// Simulate order processing
err := workflow.Sleep(ctx, 5*time.Second)
if err != nil {
return err
}
// Update order status
err = workflow.ExecuteActivity(ctx, UpdateOrderStatus, workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
}, order.ID, "completed").Get(ctx, nil)
if err != nil {
return err
}
logger.Info("OrderWorkflow completed", "OrderID", order.ID)
return nil
}
func UpdateOrderStatus(ctx workflow.Context, orderID int64, status string) error {
// TODO: Implement database update
return nil
}
This basic workflow simulates order processing by waiting for 5 seconds and then updating the order status to “completed”.
3.6.3 Integrate Workflow with API
Update the internal/api/handler.go file to include Temporal client and start the workflow:
package api
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"go.temporal.io/sdk/client"
"github.com/yourusername/order-processing-system/internal/db"
"github.com/yourusername/order-processing-system/internal/workflow"
)
type Handler struct {
queries *db.Queries
temporalClient client.Client
}
func NewHandler(queries *db.Queries, temporalClient client.Client) *Handler {
return &Handler{
queries: queries,
temporalClient: temporalClient,
}
}
// ... (previous handler methods)
func (h *Handler) CreateOrder(c *gin.Context) {
var req CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
order, err := h.queries.CreateOrder(c, db.CreateOrderParams{
CustomerID: req.CustomerId,
Status: "pending",
TotalAmount: req.TotalAmount,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Start Temporal workflow
workflowOptions := client.StartWorkflowOptions{
ID: "order-" + order.ID,
TaskQueue: "order-processing",
}
_, err = h.temporalClient.ExecuteWorkflow(context.Background(), workflowOptions, workflow.OrderWorkflow, order)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start workflow"})
return
}
c.JSON(http.StatusCreated, order)
}
// ... (implement other handler methods)
3.7 Implementing Dependency Injection
Create a new file internal/service/service.go:
package service
import (
"database/sql"
"github.com/yourusername/order-processing-system/internal/api"
"github.com/yourusername/order-processing-system/internal/db"
"go.temporal.io/sdk/client"
)
type Service struct {
DB *sql.DB
Queries *db.Queries
TemporalClient client.Client
Handler *api.Handler
}
func NewService() (*Service, error) {
// Initialize database connection
db, err := sql.Open("postgres", "postgresql://orderuser:orderpass@localhost:5432/orderdb?sslmode=disable")
if err != nil {
return nil, err
}
// Initialize Temporal client
temporalClient, err := client.NewClient(client.Options{
HostPort: "localhost:7233",
})
if err != nil {
return nil, err
}
// Initialize queries
queries := db.New(db)
// Initialize handler
handler := api.NewHandler(queries, temporalClient)
return &Service{
DB: db,
Queries: queries,
TemporalClient: temporalClient,
Handler: handler,
}, nil
}
func (s *Service) Close() {
s.DB.Close()
s.TemporalClient.Close()
}
3.8 Update Main Function
Update the cmd/api/main.go file:
package main
import (
"log"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
"github.com/yourusername/order-processing-system/internal/service"
)
func main() {
svc, err := service.NewService()
if err != nil {
log.Fatalf("Failed to initialize service: %v", err)
}
defer svc.Close()
r := gin.Default()
svc.Handler.RegisterRoutes(r)
if err := r.Run(":8080"); err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
3.9 Dockerize the Application
Create a Dockerfile in the project root:
# Build stage FROM golang:1.17-alpine AS build WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /order-processing-system ./cmd/api # Run stage FROM alpine:latest WORKDIR / COPY --from=build /order-processing-system /order-processing-system EXPOSE 8080 ENTRYPOINT ["/order-processing-system"]
Update the docker-compose.yml file to include our application:
version: '3.8'
services:
postgres:
# ... (previous postgres configuration)
temporal:
# ... (previous temporal configuration)
temporal-admin-tools:
# ... (previous temporal-admin-tools configuration)
app:
build: .
ports:
- "8080:8080"
depends_on:
- postgres
- temporal
environment:
- DB_HOST=postgres
- DB_USER=orderuser
- DB_PASSWORD=orderpass
- DB_NAME=orderdb
- TEMPORAL_HOST=temporal:7233
4. Code Examples with Detailed Comments
Throughout the implementation guide, we’ve provided code snippets with explanations. Here’s a more detailed look at a key part of our system: the Order Workflow.
package workflow
import (
"time"
"go.temporal.io/sdk/workflow"
"github.com/yourusername/order-processing-system/internal/db"
)
// OrderWorkflow defines the workflow for processing an order
func OrderWorkflow(ctx workflow.Context, order db.Order) error {
logger := workflow.GetLogger(ctx)
logger.Info("OrderWorkflow started", "OrderID", order.ID)
// Simulate order processing
// In a real-world scenario, this could involve multiple activities such as
// inventory check, payment processing, shipping arrangement, etc.
err := workflow.Sleep(ctx, 5*time.Second)
if err != nil {
return err
}
// Update order status
// We use ExecuteActivity to run the status update as an activity
// This allows for automatic retries and error handling
err = workflow.ExecuteActivity(ctx, UpdateOrderStatus, workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
}, order.ID, "completed").Get(ctx, nil)
if err != nil {
return err
}
logger.Info("OrderWorkflow completed", "OrderID", order.ID)
return nil
}
// UpdateOrderStatus is an activity that updates the status of an order
func UpdateOrderStatus(ctx workflow.Context, orderID int64, status string) error {
// TODO: Implement database update
// In a real implementation, this would use the db.Queries to update the order status
return nil
}
This workflow demonstrates several key concepts:
- Use of Temporal’s workflow.Context for managing the workflow lifecycle.
- Logging within workflows using workflow.GetLogger.
- Simulating long-running processes with workflow.Sleep.
- Executing activities within a workflow using workflow.ExecuteActivity.
- Handling errors and returning them to be managed by Temporal.
5. Testing and Validation
For this initial setup, we’ll focus on manual testing to ensure our system is working as expected. In future posts, we’ll dive into unit testing, integration testing, and end-to-end testing strategies.
To manually test our system:
- Start the services:
docker-compose up
Use a tool like cURL or Postman to send requests to our API:
Check the logs to ensure the Temporal workflow is being triggered and completed successfully.
6. Challenges and Considerations
While setting up this initial version of our order processing system, we encountered several challenges and considerations:
Database Schema Design : Designing a flexible yet efficient schema for orders is crucial. We kept it simple for now, but in a real-world scenario, we might need to consider additional tables for order items, customer information, etc.
Error Handling : Our current implementation has basic error handling. In a production system, we’d need more robust error handling and logging, especially for the Temporal workflows.
Configuration Management : We hardcoded configuration values for simplicity. In a real-world scenario, we’d use environment variables or a configuration management system.
セキュリティ : 現在の設定には認証や認可が含まれていません。実稼働システムでは、適切なセキュリティ対策を実装する必要があります。
スケーラビリティ : Temporal はワークフローのスケーラビリティに役立ちますが、高トラフィック システムではデータベースのスケーラビリティと API のパフォーマンスを考慮する必要があります。
監視と可観測性 : 監視または可観測性ツールはまだ実装されていません。運用システムでは、これらはアプリケーションの保守とトラブルシューティングに非常に重要です。
7. 次のステップとパート 2 のプレビュー
シリーズの最初の部分では、注文処理システムの基礎を構築しました。基本的な CRUD API、データベース統合、およびシンプルなテンポラル ワークフローがあります。
次のパートでは、時間的なワークフローとアクティビティについて詳しく説明します。以下について調査していきます:
- より複雑な注文処理ロジックの実装
- Temporal を使用した長時間実行ワークフローの処理
- ワークフローでの再試行ロジックとエラー処理の実装
- 安全な更新のためのバージョニングワークフロー
- 分散トランザクション用のサガ パターンの実装
- Temporal ワークフローの監視と可観測性
また、より現実的な注文処理ロジックを使用して API の肉付けを開始し、システムが複雑になるにつれてクリーンで保守可能なコードを維持するためのパターンを探索していきます。
注文処理システムを次のレベルに引き上げるパート 2 をお楽しみに!
助けが必要ですか?
困難な問題に直面していますか、それとも新しいアイデアやプロジェクトに関して外部の視点が必要ですか?お手伝いできます!大規模な投資を行う前にテクノロジーの概念実証を構築したい場合でも、難しい問題についてのガイダンスが必要な場合でも、私がお手伝いいたします。
提供されるサービス:
- 問題解決: 革新的なソリューションで複雑な問題に取り組みます。
- コンサルティング: プロジェクトに関する専門家のアドバイスと新鮮な視点を提供します。
- 概念実証: アイデアをテストおよび検証するための予備モデルを開発します。
私との仕事にご興味がございましたら、hungaikevin@gmail.com まで電子メールでご連絡ください。
課題をチャンスに変えましょう!
以上是實施訂單處理系統:建立基礎部分的詳細內容。更多資訊請關注PHP中文網其他相關文章!
熱AI工具
Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片
AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。
Undress AI Tool
免費脫衣圖片
Clothoff.io
AI脫衣器
AI Hentai Generator
免費產生 AI 無盡。
熱門文章
熱工具
記事本++7.3.1
好用且免費的程式碼編輯器
SublimeText3漢化版
中文版,非常好用
禪工作室 13.0.1
強大的PHP整合開發環境
Dreamweaver CS6
視覺化網頁開發工具
SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)
熱門話題
Debian OpenSSL有哪些漏洞
Apr 02, 2025 am 07:30 AM
OpenSSL,作為廣泛應用於安全通信的開源庫,提供了加密算法、密鑰和證書管理等功能。然而,其歷史版本中存在一些已知安全漏洞,其中一些危害極大。本文將重點介紹Debian系統中OpenSSL的常見漏洞及應對措施。 DebianOpenSSL已知漏洞:OpenSSL曾出現過多個嚴重漏洞,例如:心臟出血漏洞(CVE-2014-0160):該漏洞影響OpenSSL1.0.1至1.0.1f以及1.0.2至1.0.2beta版本。攻擊者可利用此漏洞未經授權讀取服務器上的敏感信息,包括加密密鑰等。
Go的爬蟲Colly中Queue線程的問題是什麼?
Apr 02, 2025 pm 02:09 PM
Go爬蟲Colly中的Queue線程問題探討在使用Go語言的Colly爬蟲庫時,開發者常常會遇到關於線程和請求隊列的問題。 �...
Go語言中用於浮點數運算的庫有哪些?
Apr 02, 2025 pm 02:06 PM
Go語言中用於浮點數運算的庫介紹在Go語言(也稱為Golang)中,進行浮點數的加減乘除運算時,如何確保精度是�...
從前端轉型後端開發,學習Java還是Golang更有前景?
Apr 02, 2025 am 09:12 AM
後端學習路徑:從前端轉型到後端的探索之旅作為一名從前端開發轉型的後端初學者,你已經有了nodejs的基礎,...
什麼是GO FMT命令,為什麼很重要?
Mar 20, 2025 pm 04:21 PM
本文討論了GO編程中的GO FMT命令,該命令將代碼格式化以遵守官方樣式準則。它突出了GO FMT在維持代碼一致性,可讀性和降低樣式辯論方面的重要性。 FO的最佳實踐
Beego ORM中如何指定模型關聯的數據庫?
Apr 02, 2025 pm 03:54 PM
在BeegoORM框架下,如何指定模型關聯的數據庫?許多Beego項目需要同時操作多個數據庫。當使用Beego...


