Add attendance_list in service_event, add set user permission in user
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished

update

Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
2026-02-02 21:31:33 +08:00
parent 9c945d69a9
commit 99424ee55f
15 changed files with 733 additions and 30 deletions

59
api/event/attendance.go Normal file
View File

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

View File

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

View File

@@ -14,6 +14,7 @@ endpoint:
checkin_submit: "04"
list: "05"
join: "06"
attendance_list: "07"
user:
service:
info: "01"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ type EventService interface {
GetEventInfo(*EventInfoPayload) *EventInfoResult
ListEvents(*EventListPayload) *EventListResult
JoinEvent(*EventJoinPayload) *EventJoinResult
AttendanceList(*AttendanceListPayload) *AttendanceListResult
}
type EventServiceImpl struct{}

View File

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

View File

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