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
// @Produce json
// @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 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
@@ -41,9 +41,9 @@ func (self *EventHandler) Info(c *gin.Context) {
return
}
result := self.svc.Info(&service_event.InfoPayload{
result := self.svc.GetEventInfo(&service_event.EventInfoPayload{
Context: c,
Data: &service_event.InfoData{
Data: &service_event.EventInfoData{
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.PATCH("/update", userHandler.Update)
r.GET("/list", middleware.Permission(20), userHandler.List)
r.POST("/full", middleware.Permission(40), userHandler.Full)
r.POST("/create", middleware.Permission(50), userHandler.Create)
}

View File

@@ -17,7 +17,7 @@ import (
// @Produce json
// @Param limit query string false "Maximum number of users to return (default 0)"
// @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 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)"
// @Security ApiKeyAuth

View File

@@ -12,6 +12,7 @@ endpoint:
checkin: "02"
checkin_query: "03"
checkin_submit: "04"
list: "05"
user:
service:
info: "01"

View File

@@ -24,7 +24,7 @@ auth:
invalid_redirect_uri: "00003"
user:
list:
meilisearch_failed: "00001"
database_failed: "00001"
event:
info:
not_found: "00001"
@@ -32,3 +32,5 @@ event:
gen_code_failed: "00001"
checkin_query:
record_not_found: "00001"
list:
database_failed: "00001"

View File

@@ -19,10 +19,6 @@ cache:
password: ""
db: 0
service_name: nixcn-cms-redis
search:
host: http://127.0.0.1:7700
api_key: ""
service_name: nixcn-cms-meilisearch
email:
host:
port:

View File

@@ -4,7 +4,6 @@ type config struct {
Server server `yaml:"server"`
Database database `yaml:"database"`
Cache cache `yaml:"cache"`
Search search `yaml:"search"`
Email email `yaml:"email"`
Secrets secrets `yaml:"secrets"`
TTL ttl `yaml:"ttl"`
@@ -39,12 +38,6 @@ type cache struct {
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 {
Host string `yaml:"host"`
Port string `yaml:"port"`

View File

@@ -9,7 +9,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/meilisearch/meilisearch-go"
"github.com/spf13/viper"
"gorm.io/gorm"
)
@@ -103,10 +102,6 @@ func (self *Attendance) Create(ctx context.Context) error {
return err
}
if err := self.UpdateSearchIndex(ctx); err != nil {
return err
}
return nil
}
@@ -147,60 +142,9 @@ func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID, chec
return nil, err
}
// Sync to MeiliSearch (eventual consistency)
if err := attendance.UpdateSearchIndex(ctx); err != nil {
return nil, err
}
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) {
ttl := viper.GetDuration("ttl.checkin_code_ttl")
rng := rand.New(rand.NewSource(time.Now().UnixNano()))

View File

@@ -7,7 +7,6 @@ import (
"log/slog"
"github.com/meilisearch/meilisearch-go"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"gorm.io/gorm"
@@ -15,7 +14,6 @@ import (
var Database *gorm.DB
var Redis redis.UniversalClient
var MeiliSearch meilisearch.ServiceManager
func Init(ctx context.Context) {
// Init database
@@ -62,12 +60,4 @@ func Init(ctx context.Context) {
os.Exit(1)
}
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"
"time"
"github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
"github.com/meilisearch/meilisearch-go"
"gorm.io/gorm"
)
@@ -23,7 +21,7 @@ type Event struct {
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
}
type EventSearchDoc struct {
type EventIndexDoc struct {
EventId string `json:"event_id"`
Name string `json:"name"`
Type string `json:"type"`
@@ -68,11 +66,6 @@ func (self *Event) UpdateEventById(ctx context.Context, eventId uuid.UUID) error
return err
}
// Sync search index
if err := self.UpdateSearchIndex(ctx); err != nil {
return err
}
return nil
}
@@ -90,12 +83,6 @@ func (self *Event) Create(ctx context.Context) error {
return err
}
// Search index (eventual consistency)
if err := self.UpdateSearchIndex(ctx); err != nil {
// TODO: async retry / log
return err
}
return nil
}
@@ -108,51 +95,19 @@ func (self *Event) GetFullTable(ctx context.Context) (*[]Event, error) {
return &events, err
}
func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventSearchDoc, error) {
index := MeiliSearch.Index("event")
func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventIndexDoc, error) {
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 {
return nil, err
}
var list []EventSearchDoc
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
return &results, nil
}

View File

@@ -3,9 +3,7 @@ package data
import (
"context"
"github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
"github.com/meilisearch/meilisearch-go"
"gorm.io/gorm"
)
@@ -23,7 +21,7 @@ type User struct {
AllowPublic bool `json:"allow_public" gorm:"default:false;not null"`
}
type UserSearchDoc struct {
type UserIndexDoc struct {
UserId string `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
@@ -118,12 +116,6 @@ func (self *User) Create(ctx context.Context) error {
return err
}
// Search index (eventual consistency)
if err := self.UpdateSearchIndex(&ctx); err != nil {
// TODO: async retry / log
return err
}
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 {
return err
}
return updatedUser.UpdateSearchIndex(&ctx)
return nil
})
}
@@ -153,55 +144,19 @@ func (self *User) GetFullTable(ctx context.Context) (*[]User, error) {
return &users, nil
}
func (self *User) FastListUsers(ctx context.Context, limit, offset *int64) (*[]UserSearchDoc, error) {
index := MeiliSearch.Index("user")
func (self *User) FastListUsers(ctx context.Context, limit, offset *int) (*[]UserIndexDoc, error) {
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 {
return nil, err
}
var list []UserSearchDoc
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
return &results, nil
}

View File

@@ -27,20 +27,6 @@ services:
timeout: 3s
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:
image: grafana/otel-lgtm:latest
container_name: lgtm-stack

View File

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

View File

@@ -809,7 +809,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/service_event.InfoResponse"
"$ref": "#/definitions/service_event.EventInfoResponse"
}
}
}
@@ -873,14 +873,14 @@ const docTemplate = `{
}
}
},
"/user/full": {
"/event/list": {
"get": {
"security": [
{
"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": [
"application/json"
],
@@ -888,12 +888,27 @@ const docTemplate = `{
"application/json"
],
"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": {
"200": {
"description": "Successful retrieval of full user table",
"description": "Successful paginated list retrieval",
"schema": {
"allOf": [
{
@@ -903,7 +918,28 @@ const docTemplate = `{
"type": "object",
"properties": {
"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": {
"description": "Internal Server Error (Database Error)",
"description": "Internal Server Error (Database query failed)",
"schema": {
"allOf": [
{
@@ -1072,7 +1108,7 @@ const docTemplate = `{
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/data.UserSearchDoc"
"$ref": "#/definitions/data.UserIndexDoc"
}
}
}
@@ -1226,45 +1262,30 @@ const docTemplate = `{
}
},
"definitions": {
"data.User": {
"data.EventIndexDoc": {
"type": "object",
"properties": {
"allow_public": {
"type": "boolean"
},
"avatar": {
"description": {
"type": "string"
},
"bio": {
"end_time": {
"type": "string"
},
"email": {
"event_id": {
"type": "string"
},
"id": {
"type": "integer"
},
"nickname": {
"name": {
"type": "string"
},
"permission_level": {
"type": "integer"
},
"subtitle": {
"start_time": {
"type": "string"
},
"user_id": {
"type": "string"
},
"username": {
"type": "string"
},
"uuid": {
"type": {
"type": "string"
}
}
},
"data.UserSearchDoc": {
"data.UserIndexDoc": {
"type": "object",
"properties": {
"avatar": {
@@ -1394,7 +1415,7 @@ const docTemplate = `{
}
}
},
"service_event.InfoResponse": {
"service_event.EventInfoResponse": {
"type": "object",
"properties": {
"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": {
"type": "object",
"properties": {

View File

@@ -807,7 +807,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/service_event.InfoResponse"
"$ref": "#/definitions/service_event.EventInfoResponse"
}
}
}
@@ -871,14 +871,14 @@
}
}
},
"/user/full": {
"/event/list": {
"get": {
"security": [
{
"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": [
"application/json"
],
@@ -886,12 +886,27 @@
"application/json"
],
"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": {
"200": {
"description": "Successful retrieval of full user table",
"description": "Successful paginated list retrieval",
"schema": {
"allOf": [
{
@@ -901,7 +916,28 @@
"type": "object",
"properties": {
"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": {
"description": "Internal Server Error (Database Error)",
"description": "Internal Server Error (Database query failed)",
"schema": {
"allOf": [
{
@@ -1070,7 +1106,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/data.UserSearchDoc"
"$ref": "#/definitions/data.UserIndexDoc"
}
}
}
@@ -1224,45 +1260,30 @@
}
},
"definitions": {
"data.User": {
"data.EventIndexDoc": {
"type": "object",
"properties": {
"allow_public": {
"type": "boolean"
},
"avatar": {
"description": {
"type": "string"
},
"bio": {
"end_time": {
"type": "string"
},
"email": {
"event_id": {
"type": "string"
},
"id": {
"type": "integer"
},
"nickname": {
"name": {
"type": "string"
},
"permission_level": {
"type": "integer"
},
"subtitle": {
"start_time": {
"type": "string"
},
"user_id": {
"type": "string"
},
"username": {
"type": "string"
},
"uuid": {
"type": {
"type": "string"
}
}
},
"data.UserSearchDoc": {
"data.UserIndexDoc": {
"type": "object",
"properties": {
"avatar": {
@@ -1392,7 +1413,7 @@
}
}
},
"service_event.InfoResponse": {
"service_event.EventInfoResponse": {
"type": "object",
"properties": {
"end_time": {
@@ -1438,17 +1459,6 @@
}
}
},
"service_user.UserTableResponse": {
"type": "object",
"properties": {
"user_table": {
"type": "array",
"items": {
"$ref": "#/definitions/data.User"
}
}
}
},
"utils.RespStatus": {
"type": "object",
"properties": {

View File

@@ -1,31 +1,21 @@
basePath: /api/v1
definitions:
data.User:
data.EventIndexDoc:
properties:
allow_public:
type: boolean
avatar:
description:
type: string
bio:
end_time:
type: string
email:
event_id:
type: string
id:
type: integer
nickname:
name:
type: string
permission_level:
type: integer
subtitle:
start_time:
type: string
user_id:
type: string
username:
type: string
uuid:
type:
type: string
type: object
data.UserSearchDoc:
data.UserIndexDoc:
properties:
avatar:
type: string
@@ -108,7 +98,7 @@ definitions:
checkin_code:
type: string
type: object
service_event.InfoResponse:
service_event.EventInfoResponse:
properties:
end_time:
type: string
@@ -138,13 +128,6 @@ definitions:
username:
type: string
type: object
service_user.UserTableResponse:
properties:
user_table:
items:
$ref: '#/definitions/data.User'
type: array
type: object
utils.RespStatus:
properties:
code:
@@ -608,7 +591,7 @@ paths:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
$ref: '#/definitions/service_event.InfoResponse'
$ref: '#/definitions/service_event.EventInfoResponse'
type: object
"400":
description: Invalid Input
@@ -642,26 +625,47 @@ paths:
summary: Get Event Information
tags:
- Event
/user/full:
/event/list:
get:
consumes:
- application/json
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.
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:
- application/json
responses:
"200":
description: Successful retrieval of full user table
description: Successful paginated list retrieval
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
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
"500":
description: Internal Server Error (Database Error)
description: Internal Server Error (Database query failed)
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
@@ -671,9 +675,9 @@ paths:
type: object
security:
- ApiKeyAuth: []
summary: Get Full User Table
summary: List Events
tags:
- User
- Event
/user/info:
get:
consumes:
@@ -751,7 +755,7 @@ paths:
- properties:
data:
items:
$ref: '#/definitions/data.UserSearchDoc'
$ref: '#/definitions/data.UserIndexDoc'
type: array
type: object
"400":

View File

@@ -48,4 +48,4 @@ dev-client-cms: install-cms
devenv up client-cms --verbose
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"
)
type InfoData struct {
type EventInfoData struct {
EventId uuid.UUID `json:"event_id"`
}
type InfoPayload struct {
type EventInfoPayload struct {
Context context.Context
Data *InfoData
Data *EventInfoData
}
type InfoResponse struct {
type EventInfoResponse struct {
Name string `json:"name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
type InfoResult struct {
type EventInfoResult struct {
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)
if err != nil {
exception := new(exception.Builder).
@@ -42,7 +42,7 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
SetError(err).
Throw(payload.Context)
result = &InfoResult{
result = &EventInfoResult{
Common: shared.CommonResult{
HttpCode: 404,
Exception: exception,
@@ -52,7 +52,7 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
return
}
result = &InfoResult{
result = &EventInfoResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
@@ -63,7 +63,7 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
Data: &InfoResponse{
Data: &EventInfoResponse{
Name: event.Name,
StartTime: event.StartTime,
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
CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult
CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult
Info(*InfoPayload) *InfoResult
GetEventInfo(*EventInfoPayload) *EventInfoResult
ListEvents(*EventListPayload) *EventListResult
}
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 {
Common shared.CommonResult
Data *[]data.UserSearchDoc `json:"user_list"`
Data *[]data.UserIndexDoc `json:"user_list"`
}
func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserListResult) {
var limit string
var limit string = *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 == "" {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
@@ -50,7 +52,7 @@ func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserLi
}
// Parse string to int64
limitNum, err := strconv.ParseInt(limit, 10, 64)
limitNum, err := strconv.Atoi(limit)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
@@ -72,7 +74,7 @@ func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserLi
return
}
offsetNum, err := strconv.ParseInt(offset, 10, 64)
offsetNum, err := strconv.Atoi(offset)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
@@ -103,7 +105,7 @@ func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserLi
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeSpecific).
SetOriginal(exception.UserListMeilisearchFailed).
SetOriginal(exception.UserListDatabaseFailed).
SetError(err).
Throw(payload.Context)

View File

@@ -4,7 +4,6 @@ type UserService interface {
GetUserInfo(*UserInfoPayload) *UserInfoResult
UpdateUserInfo(*UserInfoPayload) *UserInfoResult
ListUsers(*UserListPayload) *UserListResult
GetUserFullTable(*UserTablePayload) *UserTableResult
CreateUser()
}