added adapter to repository

This commit is contained in:
mzfarshad 2026-05-04 21:42:54 +03:30
parent 6ca9fd1645
commit d400991b13
6 changed files with 166 additions and 272 deletions

View File

@ -0,0 +1,123 @@
package repository
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Config struct {
CacheKartKeyPrefix string `koanf:"cache_kart_key_prefix"`
CacheTTL time.Duration `koanf:"cache_ttl"`
MysqlTTL time.Duration `koanf:"mysql_ttl"`
}
type Repo struct {
db DB
cache Cache
}
func New(db DB, cache Cache) Repo {
return Repo{db: db, cache: cache}
}
func op(s string) string {
return fmt.Sprintf("shoppingbasketapp-repository-", s)
}
func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error {
if err := r.db.addToBasket(ctx, userID, item); err != nil {
return err
}
cart, err := r.db.getCart(ctx, nil, FindCartByUserIDQuery, userID)
if err != nil {
return err
}
return r.cache.upsertCart(ctx, cart)
}
func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
cart, err := r.cache.getCart(ctx, userID)
if err != nil {
return entity.Cart{}, err
}
if cart.ID < 1 || len(cart.Items) < 1 {
c, err := r.db.getCart(ctx, nil, FindCartByUserIDQuery, userID)
if err != nil {
return entity.Cart{}, err
}
if c.ID < 1 {
return entity.Cart{}, nil
}
if err := r.cache.upsertCart(ctx, c); err != nil {
return entity.Cart{}, err
}
return c, nil
}
return cart, nil
}
func (r Repo) UpdateQuantity(ctx context.Context, cartID, itemID types.ID, quantity int) error {
cart, err := r.db.getCart(ctx, nil, FindCartByIDQuery, cartID)
if err != nil {
return err
}
for i, item := range cart.Items {
if item.ID == itemID {
cart.Items[i].Quantity = quantity
item.Quantity = quantity
if err := r.db.updateQuantity(ctx, nil, item); err != nil {
return err
}
}
}
return r.cache.upsertCart(ctx, cart)
}
func (r Repo) UpdateStatus(ctx context.Context, cartID types.ID, status entity.CartStatus) error {
if err := r.db.updateCartStatus(ctx, nil, cartID, status); err != nil {
return err
}
cart, err := r.db.getCart(ctx, nil, FindCartByIDQuery, cartID)
if err != nil {
return err
}
return r.cache.upsertCart(ctx, cart)
}
func (r Repo) DeleteItem(ctx context.Context, cartID, itemID types.ID) error {
if err := r.db.deleteItem(ctx, nil, cartID, itemID); err != nil {
return err
}
c, err := r.db.getCart(ctx, nil, FindCartByIDQuery, cartID)
if err != nil {
return err
}
return r.cache.upsertCart(ctx, c)
}
func (r Repo) DeleteCart(ctx context.Context, cartID, userID types.ID) error {
if err := r.db.deleteCart(ctx, cartID, userID); err != nil {
return err
}
return r.cache.deleteCart(ctx, userID)
}

View File

@ -207,3 +207,20 @@ func parseCartFromRedis(result map[string]string) (entity.Cart, error) {
return cart, nil
}
func (c Cache) deleteCart(ctx context.Context, userID types.ID) error {
cartKey := c.cartKey(userID)
cartIDsKey := c.kartIDsKey(userID)
pipe := c.client.Client().Pipeline()
pipe.Del(ctx, cartKey)
pipe.Del(ctx, cartIDsKey)
_, err := pipe.Exec(ctx)
if err != nil {
return richerror.New(richerror.Op(op("cache-deleteCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}

View File

@ -1,262 +0,0 @@
package repository
import (
"context"
"encoding/json"
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/redis/go-redis/v9"
"strconv"
"strings"
"time"
)
type Config struct {
CacheKartKeyPrefix string `koanf:"cache_kart_key_prefix"`
CacheTTL time.Duration `koanf:"cache_ttl"`
MysqlTTL time.Duration `koanf:"mysql_ttl"`
}
type Repo struct {
client *redis.Client
config Config
}
func New(client *redis.Client, cfg Config) Repo {
return Repo{client: client, config: cfg}
}
func op(s string) string {
return fmt.Sprintf("shoppingbasketapp-repository-", s)
}
func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error {
const op = "shoppingbasketapp.repository.AddItem"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(item.ProductID)
now := time.Now().UnixNano()
itemJson, _ := json.Marshal(item)
exists, _ := r.client.Exists(ctx, cartKey).Result()
if exists == 0 {
r.client.HSet(ctx, cartKey, map[string]interface{}{
UserIDField: userID,
itemKey: string(itemJson),
TotalPriceField: item.Price * types.Price(item.Quantity),
CreatedAtField: now,
ExpireAtField: now + r.config.CacheTTL.Nanoseconds(),
})
} else {
existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result()
if existsItem != "" {
var i entity.Item
if err := json.Unmarshal([]byte(existsItem), &i); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
item.Quantity += i.Quantity
itemJson, _ = json.Marshal(item)
}
r.client.HSet(ctx, cartKey, itemKey, string(itemJson))
r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.CacheTTL.Nanoseconds())
if err := r.updateTotalPrice(ctx, cartKey); err != nil {
return err
}
}
r.client.Expire(ctx, cartKey, r.config.CacheTTL)
return nil
}
func parsInt(s string) int64 {
i, _ := strconv.ParseInt(s, 10, 64)
return i
}
func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
const op = "shoppingbasketapp.repository.GetCart"
cartKey := r.cartKey(userID)
exists, err := r.client.Exists(ctx, cartKey).Result()
if err != nil {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if exists == 0 {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
}
allCart, err := r.client.HGetAll(ctx, cartKey).Result()
if err != nil {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c := entity.Cart{Items: []entity.Item{}}
for field, value := range allCart {
if strings.HasPrefix(field, "item:") {
var i entity.Item
if err := json.Unmarshal([]byte(value), &i); err != nil {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c.Items = append(c.Items, i)
continue
}
switch field {
case UserIDField:
c.UserID = types.ID(parsInt(value))
case TotalPriceField:
c.TotalPrice = types.Price(parsInt(value))
case CreatedAtField:
c.CreatedAt = parsInt(value)
case ExpireAtField:
c.ExpireAt = parsInt(value)
}
}
return c, nil
}
func (r Repo) DeleteItem(ctx context.Context, userID, productID types.ID) error {
const op = "shoppingbasketapp.repository.DeleteItem"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(productID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
return err
}
if err := r.client.HDel(ctx, cartKey, itemKey).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
num, err := r.client.HLen(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if num < FieldNumber {
return r.DeleteCart(ctx, userID)
}
return r.updateTotalPrice(ctx, cartKey)
}
func (r Repo) UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error {
const op = "shoppingbasketapp.repository.UpdateQuantity"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(productID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
return err
}
data, err := r.client.HGet(ctx, cartKey, itemKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var item entity.Item
if err := json.Unmarshal([]byte(data), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
item.Quantity = quantity
j, _ := json.Marshal(item)
if err := r.client.HSet(ctx, cartKey, itemKey, string(j)).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return r.updateTotalPrice(ctx, cartKey)
}
func (r Repo) DeleteCart(ctx context.Context, userID types.ID) error {
const op = "shoppingbasketapp.repository.DeleteCart"
cartKey := r.cartKey(userID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.client.Del(ctx, cartKey).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}
func (r Repo) updateTotalPrice(ctx context.Context, cartKey string) error {
const op = "shoppingbasketapp.repository.updateTotalPrice"
allFields, err := r.client.HGetAll(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var total types.Price
for field, value := range allFields {
if strings.HasPrefix(field, "item:") {
var item entity.Item
if err := json.Unmarshal([]byte(value), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
total += item.Price * types.Price(item.Quantity)
}
}
return r.client.HSet(ctx, cartKey, TotalPriceField, int64(total)).Err()
}
func (r Repo) existsCart(ctx context.Context, cartKey string) error {
const op = "shoppingbasketapp.repository.existsCart"
exists, err := r.client.Exists(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if exists == 0 {
return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
}
return nil
}
func (r Repo) existsItem(ctx context.Context, cartKey, itemKey string) error {
const op = "shoppingbasketapp.repository.existsItem"
exists, err := r.client.HExists(ctx, cartKey, itemKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if !exists {
return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found product form shopping basket")
}
return nil
}

View File

@ -5,7 +5,8 @@ import "errors"
const (
FindCartByUserIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at
FROM carts
WHERE user_id = ? AND status = 'active' AND deleted_at IS NULL`
WHERE user_id = ? AND status = 'active' AND deleted_at IS NULL
ORDER BY created_at DESC`
FindCartByIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at
FROM carts

View File

@ -302,7 +302,7 @@ func (d DB) updateTotalPrice(ctx context.Context, tx *sql.Tx, cartID types.ID) e
return nil
}
func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types.ID) error {
func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, itemID types.ID) error {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-deleteItem")
if err != nil {
return err
@ -317,9 +317,9 @@ func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types.
query := `UPDATE items
SET
deleted_at = ?
WHERE cart_id = ? AND product_id = ? AND deleted_at IS NULL`
WHERE cart_id = ? AND id = ? AND deleted_at IS NULL`
deleteTime := time.Now().UTC()
_, err = t.ExecContext(ctx, query, deleteTime, cartID, productID)
_, err = t.ExecContext(ctx, query, deleteTime, cartID, itemID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-deleteItem"))).
WithKind(richerror.KindUnexpected).WithErr(err)
@ -340,12 +340,27 @@ func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types.
}
func (d DB) deleteCart(ctx context.Context, cartID, userID types.ID) error {
query := `UPDATE items
itemQuery := `UPDATE items
SET
deleted_at = ?
WHERE cart_id = ? AND user_id = ? AND deleted_at IS NULL`
deleteTime := time.Now().UTC()
_, err := d.db.Conn().ExecContext(ctx, query, deleteTime, cartID, userID)
deleteItemTime := time.Now().UTC()
_, err := d.db.Conn().ExecContext(ctx, itemQuery, deleteItemTime, cartID, userID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-deleteCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
cartQuery := `UPDATE carts
SET
deleted_at = ?
WHERE id = ? AND user_id = ? AND deleted_at IS NULL`
deleteCartTime := time.Now().UTC()
_, err = d.db.Conn().ExecContext(ctx, cartQuery, deleteCartTime, cartID, userID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-deleteCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
@ -370,7 +385,7 @@ func (d DB) updateCartStatus(ctx context.Context, tx *sql.Tx, cartID types.ID, s
query := `UPDATE carts
SET
status = ?
WHERE cart_id = ? AND deleted_at IS NULL`
WHERE id = ? AND status = active AND deleted_at IS NULL`
_, err = t.ExecContext(ctx, query, status, cartID)
if err != nil {

View File

@ -13,8 +13,8 @@ type Repository interface {
AddItem(ctx context.Context, userID types.ID, item entity.Item) error
GetCart(ctx context.Context, userID types.ID) (entity.Cart, error)
DeleteItem(ctx context.Context, cartID, itemID types.ID) error
UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error
DeleteCart(ctx context.Context, cartID types.ID) error
UpdateQuantity(ctx context.Context, cartID, itemID types.ID, quantity int) error
DeleteCart(ctx context.Context, cartID, userID types.ID) error
UpdateStatus(ctx context.Context, cartID types.ID, status entity.CartStatus) error
}