Selamat datang ke bahagian pertama siri blog komprehensif kami tentang pelaksanaan sistem pemprosesan pesanan yang canggih menggunakan Temporal untuk orkestrasi perkhidmatan mikro. Dalam siri ini, kami akan meneroka selok-belok membina sistem yang teguh, berskala dan boleh diselenggara yang boleh mengendalikan aliran kerja yang kompleks dan berjalan lama.
Perjalanan kami bermula dengan menyediakan asas untuk projek kami. Pada penghujung siaran ini, anda akan mempunyai API CRUD REST berfungsi sepenuhnya yang dilaksanakan di Golang, disepadukan dengan Temporal untuk orkestrasi aliran kerja dan disokong oleh pangkalan data Postgres. Kami akan menggunakan alatan moden dan amalan terbaik untuk memastikan pangkalan kod kami bersih, cekap dan mudah diselenggara.
Mari menyelami dan mula membina sistem pemprosesan pesanan kami!
Sebelum kita mula melaksanakan, mari kita semak secara ringkas teknologi dan konsep utama yang akan kita gunakan:
Go ialah bahasa yang ditaip secara statik, disusun yang terkenal dengan kesederhanaan, kecekapan dan sokongan yang sangat baik untuk pengaturcaraan serentak. Perpustakaan standard dan ekosistemnya yang mantap menjadikannya pilihan terbaik untuk membina perkhidmatan mikro.
Temporal ialah platform orkestrasi perkhidmatan mikro yang memudahkan pembangunan aplikasi yang diedarkan. Ia membolehkan kami menulis aliran kerja yang kompleks dan berjalan lama sebagai kod prosedur mudah, menangani kegagalan dan cuba semula secara automatik.
Gin ialah rangka kerja web HTTP berprestasi tinggi yang ditulis dalam Go. Ia menyediakan API seperti martini dengan prestasi yang lebih baik dan penggunaan memori yang lebih rendah.
OpenAPI (dahulunya dikenali sebagai Swagger) ialah spesifikasi untuk fail antara muka yang boleh dibaca mesin untuk menerangkan, menghasilkan, menggunakan dan menggambarkan perkhidmatan web RESTful. oapi-codegen ialah alat yang menjana kod Go daripada spesifikasi OpenAPI 3.0, membolehkan kami mentakrifkan kontrak API kami terlebih dahulu dan menjana stub pelayan dan kod klien.
sqlc menjana kod Go selamat jenis daripada SQL. Ia membolehkan kami menulis pertanyaan SQL biasa dan menjana kod Go selamat taip sepenuhnya untuk berinteraksi dengan pangkalan data kami, mengurangkan kemungkinan ralat masa jalan dan meningkatkan kebolehselenggaraan.
PostgreSQL ialah sistem pangkalan data perhubungan objek sumber terbuka yang berkuasa yang terkenal dengan kebolehpercayaan, keteguhan ciri dan prestasinya.
Docker membenarkan kami membungkus aplikasi kami dan kebergantungannya ke dalam bekas, memastikan konsistensi merentas persekitaran yang berbeza. docker-compose ialah alat untuk mentakrif dan menjalankan aplikasi Docker berbilang bekas, yang akan kami gunakan untuk menyediakan persekitaran pembangunan setempat kami.
Sekarang kita telah membincangkan perkara asas, mari mula melaksanakan sistem kami.
Mula-mula, mari buat direktori projek kami dan sediakan struktur asas:
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
Struktur ini mengikut reka letak projek Go standard:
Mari kita buat Makefile untuk memudahkan tugas biasa:
touch Makefile
Tambah kandungan berikut pada 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
Fail Make ini menyediakan sasaran untuk menjana kod, membina aplikasi, menjalankannya, menjalankan ujian dan membersihkan artifak binaan.
Buat fail bernama api/openapi.yaml dan tentukan spesifikasi API kami:
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
Spesifikasi ini mentakrifkan operasi CRUD asas kami untuk pesanan.
Pasang 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.
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.
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.
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;
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
go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
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
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;
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.
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
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”.
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)
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() }
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) } }
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
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:
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:
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.
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.
Sécurité : Notre configuration actuelle n'inclut aucune authentification ou autorisation. Dans un système de production, nous devrons mettre en œuvre des mesures de sécurité appropriées.
Évolutivité : Bien que Temporal contribue à l'évolutivité du flux de travail, nous devrons prendre en compte l'évolutivité de la base de données et les performances de l'API pour un système à fort trafic.
Surveillance et observabilité : Nous n'avons pas encore mis en place d'outils de surveillance ou d'observabilité. Dans un système de production, ceux-ci seraient cruciaux pour la maintenance et le dépannage de l'application.
Dans cette première partie de notre série, nous avons posé les bases de notre système de traitement des commandes. Nous disposons d'une API CRUD de base, d'une intégration de base de données et d'un flux de travail temporel simple.
Dans la partie suivante, nous approfondirons les flux de travail et les activités temporels. Nous explorerons :
Nous commencerons également à étoffer notre API avec une logique de traitement des commandes plus réaliste et à explorer des modèles permettant de maintenir un code propre et maintenable à mesure que notre système gagne en complexité.
Restez à l'écoute pour la partie 2, où nous ferons passer notre système de traitement des commandes au niveau supérieur !
Êtes-vous confronté à des problèmes difficiles ou avez-vous besoin d'un point de vue externe sur une nouvelle idée ou un nouveau projet ? Je peux aider ! Que vous cherchiez à établir une preuve de concept technologique avant de réaliser un investissement plus important ou que vous ayez besoin de conseils sur des problèmes difficiles, je suis là pour vous aider.
Si vous souhaitez travailler avec moi, veuillez nous contacter par e-mail à hungaikevin@gmail.com.
Transformons vos défis en opportunités !
Atas ialah kandungan terperinci Melaksanakan Sistem Pemprosesan Pesanan: Bahagian Menubuhkan Yayasan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!