diff --git a/api/event/attendance.go b/api/event/attendance.go new file mode 100644 index 0000000..7a477f2 --- /dev/null +++ b/api/event/attendance.go @@ -0,0 +1,59 @@ +package event + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_event" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// AttendanceList handles the retrieval of the 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. +// @Tags Event +// @Produce json +// @Param event_id query string true "Event UUID" +// @Param X-Api-Version header string true "latest" +// @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 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Router /event/attendance [get] +func (self *EventHandler) AttendanceList(c *gin.Context) { + eventIdStr := c.Query("event_id") + + eventId, err := uuid.Parse(eventIdStr) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceAttendanceList). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). // 或者 CommonErrorUuidParseFailed + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + listData := service_event.AttendanceListData{ + EventId: eventId, + } + + result := self.svc.AttendanceList(&service_event.AttendanceListPayload{ + Context: c, + Data: &listData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/event/handler.go b/api/event/handler.go index 1e2c257..4fabe8f 100644 --- a/api/event/handler.go +++ b/api/event/handler.go @@ -22,4 +22,5 @@ func ApiHandler(r *gin.RouterGroup) { 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) } diff --git a/cmd/gen_exception/definitions/endpoint.yaml b/cmd/gen_exception/definitions/endpoint.yaml index d8c6a71..a1138d5 100644 --- a/cmd/gen_exception/definitions/endpoint.yaml +++ b/cmd/gen_exception/definitions/endpoint.yaml @@ -14,6 +14,7 @@ endpoint: checkin_submit: "04" list: "05" join: "06" + attendance_list: "07" user: service: info: "01" diff --git a/cmd/gen_exception/definitions/specific.yaml b/cmd/gen_exception/definitions/specific.yaml index 1d97cc1..df991b8 100644 --- a/cmd/gen_exception/definitions/specific.yaml +++ b/cmd/gen_exception/definitions/specific.yaml @@ -36,6 +36,9 @@ event: database_failed: "00001" join: event_invalid: "00001" + attendance: + list_error: "00001" + kyc_info_decrypt_failed: "00002" kyc: session: failed: "00001" diff --git a/data/agenda.go b/data/agenda.go index 2955ef1..cf5250b 100644 --- a/data/agenda.go +++ b/data/agenda.go @@ -1,13 +1,119 @@ package data -import "github.com/google/uuid" +import ( + "context" + + "github.com/google/uuid" + "gorm.io/gorm" +) 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"` - EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` - UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` - Name string `json:"name" gorm:"type:varchar(255);not null"` - Description string `json:"description" gorm:"type:text;not null"` + 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;not null"` + Name string `json:"name" gorm:"type:varchar(255);not null"` + Description string `json:"description" gorm:"type:text;not null"` +} + +func (self *Agenda) SetAttendanceId(id uuid.UUID) *Agenda { + self.AttendanceId = id + return self +} + +func (self *Agenda) SetName(name string) *Agenda { + self.Name = name + return self +} + +func (self *Agenda) SetDescription(desc string) *Agenda { + self.Description = desc + return self +} + +func (self *Agenda) GetByAgendaId(ctx context.Context, agendaId uuid.UUID) (*Agenda, error) { + var agenda Agenda + + err := Database.WithContext(ctx). + Where("agenda_id = ?", agendaId). + First(&agenda).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + + return &agenda, nil +} + +func (self *Agenda) GetListByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (*[]Agenda, error) { + var result []Agenda + + err := Database.WithContext(ctx). + Model(&Agenda{}). + Where("attendance_id = ?", attendanceId). + Order("id ASC"). + Scan(&result).Error + + return &result, err +} + +func (self *Agenda) Create(ctx context.Context) error { + self.UUID = uuid.New() + self.AgendaId = uuid.New() + + err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&self).Error; err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + return nil +} + +func (self *Agenda) Update(ctx context.Context, agendaId uuid.UUID) (*Agenda, error) { + var agenda Agenda + + err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx. + Where("agenda_id = ?", agendaId). + First(&agenda).Error; err != nil { + return err + } + + if err := tx. + Model(&agenda). + Updates(self).Error; err != nil { + return err + } + + return tx. + Where("agenda_id = ?", agendaId). + First(&agenda).Error + }) + + if err != nil { + return nil, err + } + + return &agenda, 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{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil + }) } diff --git a/data/attendance.go b/data/attendance.go index b1ca2ca..8eeb4d0 100644 --- a/data/attendance.go +++ b/data/attendance.go @@ -112,23 +112,6 @@ func (self *Attendance) GetEventsByUserID(ctx context.Context, userID uuid.UUID) return &result, err } -func (self *Attendance) GetAttendanceByEventIdAndUserId(ctx context.Context, eventId, userId uuid.UUID) (*Attendance, error) { - var attendance Attendance - - err := Database.WithContext(ctx). - Where("event_id = ? AND user_id = ?", eventId, userId). - First(&attendance).Error - - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - - return &attendance, nil -} - func (self *Attendance) Create(ctx context.Context) error { self.UUID = uuid.New() self.AttendanceId = uuid.New() @@ -147,6 +130,17 @@ func (self *Attendance) Create(ctx context.Context) error { return nil } +func (self *Attendance) GetAttendanceListByEventId(ctx context.Context, eventId uuid.UUID) (*[]Attendance, error) { + var result []Attendance + + err := Database.WithContext(ctx). + Where("event_id = ?", eventId). + Order("checkin_at DESC"). + Find(&result).Error + + return &result, err +} + func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID) (*Attendance, error) { var attendance Attendance diff --git a/docs/docs.go b/docs/docs.go index 331efa6..9d46ba9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -559,6 +559,111 @@ const docTemplate = `{ } } }, + "/event/attendance": { + "get": { + "description": "Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.", + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Get Attendance List", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "latest", + "name": "X-Api-Version", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful retrieval", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service_event.AttendanceListResponse" + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + } + } + } + }, "/event/checkin": { "get": { "security": [ @@ -2020,6 +2125,21 @@ const docTemplate = `{ } } }, + "service_event.AttendanceListResponse": { + "type": "object", + "properties": { + "attendance_id": { + "type": "string" + }, + "kyc_info": {}, + "kyc_type": { + "type": "string" + }, + "user_info": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + }, "service_event.CheckinQueryResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index f1a2531..7ca04d4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -557,6 +557,111 @@ } } }, + "/event/attendance": { + "get": { + "description": "Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.", + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Get Attendance List", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "latest", + "name": "X-Api-Version", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful retrieval", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service_event.AttendanceListResponse" + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + } + } + } + }, "/event/checkin": { "get": { "security": [ @@ -2018,6 +2123,21 @@ } } }, + "service_event.AttendanceListResponse": { + "type": "object", + "properties": { + "attendance_id": { + "type": "string" + }, + "kyc_info": {}, + "kyc_type": { + "type": "string" + }, + "user_info": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + }, "service_event.CheckinQueryResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1c2c889..78516c9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -85,6 +85,16 @@ definitions: refresh_token: type: string type: object + service_event.AttendanceListResponse: + properties: + attendance_id: + type: string + kyc_info: {} + kyc_type: + type: string + user_info: + $ref: '#/definitions/service_user.UserInfoData' + type: object service_event.CheckinQueryResponse: properties: checkin_at: @@ -487,6 +497,65 @@ paths: summary: Exchange Code for Token tags: - Authentication + /event/attendance: + get: + description: Retrieves the list of attendees, including user info and decrypted + KYC data for a specified event. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + - description: latest + in: header + name: X-Api-Version + required: true + type: string + produces: + - application/json + responses: + "200": + description: Successful retrieval + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + items: + $ref: '#/definitions/service_event.AttendanceListResponse' + type: array + type: object + "400": + description: Invalid Input + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + summary: Get Attendance List + tags: + - Event /event/checkin: get: consumes: diff --git a/service/service_event/attendance_list.go b/service/service_event/attendance_list.go new file mode 100644 index 0000000..482d614 --- /dev/null +++ b/service/service_event/attendance_list.go @@ -0,0 +1,201 @@ +package service_event + +import ( + "context" + "encoding/json" + "nixcn-cms/data" + "nixcn-cms/internal/cryptography" + "nixcn-cms/internal/exception" + "nixcn-cms/internal/kyc" + "nixcn-cms/service/service_user" + "nixcn-cms/service/shared" + + "github.com/google/uuid" + "github.com/spf13/viper" +) + +type AttendanceListData struct { + EventId uuid.UUID `json:"event_id" form:"event_id"` +} + +type AttendanceListPayload struct { + Context context.Context + Data *AttendanceListData +} + +type AttendanceListResponse struct { + AttendanceId string `json:"attendance_id"` + UserInfo service_user.UserInfoData `json:"user_info"` + KycType string `json:"kyc_type"` + KycInfo any `json:"kyc_info"` +} + +type AttendanceListResult struct { + Common shared.CommonResult + Data []AttendanceListResponse +} + +func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (result *AttendanceListResult) { + attList, err := new(data.Attendance).GetAttendanceListByEventId(payload.Context, payload.Data.EventId) + + if err != nil { + exc := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceAttendanceList). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventAttendanceListError). + SetError(err). + Throw(payload.Context) + + result = &AttendanceListResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exc, + }, + } + return + } + + responseList := make([]AttendanceListResponse, 0) + kycRepo := new(data.Kyc) + userRepo := new(data.User) + + for _, item := range *attList { + var userInfo service_user.UserInfoData + var kycType string + var kycInfo any + + userData, err := userRepo.GetByUserId(payload.Context, &item.UserId) + if err != nil { + exc := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceAttendanceList). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context) + + result = &AttendanceListResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exc, + }, + } + return + } + + if userData != nil { + userInfo = service_user.UserInfoData{ + UserId: userData.UserId, + Email: userData.Email, + Username: &userData.Username, + Nickname: &userData.Nickname, + Subtitle: &userData.Subtitle, + Avatar: &userData.Avatar, + Bio: &userData.Bio, + PermissionLevel: &userData.PermissionLevel, + AllowPublic: &userData.AllowPublic, + } + } + + if item.KycId != uuid.Nil { + kycData, err := kycRepo.GetByKycId(payload.Context, &item.KycId) + + 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 := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceAttendanceList). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventAttendanceKycInfoDecryptFailed). + SetError(err). + Throw(payload.Context) + + result = &AttendanceListResult{ + Common: shared.CommonResult{HttpCode: 500, Exception: exc}, + } + return + } + + // JSON Unmarshal + switch kycType { + case "cnrid": + var kycDetail kyc.CNRidInfo + if err := json.Unmarshal(decodedKycInfo, &kycDetail); err != nil { + exc := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceAttendanceList). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorJsonDecodeFailed). + SetError(err). + Throw(payload.Context) + + result = &AttendanceListResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exc, + }, + } + return + } + kycInfo = kycDetail + + case "passport": + var kycDetail kyc.PassportResp + if err := json.Unmarshal(decodedKycInfo, &kycDetail); err != nil { + exc := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceAttendanceList). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorJsonDecodeFailed). + SetError(err). + Throw(payload.Context) + + result = &AttendanceListResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exc, + }, + } + return + } + kycInfo = kycDetail + + default: + } + } + } + + responseList = append(responseList, AttendanceListResponse{ + AttendanceId: item.AttendanceId.String(), + UserInfo: userInfo, + KycType: kycType, + KycInfo: kycInfo, + }) + } + + result = &AttendanceListResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceList). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: responseList, + } + + return +} diff --git a/service/service_event/checkin.go b/service/service_event/checkin.go index fca2816..913691c 100644 --- a/service/service_event/checkin.go +++ b/service/service_event/checkin.go @@ -31,7 +31,7 @@ type CheckinResult struct { func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) { attendandeData, err := new(data.Attendance). - GetAttendanceByEventIdAndUserId(payload.Context, payload.Data.EventId, payload.UserId) + GetAttendance(payload.Context, payload.Data.EventId, payload.UserId) if err != nil { result = &CheckinResult{ Common: shared.CommonResult{ diff --git a/service/service_event/join_event.go b/service/service_event/join_event.go index 334ec29..77d8b72 100644 --- a/service/service_event/join_event.go +++ b/service/service_event/join_event.go @@ -117,7 +117,7 @@ func (self *EventServiceImpl) JoinEvent(payload *EventJoinPayload) (result *Even return } - attendenceSearch, err := new(data.Attendance).GetAttendanceByEventIdAndUserId(payload.Context, eventId, userId) + attendenceSearch, err := new(data.Attendance).GetAttendance(payload.Context, eventId, userId) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return &EventJoinResult{ diff --git a/service/service_event/service.go b/service/service_event/service.go index ffe86a1..2a8f38c 100644 --- a/service/service_event/service.go +++ b/service/service_event/service.go @@ -7,6 +7,7 @@ type EventService interface { GetEventInfo(*EventInfoPayload) *EventInfoResult ListEvents(*EventListPayload) *EventListResult JoinEvent(*EventJoinPayload) *EventJoinResult + AttendanceList(*AttendanceListPayload) *AttendanceListResult } type EventServiceImpl struct{} diff --git a/service/service_user/get_user_info.go b/service/service_user/get_user_info.go index 61a62a6..71a0623 100644 --- a/service/service_user/get_user_info.go +++ b/service/service_user/get_user_info.go @@ -17,7 +17,7 @@ type UserInfoData struct { Subtitle *string `json:"subtitle"` Avatar *string `json:"avatar"` Bio *string `json:"bio"` - PermissionLevel uint `json:"permission_level"` + PermissionLevel *uint `json:"permission_level"` AllowPublic *bool `json:"allow_public"` } @@ -109,7 +109,7 @@ func (self *UserServiceImpl) GetUserInfo(payload *UserInfoPayload) (result *User Subtitle: &userData.Subtitle, Avatar: &userData.Avatar, Bio: &userData.Bio, - PermissionLevel: userData.PermissionLevel, + PermissionLevel: &userData.PermissionLevel, AllowPublic: &userData.AllowPublic, }, } diff --git a/service/service_user/update_user_info.go b/service/service_user/update_user_info.go index 80fdbd1..3e4b19c 100644 --- a/service/service_user/update_user_info.go +++ b/service/service_user/update_user_info.go @@ -163,7 +163,35 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U return } - err = new(data.User).UpdateByUserID(payload.Context, &payload.UserId, updates) + userData := new(data.User) + + userInfo, err := userData.GetByUserId(payload.Context, &payload.UserId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceUser). + SetEndpoint(exception.EndpointUserServiceUpdate). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context) + + result = &UserInfoResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + if payload.Data.PermissionLevel != nil && userInfo.PermissionLevel >= 50 { + updates["permission_level"] = *payload.Data.PermissionLevel + } + + err = userData.UpdateByUserID(payload.Context, &payload.UserId, updates) if err != nil { exception := new(exception.Builder). SetStatus(exception.StatusServer).