Remove search engine, add event list api
Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
@@ -16,11 +16,11 @@ import (
|
|||||||
// @Tags Event
|
// @Tags Event
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param event_id query string true "Event UUID"
|
// @Param event_id query string true "Event UUID"
|
||||||
// @Success 200 {object} utils.RespStatus{data=service_event.InfoResponse} "Successful retrieval"
|
// @Success 200 {object} utils.RespStatus{data=service_event.EventInfoResponse} "Successful retrieval"
|
||||||
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
|
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
|
||||||
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /event/info [get]
|
// @Router /event/info [get]
|
||||||
func (self *EventHandler) Info(c *gin.Context) {
|
func (self *EventHandler) Info(c *gin.Context) {
|
||||||
@@ -41,9 +41,9 @@ func (self *EventHandler) Info(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result := self.svc.Info(&service_event.InfoPayload{
|
result := self.svc.GetEventInfo(&service_event.EventInfoPayload{
|
||||||
Context: c,
|
Context: c,
|
||||||
Data: &service_event.InfoData{
|
Data: &service_event.EventInfoData{
|
||||||
EventId: eventId,
|
EventId: eventId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
65
api/event/list.go
Normal file
65
api/event/list.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List retrieves a paginated list of events from the database.
|
||||||
|
//
|
||||||
|
// @Summary List Events
|
||||||
|
// @Description Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query string false "Maximum number of events to return (default 20)"
|
||||||
|
// @Param offset query string true "Number of events to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.EventIndexDoc} "Successful paginated list retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Missing offset or malformed parameters)"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database query failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/list [get]
|
||||||
|
func (self *EventHandler) List(c *gin.Context) {
|
||||||
|
type ListQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query ListQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
// Handle binding error (e.g., syntax errors in query string)
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload for the service layer
|
||||||
|
eventListPayload := &service_event.EventListPayload{
|
||||||
|
Context: c,
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service implementation
|
||||||
|
result := self.svc.ListEvents(eventListPayload)
|
||||||
|
|
||||||
|
// Check if the service returned any exception
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return successful response with event data
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"nixcn-cms/internal/exception"
|
|
||||||
"nixcn-cms/service/service_user"
|
|
||||||
"nixcn-cms/utils"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Full retrieves the complete list of users directly from the database table.
|
|
||||||
//
|
|
||||||
// @Summary Get Full User Table
|
|
||||||
// @Description Fetches all user records without pagination. This is typically used for administrative overview or data export.
|
|
||||||
// @Tags User
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResponse} "Successful retrieval of full user table"
|
|
||||||
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error)"
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Router /user/full [get]
|
|
||||||
func (self *UserHandler) Full(c *gin.Context) {
|
|
||||||
userTablePayload := &service_user.UserTablePayload{
|
|
||||||
Context: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
result := self.svc.GetUserFullTable(userTablePayload)
|
|
||||||
|
|
||||||
if result.Common.Exception.Original != exception.CommonSuccess {
|
|
||||||
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,5 @@ func ApiHandler(r *gin.RouterGroup) {
|
|||||||
r.GET("/info", userHandler.Info)
|
r.GET("/info", userHandler.Info)
|
||||||
r.PATCH("/update", userHandler.Update)
|
r.PATCH("/update", userHandler.Update)
|
||||||
r.GET("/list", middleware.Permission(20), userHandler.List)
|
r.GET("/list", middleware.Permission(20), userHandler.List)
|
||||||
r.POST("/full", middleware.Permission(40), userHandler.Full)
|
|
||||||
r.POST("/create", middleware.Permission(50), userHandler.Create)
|
r.POST("/create", middleware.Permission(50), userHandler.Create)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param limit query string false "Maximum number of users to return (default 0)"
|
// @Param limit query string false "Maximum number of users to return (default 0)"
|
||||||
// @Param offset query string true "Number of users to skip"
|
// @Param offset query string true "Number of users to skip"
|
||||||
// @Success 200 {object} utils.RespStatus{data=[]data.UserSearchDoc} "Successful paginated list retrieval"
|
// @Success 200 {object} utils.RespStatus{data=[]data.UserIndexDoc} "Successful paginated list retrieval"
|
||||||
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)"
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)"
|
||||||
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)"
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)"
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ endpoint:
|
|||||||
checkin: "02"
|
checkin: "02"
|
||||||
checkin_query: "03"
|
checkin_query: "03"
|
||||||
checkin_submit: "04"
|
checkin_submit: "04"
|
||||||
|
list: "05"
|
||||||
user:
|
user:
|
||||||
service:
|
service:
|
||||||
info: "01"
|
info: "01"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ auth:
|
|||||||
invalid_redirect_uri: "00003"
|
invalid_redirect_uri: "00003"
|
||||||
user:
|
user:
|
||||||
list:
|
list:
|
||||||
meilisearch_failed: "00001"
|
database_failed: "00001"
|
||||||
event:
|
event:
|
||||||
info:
|
info:
|
||||||
not_found: "00001"
|
not_found: "00001"
|
||||||
@@ -32,3 +32,5 @@ event:
|
|||||||
gen_code_failed: "00001"
|
gen_code_failed: "00001"
|
||||||
checkin_query:
|
checkin_query:
|
||||||
record_not_found: "00001"
|
record_not_found: "00001"
|
||||||
|
list:
|
||||||
|
database_failed: "00001"
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ cache:
|
|||||||
password: ""
|
password: ""
|
||||||
db: 0
|
db: 0
|
||||||
service_name: nixcn-cms-redis
|
service_name: nixcn-cms-redis
|
||||||
search:
|
|
||||||
host: http://127.0.0.1:7700
|
|
||||||
api_key: ""
|
|
||||||
service_name: nixcn-cms-meilisearch
|
|
||||||
email:
|
email:
|
||||||
host:
|
host:
|
||||||
port:
|
port:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ type config struct {
|
|||||||
Server server `yaml:"server"`
|
Server server `yaml:"server"`
|
||||||
Database database `yaml:"database"`
|
Database database `yaml:"database"`
|
||||||
Cache cache `yaml:"cache"`
|
Cache cache `yaml:"cache"`
|
||||||
Search search `yaml:"search"`
|
|
||||||
Email email `yaml:"email"`
|
Email email `yaml:"email"`
|
||||||
Secrets secrets `yaml:"secrets"`
|
Secrets secrets `yaml:"secrets"`
|
||||||
TTL ttl `yaml:"ttl"`
|
TTL ttl `yaml:"ttl"`
|
||||||
@@ -39,12 +38,6 @@ type cache struct {
|
|||||||
ServiceName string `yaml:"service_name"`
|
ServiceName string `yaml:"service_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type search struct {
|
|
||||||
Host string `yaml:"host"`
|
|
||||||
ApiKey string `yaml:"api_key"`
|
|
||||||
ServiceName string `yaml:"service_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type email struct {
|
type email struct {
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port string `yaml:"port"`
|
Port string `yaml:"port"`
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/meilisearch/meilisearch-go"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -103,10 +102,6 @@ func (self *Attendance) Create(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := self.UpdateSearchIndex(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,60 +142,9 @@ func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID, chec
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync to MeiliSearch (eventual consistency)
|
|
||||||
if err := attendance.UpdateSearchIndex(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &attendance, nil
|
return &attendance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Attendance) SearchUsersByEvent(ctx context.Context, eventID string) (*meilisearch.SearchResponse, error) {
|
|
||||||
index := MeiliSearch.Index("attendance")
|
|
||||||
|
|
||||||
return index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
|
|
||||||
Filter: "event_id = \"" + eventID + "\"",
|
|
||||||
Sort: []string{"checkin_at:asc"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Attendance) SearchEventsByUser(ctx context.Context, userID string) (*meilisearch.SearchResponse, error) {
|
|
||||||
index := MeiliSearch.Index("attendance")
|
|
||||||
|
|
||||||
return index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
|
|
||||||
Filter: "user_id = \"" + userID + "\"",
|
|
||||||
Sort: []string{"checkin_at:asc"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Attendance) UpdateSearchIndex(ctx context.Context) error {
|
|
||||||
doc := AttendanceSearchDoc{
|
|
||||||
AttendanceId: self.AttendanceId.String(),
|
|
||||||
EventId: self.EventId.String(),
|
|
||||||
UserId: self.UserId.String(),
|
|
||||||
CheckinAt: self.CheckinAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
index := MeiliSearch.Index("attendance")
|
|
||||||
|
|
||||||
primaryKey := "attendance_id"
|
|
||||||
opts := &meilisearch.DocumentOptions{
|
|
||||||
PrimaryKey: &primaryKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := index.UpdateDocumentsWithContext(ctx, []AttendanceSearchDoc{doc}, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Attendance) DeleteSearchIndex(ctx context.Context) error {
|
|
||||||
index := MeiliSearch.Index("attendance")
|
|
||||||
_, err := index.DeleteDocumentWithContext(ctx, self.AttendanceId.String(), nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) {
|
func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) {
|
||||||
ttl := viper.GetDuration("ttl.checkin_code_ttl")
|
ttl := viper.GetDuration("ttl.checkin_code_ttl")
|
||||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|||||||
10
data/data.go
10
data/data.go
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/meilisearch/meilisearch-go"
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -15,7 +14,6 @@ import (
|
|||||||
|
|
||||||
var Database *gorm.DB
|
var Database *gorm.DB
|
||||||
var Redis redis.UniversalClient
|
var Redis redis.UniversalClient
|
||||||
var MeiliSearch meilisearch.ServiceManager
|
|
||||||
|
|
||||||
func Init(ctx context.Context) {
|
func Init(ctx context.Context) {
|
||||||
// Init database
|
// Init database
|
||||||
@@ -62,12 +60,4 @@ func Init(ctx context.Context) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
Redis = rdb
|
Redis = rdb
|
||||||
|
|
||||||
// Init meilisearch
|
|
||||||
mDSN := drivers.MeiliDSN{
|
|
||||||
Host: viper.GetString("search.host"),
|
|
||||||
ApiKey: viper.GetString("search.api_key"),
|
|
||||||
}
|
|
||||||
mdb := drivers.MeiliSearch(mDSN)
|
|
||||||
MeiliSearch = mdb
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package drivers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/meilisearch/meilisearch-go"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MeiliSearch(dsn MeiliDSN) meilisearch.ServiceManager {
|
|
||||||
serviceName := viper.GetString("search.service_name")
|
|
||||||
|
|
||||||
otelTransport := otelhttp.NewTransport(
|
|
||||||
http.DefaultTransport,
|
|
||||||
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
|
|
||||||
return fmt.Sprintf("%s %s", serviceName, r.Method)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: otelTransport,
|
|
||||||
}
|
|
||||||
|
|
||||||
return meilisearch.New(dsn.Host,
|
|
||||||
meilisearch.WithAPIKey(dsn.ApiKey),
|
|
||||||
meilisearch.WithCustomClient(httpClient),
|
|
||||||
meilisearch.WithContentEncoding(
|
|
||||||
meilisearch.GzipEncoding,
|
|
||||||
meilisearch.BestCompression,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-viper/mapstructure/v2"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/meilisearch/meilisearch-go"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +21,7 @@ type Event struct {
|
|||||||
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
|
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventSearchDoc struct {
|
type EventIndexDoc struct {
|
||||||
EventId string `json:"event_id"`
|
EventId string `json:"event_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -68,11 +66,6 @@ func (self *Event) UpdateEventById(ctx context.Context, eventId uuid.UUID) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync search index
|
|
||||||
if err := self.UpdateSearchIndex(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +83,6 @@ func (self *Event) Create(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search index (eventual consistency)
|
|
||||||
if err := self.UpdateSearchIndex(ctx); err != nil {
|
|
||||||
// TODO: async retry / log
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,51 +95,19 @@ func (self *Event) GetFullTable(ctx context.Context) (*[]Event, error) {
|
|||||||
return &events, err
|
return &events, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventSearchDoc, error) {
|
func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventIndexDoc, error) {
|
||||||
index := MeiliSearch.Index("event")
|
var results []EventIndexDoc
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Event{}).
|
||||||
|
Select("event_id", "name", "type", "description", "start_time", "end_time").
|
||||||
|
Limit(int(limit)).
|
||||||
|
Offset(int(offset)).
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
// Fast read from MeiliSearch (no DB involved)
|
|
||||||
result, err := index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var list []EventSearchDoc
|
return &results, nil
|
||||||
if err := mapstructure.Decode(result.Hits, &list); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Event) UpdateSearchIndex(ctx context.Context) error {
|
|
||||||
doc := EventSearchDoc{
|
|
||||||
EventId: self.EventId.String(),
|
|
||||||
Name: self.Name,
|
|
||||||
Type: self.Type,
|
|
||||||
Description: self.Description,
|
|
||||||
StartTime: self.StartTime,
|
|
||||||
EndTime: self.EndTime,
|
|
||||||
}
|
|
||||||
index := MeiliSearch.Index("event")
|
|
||||||
|
|
||||||
primaryKey := "event_id"
|
|
||||||
opts := &meilisearch.DocumentOptions{
|
|
||||||
PrimaryKey: &primaryKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := index.UpdateDocumentsWithContext(ctx, []EventSearchDoc{doc}, opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Event) DeleteSearchIndex(ctx context.Context) error {
|
|
||||||
index := MeiliSearch.Index("event")
|
|
||||||
_, err := index.DeleteDocumentWithContext(ctx, self.EventId.String(), nil)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
69
data/user.go
69
data/user.go
@@ -3,9 +3,7 @@ package data
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/go-viper/mapstructure/v2"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/meilisearch/meilisearch-go"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +21,7 @@ type User struct {
|
|||||||
AllowPublic bool `json:"allow_public" gorm:"default:false;not null"`
|
AllowPublic bool `json:"allow_public" gorm:"default:false;not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSearchDoc struct {
|
type UserIndexDoc struct {
|
||||||
UserId string `json:"user_id"`
|
UserId string `json:"user_id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -118,12 +116,6 @@ func (self *User) Create(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search index (eventual consistency)
|
|
||||||
if err := self.UpdateSearchIndex(&ctx); err != nil {
|
|
||||||
// TODO: async retry / log
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,8 +131,7 @@ func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID, updates
|
|||||||
if err := tx.Where("user_id = ?", userId).First(&updatedUser).Error; err != nil {
|
if err := tx.Where("user_id = ?", userId).First(&updatedUser).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return updatedUser.UpdateSearchIndex(&ctx)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,55 +144,19 @@ func (self *User) GetFullTable(ctx context.Context) (*[]User, error) {
|
|||||||
return &users, nil
|
return &users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *User) FastListUsers(ctx context.Context, limit, offset *int64) (*[]UserSearchDoc, error) {
|
func (self *User) FastListUsers(ctx context.Context, limit, offset *int) (*[]UserIndexDoc, error) {
|
||||||
index := MeiliSearch.Index("user")
|
var results []UserIndexDoc
|
||||||
|
|
||||||
|
query := Database.WithContext(ctx).Model(&User{})
|
||||||
|
|
||||||
|
err := query.Select("user_id", "email", "username", "nickname", "subtitle", "avatar").
|
||||||
|
Limit(*limit).
|
||||||
|
Offset(*offset).
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
// Fast read from MeiliSearch, no DB involved
|
|
||||||
result, err := index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
|
|
||||||
Limit: *limit,
|
|
||||||
Offset: *offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var list []UserSearchDoc
|
return &results, nil
|
||||||
if err := mapstructure.Decode(result.Hits, &list); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *User) UpdateSearchIndex(ctx *context.Context) error {
|
|
||||||
doc := UserSearchDoc{
|
|
||||||
UserId: self.UserId.String(),
|
|
||||||
Email: self.Email,
|
|
||||||
Username: self.Username,
|
|
||||||
Nickname: self.Nickname,
|
|
||||||
Subtitle: self.Subtitle,
|
|
||||||
Avatar: self.Avatar,
|
|
||||||
}
|
|
||||||
index := MeiliSearch.Index("user")
|
|
||||||
|
|
||||||
primaryKey := "user_id"
|
|
||||||
opts := &meilisearch.DocumentOptions{
|
|
||||||
PrimaryKey: &primaryKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := index.UpdateDocumentsWithContext(
|
|
||||||
*ctx,
|
|
||||||
[]UserSearchDoc{doc},
|
|
||||||
opts,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *User) DeleteSearchIndex(ctx *context.Context) error {
|
|
||||||
index := MeiliSearch.Index("user")
|
|
||||||
_, err := index.DeleteDocumentWithContext(*ctx, self.UserId.String(), nil)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,20 +27,6 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
meilisearch:
|
|
||||||
image: getmeili/meilisearch:v1.34.3
|
|
||||||
container_name: cms-search
|
|
||||||
environment:
|
|
||||||
- MEILI_MASTER_KEY=meilisearch
|
|
||||||
volumes:
|
|
||||||
- ./data/meilisearch:/meili_data
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
["CMD-SHELL", "curl -f http://localhost:7700/health || exit 1"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
lgtm:
|
lgtm:
|
||||||
image: grafana/otel-lgtm:latest
|
image: grafana/otel-lgtm:latest
|
||||||
container_name: lgtm-stack
|
container_name: lgtm-stack
|
||||||
|
|||||||
@@ -63,8 +63,5 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
meilisearch = {
|
|
||||||
enable = true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
98
docs/docs.go
98
docs/docs.go
@@ -809,7 +809,7 @@ const docTemplate = `{
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/service_event.InfoResponse"
|
"$ref": "#/definitions/service_event.EventInfoResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,14 +873,14 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/user/full": {
|
"/event/list": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Fetches all user records without pagination. This is typically used for administrative overview or data export.",
|
"description": "Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -888,12 +888,27 @@ const docTemplate = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"User"
|
"Event"
|
||||||
|
],
|
||||||
|
"summary": "List Events",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Maximum number of events to return (default 20)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Number of events to skip",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"summary": "Get Full User Table",
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful retrieval of full user table",
|
"description": "Successful paginated list retrieval",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -903,7 +918,28 @@ const docTemplate = `{
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/service_user.UserTableResponse"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/data.EventIndexDoc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid Input (Missing offset or malformed parameters)",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/utils.RespStatus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -911,7 +947,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error (Database Error)",
|
"description": "Internal Server Error (Database query failed)",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -1072,7 +1108,7 @@ const docTemplate = `{
|
|||||||
"data": {
|
"data": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/data.UserSearchDoc"
|
"$ref": "#/definitions/data.UserIndexDoc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1226,45 +1262,30 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"data.User": {
|
"data.EventIndexDoc": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"allow_public": {
|
"description": {
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"bio": {
|
"end_time": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"email": {
|
"event_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"name": {
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"permission_level": {
|
"start_time": {
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"subtitle": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"user_id": {
|
"type": {
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"uuid": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"data.UserSearchDoc": {
|
"data.UserIndexDoc": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatar": {
|
"avatar": {
|
||||||
@@ -1394,7 +1415,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service_event.InfoResponse": {
|
"service_event.EventInfoResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"end_time": {
|
"end_time": {
|
||||||
@@ -1440,17 +1461,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service_user.UserTableResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"user_table": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/data.User"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils.RespStatus": {
|
"utils.RespStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -807,7 +807,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/service_event.InfoResponse"
|
"$ref": "#/definitions/service_event.EventInfoResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -871,14 +871,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/user/full": {
|
"/event/list": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Fetches all user records without pagination. This is typically used for administrative overview or data export.",
|
"description": "Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -886,12 +886,27 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"User"
|
"Event"
|
||||||
|
],
|
||||||
|
"summary": "List Events",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Maximum number of events to return (default 20)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Number of events to skip",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"summary": "Get Full User Table",
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful retrieval of full user table",
|
"description": "Successful paginated list retrieval",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -901,7 +916,28 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/service_user.UserTableResponse"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/data.EventIndexDoc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid Input (Missing offset or malformed parameters)",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/utils.RespStatus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -909,7 +945,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error (Database Error)",
|
"description": "Internal Server Error (Database query failed)",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -1070,7 +1106,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/data.UserSearchDoc"
|
"$ref": "#/definitions/data.UserIndexDoc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1224,45 +1260,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"data.User": {
|
"data.EventIndexDoc": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"allow_public": {
|
"description": {
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"bio": {
|
"end_time": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"email": {
|
"event_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"name": {
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"permission_level": {
|
"start_time": {
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"subtitle": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"user_id": {
|
"type": {
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"uuid": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"data.UserSearchDoc": {
|
"data.UserIndexDoc": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatar": {
|
"avatar": {
|
||||||
@@ -1392,7 +1413,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service_event.InfoResponse": {
|
"service_event.EventInfoResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"end_time": {
|
"end_time": {
|
||||||
@@ -1438,17 +1459,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service_user.UserTableResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"user_table": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/data.User"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils.RespStatus": {
|
"utils.RespStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
basePath: /api/v1
|
basePath: /api/v1
|
||||||
definitions:
|
definitions:
|
||||||
data.User:
|
data.EventIndexDoc:
|
||||||
properties:
|
properties:
|
||||||
allow_public:
|
description:
|
||||||
type: boolean
|
|
||||||
avatar:
|
|
||||||
type: string
|
type: string
|
||||||
bio:
|
end_time:
|
||||||
type: string
|
type: string
|
||||||
email:
|
event_id:
|
||||||
type: string
|
type: string
|
||||||
id:
|
name:
|
||||||
type: integer
|
|
||||||
nickname:
|
|
||||||
type: string
|
type: string
|
||||||
permission_level:
|
start_time:
|
||||||
type: integer
|
|
||||||
subtitle:
|
|
||||||
type: string
|
type: string
|
||||||
user_id:
|
type:
|
||||||
type: string
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
uuid:
|
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
data.UserSearchDoc:
|
data.UserIndexDoc:
|
||||||
properties:
|
properties:
|
||||||
avatar:
|
avatar:
|
||||||
type: string
|
type: string
|
||||||
@@ -108,7 +98,7 @@ definitions:
|
|||||||
checkin_code:
|
checkin_code:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
service_event.InfoResponse:
|
service_event.EventInfoResponse:
|
||||||
properties:
|
properties:
|
||||||
end_time:
|
end_time:
|
||||||
type: string
|
type: string
|
||||||
@@ -138,13 +128,6 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
service_user.UserTableResponse:
|
|
||||||
properties:
|
|
||||||
user_table:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/data.User'
|
|
||||||
type: array
|
|
||||||
type: object
|
|
||||||
utils.RespStatus:
|
utils.RespStatus:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
@@ -608,7 +591,7 @@ paths:
|
|||||||
- $ref: '#/definitions/utils.RespStatus'
|
- $ref: '#/definitions/utils.RespStatus'
|
||||||
- properties:
|
- properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/definitions/service_event.InfoResponse'
|
$ref: '#/definitions/service_event.EventInfoResponse'
|
||||||
type: object
|
type: object
|
||||||
"400":
|
"400":
|
||||||
description: Invalid Input
|
description: Invalid Input
|
||||||
@@ -642,26 +625,47 @@ paths:
|
|||||||
summary: Get Event Information
|
summary: Get Event Information
|
||||||
tags:
|
tags:
|
||||||
- Event
|
- Event
|
||||||
/user/full:
|
/event/list:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Fetches all user records without pagination. This is typically
|
description: Fetches a list of events with support for pagination via limit
|
||||||
used for administrative overview or data export.
|
and offset. Data is retrieved directly from the database for consistency.
|
||||||
|
parameters:
|
||||||
|
- description: Maximum number of events to return (default 20)
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: string
|
||||||
|
- description: Number of events to skip
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful retrieval of full user table
|
description: Successful paginated list retrieval
|
||||||
schema:
|
schema:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/utils.RespStatus'
|
- $ref: '#/definitions/utils.RespStatus'
|
||||||
- properties:
|
- properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/definitions/service_user.UserTableResponse'
|
items:
|
||||||
|
$ref: '#/definitions/data.EventIndexDoc'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Invalid Input (Missing offset or malformed parameters)
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/utils.RespStatus'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error (Database Error)
|
description: Internal Server Error (Database query failed)
|
||||||
schema:
|
schema:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/utils.RespStatus'
|
- $ref: '#/definitions/utils.RespStatus'
|
||||||
@@ -671,9 +675,9 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
summary: Get Full User Table
|
summary: List Events
|
||||||
tags:
|
tags:
|
||||||
- User
|
- Event
|
||||||
/user/info:
|
/user/info:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -751,7 +755,7 @@ paths:
|
|||||||
- properties:
|
- properties:
|
||||||
data:
|
data:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/data.UserSearchDoc'
|
$ref: '#/definitions/data.UserIndexDoc'
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
"400":
|
"400":
|
||||||
|
|||||||
2
justfile
2
justfile
@@ -48,4 +48,4 @@ dev-client-cms: install-cms
|
|||||||
devenv up client-cms --verbose
|
devenv up client-cms --verbose
|
||||||
|
|
||||||
dev-back: clean install-back gen-back
|
dev-back: clean install-back gen-back
|
||||||
devenv up backend postgres redis meilisearch lgtm --verbose
|
devenv up postgres redis meilisearch lgtm --verbose
|
||||||
|
|||||||
@@ -10,27 +10,27 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InfoData struct {
|
type EventInfoData struct {
|
||||||
EventId uuid.UUID `json:"event_id"`
|
EventId uuid.UUID `json:"event_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfoPayload struct {
|
type EventInfoPayload struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Data *InfoData
|
Data *EventInfoData
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfoResponse struct {
|
type EventInfoResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `json:"start_time"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime time.Time `json:"end_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfoResult struct {
|
type EventInfoResult struct {
|
||||||
Common shared.CommonResult
|
Common shared.CommonResult
|
||||||
Data *InfoResponse
|
Data *EventInfoResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
|
func (self *EventServiceImpl) GetEventInfo(payload *EventInfoPayload) (result *EventInfoResult) {
|
||||||
event, err := new(data.Event).GetEventById(payload.Context, payload.Data.EventId)
|
event, err := new(data.Event).GetEventById(payload.Context, payload.Data.EventId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exception := new(exception.Builder).
|
exception := new(exception.Builder).
|
||||||
@@ -42,7 +42,7 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
|
|||||||
SetError(err).
|
SetError(err).
|
||||||
Throw(payload.Context)
|
Throw(payload.Context)
|
||||||
|
|
||||||
result = &InfoResult{
|
result = &EventInfoResult{
|
||||||
Common: shared.CommonResult{
|
Common: shared.CommonResult{
|
||||||
HttpCode: 404,
|
HttpCode: 404,
|
||||||
Exception: exception,
|
Exception: exception,
|
||||||
@@ -52,7 +52,7 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result = &InfoResult{
|
result = &EventInfoResult{
|
||||||
Common: shared.CommonResult{
|
Common: shared.CommonResult{
|
||||||
HttpCode: 200,
|
HttpCode: 200,
|
||||||
Exception: new(exception.Builder).
|
Exception: new(exception.Builder).
|
||||||
@@ -63,7 +63,7 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
|
|||||||
SetOriginal(exception.CommonSuccess).
|
SetOriginal(exception.CommonSuccess).
|
||||||
Throw(payload.Context),
|
Throw(payload.Context),
|
||||||
},
|
},
|
||||||
Data: &InfoResponse{
|
Data: &EventInfoResponse{
|
||||||
Name: event.Name,
|
Name: event.Name,
|
||||||
StartTime: event.StartTime,
|
StartTime: event.StartTime,
|
||||||
EndTime: event.EndTime,
|
EndTime: event.EndTime,
|
||||||
131
service/service_event/list_events.go
Normal file
131
service/service_event/list_events.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package service_event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventListPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Limit *string
|
||||||
|
Offset *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventListResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *[]data.EventIndexDoc `json:"event_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) ListEvents(payload *EventListPayload) (result *EventListResult) {
|
||||||
|
var limit string
|
||||||
|
if payload.Limit == nil || *payload.Limit == "" {
|
||||||
|
limit = "20"
|
||||||
|
} else {
|
||||||
|
limit = *payload.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset string
|
||||||
|
if payload.Offset == nil || *payload.Offset == "" {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &EventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offset = *payload.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
limitNum, err := strconv.Atoi(limit)
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &EventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetNum, err := strconv.Atoi(offset)
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &EventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventList, err := new(data.Event).
|
||||||
|
FastListEvents(payload.Context, int64(limitNum), int64(offsetNum))
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventListDatabaseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &EventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successExc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &EventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: successExc,
|
||||||
|
},
|
||||||
|
Data: eventList,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ type EventService interface {
|
|||||||
Checkin(*CheckinPayload) *CheckinResult
|
Checkin(*CheckinPayload) *CheckinResult
|
||||||
CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult
|
CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult
|
||||||
CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult
|
CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult
|
||||||
Info(*InfoPayload) *InfoResult
|
GetEventInfo(*EventInfoPayload) *EventInfoResult
|
||||||
|
ListEvents(*EventListPayload) *EventListResult
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventServiceImpl struct{}
|
type EventServiceImpl struct{}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package service_user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"nixcn-cms/data"
|
|
||||||
"nixcn-cms/internal/exception"
|
|
||||||
"nixcn-cms/service/shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserTablePayload struct {
|
|
||||||
Context context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserTableResponse struct {
|
|
||||||
UserTable *[]data.User `json:"user_table"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserTableResult struct {
|
|
||||||
Common shared.CommonResult
|
|
||||||
Data *UserTableResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListUserFullTable
|
|
||||||
func (self *UserServiceImpl) GetUserFullTable(payload *UserTablePayload) (result *UserTableResult) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
userFullTable, err := new(data.User).
|
|
||||||
GetFullTable(payload.Context)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
exception := new(exception.Builder).
|
|
||||||
SetStatus(exception.StatusServer).
|
|
||||||
SetService(exception.ServiceUser).
|
|
||||||
SetEndpoint(exception.EndpointUserServiceFull).
|
|
||||||
SetType(exception.TypeCommon).
|
|
||||||
SetOriginal(exception.CommonErrorDatabase).
|
|
||||||
SetError(err).
|
|
||||||
Throw(payload.Context)
|
|
||||||
|
|
||||||
result = &UserTableResult{
|
|
||||||
Common: shared.CommonResult{
|
|
||||||
HttpCode: 500,
|
|
||||||
Exception: exception,
|
|
||||||
},
|
|
||||||
Data: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exception := new(exception.Builder).
|
|
||||||
SetStatus(exception.StatusServer).
|
|
||||||
SetService(exception.ServiceUser).
|
|
||||||
SetEndpoint(exception.EndpointUserServiceFull).
|
|
||||||
SetType(exception.TypeCommon).
|
|
||||||
SetOriginal(exception.CommonSuccess).
|
|
||||||
SetError(nil).
|
|
||||||
Throw(payload.Context)
|
|
||||||
|
|
||||||
result = &UserTableResult{
|
|
||||||
Common: shared.CommonResult{
|
|
||||||
HttpCode: 200,
|
|
||||||
Exception: exception,
|
|
||||||
},
|
|
||||||
Data: &UserTableResponse{userFullTable},
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -16,16 +16,18 @@ type UserListPayload struct {
|
|||||||
|
|
||||||
type UserListResult struct {
|
type UserListResult struct {
|
||||||
Common shared.CommonResult
|
Common shared.CommonResult
|
||||||
Data *[]data.UserSearchDoc `json:"user_list"`
|
Data *[]data.UserIndexDoc `json:"user_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserListResult) {
|
func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserListResult) {
|
||||||
var limit string
|
var limit string = *payload.Limit
|
||||||
if payload.Limit == nil || *payload.Limit == "" {
|
if payload.Limit == nil || *payload.Limit == "" {
|
||||||
limit = "0"
|
limit = "20"
|
||||||
|
} else {
|
||||||
|
limit = *payload.Limit
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset string
|
var offset string = *payload.Offset
|
||||||
if payload.Offset == nil || *payload.Offset == "" {
|
if payload.Offset == nil || *payload.Offset == "" {
|
||||||
exception := new(exception.Builder).
|
exception := new(exception.Builder).
|
||||||
SetStatus(exception.StatusUser).
|
SetStatus(exception.StatusUser).
|
||||||
@@ -50,7 +52,7 @@ func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse string to int64
|
// Parse string to int64
|
||||||
limitNum, err := strconv.ParseInt(limit, 10, 64)
|
limitNum, err := strconv.Atoi(limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exception := new(exception.Builder).
|
exception := new(exception.Builder).
|
||||||
SetStatus(exception.StatusUser).
|
SetStatus(exception.StatusUser).
|
||||||
@@ -72,7 +74,7 @@ func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserLi
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
offsetNum, err := strconv.ParseInt(offset, 10, 64)
|
offsetNum, err := strconv.Atoi(offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exception := new(exception.Builder).
|
exception := new(exception.Builder).
|
||||||
SetStatus(exception.StatusUser).
|
SetStatus(exception.StatusUser).
|
||||||
@@ -103,7 +105,7 @@ func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserLi
|
|||||||
SetService(exception.ServiceUser).
|
SetService(exception.ServiceUser).
|
||||||
SetEndpoint(exception.EndpointUserServiceList).
|
SetEndpoint(exception.EndpointUserServiceList).
|
||||||
SetType(exception.TypeSpecific).
|
SetType(exception.TypeSpecific).
|
||||||
SetOriginal(exception.UserListMeilisearchFailed).
|
SetOriginal(exception.UserListDatabaseFailed).
|
||||||
SetError(err).
|
SetError(err).
|
||||||
Throw(payload.Context)
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ type UserService interface {
|
|||||||
GetUserInfo(*UserInfoPayload) *UserInfoResult
|
GetUserInfo(*UserInfoPayload) *UserInfoResult
|
||||||
UpdateUserInfo(*UserInfoPayload) *UserInfoResult
|
UpdateUserInfo(*UserInfoPayload) *UserInfoResult
|
||||||
ListUsers(*UserListPayload) *UserListResult
|
ListUsers(*UserListPayload) *UserListResult
|
||||||
GetUserFullTable(*UserTablePayload) *UserTableResult
|
|
||||||
CreateUser()
|
CreateUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user