Selamat datang di bagian paling seru dari seri ini! Setelah membangun fondasi autentikasi yang solid, saatnya kita masuk ke jantung dari aplikasi e-commerce: produk.
Di bagian ini, kita akan membangun sistem manajemen produk yang lengkap, termasuk fungsionalitas yang paling ditunggu: upload gambar. Secara spesifik, kita akan:
- Membuat tabel
products
melalui sistem migrasi kita. - Membangun halaman admin untuk menambah dan mengelola produk.
- Mengimplementasikan operasi Create dari CRUD untuk produk.
- Mengintegrasikan Go dengan Minio untuk menangani upload gambar produk.
Ini adalah bagian yang padat dan sangat memuaskan. Mari kita mulai!
Langkah 1: Skema Database dan Model Produk
Pertama, kita definisikan struktur tabel produk kita.
1. Buat File Migrasi Baru
Gunakan migrate
CLI yang sudah kita instal sebelumnya untuk membuat file migrasi baru:
migrate create -ext sql -dir internal/database/migration -seq create_products_table
Buka file ..._create_products_table.up.sql
dan tambahkan skema berikut:
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
price NUMERIC(10, 2) NOT NULL,
stock INT NOT NULL,
image_url VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Jangan lupa isi file ..._create_products_table.down.sql
untuk rollback:
DROP TABLE IF EXISTS products;
Jalankan migrasi untuk menerapkan skema baru ini ke database Anda:
migrate -path internal/database/migration -database "postgres://admin:secret@127.0.0.1:5432/ecommerce_db?sslmode=disable" -verbose up
2. Buat Model Go untuk Produk
Buat file baru internal/models/product.go
dan definisikan struct Product
:
// internal/models/product.go
package models
import (
"time"
"github.com/google/uuid"
)
type Product struct {
ID uuid.UUID `db:"id"`
Name string `db:"name"`
Description string `db:"description"`
Price float64 `db:"price"`
Stock int `db:"stock"`
ImageURL string `db:"image_url"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Langkah 2: Menyiapkan Integrasi Minio
Sebelum kita membuat form, mari siapkan koneksi ke Minio.
1. Instal Minio Go SDK
go get github.com/minio/minio-go/v7
Setelah itu, jalankan go mod tidy
untuk memastikan semua dependensi sinkron.
2. Buat Modul Koneksi Minio
Buat file baru internal/storage/minio.go
:
mkdir -p internal/storage
touch internal/storage/minio.go
Isi dengan kode berikut:
// internal/storage/minio.go
package storage
import (
"context"
"log"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var MinioClient *minio.Client
func InitMinio() {
endpoint := "127.0.0.1:9000"
accessKeyID := "minioadmin"
secretAccessKey := "miniosecret"
useSSL := false
var err error
MinioClient, err = minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL,
})
if err != nil {
log.Fatalf("Gagal terhubung ke Minio: %v", err)
}
log.Println("Berhasil terhubung ke Minio!")
bucketName := "products"
err = MinioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{})
if err != nil {
exists, errBucketExists := MinioClient.BucketExists(context.Background(), bucketName)
if errBucketExists == nil && exists {
log.Printf("Bucket '%s' sudah ada.\n", bucketName)
} else {
log.Fatalf("Gagal membuat bucket: %v", err)
}
} else {
log.Printf("Berhasil membuat bucket '%s'.\n", bucketName)
policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::` + bucketName + `/*"]}]}`
err = MinioClient.SetBucketPolicy(context.Background(), bucketName, policy)
if err != nil {
log.Fatalf("Gagal mengatur kebijakan bucket: %v", err)
}
}
}
Catatan: Kredensial Minio di-hardcode di sini untuk kesederhanaan. Di aplikasi produksi, simpan ini di .env
.
Panggil fungsi InitMinio()
ini dari cmd/web/main.go
:
// cmd/web/main.go
import (
// ...
"github.com/kodekilat/go-ecommerce/internal/storage"
)
func main() {
// ... (koneksi database)
storage.InitMinio() // Inisialisasi Minio
// ... (sisa kode)
}
Langkah 3: Membuat Repository Produk
Buat file internal/repository/product_repo.go
. Ini akan berisi semua query database yang berhubungan dengan produk.
// internal/repository/product_repo.go
package repository
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kodekilat/go-ecommerce/internal/models"
)
type ProductRepository struct {
DB *pgxpool.Pool
}
func (r *ProductRepository) CreateProduct(product *models.Product) error {
query := `
INSERT INTO products (name, description, price, stock, image_url)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at, updated_at
`
err := r.DB.QueryRow(context.Background(), query,
product.Name,
product.Description,
product.Price,
product.Stock,
product.ImageURL,
).Scan(&product.ID, &product.CreatedAt, &product.UpdatedAt)
return err
}
Langkah 4: Halaman Admin & Handler Tambah Produk
Sekarang kita akan membuat halaman admin dan menghubungkan semuanya.
1. Buat Template Admin
Buat file web/templates/admin_products.page.html
:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Admin - Manajemen Produk</title></head>
<body>
<h1>Manajemen Produk</h1>
<h2>Tambah Produk Baru</h2>
<form action="/admin/products" method="POST" enctype="multipart/form-data">
<div><label>Nama Produk:</label><input type="text" name="name" required></div>
<div><label>Deskripsi:</label><textarea name="description"></textarea></div>
<div><label>Harga:</label><input type="number" step="0.01" name="price" required></div>
<div><label>Stok:</label><input type="number" name="stock" required></div>
<div><label>Gambar Produk:</label><input type="file" name="image" accept="image/*"></div>
<button type="submit">Simpan Produk</button>
</form>
<hr>
<h2>Daftar Produk</h2>
<!-- Daftar produk akan ditampilkan di sini nanti -->
</body>
</html>
Perhatikan enctype="multipart/form-data"
. Ini wajib untuk form yang mengandung upload file.
2. Buat Handler Admin
Buat file baru cmd/web/handler/admin.go
:
// cmd/web/handler/admin.go
package handler
import (
"context"
"fmt"
"log"
"net/http"
"path/filepath"
"strconv"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/kodekilat/go-ecommerce/cmd/web/view"
"github.com/kodekilat/go-ecommerce/internal/models"
"github.com/kodekilat/go-ecommerce/internal/repository"
"github.com/kodekilat/go-ecommerce/internal/storage"
)
type AdminHandler struct {
ProductRepo *repository.ProductRepository
}
func (h *AdminHandler) ShowAdminProducts(w http.ResponseWriter, r *http.Request) {
view.Render(w, "admin_products.page.html", nil)
}
func (h *AdminHandler) HandleAddProduct(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(10 << 20) // 10 MB
name := r.PostFormValue("name")
description := r.PostFormValue("description")
price, _ := strconv.ParseFloat(r.PostFormValue("price"), 64)
stock, _ := strconv.Atoi(r.PostFormValue("stock"))
var imageURL string
file, header, err := r.FormFile("image")
if err == nil {
defer file.Close()
bucketName := "products"
objectName := uuid.New().String() + filepath.Ext(header.Filename)
_, err = storage.MinioClient.PutObject(context.Background(), bucketName, objectName, file, header.Size, minio.PutObjectOptions{ContentType: header.Header.Get("Content-Type")})
if err != nil {
http.Error(w, "Gagal mengunggah gambar", http.StatusInternalServerError)
return
}
imageURL = fmt.Sprintf("http://127.0.0.1:9000/%s/%s", bucketName, objectName)
}
newProduct := &models.Product{
Name: name, Description: description, Price: price, Stock: stock, ImageURL: imageURL,
}
err = h.ProductRepo.CreateProduct(newProduct)
if err != nil {
log.Printf("Gagal menyimpan produk: %v", err)
http.Error(w, "Gagal menyimpan produk", http.StatusInternalServerError)
return
}
log.Printf("Produk baru berhasil disimpan dengan ID: %s", newProduct.ID)
http.Redirect(w, r, "/admin/products", http.StatusSeeOther)
}
Catatan: Penanganan error untuk strconv
diabaikan untuk keringkasan, tetapi di aplikasi riil Anda harus menanganinya.
Langkah 5: Hubungkan Semuanya (Dependency Injection)
Terakhir, kita perlu membuat instance ProductRepository
dan AdminHandler
lalu mendaftarkan rutenya.
1. Perbarui main.go
// cmd/web/main.go
func main() {
// ...
db, err := database.NewConnection()
// ...
storage.InitMinio()
userRepo := &repository.UserRepository{DB: db}
productRepo := &repository.ProductRepository{DB: db} // Tambahkan ini
appRouter := router.New(userRepo, productRepo) // Ubah ini
log.Println("Memulai server di http://localhost:8080")
http.ListenAndServe(":8080", appRouter)
}
2. Perbarui router.go
// cmd/web/router/router.go
func New(userRepo *repository.UserRepository, productRepo *repository.ProductRepository) http.Handler { // Ubah signature
r := chi.NewRouter()
// ... (middleware)
authHandler := &handler.AuthHandler{UserRepo: userRepo}
adminHandler := &handler.AdminHandler{ProductRepo: productRepo} // Tambahkan ini
// ... (rute publik & auth)
// Grup rute yang memerlukan autentikasi
r.Group(func(r chi.Router) {
r.Use(authMiddleware.RequireAuthentication)
r.Get("/account", handler.ShowAccountPage)
// Rute admin baru
r.Get("/admin/products", adminHandler.ShowAdminProducts)
r.Post("/admin/products", adminHandler.HandleAddProduct)
})
return r
}
Jalankan aplikasi Anda, login, dan buka http://localhost:8080/admin/products
. Coba tambahkan produk baru dengan gambar. Jika semua berjalan lancar, Anda akan melihat log sukses di terminal dan gambar baru di konsol Minio Anda (http://localhost:9001
)!
Penutup
Luar biasa! Ini adalah bagian yang paling padat sejauh ini, tetapi hasilnya sangat nyata. Kita telah berhasil membangun fitur inti dari backend e-commerce mana pun.
Di bagian selanjutnya dan terakhir, kita akan fokus pada sisi pengguna: menampilkan produk-produk ini di halaman utama dan menata semuanya dengan UnoCSS agar terlihat seperti toko online sungguhan. Kita hampir sampai di garis finis!
Untuk referensi, Anda dapat melihat kode final dari tutorial ini di repositori GitHub berikut: Source code lengkap