added mysql db

This commit is contained in:
mzfarshad 2026-04-28 20:54:47 +03:30
parent 4ae65e95e1
commit dbe9cb7df8
7 changed files with 457 additions and 19 deletions

View File

@ -2,20 +2,36 @@ package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type CartStatus string
type Item struct {
ID types.ID
CartID types.ID
ProductID types.ID
UserID types.ID
Quantity int
Price types.Price
Price float64
Name string
AddedAt int64
AddedAt time.Time
UpdatedAt time.Time
}
type Cart struct {
ID types.ID
UserID types.ID
Items []Item
TotalPrice types.Price
ExpireAt int64
CreatedAt int64
Status CartStatus
TotalPrice float64
ExpireAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
const (
CartStatusActive CartStatus = "active"
CartStatusExpired CartStatus = "expired"
CartStatusCheckedOut CartStatus = "checked_out"
)

View File

@ -35,6 +35,10 @@ 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) cartKey(userID types.ID) string {
return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID)
}

View File

@ -0,0 +1,18 @@
package repository
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`
FindCartByIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at
FROM carts
WHERE id = ? AND status = 'active' AND deleted_at IS NULL`
)
var (
ErrNotExistsCart = errors.New("not exists cart")
ErrExpiredCart = errors.New("expired cart")
)

View File

@ -1,15 +1,19 @@
-- +migrate Up
CREATE TABLE `carts` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`total_price` DECIMAL(10,2) DEFAULT 0,
`expire_at` TIMESTAMP NULL,
`status` ENUM('active', 'expired', 'checked_out') NOT NULL DEFAULT 'active',
`expire_at` TIMESTAMP NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`deleted_at` TIMESTAMP NULL DEFAULT NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE INDEX `idx_carts_user_id` ON `carts`(`user_id`);
CREATE INDEX `idx_carts_id` ON `carts`(`id`);
-- +migrate Down
DROP INDEX `idx_carts_user_id` ON `carts`;
DROP INDEX `idx_carts_id` ON `carts`;
DROP TABLE `carts`;

View File

@ -1,13 +1,16 @@
-- +migrate Up
CREATE TABLE `items` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`cart_id` INT NOT NULL,
`product_id` INT NOT NULL,
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`cart_id` BIGINT UNSIGNED NOT NULL,
`product_id` BIGINT UNSIGNED NOT NULL,
`user_id` BIGINT UNSIGNED NOT NULL,
`quantity` INT NOT NULL DEFAULT 1,
`price` DECIMAL(10,2) NOT NULL,
`name` VARCHAR(256),
`name` VARCHAR(256) NOT NULL ,
`added_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`cart_id`) REFERENCES `cart`(`id`)
`deleted_at` TIMESTAMP NULL DEFAULT NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`cart_id`) REFERENCES `carts`(`id`)
);
CREATE INDEX `idx_items_cart_id` ON `items`(`cart_id`);

View File

@ -1,15 +1,408 @@
package repository
import querier "git.gocasts.ir/ebhomengo/niki/pkg/query_transaction/sql"
import (
"context"
"database/sql"
"errors"
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type DB struct {
db *querier.SQLDB
db *mysql.DB
config Config
}
func NewDB(db *querier.SQLDB) DB {
func NewDB(db *mysql.DB) DB {
return DB{db: db}
}
func (d DB) addToBasket() {
func (d DB) getCart(ctx context.Context, tx *sql.Tx, query string, by types.ID) (entity.Cart, error) {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-getCart")
if err != nil {
return entity.Cart{}, err
}
if ok {
defer func() {
_ = tx.Rollback()
}()
}
var c entity.Cart
err = t.QueryRowContext(ctx, query, by).
Scan(&c.ID, &c.UserID, &c.TotalPrice, &c.Status, &c.ExpireAt, &c.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.KindNotFound).
WithErr(ErrNotExistsCart)
}
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
if c.ExpireAt.Before(time.Now().UTC()) {
if err := d.updateCartStatus(ctx, t, c.ID, entity.CartStatusExpired); err != nil {
return entity.Cart{}, err
}
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.KindInvalid).WithErr(ErrExpiredCart)
}
itemsQuery := `SELECT id, cart_id, user_id, product_id, quantity, price, name, added_at, updated_at
FROM items
WHERE cart_id=? AND deleted_at IS NULL
ORDER BY added_at`
rows, err := t.QueryContext(ctx, itemsQuery, c.ID)
if err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
defer rows.Close()
items := make([]entity.Item, 0)
for rows.Next() {
var i entity.Item
if err := rows.Scan(&i.ID, &i.CartID, &i.UserID, &i.ProductID,
&i.Quantity, &i.Price, &i.Name, &i.AddedAt, &i.UpdatedAt); err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.KindUnexpected).
WithErr(fmt.Errorf("error scan rows: %w", err))
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
c.Items = items
if ok {
if err := tx.Commit(); err != nil {
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
WithKind(richerror.
KindUnexpected).WithErr(err)
}
}
return c, nil
}
func (d DB) addToBasket(ctx context.Context, userID types.ID, item entity.Item) error {
tx, err := d.db.Conn().BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return richerror.New(richerror.Op(op("mysql-addToBasket"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
defer func() {
_ = tx.Rollback()
}()
c, err := d.getCart(ctx, tx, FindCartByUserIDQuery, userID)
if err != nil {
if err.Error() == ErrExpiredCart.Error() || err.Error() == ErrNotExistsCart.Error() {
cartQuery := `INSERT INTO carts user_id, total_price, status, expire_at, created_at
VALUES (?,?,?,?)`
newCart := entity.Cart{
UserID: userID,
TotalPrice: item.Price * float64(item.Quantity),
Status: entity.CartStatusActive,
CreatedAt: time.Now().UTC(),
ExpireAt: time.Now().Add(d.config.TTL).UTC(),
}
result, err := tx.ExecContext(ctx, cartQuery,
newCart.UserID, newCart.TotalPrice, newCart.Status, newCart.ExpireAt, newCart.CreatedAt)
if err != nil {
return richerror.New(richerror.Op(op("mysql-addToBasket"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
cartID, _ := result.LastInsertId()
item.CartID = types.ID(cartID)
if err := d.addItem(ctx, tx, item); err != nil {
return err
}
if err := d.updateTotalPrice(ctx, tx, types.ID(cartID)); err != nil {
return err
}
}
return err
}
isNew := true
if c.ID > 0 {
for _, i := range c.Items {
if i.ProductID == item.ProductID {
item.Quantity = i.Quantity + item.Quantity
if err := d.updateQuantity(ctx, tx, item); err != nil {
return err
}
isNew = false
}
}
}
if isNew {
err := d.addItem(ctx, tx, item)
if err != nil {
return err
}
}
if err := d.updateTotalPrice(ctx, tx, c.ID); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return richerror.New(richerror.Op(op("mysql-addToBasket"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
return nil
}
func (d DB) addItem(ctx context.Context, tx *sql.Tx, item entity.Item) error {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-addItem")
if err != nil {
return richerror.New(richerror.Op(op("mysql-addItem"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
if ok {
defer func() {
_ = tx.Rollback()
}()
}
query := `INSERT INTO items cart_id, user_id, product_id, quantity, price, name, added_at
VALUES (?,?,?,?,?,?,?)`
_, err = t.ExecContext(ctx, query,
item.CartID, item.UserID, item.ProductID, item.Quantity, item.Price, item.Name, item.AddedAt)
if err != nil {
return richerror.New(richerror.Op(op("mysql-addItem"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
if ok {
if err := tx.Commit(); err != nil {
return richerror.New(richerror.Op(op("mysql-addItem"))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
}
return nil
}
func (d DB) updateQuantity(ctx context.Context, tx *sql.Tx, item entity.Item) error {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateQuantity")
if err != nil {
return err
}
if ok {
defer func() {
_ = tx.Rollback()
}()
}
query := `UPDATE items
SET
quantity = ?
WHERE id = ?`
_, err = t.ExecContext(ctx, query, item.Quantity, item.ID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-updateQuantity"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
if ok {
if err := t.Commit(); err != nil {
return richerror.New(richerror.Op(op("mysql-updateQuantity"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
}
return nil
}
func (d DB) updateTotalPrice(ctx context.Context, tx *sql.Tx, cartID types.ID) error {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateTotalPrice")
if err != nil {
return err
}
if ok {
defer func() {
_ = tx.Rollback()
}()
}
c, err := d.getCart(ctx, t, FindCartByIDQuery, cartID)
if err != nil {
return err
}
var totalPrice float64
for _, i := range c.Items {
totalPrice += i.Price * float64(i.Quantity)
}
query := `UPDATE carts
SET
total_price = ?
WHERE cart_id=?`
_, err = tx.ExecContext(ctx, query, totalPrice, c.ID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-updateTotalPrice"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
if ok {
if err := tx.Commit(); err != nil {
return richerror.New(richerror.Op(op("mysql-updateTotalPrice"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
}
return nil
}
func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types.ID) error {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-deleteItem")
if err != nil {
return err
}
if ok {
defer func() {
_ = tx.Rollback()
}()
}
query := `UPDATE items
SET
deleted_at = ?
WHERE cart_id = ? AND product_id = ? AND deleted_at IS NULL`
deleteTime := time.Now().UTC()
_, err = t.ExecContext(ctx, query, deleteTime, cartID, productID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-deleteItem"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
if err := d.updateTotalPrice(ctx, t, cartID); err != nil {
return err
}
if ok {
if err := t.Commit(); err != nil {
return richerror.New(richerror.Op(op("mysql-deleteItem"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
}
return nil
}
func (d DB) deleteCart(ctx context.Context, cartID, userID types.ID) error {
query := `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)
if err != nil {
return richerror.New(richerror.Op(op("mysql-deleteCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}
func (d DB) updateCartStatus(ctx context.Context, tx *sql.Tx, cartID types.ID, status entity.CartStatus) error {
t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateCartStatus")
if err != nil {
return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
if ok {
defer func() {
_ = t.Rollback()
}()
}
query := `UPDATE carts
SET
status = ?
WHERE cart_id = ? AND deleted_at IS NULL`
_, err = t.ExecContext(ctx, query, status, cartID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
if ok {
if err := t.Commit(); err != nil {
return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
}
return nil
}
func (d DB) getLocalTX(ctx context.Context, tx *sql.Tx, p string) (*sql.Tx, bool, error) {
var err error
var ok bool
if tx == nil {
tx, err = d.db.Conn().BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return nil, false, richerror.New(richerror.Op(op(fmt.Sprintf("%s", p)))).
WithKind(richerror.KindUnexpected).
WithErr(err)
}
ok = true
}
return tx, ok, nil
}

View File

@ -12,9 +12,9 @@ import (
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, userID, productID types.ID) 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, userID types.ID) error
DeleteCart(ctx context.Context, cartID types.ID) error
}
type Service struct {