feat: add posts

This commit is contained in:
Rogee
2025-01-15 14:47:10 +08:00
parent ab827715fb
commit 9002862415
13 changed files with 374 additions and 104 deletions

View File

@@ -70,8 +70,8 @@ func (ctl *Controller) Upload(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug strin
TenantID: tenant.ID,
UserID: claim.UserID,
StorageID: defaultStorage.ID,
Hash: uploadedFile.Hash,
Name: uploadedFile.Name,
UUID: uploadedFile.Hash,
MimeType: uploadedFile.MimeType,
Size: uploadedFile.Size,
Path: uploadedFile.Path,

View File

@@ -9,6 +9,7 @@ import (
"backend/providers/otel"
. "github.com/go-jet/jet/v2/postgres"
"github.com/samber/lo"
log "github.com/sirupsen/logrus"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
@@ -40,3 +41,34 @@ func (svc *Service) Create(ctx context.Context, m *model.Medias) (*model.Medias,
}
return &ret, nil
}
// GetMediasByHash
func (svc *Service) GetMediasByHash(ctx context.Context, tenantID, userID int64, hashes []string) ([]*model.Medias, error) {
_, span := otel.Start(ctx, "medias.service.GetMediasByHash")
defer span.End()
hashExpr := lo.Map(hashes, func(item string, index int) Expression { return String(item) })
tbl := table.Medias
stmt := tbl.
SELECT(tbl.AllColumns).
WHERE(
tbl.TenantID.
EQ(Int64(tenantID)).
AND(
tbl.UserID.EQ(Int64(userID)),
).
AND(
tbl.Hash.IN(hashExpr...),
),
)
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var ret []model.Medias
if err := stmt.QueryContext(ctx, svc.db, &ret); err != nil {
return nil, err
}
return lo.Map(ret, func(item model.Medias, _ int) *model.Medias {
return &item
}), nil
}

View File

@@ -4,6 +4,7 @@ import (
"time"
"backend/app/errorx"
"backend/app/http/medias"
"backend/app/http/tenants"
"backend/app/http/users"
"backend/app/requests"
@@ -24,6 +25,7 @@ type Controller struct {
hashIds *hashids.HashID
userSvc *users.Service
tenantSvc *tenants.Service
mediaSvc *medias.Service
log *log.Entry `inject:"false"`
}
@@ -153,24 +155,37 @@ func (ctl *Controller) Create(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug strin
return err
}
// check media assets exists
hashes := lo.Map(body.Assets.Data, func(item fields.MediaAsset, _ int) string { return item.Hash })
medias, err := ctl.mediaSvc.GetMediasByHash(ctx.Context(), tenant.ID, user.ID, hashes)
if err != nil {
return err
}
if len(medias) != len(lo.Uniq(hashes)) {
return errorx.BadRequest
}
post := &model.Posts{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
TenantID: tenant.ID,
UserID: user.ID,
Title: body.Title,
Description: body.Description,
Content: body.Content,
PosterAssetID: 0,
Stage: fields.PostStagePending,
Status: fields.PostStatusPending,
Price: body.Price,
Discount: body.Discount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
TenantID: tenant.ID,
UserID: user.ID,
Title: body.Title,
Description: body.Description,
Content: body.Content,
Stage: fields.PostStagePending,
Status: fields.PostStatusPending,
Price: body.Price,
Discount: body.Discount,
Assets: body.Assets,
Tags: body.Tags,
}
if err := ctl.svc.Create(ctx.Context(), tenant, user, post); err != nil {
return err
}
// TODO: trigger event && jobs
return nil
}

View File

@@ -2,6 +2,8 @@ package posts
import (
"time"
"backend/database/fields"
)
type UserPost struct {
@@ -32,9 +34,11 @@ type UserPostFilter struct {
}
type PostBody struct {
Title string
Description string
Content string
Price int64
Discount int16
Title string `json:"title,omitempty"`
Tags fields.Json[[]string] `json:"tags,omitempty"`
Description string `json:"description,omitempty"`
Content string `json:"content,omitempty"`
Price int64 `json:"price,omitempty"`
Discount int16 `json:"discount,omitempty"`
Assets fields.Json[[]fields.MediaAsset] `json:"assets,omitempty"`
}

View File

@@ -0,0 +1,199 @@
// Code generated by go-enum DO NOT EDIT.
// Version: -
// Revision: -
// Build Date: -
// Built By: -
package fields
import (
"database/sql/driver"
"errors"
"fmt"
"strings"
)
const (
// MediaAssetTypeUnknown is a MediaAssetType of type Unknown.
MediaAssetTypeUnknown MediaAssetType = "unknown"
// MediaAssetTypePoster is a MediaAssetType of type Poster.
MediaAssetTypePoster MediaAssetType = "poster"
// MediaAssetTypeImage is a MediaAssetType of type Image.
MediaAssetTypeImage MediaAssetType = "image"
// MediaAssetTypeVideo is a MediaAssetType of type Video.
MediaAssetTypeVideo MediaAssetType = "video"
// MediaAssetTypeAudio is a MediaAssetType of type Audio.
MediaAssetTypeAudio MediaAssetType = "audio"
// MediaAssetTypeDocument is a MediaAssetType of type Document.
MediaAssetTypeDocument MediaAssetType = "document"
// MediaAssetTypeOther is a MediaAssetType of type Other.
MediaAssetTypeOther MediaAssetType = "other"
)
var ErrInvalidMediaAssetType = fmt.Errorf("not a valid MediaAssetType, try [%s]", strings.Join(_MediaAssetTypeNames, ", "))
var _MediaAssetTypeNames = []string{
string(MediaAssetTypeUnknown),
string(MediaAssetTypePoster),
string(MediaAssetTypeImage),
string(MediaAssetTypeVideo),
string(MediaAssetTypeAudio),
string(MediaAssetTypeDocument),
string(MediaAssetTypeOther),
}
// MediaAssetTypeNames returns a list of possible string values of MediaAssetType.
func MediaAssetTypeNames() []string {
tmp := make([]string, len(_MediaAssetTypeNames))
copy(tmp, _MediaAssetTypeNames)
return tmp
}
// MediaAssetTypeValues returns a list of the values for MediaAssetType
func MediaAssetTypeValues() []MediaAssetType {
return []MediaAssetType{
MediaAssetTypeUnknown,
MediaAssetTypePoster,
MediaAssetTypeImage,
MediaAssetTypeVideo,
MediaAssetTypeAudio,
MediaAssetTypeDocument,
MediaAssetTypeOther,
}
}
// String implements the Stringer interface.
func (x MediaAssetType) String() string {
return string(x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x MediaAssetType) IsValid() bool {
_, err := ParseMediaAssetType(string(x))
return err == nil
}
var _MediaAssetTypeValue = map[string]MediaAssetType{
"unknown": MediaAssetTypeUnknown,
"poster": MediaAssetTypePoster,
"image": MediaAssetTypeImage,
"video": MediaAssetTypeVideo,
"audio": MediaAssetTypeAudio,
"document": MediaAssetTypeDocument,
"other": MediaAssetTypeOther,
}
// ParseMediaAssetType attempts to convert a string to a MediaAssetType.
func ParseMediaAssetType(name string) (MediaAssetType, error) {
if x, ok := _MediaAssetTypeValue[name]; ok {
return x, nil
}
return MediaAssetType(""), fmt.Errorf("%s is %w", name, ErrInvalidMediaAssetType)
}
var errMediaAssetTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes
// Scan implements the Scanner interface.
func (x *MediaAssetType) Scan(value interface{}) (err error) {
if value == nil {
*x = MediaAssetType("")
return
}
// A wider range of scannable types.
// driver.Value values at the top of the list for expediency
switch v := value.(type) {
case string:
*x, err = ParseMediaAssetType(v)
case []byte:
*x, err = ParseMediaAssetType(string(v))
case MediaAssetType:
*x = v
case *MediaAssetType:
if v == nil {
return errMediaAssetTypeNilPtr
}
*x = *v
case *string:
if v == nil {
return errMediaAssetTypeNilPtr
}
*x, err = ParseMediaAssetType(*v)
default:
return errors.New("invalid type for MediaAssetType")
}
return
}
// Value implements the driver Valuer interface.
func (x MediaAssetType) Value() (driver.Value, error) {
return x.String(), nil
}
// Set implements the Golang flag.Value interface func.
func (x *MediaAssetType) Set(val string) error {
v, err := ParseMediaAssetType(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *MediaAssetType) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *MediaAssetType) Type() string {
return "MediaAssetType"
}
type NullMediaAssetType struct {
MediaAssetType MediaAssetType
Valid bool
}
func NewNullMediaAssetType(val interface{}) (x NullMediaAssetType) {
err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
_ = err // make any errcheck linters happy
return
}
// Scan implements the Scanner interface.
func (x *NullMediaAssetType) Scan(value interface{}) (err error) {
if value == nil {
x.MediaAssetType, x.Valid = MediaAssetType(""), false
return
}
err = x.MediaAssetType.Scan(value)
x.Valid = (err == nil)
return
}
// Value implements the driver Valuer interface.
func (x NullMediaAssetType) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
// driver.Value accepts int64 for int values.
return string(x.MediaAssetType), nil
}
type NullMediaAssetTypeStr struct {
NullMediaAssetType
}
func NewNullMediaAssetTypeStr(val interface{}) (x NullMediaAssetTypeStr) {
x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
return
}
// Value implements the driver Valuer interface.
func (x NullMediaAssetTypeStr) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
return x.MediaAssetType.String(), nil
}

View File

@@ -0,0 +1,18 @@
package fields
type MediaAsset struct {
Type MediaAssetType `json:"type"`
Hash string `json:"hash"`
}
// swagger:enum MediaAssetType
// ENUM(
// Unknown = "unknown",
// Poster = "poster",
// Image = "image",
// Video = "video",
// Audio = "audio",
// Document = "document",
// Other = "other"
// )
type MediaAssetType string

View File

@@ -17,13 +17,13 @@ CREATE TABLE
title VARCHAR(128) NOT NULL,
description VARCHAR(256) NOT NULL,
poster_asset_id INT8 NOT NULL,
content TEXT NOT NULL,
price INT8 NOT NULL default 0,
discount INT2 NOT NULL default 100,
views INT8 NOT NULL default 0,
likes INT8 NOT NULL default 0,
meta jsonb default '{}'::jsonb,
tags jsonb default '{}'::jsonb,
assets jsonb default '{}'::jsonb
);
-- create indexes

View File

@@ -10,8 +10,8 @@ CREATE TABLE medias (
user_id INT8 NOT NULL,
post_id INT8 NOT NULL,
storage_id INT8 NOT NULL,
hash VARCHAR(32) NOT NULL,
name VARCHAR(255) NOT NULL default '',
uuid VARCHAR(128) NOT NULL,
mime_type VARCHAR(128) NOT NULL default '',
size INT8 NOT NULL default 0,
path VARCHAR(255) NOT NULL default ''

View File

@@ -19,8 +19,8 @@ type Medias struct {
UserID int64 `json:"user_id"`
PostID int64 `json:"post_id"`
StorageID int64 `json:"storage_id"`
Hash string `json:"hash"`
Name string `json:"name"`
UUID string `json:"uuid"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Path string `json:"path"`

View File

@@ -13,23 +13,23 @@ import (
)
type Posts struct {
ID int64 `sql:"primary_key" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
Type fields.PostType `json:"type"`
Stage fields.PostStage `json:"stage"`
Status fields.PostStatus `json:"status"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
Title string `json:"title"`
Description string `json:"description"`
PosterAssetID int64 `json:"poster_asset_id"`
Content string `json:"content"`
Price int64 `json:"price"`
Discount int16 `json:"discount"`
Views int64 `json:"views"`
Likes int64 `json:"likes"`
Meta *string `json:"meta"`
Assets *string `json:"assets"`
ID int64 `sql:"primary_key" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
Type fields.PostType `json:"type"`
Stage fields.PostStage `json:"stage"`
Status fields.PostStatus `json:"status"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
Price int64 `json:"price"`
Discount int16 `json:"discount"`
Views int64 `json:"views"`
Likes int64 `json:"likes"`
Meta *string `json:"meta"`
Tags fields.Json[[]string] `json:"tags"`
Assets fields.Json[[]fields.MediaAsset] `json:"assets"`
}

View File

@@ -24,8 +24,8 @@ type mediasTable struct {
UserID postgres.ColumnInteger
PostID postgres.ColumnInteger
StorageID postgres.ColumnInteger
Hash postgres.ColumnString
Name postgres.ColumnString
UUID postgres.ColumnString
MimeType postgres.ColumnString
Size postgres.ColumnInteger
Path postgres.ColumnString
@@ -76,13 +76,13 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
UserIDColumn = postgres.IntegerColumn("user_id")
PostIDColumn = postgres.IntegerColumn("post_id")
StorageIDColumn = postgres.IntegerColumn("storage_id")
HashColumn = postgres.StringColumn("hash")
NameColumn = postgres.StringColumn("name")
UUIDColumn = postgres.StringColumn("uuid")
MimeTypeColumn = postgres.StringColumn("mime_type")
SizeColumn = postgres.IntegerColumn("size")
PathColumn = postgres.StringColumn("path")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn}
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, HashColumn, NameColumn, MimeTypeColumn, SizeColumn, PathColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, HashColumn, NameColumn, MimeTypeColumn, SizeColumn, PathColumn}
)
return mediasTable{
@@ -96,8 +96,8 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
UserID: UserIDColumn,
PostID: PostIDColumn,
StorageID: StorageIDColumn,
Hash: HashColumn,
Name: NameColumn,
UUID: UUIDColumn,
MimeType: MimeTypeColumn,
Size: SizeColumn,
Path: PathColumn,

View File

@@ -17,25 +17,25 @@ type postsTable struct {
postgres.Table
// Columns
ID postgres.ColumnInteger
CreatedAt postgres.ColumnTimestamp
UpdatedAt postgres.ColumnTimestamp
DeletedAt postgres.ColumnTimestamp
Type postgres.ColumnInteger
Stage postgres.ColumnInteger
Status postgres.ColumnInteger
TenantID postgres.ColumnInteger
UserID postgres.ColumnInteger
Title postgres.ColumnString
Description postgres.ColumnString
PosterAssetID postgres.ColumnInteger
Content postgres.ColumnString
Price postgres.ColumnInteger
Discount postgres.ColumnInteger
Views postgres.ColumnInteger
Likes postgres.ColumnInteger
Meta postgres.ColumnString
Assets postgres.ColumnString
ID postgres.ColumnInteger
CreatedAt postgres.ColumnTimestamp
UpdatedAt postgres.ColumnTimestamp
DeletedAt postgres.ColumnTimestamp
Type postgres.ColumnInteger
Stage postgres.ColumnInteger
Status postgres.ColumnInteger
TenantID postgres.ColumnInteger
UserID postgres.ColumnInteger
Title postgres.ColumnString
Description postgres.ColumnString
Content postgres.ColumnString
Price postgres.ColumnInteger
Discount postgres.ColumnInteger
Views postgres.ColumnInteger
Likes postgres.ColumnInteger
Meta postgres.ColumnString
Tags postgres.ColumnString
Assets postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@@ -76,52 +76,52 @@ func newPostsTable(schemaName, tableName, alias string) *PostsTable {
func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
var (
IDColumn = postgres.IntegerColumn("id")
CreatedAtColumn = postgres.TimestampColumn("created_at")
UpdatedAtColumn = postgres.TimestampColumn("updated_at")
DeletedAtColumn = postgres.TimestampColumn("deleted_at")
TypeColumn = postgres.IntegerColumn("type")
StageColumn = postgres.IntegerColumn("stage")
StatusColumn = postgres.IntegerColumn("status")
TenantIDColumn = postgres.IntegerColumn("tenant_id")
UserIDColumn = postgres.IntegerColumn("user_id")
TitleColumn = postgres.StringColumn("title")
DescriptionColumn = postgres.StringColumn("description")
PosterAssetIDColumn = postgres.IntegerColumn("poster_asset_id")
ContentColumn = postgres.StringColumn("content")
PriceColumn = postgres.IntegerColumn("price")
DiscountColumn = postgres.IntegerColumn("discount")
ViewsColumn = postgres.IntegerColumn("views")
LikesColumn = postgres.IntegerColumn("likes")
MetaColumn = postgres.StringColumn("meta")
AssetsColumn = postgres.StringColumn("assets")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TypeColumn, StageColumn, StatusColumn, TenantIDColumn, UserIDColumn, TitleColumn, DescriptionColumn, PosterAssetIDColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TypeColumn, StageColumn, StatusColumn, TenantIDColumn, UserIDColumn, TitleColumn, DescriptionColumn, PosterAssetIDColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn}
IDColumn = postgres.IntegerColumn("id")
CreatedAtColumn = postgres.TimestampColumn("created_at")
UpdatedAtColumn = postgres.TimestampColumn("updated_at")
DeletedAtColumn = postgres.TimestampColumn("deleted_at")
TypeColumn = postgres.IntegerColumn("type")
StageColumn = postgres.IntegerColumn("stage")
StatusColumn = postgres.IntegerColumn("status")
TenantIDColumn = postgres.IntegerColumn("tenant_id")
UserIDColumn = postgres.IntegerColumn("user_id")
TitleColumn = postgres.StringColumn("title")
DescriptionColumn = postgres.StringColumn("description")
ContentColumn = postgres.StringColumn("content")
PriceColumn = postgres.IntegerColumn("price")
DiscountColumn = postgres.IntegerColumn("discount")
ViewsColumn = postgres.IntegerColumn("views")
LikesColumn = postgres.IntegerColumn("likes")
MetaColumn = postgres.StringColumn("meta")
TagsColumn = postgres.StringColumn("tags")
AssetsColumn = postgres.StringColumn("assets")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TypeColumn, StageColumn, StatusColumn, TenantIDColumn, UserIDColumn, TitleColumn, DescriptionColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, TagsColumn, AssetsColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TypeColumn, StageColumn, StatusColumn, TenantIDColumn, UserIDColumn, TitleColumn, DescriptionColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, TagsColumn, AssetsColumn}
)
return postsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
DeletedAt: DeletedAtColumn,
Type: TypeColumn,
Stage: StageColumn,
Status: StatusColumn,
TenantID: TenantIDColumn,
UserID: UserIDColumn,
Title: TitleColumn,
Description: DescriptionColumn,
PosterAssetID: PosterAssetIDColumn,
Content: ContentColumn,
Price: PriceColumn,
Discount: DiscountColumn,
Views: ViewsColumn,
Likes: LikesColumn,
Meta: MetaColumn,
Assets: AssetsColumn,
ID: IDColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
DeletedAt: DeletedAtColumn,
Type: TypeColumn,
Stage: StageColumn,
Status: StatusColumn,
TenantID: TenantIDColumn,
UserID: UserIDColumn,
Title: TitleColumn,
Description: DescriptionColumn,
Content: ContentColumn,
Price: PriceColumn,
Discount: DiscountColumn,
Views: ViewsColumn,
Likes: LikesColumn,
Meta: MetaColumn,
Tags: TagsColumn,
Assets: AssetsColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

View File

@@ -20,6 +20,8 @@ types:
stage: PostStage
status: PostStatus
type: PostType
assets: Json[[]MediaAsset]
tags: Json[[]string]
orders:
type: OrderType