Add agenda and stats and event management service with api
All checks were successful
Server Check Build (NixCN CMS) TeamCity build finished
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:
@@ -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
106
api/agenda/list.go
Normal 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
105
api/agenda/my_list.go
Normal 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
110
api/agenda/review.go
Normal 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
98
api/agenda/schedule.go
Normal 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())
|
||||
}
|
||||
80
api/agenda/schedule_get.go
Normal file
80
api/agenda/schedule_get.go
Normal 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
113
api/agenda/update.go
Normal 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())
|
||||
}
|
||||
@@ -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
91
api/event/create.go
Normal 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
74
api/event/delete.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
78
api/event/stats.go
Normal 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
77
api/event/update.go
Normal 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())
|
||||
}
|
||||
@@ -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
44
api/stats/global.go
Normal 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
22
api/stats/handler.go
Normal 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
115
api/user/admin_update.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
102
data/agenda.go
102
data/agenda.go
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
130
data/event.go
130
data/event.go
@@ -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
70
data/stats.go
Normal 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
|
||||
}
|
||||
56
data/user.go
56
data/user.go
@@ -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
|
||||
}
|
||||
|
||||
94
service/service_agenda/list.go
Normal file
94
service/service_agenda/list.go
Normal 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
|
||||
}
|
||||
94
service/service_agenda/my_list.go
Normal file
94
service/service_agenda/my_list.go
Normal 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
|
||||
}
|
||||
139
service/service_agenda/review.go
Normal file
139
service/service_agenda/review.go
Normal 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
|
||||
}
|
||||
126
service/service_agenda/schedule.go
Normal file
126
service/service_agenda/schedule.go
Normal 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
|
||||
}
|
||||
107
service/service_agenda/schedule_get.go
Normal file
107
service/service_agenda/schedule_get.go
Normal 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
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
|
||||
193
service/service_agenda/update.go
Normal file
193
service/service_agenda/update.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
160
service/service_event/create.go
Normal file
160
service/service_event/create.go
Normal 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
|
||||
}
|
||||
91
service/service_event/delete.go
Normal file
91
service/service_event/delete.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
187
service/service_event/stats.go
Normal file
187
service/service_event/stats.go
Normal 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
|
||||
}
|
||||
250
service/service_event/update.go
Normal file
250
service/service_event/update.go
Normal 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
|
||||
}
|
||||
98
service/service_stats/global.go
Normal file
98
service/service_stats/global.go
Normal 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
|
||||
}
|
||||
11
service/service_stats/service.go
Normal file
11
service/service_stats/service.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package service_stats
|
||||
|
||||
type StatsService interface {
|
||||
Global(*GlobalStatsPayload) *GlobalStatsResult
|
||||
}
|
||||
|
||||
type StatsServiceImpl struct{}
|
||||
|
||||
func NewStatsService() StatsService {
|
||||
return &StatsServiceImpl{}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user