Remove search engine, add event list api
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
2026-01-30 11:54:13 +08:00
parent 2aa344a11f
commit 39f555b780
26 changed files with 401 additions and 499 deletions

View File

@@ -17,7 +17,7 @@ import (
// @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"
@@ -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
View 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)
}

View File

@@ -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)
}

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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:

View File

@@ -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"`

View File

@@ -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()))

View File

@@ -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
} }

View File

@@ -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,
),
)
}

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -63,8 +63,5 @@
} }
]; ];
}; };
meilisearch = {
enable = true;
};
}; };
} }

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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":

View File

@@ -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

View File

@@ -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,

View 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
}

View File

@@ -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{}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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()
} }