Add agenda and stats and event management service with api
All checks were successful
Server Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
2026-03-26 16:15:07 +08:00
parent 7173abe80e
commit 2f3eaf17ea
49 changed files with 3754 additions and 377 deletions

View File

@@ -15,6 +15,18 @@ func ApiHandler(r *gin.RouterGroup) {
agendaSvc := service_agenda.NewAgendaService()
agendaHandler := &AgendaHandler{agendaSvc}
r.Use(middleware.JWTAuth(), middleware.Permission(10))
r.POST("/submit", agendaHandler.Submit)
// Lv10+ attendee routes
attendee := r.Group("")
attendee.Use(middleware.JWTAuth(), middleware.Permission(10))
attendee.POST("/submit", agendaHandler.Submit)
attendee.PATCH("/update", agendaHandler.Update)
attendee.GET("/my-list", agendaHandler.MyList)
attendee.GET("/schedule", agendaHandler.ScheduleGet)
// Manager routes (Lv30+)
manager := r.Group("")
manager.Use(middleware.JWTAuth(), middleware.Permission(30))
manager.PATCH("/review", agendaHandler.Review)
manager.PATCH("/schedule", agendaHandler.Schedule)
manager.GET("/list", agendaHandler.List)
}

106
api/agenda/list.go Normal file
View File

@@ -0,0 +1,106 @@
package agenda
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_agenda"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// List retrieves all agenda items for an event. Manager only.
//
// @Summary List All Agendas
// @Description Returns all agendas for the specified event, regardless of status. Manager only.
// @Tags Agenda
// @Accept json
// @Produce json
// @Security Bearer
// @Param event_id query string true "Event ID"
// @Success 200 {object} utils.RespStatus{data=[]data.AgendaDoc}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /agenda/list [get]
func (self *AgendaHandler) List(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_agenda",
"list",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaList)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
type ListQuery struct {
EventId string `form:"event_id"`
}
var query ListQuery
if err := c.ShouldBindQuery(&query); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
eventId, err := uuid.Parse(query.EventId)
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("invalid event_id")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.List(&service_agenda.AgendaListPayload{
Context: ctx,
UserId: userId,
Data: &service_agenda.AgendaListData{
EventId: eventId,
},
})
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)
}

105
api/agenda/my_list.go Normal file
View File

@@ -0,0 +1,105 @@
package agenda
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_agenda"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// MyList retrieves the current user's own agenda submissions for an event.
//
// @Summary My Agenda List
// @Description Returns the calling user's agenda submissions for the specified event. User must be a joined attendee (Lv10+).
// @Tags Agenda
// @Accept json
// @Produce json
// @Security Bearer
// @Param event_id query string true "Event ID"
// @Success 200 {object} utils.RespStatus{data=[]data.Agenda}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 403 {object} utils.RespStatus{data=nil} "Not an Attendee"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /agenda/my-list [get]
func (self *AgendaHandler) MyList(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_agenda",
"my_list",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaMyList)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
type MyListQuery struct {
EventId string `form:"event_id"`
}
var query MyListQuery
if err := c.ShouldBindQuery(&query); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
eventId, err := uuid.Parse(query.EventId)
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("invalid event_id")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.MyList(&service_agenda.AgendaMyListPayload{
Context: ctx,
UserId: userId,
Data: &service_agenda.AgendaMyListData{
EventId: eventId,
},
})
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)
}

110
api/agenda/review.go Normal file
View File

@@ -0,0 +1,110 @@
package agenda
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_agenda"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Review handles manager approval or rejection of an agenda item.
//
// @Summary Review Agenda
// @Description Manager sets the status of an agenda to approved or rejected. Not allowed after agenda is published.
// @Tags Agenda
// @Accept json
// @Produce json
// @Security Bearer
// @Param body body service_agenda.AgendaReviewData true "Review Data"
// @Success 200 {object} utils.RespStatus{data=nil}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Already Published"
// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event or Agenda Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /agenda/review [patch]
func (self *AgendaHandler) Review(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_agenda",
"review",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaReview)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
data := new(service_agenda.AgendaReviewData)
if err := c.ShouldBindJSON(data); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
if data.AgendaId == uuid.Nil || data.EventId == uuid.Nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("agenda_id and event_id are required")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
if data.Status != "approved" && data.Status != "rejected" {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("status must be 'approved' or 'rejected'")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Review(&service_agenda.AgendaReviewPayload{
Context: ctx,
UserId: userId,
Data: data,
})
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())
}

98
api/agenda/schedule.go Normal file
View File

@@ -0,0 +1,98 @@
package agenda
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_agenda"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Schedule handles setting start/end times for an approved agenda item.
//
// @Summary Schedule Agenda
// @Description Manager sets start_time and end_time on an approved agenda item. Available even after publish.
// @Tags Agenda
// @Accept json
// @Produce json
// @Security Bearer
// @Param body body service_agenda.AgendaScheduleData true "Schedule Data"
// @Success 200 {object} utils.RespStatus{data=nil}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Not Approved"
// @Failure 404 {object} utils.RespStatus{data=nil} "Agenda Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /agenda/schedule [patch]
func (self *AgendaHandler) Schedule(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_agenda",
"schedule",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaSchedule)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
data := new(service_agenda.AgendaScheduleData)
if err := c.ShouldBindJSON(data); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
if data.AgendaId == uuid.Nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("agenda_id is required")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Schedule(&service_agenda.AgendaSchedulePayload{
Context: ctx,
UserId: userId,
Data: data,
})
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())
}

View File

@@ -0,0 +1,80 @@
package agenda
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_agenda"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ScheduleGet returns the published, scheduled agenda for an event.
//
// @Summary Get Agenda Schedule
// @Description Returns all approved and scheduled agenda items, sorted by start_time ascending. Returns 403 if the agenda has not been published.
// @Tags Agenda
// @Produce json
// @Security Bearer
// @Param event_id query string true "Event ID"
// @Success 200 {object} utils.RespStatus{data=[]data.AgendaDoc}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 403 {object} utils.RespStatus{data=nil} "Agenda Not Published"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /agenda/schedule [get]
func (self *AgendaHandler) ScheduleGet(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_agenda",
"schedule_get",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaScheduleGet)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
type ScheduleQuery struct {
EventId string `form:"event_id"`
}
var query ScheduleQuery
if err := c.ShouldBindQuery(&query); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
eventId, err := uuid.Parse(query.EventId)
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("invalid event_id")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.ScheduleGet(&service_agenda.AgendaScheduleGetPayload{
Context: ctx,
Data: &service_agenda.AgendaScheduleGetData{
EventId: eventId,
},
})
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)
}

113
api/agenda/update.go Normal file
View File

@@ -0,0 +1,113 @@
package agenda
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_agenda"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Update handles editing an agenda item's name or description.
//
// @Summary Update Agenda
// @Description Submitter may edit their own pending agendas before the event deadline. Managers may edit any agenda with no restrictions.
// @Tags Agenda
// @Accept json
// @Produce json
// @Security Bearer
// @Param body body service_agenda.AgendaUpdateData true "Agenda Update Data"
// @Success 200 {object} utils.RespStatus{data=nil}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Not Pending / Deadline Passed"
// @Failure 403 {object} utils.RespStatus{data=nil} "Not Submitter"
// @Failure 404 {object} utils.RespStatus{data=nil} "Agenda Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /agenda/update [patch]
func (self *AgendaHandler) Update(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_agenda",
"update",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaUpdate)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
permissionLevelOrig, ok := c.Get("permission_level")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("Missing PermissionLevel")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
data := new(service_agenda.AgendaUpdateData)
if err := c.ShouldBindJSON(data); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
if data.AgendaId == uuid.Nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("agenda_id is required")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
data.PermissionLevel = permissionLevelOrig.(uint)
result := self.svc.Update(&service_agenda.AgendaUpdatePayload{
Context: ctx,
UserId: userId,
Data: data,
})
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())
}

View File

@@ -1,6 +1,7 @@
package event
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/tracer"
@@ -10,17 +11,25 @@ import (
"github.com/google/uuid"
)
// AttendanceList handles the retrieval of the attendance list for a specific event.
// AttendanceList handles the retrieval of the paginated attendance list for a specific event.
//
// @Summary Get Attendance List
// @Description Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
// @Description Retrieves the paginated list of attendees with optional filters. Only accessible by the event owner (Manager). Supports name substring search and KYC status filtering.
// @Tags Event
// @Produce json
// @Security Bearer
// @Param event_id query string true "Event UUID"
// @Param name query string false "Substring filter on attendee nickname"
// @Param kyc_status query string false "KYC filter: 'with_kyc' or 'without_kyc'"
// @Param limit query int false "Maximum number of results to return (default 20)"
// @Param offset query int false "Number of results to skip (default 0)"
// @Param sort_by query string false "Sort field: 'checkin_at' (default) or 'id'"
// @Param sort_order query string false "Sort direction: 'asc' or 'desc' (default)"
// @Success 200 {object} utils.RespStatus{data=[]service_event.AttendanceListResponse} "Successful retrieval"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 403 {object} utils.RespStatus{data=nil} "Not Event Owner"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /event/attendance [get]
func (self *EventHandler) AttendanceList(c *gin.Context) {
@@ -34,8 +43,31 @@ func (self *EventHandler) AttendanceList(c *gin.Context) {
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventAttendanceList)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
eventIdStr := c.Query("event_id")
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
eventIdStr := c.Query("event_id")
eventId, err := uuid.Parse(eventIdStr)
if err != nil {
errorCode := exception.New(
@@ -44,17 +76,28 @@ func (self *EventHandler) AttendanceList(c *gin.Context) {
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
limit := c.Query("limit")
offset := c.Query("offset")
sortBy := c.Query("sort_by")
sortOrder := c.Query("sort_order")
listData := service_event.AttendanceListData{
EventId: eventId,
EventId: eventId,
Name: c.Query("name"),
KycStatus: c.Query("kyc_status"),
Limit: &limit,
Offset: &offset,
SortBy: &sortBy,
SortOrder: &sortOrder,
}
result := self.svc.AttendanceList(&service_event.AttendanceListPayload{
Context: ctx,
UserId: userId,
Data: &listData,
})
@@ -63,5 +106,5 @@ func (self *EventHandler) AttendanceList(c *gin.Context) {
return
}
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

91
api/event/create.go Normal file
View File

@@ -0,0 +1,91 @@
package event
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Create handles the request to create a new event.
//
// @Summary Create an Event
// @Description Allows a Lv30+ user to create a new event. Users at exactly Lv30 may only create events with type 'party'. Sets type and enable_kyc, which are immutable after creation.
// @Tags Event
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body service_event.EventCreateData true "Event Creation Details"
// @Success 200 {object} utils.RespStatus{data=service_event.EventCreateResponse} "Successfully created the event"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
// @Failure 403 {object} utils.RespStatus{data=nil} "Permission Denied / Type Not Allowed for this level"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / Database Error"
// @Router /event/create [post]
func (self *EventHandler) Create(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_event",
"create",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventCreate)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
permissionLevelOrig, ok := c.Get("permission_level")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("Missing PermissionLevel")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
var createData service_event.EventCreateData
if err := c.ShouldBindJSON(&createData); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
createData.UserId = userIdOrig.(string)
createData.PermissionLevel = permissionLevelOrig.(uint)
payload := &service_event.EventCreatePayload{
Context: ctx,
Data: &createData,
}
result := self.svc.Create(payload)
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)
}

74
api/event/delete.go Normal file
View File

@@ -0,0 +1,74 @@
package event
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Delete removes an event by event_id.
//
// @Summary Delete an Event
// @Description Permanently deletes an event. Requires Lv40+.
// @Tags Event
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body service_event.EventDeleteData true "Event to delete"
// @Success 200 {object} utils.RespStatus{data=nil} "Successfully deleted"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /event/delete [delete]
func (self *EventHandler) Delete(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_event",
"delete",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventDelete)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
_, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
var deleteData service_event.EventDeleteData
if err := c.ShouldBindJSON(&deleteData); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Delete(&service_event.EventDeletePayload{
Context: ctx,
Data: &deleteData,
})
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())
}

View File

@@ -15,13 +15,31 @@ func ApiHandler(r *gin.RouterGroup) {
eventSvc := service_event.NewEventService()
eventHandler := &EventHandler{eventSvc}
r.Use(middleware.JWTAuth(), middleware.Permission(10))
r.GET("/info", eventHandler.Info)
r.GET("/checkin", eventHandler.Checkin)
r.GET("/checkin/query", eventHandler.CheckinQuery)
r.POST("/checkin/submit", middleware.Permission(20), eventHandler.CheckinSubmit)
r.POST("/join", eventHandler.Join)
r.GET("/list", eventHandler.List)
r.GET("/attendance", middleware.Permission(40), eventHandler.AttendanceList)
r.GET("/guide", eventHandler.Guide)
// Lv10+ routes
lv10 := r.Group("")
lv10.Use(middleware.JWTAuth(), middleware.Permission(10))
lv10.GET("/info", eventHandler.Info)
lv10.GET("/checkin", eventHandler.Checkin)
lv10.GET("/checkin/query", eventHandler.CheckinQuery)
lv10.POST("/join", eventHandler.Join)
lv10.GET("/guide", eventHandler.Guide)
// Lv20+ routes
lv20 := r.Group("")
lv20.Use(middleware.JWTAuth(), middleware.Permission(20))
lv20.POST("/checkin/submit", eventHandler.CheckinSubmit)
// Lv30+ routes
lv30 := r.Group("")
lv30.Use(middleware.JWTAuth(), middleware.Permission(30))
lv30.GET("/list", eventHandler.List)
lv30.POST("/create", eventHandler.Create)
lv30.PATCH("/update", eventHandler.Update)
lv30.GET("/attendance", eventHandler.AttendanceList)
lv30.GET("/stats", eventHandler.Stats)
// Lv40+ routes
lv40 := r.Group("")
lv40.Use(middleware.JWTAuth(), middleware.Permission(40))
lv40.DELETE("/delete", eventHandler.Delete)
}

View File

@@ -11,20 +11,23 @@ import (
"github.com/google/uuid"
)
// List retrieves a paginated list of events from the database.
// List retrieves a paginated, filterable list of events.
//
// @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.
// @Description Returns a paginated list of events. Supports filtering by type and sorting. Lv30 users are automatically scoped to events they own.
// @Tags Event
// @Accept json
// @Produce json
// @Security Bearer
// @Param limit query int false "Maximum number of events to return (default 20)"
// @Param offset query int false "Number of events to skip"
// @Success 200 {object} utils.RespStatus{data=[]service_event.EventListResponse} "Successful paginated list retrieval"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Missing offset or malformed parameters)"
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database query failed)"
// @Param limit query int false "Maximum number of events to return (default 20)"
// @Param offset query int true "Number of events to skip"
// @Param type query string false "Filter by event type: 'official' or 'party'"
// @Param sort_by query string false "Sort field: 'start_time' (default), 'end_time', 'name'"
// @Param sort_order query string false "Sort direction: 'asc' or 'desc' (default)"
// @Success 200 {object} utils.RespStatus{data=[]service_event.EventListResponse} "Successful paginated list retrieval"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /event/list [get]
func (self *EventHandler) List(c *gin.Context) {
ctx, span := tracer.StartSpan(
@@ -61,44 +64,58 @@ func (self *EventHandler) List(c *gin.Context) {
return
}
permissionLevelOrig, ok := c.Get("permission_level")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("Missing PermissionLevel")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
type ListQuery struct {
Limit *string `form:"limit"`
Offset *string `form:"offset"`
Limit *string `form:"limit"`
Offset *string `form:"offset"`
Type *string `form:"type"`
SortBy *string `form:"sort_by"`
SortOrder *string `form:"sort_order"`
}
var query ListQuery
if err := c.ShouldBindQuery(&query); err != nil {
// Handle binding error (e.g., syntax errors in query string)
errorCode := exception.New(
exception.WithStatus(exception.StatusClient),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
// Prepare payload for the service layer
eventListPayload := &service_event.EventListPayload{
result := self.svc.List(&service_event.EventListPayload{
Context: ctx,
UserId: userId,
Data: &service_event.EventListData{
Limit: query.Limit,
Offset: query.Offset,
Limit: query.Limit,
Offset: query.Offset,
Type: query.Type,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
PermissionLevel: permissionLevelOrig.(uint),
},
}
})
// Call the service implementation
result := self.svc.List(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)
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), gin.H{
"total": result.Total,
"items": result.Data,
})
}

78
api/event/stats.go Normal file
View File

@@ -0,0 +1,78 @@
package event
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Stats returns aggregate statistics for an event.
//
// @Summary Get Event Statistics
// @Description Returns join count, checkin count, KYC pass rate, and agenda submission count. Only accessible by the event owner (Manager).
// @Tags Event
// @Produce json
// @Security Bearer
// @Param event_id query string true "Event UUID"
// @Success 200 {object} utils.RespStatus{data=service_event.EventStatsResponse} "Statistics retrieved successfully"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 403 {object} utils.RespStatus{data=nil} "Not Event Owner"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /event/stats [get]
func (self *EventHandler) Stats(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_event",
"stats",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventStats)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
result := self.svc.Stats(&service_event.EventStatsPayload{
Context: ctx,
UserId: userId,
Data: &service_event.EventStatsData{
EventId: c.Query("event_id"),
},
})
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)
}

77
api/event/update.go Normal file
View File

@@ -0,0 +1,77 @@
package event
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Update modifies editable fields of an event owned by the requesting user.
//
// @Summary Update an Event
// @Description Allows the event owner (Manager) to update name, subtitle, description, start_time, end_time, thumbnail, and is_agenda_published. Changes to type or enable_kyc are rejected. is_agenda_published is write-once: it can only be set to true (requires at least one agenda submission) and cannot be reverted.
// @Tags Event
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body service_event.EventUpdateData true "Fields to update (all optional except event_id)"
// @Success 200 {object} utils.RespStatus{data=nil} "Successfully updated"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Immutable Field / Agenda Pre-flight Failed"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 403 {object} utils.RespStatus{data=nil} "Not Event Owner"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /event/update [patch]
func (self *EventHandler) Update(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_event",
"update",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventUpdate)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
var updateData service_event.EventUpdateData
if err := c.ShouldBindJSON(&updateData); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
updateData.UserId = userIdOrig.(string)
result := self.svc.Update(&service_event.EventUpdatePayload{
Context: ctx,
Data: &updateData,
})
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())
}

View File

@@ -1,9 +1,11 @@
package api
import (
"nixcn-cms/api/agenda"
"nixcn-cms/api/auth"
"nixcn-cms/api/event"
"nixcn-cms/api/kyc"
"nixcn-cms/api/stats"
"nixcn-cms/api/user"
"github.com/gin-gonic/gin"
@@ -14,4 +16,6 @@ func Handler(r *gin.RouterGroup) {
user.ApiHandler(r.Group("/user"))
event.ApiHandler(r.Group("/event"))
kyc.ApiHandler(r.Group("/kyc"))
agenda.ApiHandler(r.Group("/agenda"))
stats.ApiHandler(r.Group("/stats"))
}

44
api/stats/global.go Normal file
View File

@@ -0,0 +1,44 @@
package stats
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_stats"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Global returns platform-wide statistics. Lv40+ only.
//
// @Summary Global Stats
// @Description Returns total users, user counts per permission_level, and per-event join/checkin counts.
// @Tags Stats
// @Produce json
// @Security Bearer
// @Success 200 {object} utils.RespStatus{data=service_stats.GlobalStatsResponse}
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /stats/global [get]
func (self *StatsHandler) Global(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_stats",
"global",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointStatsGlobal)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
result := self.svc.Global(&service_stats.GlobalStatsPayload{
Context: ctx,
})
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)
}

22
api/stats/handler.go Normal file
View File

@@ -0,0 +1,22 @@
package stats
import (
"nixcn-cms/middleware"
"nixcn-cms/service/service_stats"
"github.com/gin-gonic/gin"
)
type StatsHandler struct {
svc service_stats.StatsService
}
func ApiHandler(r *gin.RouterGroup) {
statsSvc := service_stats.NewStatsService()
statsHandler := &StatsHandler{statsSvc}
// Lv40+ routes
lv40 := r.Group("")
lv40.Use(middleware.JWTAuth(), middleware.Permission(40))
lv40.GET("/global", statsHandler.Global)
}

115
api/user/admin_update.go Normal file
View File

@@ -0,0 +1,115 @@
package user
import (
"errors"
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// AdminUpdate modifies another user's profile. Lv40+ only.
//
// @Summary Admin Update User
// @Description Lv40+ operators may update any user with a strictly lower permission_level. Editable fields: all profile fields plus permission_level (new value must be below operator's own level).
// @Tags User
// @Accept json
// @Produce json
// @Security Bearer
// @Param user_id path string true "Target User ID"
// @Param payload body service_user.UserInfoUpdateData true "Fields to update"
// @Success 200 {object} utils.RespStatus{data=nil}
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 403 {object} utils.RespStatus{data=nil} "Permission Matrix Violation"
// @Failure 404 {object} utils.RespStatus{data=nil} "Target User Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /user/update/{user_id} [patch]
func (self *UserHandler) AdminUpdate(c *gin.Context) {
ctx, span := tracer.StartSpan(
c.Request.Context(),
"api_user",
"admin_update",
)
defer span.End()
ctx = exception.ContextWithEndpoint(ctx, exception.EndpointUserAdminUpdate)
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
operatorIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorMissingUserId),
exception.WithError(errors.New("Missing UserId")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
operatorId, err := uuid.Parse(operatorIdOrig.(string))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
permissionLevelOrig, ok := c.Get("permission_level")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("Missing PermissionLevel")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
targetId, err := uuid.Parse(c.Param("user_id"))
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("invalid user_id")),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
var data service_user.UserInfoData
if err := c.ShouldBindJSON(&data); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.UpdateInfo(&service_user.UserInfoPayload{
Context: ctx,
UserId: targetId,
OperatorId: operatorId,
OperatorLevel: permissionLevelOrig.(uint),
Data: &data,
})
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())
}

View File

@@ -15,9 +15,16 @@ func ApiHandler(r *gin.RouterGroup) {
userSvc := service_user.NewUserService()
userHandler := &UserHandler{userSvc}
r.Use(middleware.JWTAuth(), middleware.Permission(5))
r.GET("/info", userHandler.Info)
r.GET("/info/:user_id", userHandler.Other)
r.PATCH("/update", userHandler.Update)
r.GET("/list", middleware.Permission(40), userHandler.List)
// Lv5+ routes
lv5 := r.Group("")
lv5.Use(middleware.JWTAuth(), middleware.Permission(5))
lv5.GET("/info", userHandler.Info)
lv5.GET("/info/:user_id", userHandler.Other)
lv5.PATCH("/update", userHandler.Update)
// Lv40+ routes
lv40 := r.Group("")
lv40.Use(middleware.JWTAuth(), middleware.Permission(40))
lv40.GET("/list", userHandler.List)
lv40.PATCH("/update/:user_id", userHandler.AdminUpdate)
}

View File

@@ -5,24 +5,28 @@ import (
"nixcn-cms/service/service_user"
"nixcn-cms/tracer"
"nixcn-cms/utils"
"strconv"
"github.com/gin-gonic/gin"
)
// List retrieves a paginated list of users from the search engine.
// List retrieves a paginated, filterable list of users. Lv40+ only.
//
// @Summary List Users
// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
// @Summary List Users (Admin)
// @Description Returns a paginated list of users with permission_level included. Supports filtering by permission_level and sorting.
// @Tags User
// @Accept json
// @Produce json
// @Security Bearer
// @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=[]service_user.UserListResponse} "Successful paginated list retrieval"
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
// @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)"
// @Param limit query string false "Maximum number of users to return (default 20)"
// @Param offset query string true "Number of users to skip"
// @Param sort_by query string false "Sort field: 'id' (default) | 'permission_level'"
// @Param sort_order query string false "Sort direction: 'asc' (default) | 'desc'"
// @Param permission_level query int false "Filter by exact permission level"
// @Success 200 {object} utils.RespStatus{data=[]service_user.UserListResponse} "Successful paginated list retrieval"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /user/list [get]
func (self *UserHandler) List(c *gin.Context) {
ctx, span := tracer.StartSpan(
@@ -36,8 +40,11 @@ func (self *UserHandler) List(c *gin.Context) {
ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
type ListQuery struct {
Limit *string `form:"limit"`
Offset *string `form:"offset"`
Limit *string `form:"limit"`
Offset *string `form:"offset"`
SortBy *string `form:"sort_by"`
SortOrder *string `form:"sort_order"`
PermissionLevel *string `form:"permission_level"`
}
var query ListQuery
@@ -48,23 +55,43 @@ func (self *UserHandler) List(c *gin.Context) {
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
userListPayload := &service_user.UserListPayload{
Context: ctx,
Limit: query.Limit,
Offset: query.Offset,
var permLevel *uint
if query.PermissionLevel != nil && *query.PermissionLevel != "" {
v, err := strconv.ParseUint(*query.PermissionLevel, 10, 64)
if err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
u := uint(v)
permLevel = &u
}
result := self.svc.List(userListPayload)
result := self.svc.List(&service_user.UserListPayload{
Context: ctx,
Limit: query.Limit,
Offset: query.Offset,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
PermissionLevel: permLevel,
})
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)
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), gin.H{
"total": result.Total,
"items": result.Data,
})
}

View File

@@ -22,9 +22,9 @@ import (
// @Security Bearer
// @Param payload body service_user.UserInfoUpdateData true "Updated User Profile Data"
// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)"
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /user/update [patch]
func (self *UserHandler) Update(c *gin.Context) {
ctx, span := tracer.StartSpan(
@@ -61,13 +61,26 @@ func (self *UserHandler) Update(c *gin.Context) {
return
}
userInfoPayload := &service_user.UserInfoPayload{
Context: ctx,
UserId: userId,
permissionLevelOrig, ok := c.Get("permission_level")
if !ok {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("Missing PermissionLevel")),
).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
err = c.ShouldBindJSON(&userInfoPayload.Data)
if err != nil {
payload := &service_user.UserInfoPayload{
Context: ctx,
UserId: userId,
OperatorId: userId,
OperatorLevel: permissionLevelOrig.(uint),
}
if err := c.ShouldBindJSON(&payload.Data); err != nil {
errorCode := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
@@ -78,12 +91,12 @@ func (self *UserHandler) Update(c *gin.Context) {
return
}
result := self.svc.UpdateInfo(userInfoPayload)
result := self.svc.UpdateInfo(payload)
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)
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
}

View File

@@ -15,14 +15,27 @@ endpoint:
join: "206"
attendance_list: "207"
attendance_guide: "208"
create: "209"
update: "210"
delete: "211"
stats: "212"
user:
info: "301"
update: "302"
list: "303"
full: "304"
create: "305"
admin_update: "306"
stats:
global: "601"
kyc:
session: "401"
query: "402"
agenda:
submit: "501"
update: "502"
review: "503"
schedule: "504"
list: "505"
my_list: "506"
schedule_get: "507"

View File

@@ -10,6 +10,9 @@ service:
get_info: "201"
list: "202"
update_info: "203"
admin_update: "204"
stats:
global: "601"
event:
attendance_list: "301"
checkin: "302"
@@ -17,8 +20,18 @@ service:
get_event_info: "304"
join: "305"
list: "306"
create: "307"
update: "308"
delete: "309"
stats: "310"
kyc:
query: "401"
session: "402"
agenda:
submit: "501"
update: "502"
review: "503"
schedule: "504"
list: "505"
my_list: "506"
schedule_get: "507"

View File

@@ -25,6 +25,9 @@ auth:
user:
list:
database_failed: "00001"
update:
permission_matrix_violated: "00001"
permission_level_too_high: "00002"
event:
info:
not_found: "00001"
@@ -37,6 +40,33 @@ event:
limit_exceeded: "00002"
attendance:
list_error: "00001"
create:
type_not_allowed: "00001"
update:
immutable_field: "00001"
not_owner: "00002"
agenda_already_published: "00003"
agenda_preflight_failed: "00004"
delete:
not_found: "00001"
not_owner: "00002"
stats:
not_owner: "00001"
agenda:
submit:
event_started: "00001"
event_published: "00002"
pending_limit_reached: "00003"
update:
not_pending: "00001"
not_submitter: "00002"
deadline_passed: "00003"
review:
event_published: "00001"
schedule:
not_approved: "00001"
schedule_get:
not_published: "00001"
kyc:
session:
failed: "00001"

View File

@@ -12,14 +12,24 @@ type Agenda struct {
Id uint `json:"id" gorm:"primarykey;autoIncrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
AgendaId uuid.UUID `json:"agenda_id" gorm:"type:uuid;uniqueIndex;not null"`
AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;uniqueIndex;not null"`
AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;index;not null"`
Name string `json:"name" gorm:"type:varchar(255);index;not null"`
Description string `json:"description" gorm:"type:text;index;not null"` // base64 encoded markdown
IsApproved bool `json:"is_approved" gorm:"type:boolean;not null;default:false"`
Description string `json:"description" gorm:"type:text;not null"` // base64 encoded markdown
Status string `json:"status" gorm:"type:varchar(32);index;not null;default:pending"` // pending | approved | rejected
StartTime time.Time `json:"start_time" gorm:"index"`
EndTime time.Time `json:"end_time" gorm:"index"`
}
type AgendaDoc struct {
AgendaId uuid.UUID `json:"agenda_id"`
AttendanceId uuid.UUID `json:"attendance_id"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
func (self *Agenda) SetAttendanceId(id uuid.UUID) *Agenda {
self.AttendanceId = id
return self
@@ -35,8 +45,8 @@ func (self *Agenda) SetDescription(desc string) *Agenda {
return self
}
func (self *Agenda) SetIsApproved(approved bool) *Agenda {
self.IsApproved = approved
func (self *Agenda) SetStatus(status string) *Agenda {
self.Status = status
return self
}
@@ -69,6 +79,46 @@ func (self *Agenda) GetListByAttendanceId(ctx context.Context, attendanceId uuid
return &result, err
}
func (self *Agenda) GetListByEventId(ctx context.Context, eventId uuid.UUID) (*[]AgendaDoc, error) {
var result []AgendaDoc
err := Database.WithContext(ctx).
Model(&Agenda{}).
Select("agendas.agenda_id, agendas.attendance_id, agendas.name, agendas.description, agendas.status, agendas.start_time, agendas.end_time").
Joins("JOIN attendances ON attendances.attendance_id = agendas.attendance_id").
Where("attendances.event_id = ?", eventId).
Order("agendas.id ASC").
Scan(&result).Error
if err != nil {
return nil, err
}
return &result, nil
}
func (self *Agenda) GetScheduledByEventId(ctx context.Context, eventId uuid.UUID) (*[]AgendaDoc, error) {
var result []AgendaDoc
zero := time.Time{}
err := Database.WithContext(ctx).
Model(&Agenda{}).
Select("agendas.agenda_id, agendas.attendance_id, agendas.name, agendas.description, agendas.status, agendas.start_time, agendas.end_time").
Joins("JOIN attendances ON attendances.attendance_id = agendas.attendance_id").
Where("attendances.event_id = ?", eventId).
Where("agendas.status = ?", "approved").
Where("agendas.start_time > ? AND agendas.end_time > ?", zero, zero).
Order("agendas.start_time ASC").
Scan(&result).Error
if err != nil {
return nil, err
}
return &result, nil
}
func (self *Agenda) Create(ctx context.Context) error {
self.UUID = uuid.New()
self.AgendaId = uuid.New()
@@ -114,6 +164,48 @@ func (self *Agenda) Update(ctx context.Context, agendaId uuid.UUID) (*Agenda, er
return &agenda, nil
}
func (self *Agenda) UpdateFieldsByAgendaId(ctx context.Context, agendaId uuid.UUID, updates map[string]any) error {
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&Agenda{}).
Where("agenda_id = ?", agendaId).
Updates(updates).Error; err != nil {
return err
}
return tx.Where("agenda_id = ?", agendaId).First(self).Error
})
}
func (self *Agenda) CountByEventId(ctx context.Context, eventId uuid.UUID) (int64, error) {
var count int64
err := Database.WithContext(ctx).
Model(&Agenda{}).
Joins("JOIN attendances ON attendances.attendance_id = agendas.attendance_id").
Where("attendances.event_id = ?", eventId).
Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}
func (self *Agenda) CountPendingByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (int64, error) {
var count int64
err := Database.WithContext(ctx).
Model(&Agenda{}).
Where("attendance_id = ? AND status = ?", attendanceId, "pending").
Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}
func (self *Agenda) Delete(ctx context.Context, agendaId uuid.UUID) error {
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Where("agenda_id = ?", agendaId).Delete(&Agenda{})

View File

@@ -58,6 +58,23 @@ func (self *Attendance) SetState(s string) *Attendance {
return self
}
func (self *Attendance) GetAttendanceByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (*Attendance, error) {
var attendance Attendance
err := Database.WithContext(ctx).
Where("attendance_id = ?", attendanceId).
First(&attendance).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &attendance, nil
}
func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) {
var attendance Attendance
@@ -211,6 +228,78 @@ func (self *Attendance) CountUsersByEventID(ctx context.Context, eventID uuid.UU
return count, nil
}
type AttendanceListFilter struct {
EventId uuid.UUID
Name string // substring match on users.nickname
KycStatus string // "with_kyc" | "without_kyc" | ""
SortBy string // "checkin_at" | "id"
SortOrder string // "asc" | "desc"
Limit int
Offset int
}
func (self *Attendance) GetAttendanceListFiltered(ctx context.Context, filter AttendanceListFilter) (*[]Attendance, int64, error) {
var results []Attendance
var total int64
base := Database.WithContext(ctx).
Table("attendances").
Joins("JOIN users ON users.user_id = attendances.user_id").
Where("attendances.event_id = ?", filter.EventId)
if filter.Name != "" {
base = base.Where("users.nickname LIKE ?", "%"+filter.Name+"%")
}
switch filter.KycStatus {
case "with_kyc":
base = base.Where("attendances.kyc_id != ?", uuid.Nil)
case "without_kyc":
base = base.Where("attendances.kyc_id = ?", uuid.Nil)
}
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
sortField := "attendances.checkin_at"
if filter.SortBy == "id" {
sortField = "attendances.id"
}
sortOrder := "DESC"
if filter.SortOrder == "asc" {
sortOrder = "ASC"
}
err := base.
Select("attendances.*").
Order(sortField + " " + sortOrder).
Limit(filter.Limit).
Offset(filter.Offset).
Find(&results).Error
if err != nil {
return nil, 0, err
}
return &results, total, nil
}
func (self *Attendance) CountWithKycByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
var count int64
err := Database.WithContext(ctx).
Model(&Attendance{}).
Where("event_id = ? AND kyc_id != ?", eventID, uuid.Nil).
Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}
func (self *Attendance) CountCheckedInUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
var count int64

View File

@@ -9,33 +9,45 @@ import (
)
type Event struct {
Id uint `json:"id" gorm:"primarykey;autoincrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
Name string `json:"name" gorm:"type:varchar(255);index;not null"`
Type string `json:"type" gorm:"type:varchar(255);index;not null"` // official | party
Subtitle string `json:"subtitle" gorm:"type:text;not null;default:an amazing event"`
Description string `json:"description" gorm:"type:text"` // base64 markdown
AttendanceGuide string `json:"attendance_guide" gorm:"type:text"` // base64 markdown
StartTime time.Time `json:"start_time" gorm:"index;not null"`
EndTime time.Time `json:"end_time" gorm:"index;not null"`
Thumbnail string `json:"thumbnail" gorm:"type:varchar(255)"`
Owner uuid.UUID `json:"owner" gorm:"type:uuid;index;not null"`
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
Quota int64 `json:"quota" gorm:"not null"`
Limit int64 `json:"limit" gorm:"not null"`
Id uint `json:"id" gorm:"primarykey;autoincrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
Name string `json:"name" gorm:"type:varchar(255);index;not null"`
Type string `json:"type" gorm:"type:varchar(255);index;not null"` // official | party
Subtitle string `json:"subtitle" gorm:"type:text;not null;default:an amazing event"`
Description string `json:"description" gorm:"type:text"` // base64 markdown
AttendanceGuide string `json:"attendance_guide" gorm:"type:text"` // base64 markdown
StartTime time.Time `json:"start_time" gorm:"index;not null"`
EndTime time.Time `json:"end_time" gorm:"index;not null"`
Thumbnail string `json:"thumbnail" gorm:"type:varchar(255)"`
Owner uuid.UUID `json:"owner" gorm:"type:uuid;index;not null"`
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
IsAgendaPublished bool `json:"is_agenda_published" gorm:"not null;default:false"`
Quota int64 `json:"quota" gorm:"not null"`
Limit int64 `json:"limit" gorm:"not null"`
}
type EventIndexDoc struct {
EventId string `json:"event_id" validate:"required"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
Subtitle string `json:"subtitle" validate:"required"`
Description string `json:"description"`
StartTime time.Time `json:"start_time" validate:"required"`
EndTime time.Time `json:"end_time" validate:"required"`
Thumbnail string `json:"thumbnail"`
EnableKYC bool `json:"enable_kyc" validate:"required"`
EventId string `json:"event_id" validate:"required"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
Subtitle string `json:"subtitle" validate:"required"`
Description string `json:"description"`
StartTime time.Time `json:"start_time" validate:"required"`
EndTime time.Time `json:"end_time" validate:"required"`
Thumbnail string `json:"thumbnail"`
EnableKYC bool `json:"enable_kyc" validate:"required"`
IsAgendaPublished bool `json:"is_agenda_published"`
Owner string `json:"owner"`
}
type EventListOptions struct {
TypeFilter string
OwnerId *uuid.UUID
SortBy string
SortOrder string
Limit int64
Offset int64
}
func (self *Event) GetEventById(ctx context.Context, eventId uuid.UUID) (*Event, error) {
@@ -99,7 +111,7 @@ func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]
err := Database.WithContext(ctx).
Model(&Event{}).
Select("event_id", "name", "type", "subtitle", "description", "start_time", "end_time", "thumbnail", "enable_kyc").
Select("event_id", "name", "type", "subtitle", "description", "start_time", "end_time", "thumbnail", "enable_kyc", "is_agenda_published", "owner").
Limit(int(limit)).
Offset(int(offset)).
Scan(&results).Error
@@ -111,6 +123,74 @@ func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]
return &results, nil
}
func (self *Event) ListEventsWithOptions(ctx context.Context, opts EventListOptions) (*[]EventIndexDoc, int64, error) {
var results []EventIndexDoc
var total int64
selectCols := "event_id, name, type, subtitle, description, start_time, end_time, thumbnail, enable_kyc, is_agenda_published, owner"
baseQuery := Database.WithContext(ctx).Model(&Event{})
if opts.TypeFilter != "" {
baseQuery = baseQuery.Where("type = ?", opts.TypeFilter)
}
if opts.OwnerId != nil {
baseQuery = baseQuery.Where("owner = ?", opts.OwnerId)
}
if err := baseQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
sortField := "start_time"
switch opts.SortBy {
case "end_time", "name":
sortField = opts.SortBy
}
sortOrder := "DESC"
if opts.SortOrder == "asc" {
sortOrder = "ASC"
}
err := baseQuery.
Select(selectCols).
Order(sortField + " " + sortOrder).
Limit(int(opts.Limit)).
Offset(int(opts.Offset)).
Scan(&results).Error
if err != nil {
return nil, 0, err
}
return &results, total, nil
}
func (self *Event) UpdateEventFieldsById(ctx context.Context, eventId uuid.UUID, updates map[string]any) error {
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&Event{}).
Where("event_id = ?", eventId).
Updates(updates).Error; err != nil {
return err
}
return tx.Where("event_id = ?", eventId).First(self).Error
})
}
func (self *Event) DeleteEventById(ctx context.Context, eventId uuid.UUID) error {
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Where("event_id = ?", eventId).Delete(&Event{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
}
func (self *Event) GetEventsByUserId(ctx context.Context, userId uuid.UUID, limit, offset int64) (*[]EventIndexDoc, error) {
var results []EventIndexDoc

70
data/stats.go Normal file
View File

@@ -0,0 +1,70 @@
package data
import (
"context"
"time"
"github.com/google/uuid"
)
type PermissionLevelCount struct {
PermissionLevel uint `json:"permission_level"`
Count int64 `json:"count"`
}
type EventStatDoc struct {
EventId uuid.UUID `json:"event_id"`
Name string `json:"name"`
JoinCount int64 `json:"join_count"`
CheckinCount int64 `json:"checkin_count"`
}
type GlobalStats struct{}
func (self *GlobalStats) TotalUsers(ctx context.Context) (int64, error) {
var count int64
err := Database.WithContext(ctx).Model(&User{}).Count(&count).Error
return count, err
}
func (self *GlobalStats) UsersPerPermissionLevel(ctx context.Context) (*[]PermissionLevelCount, error) {
var results []PermissionLevelCount
err := Database.WithContext(ctx).
Model(&User{}).
Select("permission_level, COUNT(*) as count").
Group("permission_level").
Order("permission_level ASC").
Scan(&results).Error
if err != nil {
return nil, err
}
return &results, nil
}
func (self *GlobalStats) EventJoinCheckinCounts(ctx context.Context) (*[]EventStatDoc, error) {
var results []EventStatDoc
zero := time.Time{}
err := Database.WithContext(ctx).
Table("events").
Select(`
events.event_id,
events.name,
COUNT(attendances.id) AS join_count,
SUM(CASE WHEN attendances.checkin_at > ? THEN 1 ELSE 0 END) AS checkin_count
`, zero).
Joins("LEFT JOIN attendances ON attendances.event_id = events.event_id").
Group("events.event_id, events.name").
Order("events.id ASC").
Scan(&results).Error
if err != nil {
return nil, err
}
return &results, nil
}

View File

@@ -31,6 +31,24 @@ type UserIndexDoc struct {
Avatar string `json:"avatar"`
}
type UserAdminDoc struct {
UserId string `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
PermissionLevel uint `json:"permission_level"`
}
type UserListOptions struct {
PermissionLevel *uint
SortBy string // "id" | "permission_level"
SortOrder string // "asc" | "desc"
Limit int
Offset int
}
func (self *User) SetEmail(s string) *User {
self.Email = s
return self
@@ -151,3 +169,41 @@ func (self *User) FastListUsers(ctx context.Context, limit, offset *int) (*[]Use
return &results, nil
}
func (self *User) ListUsersFiltered(ctx context.Context, opts UserListOptions) (*[]UserAdminDoc, int64, error) {
var results []UserAdminDoc
var total int64
base := Database.WithContext(ctx).Model(&User{})
if opts.PermissionLevel != nil {
base = base.Where("permission_level = ?", *opts.PermissionLevel)
}
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
sortField := "id"
if opts.SortBy == "permission_level" {
sortField = "permission_level"
}
sortOrder := "ASC"
if opts.SortOrder == "desc" {
sortOrder = "DESC"
}
err := base.
Select("user_id", "email", "username", "nickname", "subtitle", "avatar", "permission_level").
Order(sortField + " " + sortOrder).
Limit(opts.Limit).
Offset(opts.Offset).
Scan(&results).Error
if err != nil {
return nil, 0, err
}
return &results, total, nil
}

View File

@@ -0,0 +1,94 @@
package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"github.com/google/uuid"
)
type AgendaListData struct {
EventId uuid.UUID `json:"event_id" form:"event_id"`
}
type AgendaListPayload struct {
Context context.Context
UserId uuid.UUID
Data *AgendaListData
}
type AgendaListResult struct {
Common shared.CommonResult
Data *[]data.AgendaDoc
}
func (self *AgendaServiceImpl) List(payload *AgendaListPayload) (result *AgendaListResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_agenda",
"list",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaList)
eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaListResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &AgendaListResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
agendas, err := new(data.Agenda).GetListByEventId(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaListResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaListResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Data: agendas,
}
return
}

View File

@@ -0,0 +1,94 @@
package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"github.com/google/uuid"
)
type AgendaMyListData struct {
EventId uuid.UUID `json:"event_id" form:"event_id"`
}
type AgendaMyListPayload struct {
Context context.Context
UserId uuid.UUID
Data *AgendaMyListData
}
type AgendaMyListResult struct {
Common shared.CommonResult
Data *[]data.Agenda
}
func (self *AgendaServiceImpl) MyList(payload *AgendaMyListPayload) (result *AgendaMyListResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_agenda",
"my_list",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaMyList)
attendanceData, err := new(data.Attendance).GetAttendance(ctx, payload.UserId, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaMyListResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if attendanceData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("user is not an attendee of this event")),
).Throw(ctx)
result = &AgendaMyListResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
agendas, err := new(data.Agenda).GetListByAttendanceId(ctx, attendanceData.AttendanceId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaMyListResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaMyListResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Data: agendas,
}
return
}

View File

@@ -0,0 +1,139 @@
package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"github.com/google/uuid"
)
type AgendaReviewData struct {
AgendaId uuid.UUID `json:"agenda_id"`
EventId uuid.UUID `json:"event_id"`
Status string `json:"status"` // approved | rejected
}
type AgendaReviewPayload struct {
Context context.Context
UserId uuid.UUID
Data *AgendaReviewData
}
type AgendaReviewResult struct {
Common shared.CommonResult
}
func (self *AgendaServiceImpl) Review(payload *AgendaReviewPayload) (result *AgendaReviewResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_agenda",
"review",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaReview)
eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if eventData.IsAgendaPublished {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaReviewEventPublished),
exception.WithError(errors.New("cannot review agendas after agenda has been published")),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
agendaData, err := new(data.Agenda).GetByAgendaId(ctx, payload.Data.AgendaId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if agendaData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("agenda not found")),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
holder := new(data.Agenda)
if err := holder.UpdateFieldsByAgendaId(ctx, payload.Data.AgendaId, map[string]any{
"status": payload.Data.Status,
}); err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaReviewResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}

View File

@@ -0,0 +1,126 @@
package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"time"
"github.com/google/uuid"
)
type AgendaScheduleData struct {
AgendaId uuid.UUID `json:"agenda_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
type AgendaSchedulePayload struct {
Context context.Context
UserId uuid.UUID
Data *AgendaScheduleData
}
type AgendaScheduleResult struct {
Common shared.CommonResult
}
func (self *AgendaServiceImpl) Schedule(payload *AgendaSchedulePayload) (result *AgendaScheduleResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_agenda",
"schedule",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaSchedule)
if !payload.Data.StartTime.Before(payload.Data.EndTime) {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("start_time must be before end_time")),
).Throw(ctx)
result = &AgendaScheduleResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
agendaData, err := new(data.Agenda).GetByAgendaId(ctx, payload.Data.AgendaId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaScheduleResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if agendaData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("agenda not found")),
).Throw(ctx)
result = &AgendaScheduleResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if agendaData.Status != "approved" {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaScheduleNotApproved),
exception.WithError(errors.New("only approved agendas can be scheduled")),
).Throw(ctx)
result = &AgendaScheduleResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
holder := new(data.Agenda)
if err := holder.UpdateFieldsByAgendaId(ctx, payload.Data.AgendaId, map[string]any{
"start_time": payload.Data.StartTime,
"end_time": payload.Data.EndTime,
}); err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaScheduleResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaScheduleResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}

View File

@@ -0,0 +1,107 @@
package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"github.com/google/uuid"
)
type AgendaScheduleGetData struct {
EventId uuid.UUID `json:"event_id" form:"event_id"`
}
type AgendaScheduleGetPayload struct {
Context context.Context
Data *AgendaScheduleGetData
}
type AgendaScheduleGetResult struct {
Common shared.CommonResult
Data *[]data.AgendaDoc
}
func (self *AgendaServiceImpl) ScheduleGet(payload *AgendaScheduleGetPayload) (result *AgendaScheduleGetResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_agenda",
"schedule_get",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaScheduleGet)
eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaScheduleGetResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &AgendaScheduleGetResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if !eventData.IsAgendaPublished {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaScheduleGetNotPublished),
exception.WithError(errors.New("agenda has not been published for this event")),
).Throw(ctx)
result = &AgendaScheduleGetResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
agendas, err := new(data.Agenda).GetScheduledByEventId(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaScheduleGetResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaScheduleGetResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Data: agendas,
}
return
}

View File

@@ -2,6 +2,12 @@ package service_agenda
type AgendaService interface {
Submit(*SubmitPayload) *SubmitResult
Update(*AgendaUpdatePayload) *AgendaUpdateResult
Review(*AgendaReviewPayload) *AgendaReviewResult
Schedule(*AgendaSchedulePayload) *AgendaScheduleResult
List(*AgendaListPayload) *AgendaListResult
MyList(*AgendaMyListPayload) *AgendaMyListResult
ScheduleGet(*AgendaScheduleGetPayload) *AgendaScheduleGetResult
}
type AgendaServiceImpl struct{}

View File

@@ -2,13 +2,14 @@ package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type SubmitData struct {
@@ -42,30 +43,10 @@ func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitRes
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaSubmit)
var err error
attendanceData, err := new(data.Attendance).
GetAttendance(ctx, payload.UserId, payload.Data.EventId)
if err != nil {
if err == gorm.ErrRecordNotFound {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(err),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{
HttpCode: 403,
Exception: exc,
},
Data: nil,
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
@@ -74,13 +55,108 @@ func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitRes
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if attendanceData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorPermissionDenied),
exception.WithError(errors.New("user is not an attendee of this event")),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if eventData.StartTime.Before(time.Now()) {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaSubmitEventStarted),
exception.WithError(errors.New("cannot submit agenda after event has started")),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
if eventData.IsAgendaPublished {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaSubmitEventPublished),
exception.WithError(errors.New("cannot submit agenda after agenda has been published")),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
pendingCount, err := new(data.Agenda).CountPendingByAttendanceId(ctx, attendanceData.AttendanceId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if pendingCount >= 5 {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaSubmitPendingLimitReached),
exception.WithError(errors.New("maximum of 5 pending agendas allowed")),
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
@@ -88,7 +164,7 @@ func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitRes
SetAttendanceId(attendanceData.AttendanceId).
SetName(payload.Data.Name).
SetDescription(payload.Data.Description).
SetIsApproved(false)
SetStatus("pending")
err = agendaModel.Create(ctx)
if err != nil {
@@ -100,13 +176,8 @@ func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitRes
).Throw(ctx)
result = &SubmitResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
@@ -116,16 +187,14 @@ func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitRes
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
resultData := &SubmitResponse{
AgendaId: agendaModel.AgendaId,
}
result = &SubmitResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exc,
},
Data: resultData,
Data: &SubmitResponse{
AgendaId: agendaModel.AgendaId,
},
}
return

View File

@@ -0,0 +1,193 @@
package service_agenda
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"time"
"github.com/google/uuid"
)
type AgendaUpdateData struct {
AgendaId uuid.UUID `json:"agenda_id"`
Name *string `json:"name"`
Description *string `json:"description"`
PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
}
type AgendaUpdatePayload struct {
Context context.Context
UserId uuid.UUID
Data *AgendaUpdateData
}
type AgendaUpdateResult struct {
Common shared.CommonResult
}
func (self *AgendaServiceImpl) Update(payload *AgendaUpdatePayload) (result *AgendaUpdateResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_agenda",
"update",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceAgendaUpdate)
agendaData, err := new(data.Agenda).GetByAgendaId(ctx, payload.Data.AgendaId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if agendaData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("agenda not found")),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
isManager := payload.Data.PermissionLevel >= 30
if !isManager {
myAttendance, err := new(data.Attendance).GetAttendanceByAttendanceId(ctx, agendaData.AttendanceId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if myAttendance == nil || myAttendance.UserId != payload.UserId {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaUpdateNotSubmitter),
exception.WithError(errors.New("you are not the submitter of this agenda")),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
if agendaData.Status != "pending" {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaUpdateNotPending),
exception.WithError(errors.New("submitters may only edit pending agendas")),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
eventData, err := new(data.Event).GetEventById(ctx, myAttendance.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData != nil && eventData.StartTime.Before(time.Now()) {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.AgendaUpdateDeadlinePassed),
exception.WithError(errors.New("submission deadline has passed")),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
}
updates := map[string]any{}
if payload.Data.Name != nil {
updates["name"] = *payload.Data.Name
}
if payload.Data.Description != nil {
updates["description"] = *payload.Data.Description
}
if len(updates) == 0 {
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}
holder := new(data.Agenda)
if err := holder.UpdateFieldsByAgendaId(ctx, payload.Data.AgendaId, updates); err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AgendaUpdateResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}

View File

@@ -3,6 +3,7 @@ package service_event
import (
"context"
"encoding/json"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"nixcn-cms/internal/exception"
@@ -10,17 +11,25 @@ import (
"nixcn-cms/service/service_user"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"strconv"
"github.com/google/uuid"
"github.com/spf13/viper"
)
type AttendanceListData struct {
EventId uuid.UUID `json:"event_id" form:"event_id"`
EventId uuid.UUID `json:"event_id" form:"event_id"`
Name string `json:"name" form:"name"`
KycStatus string `json:"kyc_status" form:"kyc_status"` // "with_kyc" | "without_kyc" | ""
Limit *string `json:"limit" form:"limit"`
Offset *string `json:"offset" form:"offset"`
SortBy *string `json:"sort_by" form:"sort_by"`
SortOrder *string `json:"sort_order" form:"sort_order"`
}
type AttendanceListPayload struct {
Context context.Context
UserId uuid.UUID
Data *AttendanceListData
}
@@ -31,9 +40,14 @@ type AttendanceListResponse struct {
KycInfo any `json:"kyc_info"`
}
type AttendanceListPagedResponse struct {
Total int64 `json:"total"`
Items []AttendanceListResponse `json:"items"`
}
type AttendanceListResult struct {
Common shared.CommonResult
Data []AttendanceListResponse
Data *AttendanceListPagedResponse
}
func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (result *AttendanceListResult) {
@@ -44,10 +58,85 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
)
defer span.End()
attList, err := new(data.Attendance).GetAttendanceListByEventId(ctx, payload.Data.EventId)
ctx = exception.ContextWithService(ctx, exception.ServiceEventAttendanceList)
eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if eventData.Owner != payload.UserId {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventAttendanceListError),
exception.WithError(errors.New("only the event owner may view the attendance list")),
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
limit := 20
if payload.Data.Limit != nil && *payload.Data.Limit != "" {
if v, err := strconv.Atoi(*payload.Data.Limit); err == nil {
limit = v
}
}
offset := 0
if payload.Data.Offset != nil && *payload.Data.Offset != "" {
if v, err := strconv.Atoi(*payload.Data.Offset); err == nil {
offset = v
}
}
sortBy := ""
if payload.Data.SortBy != nil {
sortBy = *payload.Data.SortBy
}
sortOrder := ""
if payload.Data.SortOrder != nil {
sortOrder = *payload.Data.SortOrder
}
filter := data.AttendanceListFilter{
EventId: payload.Data.EventId,
Name: payload.Data.Name,
KycStatus: payload.Data.KycStatus,
SortBy: sortBy,
SortOrder: sortOrder,
Limit: limit,
Offset: offset,
}
attList, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, filter)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
@@ -57,10 +146,7 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
@@ -84,10 +170,7 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
@@ -112,7 +195,6 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
if err == nil && kycData != nil {
kycType = kycData.Type
// AES Decrypt
decodedKycInfo, err := cryptography.AESCBCDecrypt(string(kycData.KycInfo), []byte(viper.GetString("secrets.kyc_info_key")))
if err != nil {
exc := exception.New(
@@ -128,7 +210,6 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
return
}
// JSON Unmarshal
switch kycType {
case "cnrid":
var kycDetail kyc.CNRidInfo
@@ -141,10 +222,7 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
@@ -161,10 +239,7 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
@@ -183,17 +258,18 @@ func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (re
})
}
result = &AttendanceListResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx),
},
Data: responseList,
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &AttendanceListResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Data: &AttendanceListPagedResponse{
Total: total,
Items: responseList,
},
}
return
}

View File

@@ -0,0 +1,160 @@
package service_event
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"time"
"github.com/google/uuid"
)
type EventCreateData struct {
Type string `json:"type"`
EnableKYC bool `json:"enable_kyc"`
Name string `json:"name"`
Subtitle string `json:"subtitle"`
Description string `json:"description"`
AttendanceGuide string `json:"attendance_guide"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Thumbnail string `json:"thumbnail"`
Quota int64 `json:"quota"`
Limit int64 `json:"limit"`
UserId string `json:"user_id" swaggerignore:"true"`
PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
}
type EventCreatePayload struct {
Context context.Context
Data *EventCreateData
}
type EventCreateResponse struct {
EventId string `json:"event_id" validate:"required"`
}
type EventCreateResult struct {
Common shared.CommonResult
Data *EventCreateResponse
}
func (self *EventServiceImpl) Create(payload *EventCreatePayload) (result *EventCreateResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_event",
"create",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceEventCreate)
if payload.Data.Type != "party" && payload.Data.Type != "official" {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("type must be 'party' or 'official'")),
).Throw(ctx)
result = &EventCreateResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
}
return
}
if payload.Data.PermissionLevel == 30 && payload.Data.Type != "party" {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventCreateTypeNotAllowed),
exception.WithError(errors.New("Lv30 users may only create events with type 'party'")),
).Throw(ctx)
result = &EventCreateResult{
Common: shared.CommonResult{
HttpCode: 403,
Exception: exc,
},
}
return
}
ownerId, err := uuid.Parse(payload.Data.UserId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx)
result = &EventCreateResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
}
return
}
eventData := &data.Event{
Type: payload.Data.Type,
EnableKYC: payload.Data.EnableKYC,
Name: payload.Data.Name,
Subtitle: payload.Data.Subtitle,
Description: payload.Data.Description,
AttendanceGuide: payload.Data.AttendanceGuide,
StartTime: payload.Data.StartTime,
EndTime: payload.Data.EndTime,
Thumbnail: payload.Data.Thumbnail,
Quota: payload.Data.Quota,
Limit: payload.Data.Limit,
Owner: ownerId,
}
if err := eventData.Create(ctx); err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventCreateResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &EventCreateResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exc,
},
Data: &EventCreateResponse{
EventId: eventData.EventId.String(),
},
}
return
}

View File

@@ -0,0 +1,91 @@
package service_event
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"github.com/google/uuid"
"gorm.io/gorm"
)
type EventDeleteData struct {
EventId string `json:"event_id"`
}
type EventDeletePayload struct {
Context context.Context
Data *EventDeleteData
}
type EventDeleteResult struct {
Common shared.CommonResult
}
func (self *EventServiceImpl) Delete(payload *EventDeletePayload) (result *EventDeleteResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_event",
"delete",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceEventDelete)
eventId, err := uuid.Parse(payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx)
result = &EventDeleteResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
if err := new(data.Event).DeleteEventById(ctx, eventId); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventDeleteNotFound),
exception.WithError(err),
).Throw(ctx)
result = &EventDeleteResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventDeleteResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &EventDeleteResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}

View File

@@ -13,8 +13,12 @@ import (
)
type EventListData struct {
Limit *string `json:"limit"`
Offset *string `json:"offset"`
Limit *string `json:"limit"`
Offset *string `json:"offset"`
Type *string `json:"type"`
SortBy *string `json:"sort_by"`
SortOrder *string `json:"sort_order"`
PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
}
type EventListPayload struct {
@@ -31,6 +35,7 @@ type EventListResponse struct {
type EventListResult struct {
Common shared.CommonResult
Total int64 `json:"total"`
Data *[]EventListResponse `json:"event_list"`
}
@@ -44,7 +49,6 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
ctx = exception.ContextWithService(ctx, exception.ServiceEventList)
var err error
var limit string
if payload.Data.Limit == nil || *payload.Data.Limit == "" {
limit = "20"
@@ -62,14 +66,8 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
return &EventListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
} else {
offset = *payload.Data.Offset
}
limitNum, err := strconv.Atoi(limit)
@@ -82,13 +80,8 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
result = &EventListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
@@ -102,18 +95,34 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
result = &EventListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
eventList, err := new(data.Event).
FastListEvents(ctx, int64(limitNum), int64(offsetNum))
opts := data.EventListOptions{
Limit: int64(limitNum),
Offset: int64(offsetNum),
}
if payload.Data.Type != nil && *payload.Data.Type != "" {
opts.TypeFilter = *payload.Data.Type
}
if payload.Data.SortBy != nil {
opts.SortBy = *payload.Data.SortBy
}
if payload.Data.SortOrder != nil {
opts.SortOrder = *payload.Data.SortOrder
}
// Lv30 users only see their own events
if payload.Data.PermissionLevel == 30 {
opts.OwnerId = &payload.UserId
}
eventList, total, err := new(data.Event).ListEventsWithOptions(ctx, opts)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
@@ -123,11 +132,7 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
return &EventListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
}
@@ -136,14 +141,12 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
if eventList != nil && len(*eventList) > 0 {
var eventIds []uuid.UUID
for _, e := range *eventList {
parsedId, parseErr := uuid.Parse(e.EventId)
if parseErr == nil {
if parsedId, parseErr := uuid.Parse(e.EventId); parseErr == nil {
eventIds = append(eventIds, parsedId)
}
}
joinedMap, err := new(data.Attendance).GetJoinedEventIDs(ctx, payload.UserId, eventIds)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
@@ -153,18 +156,12 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
result = &EventListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
checkedInMap, err := new(data.Attendance).GetCheckedInEventIDs(ctx, payload.UserId, eventIds)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
@@ -174,25 +171,18 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
result = &EventListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
for _, i := range *eventList {
currentIdUuid, _ := uuid.Parse(i.EventId)
response := EventListResponse{
responseList = append(responseList, EventListResponse{
EventIndexDoc: i,
IsJoined: joinedMap[currentIdUuid],
IsCheckedIn: checkedInMap[currentIdUuid],
}
responseList = append(responseList, response)
})
}
}
@@ -203,12 +193,9 @@ func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventList
).Throw(ctx)
result = &EventListResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exc,
},
Data: &responseList,
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Total: total,
Data: &responseList,
}
return
}

View File

@@ -9,6 +9,10 @@ type EventService interface {
Join(*EventJoinPayload) *EventJoinResult
AttendanceList(*AttendanceListPayload) *AttendanceListResult
GetAttendanceGuide(*AttendanceGuidePayload) *AttendanceGuideResult
Create(*EventCreatePayload) *EventCreateResult
Update(*EventUpdatePayload) *EventUpdateResult
Delete(*EventDeletePayload) *EventDeleteResult
Stats(*EventStatsPayload) *EventStatsResult
}
type EventServiceImpl struct{}

View File

@@ -0,0 +1,187 @@
package service_event
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"github.com/google/uuid"
)
type EventStatsData struct {
EventId string `json:"event_id"`
}
type EventStatsPayload struct {
Context context.Context
UserId uuid.UUID
Data *EventStatsData
}
type EventStatsResponse struct {
JoinCount int64 `json:"join_count"`
CheckinCount int64 `json:"checkin_count"`
KycPassRate float64 `json:"kyc_pass_rate"`
AgendaSubmissionCount int64 `json:"agenda_submission_count"`
}
type EventStatsResult struct {
Common shared.CommonResult
Data *EventStatsResponse
}
func (self *EventServiceImpl) Stats(payload *EventStatsPayload) (result *EventStatsResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_event",
"stats",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceEventStats)
eventId, err := uuid.Parse(payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
eventData, err := new(data.Event).GetEventById(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if eventData.Owner != payload.UserId {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventStatsNotOwner),
exception.WithError(errors.New("only the event owner may view event stats")),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
attRepo := new(data.Attendance)
joinCount, err := attRepo.CountUsersByEventID(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
checkinCount, err := attRepo.CountCheckedInUsersByEventID(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
withKycCount, err := attRepo.CountWithKycByEventID(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
agendaCount, err := new(data.Agenda).CountByEventId(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
var kycPassRate float64
if joinCount > 0 {
kycPassRate = float64(withKycCount) / float64(joinCount) * 100
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &EventStatsResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Data: &EventStatsResponse{
JoinCount: joinCount,
CheckinCount: checkinCount,
KycPassRate: kycPassRate,
AgendaSubmissionCount: agendaCount,
},
}
return
}

View File

@@ -0,0 +1,250 @@
package service_event
import (
"context"
"errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
"time"
"github.com/google/uuid"
)
type EventUpdateData struct {
EventId string `json:"event_id"`
Name *string `json:"name"`
Subtitle *string `json:"subtitle"`
Description *string `json:"description"`
AttendanceGuide *string `json:"attendance_guide"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
Thumbnail *string `json:"thumbnail"`
IsAgendaPublished *bool `json:"is_agenda_published"`
// immutable — presence triggers rejection
Type *string `json:"type" swaggerignore:"true"`
EnableKYC *bool `json:"enable_kyc" swaggerignore:"true"`
UserId string `json:"user_id" swaggerignore:"true"`
}
type EventUpdatePayload struct {
Context context.Context
Data *EventUpdateData
}
type EventUpdateResult struct {
Common shared.CommonResult
}
func (self *EventServiceImpl) Update(payload *EventUpdatePayload) (result *EventUpdateResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_event",
"update",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceEventUpdate)
if payload.Data.Type != nil || payload.Data.EnableKYC != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventUpdateImmutableField),
exception.WithError(errors.New("type and enable_kyc are immutable after creation")),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
eventId, err := uuid.Parse(payload.Data.EventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
userId, err := uuid.Parse(payload.Data.UserId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUuidParseFailed),
exception.WithError(err),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
eventData, err := new(data.Event).GetEventById(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if eventData == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(errors.New("event not found")),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
if eventData.Owner != userId {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventUpdateNotOwner),
exception.WithError(errors.New("only the event owner may update this event")),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
updates := map[string]any{}
if payload.Data.Name != nil {
updates["name"] = *payload.Data.Name
}
if payload.Data.Subtitle != nil {
updates["subtitle"] = *payload.Data.Subtitle
}
if payload.Data.Description != nil {
updates["description"] = *payload.Data.Description
}
if payload.Data.StartTime != nil {
updates["start_time"] = *payload.Data.StartTime
}
if payload.Data.EndTime != nil {
updates["end_time"] = *payload.Data.EndTime
}
if payload.Data.Thumbnail != nil {
updates["thumbnail"] = *payload.Data.Thumbnail
}
if payload.Data.AttendanceGuide != nil {
updates["attendance_guide"] = *payload.Data.AttendanceGuide
}
if payload.Data.IsAgendaPublished != nil {
want := *payload.Data.IsAgendaPublished
if eventData.IsAgendaPublished && !want {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventUpdateAgendaAlreadyPublished),
exception.WithError(errors.New("is_agenda_published is write-once and cannot be reverted")),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
if want && !eventData.IsAgendaPublished {
agendaCount, err := new(data.Agenda).CountByEventId(ctx, eventId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if agendaCount == 0 {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.EventUpdateAgendaPreflightFailed),
exception.WithError(errors.New("cannot publish agenda: no agenda submissions exist for this event")),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
updates["is_agenda_published"] = true
}
}
if len(updates) == 0 {
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}
holder := new(data.Event)
if err := holder.UpdateEventFieldsById(ctx, eventId, updates); err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &EventUpdateResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}

View File

@@ -0,0 +1,98 @@
package service_stats
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"nixcn-cms/tracer"
)
type GlobalStatsPayload struct {
Context context.Context
}
type GlobalStatsResponse struct {
TotalUsers int64 `json:"total_users"`
UsersPerLevel *[]data.PermissionLevelCount `json:"users_per_level"`
EventJoinCheckin *[]data.EventStatDoc `json:"event_join_checkin"`
}
type GlobalStatsResult struct {
Common shared.CommonResult
Data *GlobalStatsResponse
}
func (self *StatsServiceImpl) Global(payload *GlobalStatsPayload) (result *GlobalStatsResult) {
ctx, span := tracer.StartSpan(
payload.Context,
"service_stats",
"global",
)
defer span.End()
ctx = exception.ContextWithService(ctx, exception.ServiceStatsGlobal)
statsRepo := new(data.GlobalStats)
totalUsers, err := statsRepo.TotalUsers(ctx)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &GlobalStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
usersPerLevel, err := statsRepo.UsersPerPermissionLevel(ctx)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &GlobalStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
eventStats, err := statsRepo.EventJoinCheckinCounts(ctx)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &GlobalStatsResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonSuccess),
).Throw(ctx)
result = &GlobalStatsResult{
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Data: &GlobalStatsResponse{
TotalUsers: totalUsers,
UsersPerLevel: usersPerLevel,
EventJoinCheckin: eventStats,
},
}
return
}

View File

@@ -0,0 +1,11 @@
package service_stats
type StatsService interface {
Global(*GlobalStatsPayload) *GlobalStatsResult
}
type StatsServiceImpl struct{}
func NewStatsService() StatsService {
return &StatsServiceImpl{}
}

View File

@@ -23,10 +23,12 @@ type UserInfoData struct {
}
type UserInfoPayload struct {
Context context.Context
UserId uuid.UUID
IsOther bool
Data *UserInfoData
Context context.Context
UserId uuid.UUID // target user
OperatorId uuid.UUID // calling user (for permission matrix on update)
OperatorLevel uint
IsOther bool
Data *UserInfoData
}
type UserInfoResult struct {

View File

@@ -11,17 +11,21 @@ import (
)
type UserListPayload struct {
Context context.Context
Limit *string
Offset *string
Context context.Context
Limit *string
Offset *string
SortBy *string
SortOrder *string
PermissionLevel *uint
}
type UserListResponse struct {
data.UserIndexDoc
data.UserAdminDoc
}
type UserListResult struct {
Common shared.CommonResult
Total int64
Data *[]UserListResponse `json:"user_list"`
}
@@ -35,11 +39,23 @@ func (self *UserServiceImpl) List(payload *UserListPayload) (result *UserListRes
ctx = exception.ContextWithService(ctx, exception.ServiceUserList)
var limit string
if payload.Limit == nil || *payload.Limit == "" {
limit = "20"
} else {
limit = *payload.Limit
limit := 20
if payload.Limit != nil && *payload.Limit != "" {
v, err := strconv.Atoi(*payload.Limit)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx)
result = &UserListResult{
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
limit = v
}
var offset string
@@ -52,39 +68,12 @@ func (self *UserServiceImpl) List(payload *UserListPayload) (result *UserListRes
).Throw(ctx)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
} else {
offset = *payload.Offset
}
// Parse string to int64
limitNum, err := strconv.Atoi(limit)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorInvalidInput),
exception.WithError(err),
).Throw(ctx)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
}
return
}
offsetNum, err := strconv.Atoi(offset)
if err != nil {
exc := exception.New(
@@ -95,19 +84,25 @@ func (self *UserServiceImpl) List(payload *UserListPayload) (result *UserListRes
).Throw(ctx)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
// Get user list from search engine
userList, err := new(data.User).
FastListUsers(ctx, &limitNum, &offsetNum)
opts := data.UserListOptions{
Limit: limit,
Offset: offsetNum,
PermissionLevel: payload.PermissionLevel,
}
if payload.SortBy != nil {
opts.SortBy = *payload.SortBy
}
if payload.SortOrder != nil {
opts.SortOrder = *payload.SortOrder
}
userList, total, err := new(data.User).ListUsersFiltered(ctx, opts)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
@@ -117,19 +112,14 @@ func (self *UserServiceImpl) List(payload *UserListPayload) (result *UserListRes
).Throw(ctx)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
response := make([]UserListResponse, 0)
response := make([]UserListResponse, 0, len(*userList))
for _, doc := range *userList {
response = append(response, UserListResponse{
UserIndexDoc: doc,
})
response = append(response, UserListResponse{UserAdminDoc: doc})
}
exc := exception.New(
@@ -139,12 +129,9 @@ func (self *UserServiceImpl) List(payload *UserListPayload) (result *UserListRes
).Throw(ctx)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exc,
},
Data: &response,
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
Total: total,
Data: &response,
}
return
}

View File

@@ -12,6 +12,7 @@ import (
)
type UserInfoUpdateData struct {
TargetUserId string `json:"user_id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
@@ -31,7 +32,7 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
ctx = exception.ContextWithService(ctx, exception.ServiceUserUpdateInfo)
var err error
isAdminEdit := payload.OperatorLevel >= 40 && payload.UserId != payload.OperatorId
updates := make(map[string]any)
@@ -47,13 +48,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
}
@@ -71,13 +67,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
if utf8.RuneCountInString(val) > 24 {
@@ -89,13 +80,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
updates["nickname"] = val
@@ -112,13 +98,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
updates["subtitle"] = *payload.Data.Subtitle
@@ -137,13 +118,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
}
@@ -162,13 +138,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
}
return
}
}
@@ -179,6 +150,76 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
updates["allow_public"] = *payload.Data.AllowPublic
}
userData := new(data.User)
targetInfo, err := userData.GetByUserId(ctx, &payload.UserId)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if targetInfo == nil {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorUserNotFound),
exception.WithError(errors.New("target user not found")),
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{HttpCode: 404, Exception: exc},
}
return
}
// Permission matrix: admin editing another user
if isAdminEdit {
if targetInfo.PermissionLevel >= payload.OperatorLevel {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.UserUpdatePermissionMatrixViolated),
exception.WithError(errors.New("cannot edit a user with equal or higher permission level")),
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
}
if payload.Data.PermissionLevel != nil {
newLevel := *payload.Data.PermissionLevel
// Only admins may change permission_level; the new level must be strictly below the operator's
if payload.OperatorLevel >= 40 {
if newLevel >= payload.OperatorLevel {
exc := exception.New(
exception.WithStatus(exception.StatusUser),
exception.WithType(exception.TypeSpecific),
exception.WithOriginal(exception.UserUpdatePermissionLevelTooHigh),
exception.WithError(errors.New("cannot grant a permission level equal to or higher than your own")),
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{HttpCode: 403, Exception: exc},
}
return
}
updates["permission_level"] = newLevel
}
// Non-admins silently ignore permission_level changes
}
if len(updates) == 0 {
exc := exception.New(
exception.WithStatus(exception.StatusSuccess),
@@ -187,20 +228,12 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}
userData := new(data.User)
userInfo, err := userData.GetByUserId(ctx, &payload.UserId)
if err != nil {
if err := userData.UpdateByUserID(ctx, &payload.UserId, updates); err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
@@ -209,37 +242,8 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
}
return
}
if payload.Data.PermissionLevel != nil && userInfo.PermissionLevel >= 50 {
updates["permission_level"] = *payload.Data.PermissionLevel
}
err = userData.UpdateByUserID(ctx, &payload.UserId, updates)
if err != nil {
exc := exception.New(
exception.WithStatus(exception.StatusServer),
exception.WithType(exception.TypeCommon),
exception.WithOriginal(exception.CommonErrorDatabase),
exception.WithError(err),
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exc,
},
Data: nil,
}
return
}
@@ -250,12 +254,7 @@ func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserI
).Throw(ctx)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exc,
},
Data: nil,
Common: shared.CommonResult{HttpCode: 200, Exception: exc},
}
return
}