>  기사  >  백엔드 개발  >  주문 처리 시스템 구현: 기반 구축 부분

주문 처리 시스템 구현: 기반 구축 부분

王林
王林원래의
2024-09-05 22:31:331099검색

Implementing an Order Processing System: Part  Setting Up the Foundation

1. 소개 및 목표

마이크로서비스 오케스트레이션을 위해 Temporal을 사용한 정교한 주문 처리 시스템 구현에 관한 포괄적인 블로그 시리즈의 첫 번째 부분에 오신 것을 환영합니다. 이 시리즈에서는 복잡하고 장기 실행되는 워크플로를 처리할 수 있는 강력하고 확장 가능하며 유지 관리 가능한 시스템을 구축하는 과정의 복잡함을 살펴보겠습니다.

우리의 여정은 프로젝트의 기반을 마련하는 것부터 시작됩니다. 이 게시물이 끝나면 Golang에서 구현되고 워크플로 조정을 위해 Temporal과 통합되고 Postgres 데이터베이스에서 지원되는 완전한 기능의 CRUD REST API를 갖게 됩니다. 우리는 최신 도구와 모범 사례를 사용하여 코드베이스를 깔끔하고 효율적이며 유지 관리하기 쉽게 만들 것입니다.

이 게시물의 목표:

  1. Go 모듈을 사용하여 잘 구성된 프로젝트 설정
  2. Gin 및 oapi-codegen을 사용하여 기본 CRUD API 구현
  3. Postgres 데이터베이스 설정 및 마이그레이션 구현
  4. 데이터베이스 상호작용을 통해 간단한 임시 워크플로우 생성
  5. 더 나은 테스트 용이성과 유지 관리성을 위해 종속성 주입 구현
  6. Docker를 사용하여 애플리케이션 컨테이너화
  7. docker-compose를 사용하여 완전한 로컬 개발 환경 제공

자세히 알아보고 주문 처리 시스템 구축을 시작해 보세요!

2. 이론적 배경과 개념

구현을 시작하기 전에 사용할 주요 기술과 개념을 간략하게 살펴보겠습니다.

골랑

Go는 단순성, 효율성 및 탁월한 동시 프로그래밍 지원으로 잘 알려진 정적으로 유형이 지정되고 컴파일된 언어입니다. 표준 라이브러리와 강력한 에코시스템은 마이크로서비스 구축을 위한 탁월한 선택입니다.

일시적인

Temporal은 분산 애플리케이션 개발을 단순화하는 마이크로서비스 오케스트레이션 플랫폼입니다. 이를 통해 복잡하고 오래 실행되는 워크플로를 간단한 절차 코드로 작성하여 실패와 재시도를 자동으로 처리할 수 있습니다.

진 웹 프레임워크

Gin은 Go로 작성된 고성능 HTTP 웹 프레임워크입니다. 훨씬 더 나은 성능과 낮은 메모리 사용량을 갖춘 마티니와 유사한 API를 제공합니다.

OpenAPI 및 oapi-codegen

OpenAPI(이전의 Swagger)는 RESTful 웹 서비스를 설명, 생성, 소비 및 시각화하기 위한 기계 판독 가능 인터페이스 파일에 대한 사양입니다. oapi-codegen은 OpenAPI 3.0 사양에서 Go 코드를 생성하는 도구로, 이를 통해 API 계약을 먼저 정의하고 서버 스텁 및 클라이언트 코드를 생성할 수 있습니다.

sqlc

sqlc는 SQL에서 유형이 안전한 Go 코드를 생성합니다. 이를 통해 일반 SQL 쿼리를 작성하고 완전히 유형이 안전한 Go 코드를 생성하여 데이터베이스와 상호 작용할 수 있으므로 런타임 오류 가능성이 줄어들고 유지 관리성이 향상됩니다.

포스트그레스

PostgreSQL은 신뢰성, 기능 견고성 및 성능으로 잘 알려진 강력한 오픈 소스 객체 관계형 데이터베이스 시스템입니다.

도커 및 도커 작성

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을 만들어 보겠습니다.

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:

  1. Use of Temporal’s workflow.Context for managing the workflow lifecycle.
  2. Logging within workflows using workflow.GetLogger.
  3. Simulating long-running processes with workflow.Sleep.
  4. Executing activities within a workflow using workflow.ExecuteActivity.
  5. 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:

  1. Start the services:
docker-compose up

  1. Use a tool like cURL or Postman to send requests to our API:

  2. 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:

  1. 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.

  2. 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.

  3. Configuration Management : We hardcoded configuration values for simplicity. In a real-world scenario, we’d use environment variables or a configuration management system.

  4. Keselamatan : Persediaan semasa kami tidak termasuk sebarang pengesahan atau kebenaran. Dalam sistem pengeluaran, kami perlu melaksanakan langkah keselamatan yang betul.

  5. Skalabiliti : Walaupun Temporal membantu dengan kebolehskalaan aliran kerja, kami perlu mempertimbangkan kebolehskalaan pangkalan data dan prestasi API untuk sistem trafik tinggi.

  6. Pemantauan dan Kebolehmerhatian : Kami belum melaksanakan sebarang alat pemantauan atau kebolehmerhatian lagi. Dalam sistem pengeluaran, ini akan menjadi penting untuk menyelenggara dan menyelesaikan masalah aplikasi.

7. Langkah Seterusnya dan Pratonton Bahagian 2

Dalam bahagian pertama siri kami ini, kami telah menyediakan asas untuk sistem pemprosesan pesanan kami. Kami mempunyai API CRUD asas, penyepaduan pangkalan data dan aliran kerja Temporal yang ringkas.

Dalam bahagian seterusnya, kita akan menyelami lebih mendalam aliran kerja dan aktiviti Temporal. Kami akan meneroka:

  1. Melaksanakan logik pemprosesan pesanan yang lebih kompleks
  2. Mengendalikan aliran kerja yang berjalan lama dengan Temporal
  3. Melaksanakan logik cuba semula dan pengendalian ralat dalam aliran kerja
  4. Mengubah versi aliran kerja untuk kemas kini yang selamat
  5. Melaksanakan corak saga untuk transaksi yang diedarkan
  6. Pemantauan dan pemerhatian untuk aliran kerja Temporal

Kami juga akan mula menyempurnakan API kami dengan logik pemprosesan pesanan yang lebih realistik dan meneroka corak untuk mengekalkan kod yang bersih dan boleh diselenggara apabila sistem kami semakin kompleks.

Nantikan Bahagian 2, di mana kami akan membawa sistem pemprosesan pesanan kami ke peringkat seterusnya!


Perlukan Bantuan?

Adakah anda menghadapi masalah yang mencabar, atau memerlukan perspektif luaran tentang idea atau projek baharu? Saya boleh tolong! Sama ada anda ingin membina konsep bukti teknologi sebelum membuat pelaburan yang lebih besar, atau anda memerlukan panduan tentang isu yang sukar, saya sedia membantu.

Perkhidmatan yang Ditawarkan:

  • Penyelesaian Masalah: Menangani isu yang rumit dengan penyelesaian yang inovatif.
  • Perundingan: Memberikan nasihat pakar dan pandangan baharu tentang projek anda.
  • Bukti Konsep: Membangunkan model awal untuk menguji dan mengesahkan idea anda.

Jika anda berminat untuk bekerja dengan saya, sila hubungi melalui e-mel di hungaikevin@gmail.com.

Mari jadikan cabaran anda sebagai peluang!

위 내용은 주문 처리 시스템 구현: 기반 구축 부분의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.