diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..c2c2f54 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,27 @@ +name: golangci-lint +on: + push: + branches: + - main + - master + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + args: --timeout 3m --verbose diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d79c6ee --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,12 @@ +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gocyclo + - gofmt + - goimports + fast: true diff --git a/cmd/import_from_jwc/import_from_jwc.go b/cmd/import_from_jwc/import_from_jwc.go index a62843f..3374424 100644 --- a/cmd/import_from_jwc/import_from_jwc.go +++ b/cmd/import_from_jwc/import_from_jwc.go @@ -11,6 +11,7 @@ import ( pinyin2 "github.com/mozillazg/go-pinyin" "gorm.io/gorm" "gorm.io/gorm/clause" + "jcourse_go/dal" "jcourse_go/model/po" ) @@ -39,7 +40,9 @@ func initDB() { func readRawCSV(filename string) [][]string { fs, err := os.Open(filename) - defer fs.Close() + defer func(fs *os.File) { + _ = fs.Close() + }(fs) if err != nil { panic(err) } @@ -229,7 +232,6 @@ func queryAllTeacher() { teacherKeyMap[makeTeacherKey(teacher.Code)] = teacher teacherIDMap[teacher.ID] = teacher } - return } func parseCourseFromLine(line []string) po.CoursePO { @@ -263,7 +265,6 @@ func queryAllCourse() { courseKeyMap[makeCourseKey(course.Code, course.MainTeacherName)] = course courseIDMap[course.ID] = course } - return } func parseOfferedCourseFromLine(line []string) po.OfferedCoursePO { @@ -297,7 +298,6 @@ func queryAllOfferedCourse() { offeredCourseIDMap[offeredCourse.ID] = offeredCourse offeredCourseKeyMap[makeOfferedCourseKey(offeredCourse.CourseID, offeredCourse.Semester)] = offeredCourse } - return } func parseOfferedCourseTeacherGroup(line []string) []po.OfferedCourseTeacherPO { @@ -329,7 +329,6 @@ func queryAllOfferedCourseTeacherGroup() { for _, offeredCourseTeacher := range offeredCourseTeachers { offeredCourseTeacherKeyMap[makeOfferedCourseTeacherKey(offeredCourseTeacher.OfferedCourseID, offeredCourseTeacher.TeacherID)] = offeredCourseTeacher } - return } func parseCourseCategories(line []string) []po.CourseCategoryPO { @@ -364,7 +363,6 @@ func queryAllCourseCategory() { } courseCategoryMap[makeCourseCategoryKey(int64(course.ID), courseCategory.Category)] = courseCategory } - return } func generatePinyin(name string) string { diff --git a/cmd/migrate/migrate.go b/cmd/migrate/migrate.go index 2dfaccf..dcdaee0 100644 --- a/cmd/migrate/migrate.go +++ b/cmd/migrate/migrate.go @@ -1,10 +1,10 @@ package main import ( + "github.com/joho/godotenv" + "jcourse_go/dal" "jcourse_go/model/po" - - "github.com/joho/godotenv" ) func main() { diff --git a/dal/sql.go b/dal/sql.go index 6522b4c..8c008a4 100644 --- a/dal/sql.go +++ b/dal/sql.go @@ -5,13 +5,14 @@ import ( "log" "os" + "jcourse_go/util" + "github.com/DATA-DOG/go-sqlmock" _ "github.com/lib/pq" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" - "jcourse_go/util" ) var dbClient *gorm.DB diff --git a/handler/admin.go b/handler/admin.go new file mode 100644 index 0000000..e5b4970 --- /dev/null +++ b/handler/admin.go @@ -0,0 +1,35 @@ +package handler + +import ( + "jcourse_go/model/domain" + "jcourse_go/model/dto" + "jcourse_go/service" + "net/http" + + "github.com/gin-gonic/gin" +) + +func AdminGetUserList(c *gin.Context) { + var request dto.UserListRequest + if err := c.ShouldBindQuery(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + + filter := domain.UserFilter{ + Page: request.Page, + PageSize: request.PageSize, + } + users, err := service.AdminGetUserList(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + } + total, _ := service.GetUserCount(c, filter) + response := dto.UserListResponseForAdmin{ + Page: request.Page, + PageSize: request.PageSize, + Total: total, + Data: users, + } + c.JSON(http.StatusOK, response) +} diff --git a/handler/auth.go b/handler/auth.go index 07c3659..8bc4702 100644 --- a/handler/auth.go +++ b/handler/auth.go @@ -4,13 +4,13 @@ import ( "errors" "net/http" + "github.com/gin-gonic/contrib/sessions" + "github.com/gin-gonic/gin" + "jcourse_go/constant" "jcourse_go/model/domain" "jcourse_go/model/dto" "jcourse_go/service" - - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" ) func LoginHandler(c *gin.Context) { diff --git a/handler/course.go b/handler/course.go index 53f8d17..96bc33b 100644 --- a/handler/course.go +++ b/handler/course.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "jcourse_go/constant" "jcourse_go/model/converter" "jcourse_go/model/domain" @@ -85,6 +86,10 @@ func GetCourseListHandler(c *gin.Context) { return } total, err := service.GetCourseCount(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + return + } resp := dto.CourseListResponse{ Total: total, diff --git a/handler/review.go b/handler/review.go index e559a5f..fe2c7d8 100644 --- a/handler/review.go +++ b/handler/review.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "jcourse_go/middleware" "jcourse_go/model/converter" "jcourse_go/model/domain" @@ -48,6 +49,10 @@ func GetReviewListHandler(c *gin.Context) { return } total, err := service.GetReviewCount(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + return + } response := dto.ReviewListResponse{ Page: request.Page, @@ -96,7 +101,7 @@ func UpdateReviewHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, dto.UpdateReviewResponse{ReviewID: request.ReviewID}) + c.JSON(http.StatusOK, dto.UpdateReviewResponse{ReviewID: request.ReviewID}) // nolint: gosimple } func DeleteReviewHandler(c *gin.Context) { @@ -110,7 +115,8 @@ func DeleteReviewHandler(c *gin.Context) { c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) return } - c.JSON(http.StatusOK, dto.DeleteReviewResponse{ReviewID: request.ReviewID}) + + c.JSON(http.StatusOK, dto.DeleteReviewResponse{ReviewID: request.ReviewID}) // nolint: gosimple } func GetReviewListForCourseHandler(c *gin.Context) {} diff --git a/handler/user.go b/handler/user.go index 5360778..30a82a2 100644 --- a/handler/user.go +++ b/handler/user.go @@ -1,15 +1,199 @@ package handler -import "github.com/gin-gonic/gin" +import ( + "errors" + "jcourse_go/constant" + "jcourse_go/model/converter" + "jcourse_go/model/domain" + "jcourse_go/model/dto" + "jcourse_go/service" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) func GetSuggestedUserHandler(c *gin.Context) {} -func GetUserListHandler(c *gin.Context) {} +func GetUserListHandler(c *gin.Context) { + var request dto.UserListRequest + if err := c.ShouldBindQuery(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + + filter := domain.UserFilter{ + Page: request.Page, + PageSize: request.PageSize, + } + users, err := service.GetUserList(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + } + total, _ := service.GetUserCount(c, filter) + response := dto.UserListResponse{ + Page: request.Page, + PageSize: request.PageSize, + Total: total, + Data: users, + } + c.JSON(http.StatusOK, response) +} + +func getUserIDFromRequest(c *gin.Context) (int64, error) { + userIDStr := c.Param("userID") + userID, err := strconv.Atoi(userIDStr) + if err != nil { + return -1, errors.New("非法用户ID") + } + return int64(userID), nil +} + +// 非公开信息? +func GetUserSummaryHandler(c *gin.Context) { + userID, err := getUserIDFromRequest(c) + if err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "非法用户ID"}) + return + } + + // UserSummary鉴权 + userInterface, exists := c.Get(constant.CtxKeyUser) + if !exists { + c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "用户未登录!"}) + return + } + user, _ := userInterface.(*domain.User) + + if user.ID != userID { + c.JSON(http.StatusForbidden, dto.BaseResponse{Message: "无权查看他人信息!"}) + return + } + + userSummary, err := service.GetUserSummaryByID(c, user.ID) + if err != nil { + c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "此用户不存在!"}) + return + } + c.JSON(http.StatusOK, userSummary) +} + +// 公开信息 +func GetUserDetailHandler(c *gin.Context) { + userID, err := getUserIDFromRequest(c) + if err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "非法用户ID"}) + return + } + + userDomain, err := service.GetUserDomainByID(c, userID) + if err != nil { + c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "此用户不存在!"}) + return + } + + userDetail := converter.ConvertUserDomainToUserDetailDTO(userDomain) + c.JSON(http.StatusOK, userDetail) +} + +// 非公开信息 +func GetUserProfileHandler(c *gin.Context) { + userID, err := getUserIDFromRequest(c) + if err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "非法用户ID"}) + return + } + + // UserProfile鉴权 + userInterface, exists := c.Get(constant.CtxKeyUser) + if !exists { + c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "用户未登录!"}) + return + } + user, _ := userInterface.(*domain.User) -func GetCurrentUserHandler(c *gin.Context) {} + if user.ID != userID { + c.JSON(http.StatusForbidden, dto.BaseResponse{Message: "无权查看他人信息!"}) + return + } -func GetUserDetailHandler(c *gin.Context) {} + userDomain, err := service.GetUserDomainByID(c, user.ID) + if err != nil { + c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "此用户不存在!"}) + return + } + + userProfile := converter.ConvertUserDomainToUserProfileDTO(userDomain) + c.JSON(http.StatusOK, userProfile) +} func WatchUserHandler(c *gin.Context) {} func UnWatchUserHandler(c *gin.Context) {} + +func UpdateUserProfileHandler(c *gin.Context) { + userInterface, exists := c.Get(constant.CtxKeyUser) + if !exists { + c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "用户未登录!"}) + return + } + user, _ := userInterface.(*domain.User) + + var request dto.UserProfileDTO + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + + if user.ID != request.UserID { + c.JSON(http.StatusForbidden, dto.BaseResponse{Message: "无权更新其他用户信息!"}) + return + } + + err := service.UpdateUserProfileByID(c, &request) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "用户信息更新失败。"}) + return + } + c.JSON(http.StatusOK, dto.BaseResponse{Message: "用户信息更新成功。"}) +} + +func GetUserReviewsHandler(c *gin.Context) { + userID, err := getUserIDFromRequest(c) + if err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "非法用户ID"}) + return + } + + var request dto.ReviewListRequest + if err := c.ShouldBind(&request); err != nil { + c.JSON(http.StatusBadRequest, dto.BaseResponse{Message: "参数错误"}) + return + } + + filter := domain.ReviewFilter{ + Page: request.Page, + PageSize: request.PageSize, + UserID: userID, + } + + reviews, err := service.GetReviewList(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + return + } + + total, err := service.GetReviewCount(c, filter) + if err != nil { + c.JSON(http.StatusInternalServerError, dto.BaseResponse{Message: "内部错误。"}) + return + } + + response := dto.ReviewListResponse{ + Page: request.Page, + PageSize: request.PageSize, + Total: total, + Data: converter.ConvertReviewDomainToListDTO(reviews, true), + } + c.JSON(http.StatusOK, response) +} diff --git a/main.go b/main.go index c6a1f97..1610bcd 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,11 @@ package main import ( - "github.com/gin-gonic/gin" - "github.com/joho/godotenv" "jcourse_go/dal" "jcourse_go/rpc" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" ) func Init() { diff --git a/middleware/auth.go b/middleware/auth.go index 57382c3..f832853 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" + "jcourse_go/constant" "jcourse_go/model/domain" "jcourse_go/model/dto" diff --git a/middleware/csrf.go b/middleware/csrf.go index 9715256..eaa99b1 100644 --- a/middleware/csrf.go +++ b/middleware/csrf.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/csrf" adapter "github.com/gwatts/gin-adapter" + "jcourse_go/constant" "jcourse_go/model/dto" "jcourse_go/util" diff --git a/middleware/session.go b/middleware/session.go index 535b29e..0e26597 100644 --- a/middleware/session.go +++ b/middleware/session.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" + "jcourse_go/constant" "jcourse_go/dal" "jcourse_go/model/domain" diff --git a/model/converter/review.go b/model/converter/review.go index 0013bc4..d7772fa 100644 --- a/model/converter/review.go +++ b/model/converter/review.go @@ -12,7 +12,7 @@ func ConvertReviewPOToDomain(review po.ReviewPO) domain.Review { Course: domain.Course{ID: review.CourseID}, User: domain.User{ID: review.UserID}, Semester: review.Semester, - Rate: review.Rating, + Rating: review.Rating, IsAnonymous: review.IsAnonymous, Comment: review.Comment, CreatedAt: review.CreatedAt, @@ -39,7 +39,7 @@ func ConvertReviewDomainToDTO(review domain.Review, hideUser bool) dto.ReviewDTO ID: review.ID, Course: ConvertCourseDomainToListDTO(review.Course), Semester: review.Semester, - Rating: review.Rate, + Rating: review.Rating, IsAnonymous: review.IsAnonymous, Comment: review.Comment, UpdatedAt: review.UpdatedAt, diff --git a/model/converter/user.go b/model/converter/user.go index f16f5dc..eea9dab 100644 --- a/model/converter/user.go +++ b/model/converter/user.go @@ -4,6 +4,7 @@ import ( "jcourse_go/model/domain" "jcourse_go/model/dto" "jcourse_go/model/po" + "time" ) func ConvertUserPOToDomain(userPO po.UserPO) domain.User { @@ -26,6 +27,7 @@ func ConvertUserProfilePOToDomain(userProfile po.UserProfilePO) domain.UserProfi Major: userProfile.Major, Degree: userProfile.Degree, Grade: userProfile.Grade, + Bio: userProfile.Bio, } } @@ -44,3 +46,74 @@ func ConvertUserDomainToReviewDTO(user domain.User) dto.UserInReviewDTO { Avatar: user.Profile.Avatar, } } + +func ConvertUserDomainToUserSummaryDTO(id int64, reviewCount int64, likeReceive int64, tipReceive int64, followingCourseCount int64) *dto.UserSummaryDTO { + return &dto.UserSummaryDTO{ + ID: id, + ReviewCount: reviewCount, + LikeReceive: likeReceive, + TipReceive: tipReceive, + FollowingCourseCount: followingCourseCount, + } +} + +func ConvertUserDomainToUserDetailDTO(userDomain *domain.User) *dto.UserDetailDTO { + if userDomain == nil { + return nil + } + return &dto.UserDetailDTO{ + ID: userDomain.ID, + Username: userDomain.Username, + Avatar: userDomain.Profile.Avatar, + Bio: userDomain.Profile.Bio, + } +} + +func ConvertUserDomainToUserProfileDTO(userDomain *domain.User) *dto.UserProfileDTO { + if userDomain == nil { + return nil + } + return &dto.UserProfileDTO{ + UserID: userDomain.ID, + Username: userDomain.Username, + Bio: userDomain.Profile.Bio, + Email: userDomain.Email, + Avatar: userDomain.Profile.Avatar, + Role: userDomain.Role, + Department: userDomain.Profile.Department, + Major: userDomain.Profile.Major, + Grade: userDomain.Profile.Grade, + } +} + +func ConvertUpdateUserProfileDTOToUserPO(userProfileDTO *dto.UserProfileDTO, userPO *po.UserPO) po.UserPO { + updatedUserPO := po.UserPO{ + Username: userProfileDTO.Username, + Email: userPO.Email, + Password: userPO.Password, + UserRole: userPO.UserRole, + LastSeenAt: time.Now(), + } + if userProfileDTO.UserID != 0 { + updatedUserPO.ID = uint(userProfileDTO.UserID) + } + return updatedUserPO +} + +func ConvertUpdateUserProfileDTOToUsrProfilePO(userProfileDTO *dto.UserProfileDTO, userProfilePO *po.UserProfilePO) po.UserProfilePO { + // 保留一些immutable的属性 + updatedUserProfilePO := po.UserProfilePO{ + UserID: userProfilePO.UserID, + Avatar: userProfileDTO.Avatar, + Department: userProfileDTO.Department, + Type: userProfilePO.Type, + Major: userProfileDTO.Major, + Degree: userProfilePO.Degree, + Grade: userProfileDTO.Grade, + Bio: userProfileDTO.Bio, + } + if userProfileDTO.UserID != 0 { + updatedUserProfilePO.ID = uint(userProfileDTO.UserID) + } + return updatedUserProfilePO +} diff --git a/model/domain/review.go b/model/domain/review.go index bfa24e1..dfa7822 100644 --- a/model/domain/review.go +++ b/model/domain/review.go @@ -16,7 +16,7 @@ type Review struct { Course Course User User Comment string - Rate int64 + Rating int64 Semester string IsAnonymous bool CreatedAt time.Time diff --git a/model/domain/user.go b/model/domain/user.go index 1cc3c1f..102c2ed 100644 --- a/model/domain/user.go +++ b/model/domain/user.go @@ -35,4 +35,11 @@ type UserProfile struct { Major string Degree string Grade string + Bio string +} + +type UserFilter struct { + Page int64 + PageSize int64 + // To be continued ... (add more fields) } diff --git a/model/dto/user.go b/model/dto/user.go new file mode 100644 index 0000000..257b704 --- /dev/null +++ b/model/dto/user.go @@ -0,0 +1,41 @@ +package dto + +type UserRole = string + +type UserType = string + +type UserListRequest struct { + Page int64 `json:"page" form:"page"` + PageSize int64 `json:"page_size" form:"page_size"` +} + +type UserListResponse = BasePaginateResponse[UserDetailDTO] + +type UserListResponseForAdmin = BasePaginateResponse[UserProfileDTO] + +type UserSummaryDTO struct { + ID int64 `json:"id"` + ReviewCount int64 `json:"review_count"` + LikeReceive int64 `json:"like_receive"` + TipReceive int64 `json:"tip_receive"` + FollowingCourseCount int64 `json:"following_course_count"` +} + +type UserDetailDTO struct { + ID int64 `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Bio string `json:"bio"` +} + +type UserProfileDTO struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` + Bio string `json:"bio"` + Email string `json:"email"` + Avatar string `json:"avatar"` + Role string `json:"user_role"` + Department string `json:"department"` + Major string `json:"major"` + Grade string `json:"grade"` +} diff --git a/model/po/user.go b/model/po/user.go index 02b8610..8301320 100644 --- a/model/po/user.go +++ b/model/po/user.go @@ -28,8 +28,19 @@ type UserProfilePO struct { Major string Degree string Grade string + Bio string } func (profile *UserProfilePO) TableName() string { return "user_profiles" } + +type UserActivityPO struct { + gorm.Model + UserID int64 // 用户ID + ActivityType string // 活动类型,如发布课程点评、点赞、回复、关注/屏蔽用户/课程等。 + TargetID string // 活动对象ID + CreatedAt time.Time // 活动发生时间 +} + +func (userActivity *UserActivityPO) TableName() string { return "user_activities" } diff --git a/repository/auth_test.go b/repository/auth_test.go index b42306f..7a94756 100644 --- a/repository/auth_test.go +++ b/repository/auth_test.go @@ -7,6 +7,7 @@ import ( "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" + "jcourse_go/dal" ) diff --git a/repository/course.go b/repository/course.go index 95128f2..76b5567 100644 --- a/repository/course.go +++ b/repository/course.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" + "gorm.io/gorm" + "jcourse_go/dal" "jcourse_go/model/po" - - "gorm.io/gorm" ) type IBaseCourseQuery interface { diff --git a/repository/review.go b/repository/review.go index 338d59e..fa5034f 100644 --- a/repository/review.go +++ b/repository/review.go @@ -4,6 +4,7 @@ import ( "context" "gorm.io/gorm" + "jcourse_go/dal" "jcourse_go/model/po" ) diff --git a/repository/teacher.go b/repository/teacher.go index 9b953d9..614df6d 100644 --- a/repository/teacher.go +++ b/repository/teacher.go @@ -4,10 +4,10 @@ import ( "context" "errors" + "gorm.io/gorm" + "jcourse_go/dal" "jcourse_go/model/po" - - "gorm.io/gorm" ) // TODO: 暂时没有添加从ProfileDesc中搜索的功能 diff --git a/repository/user.go b/repository/user.go index b4a30d3..aa9caee 100644 --- a/repository/user.go +++ b/repository/user.go @@ -6,6 +6,7 @@ import ( "time" "gorm.io/gorm" + "jcourse_go/constant" "jcourse_go/dal" "jcourse_go/model/po" @@ -16,16 +17,29 @@ type DBOption func(*gorm.DB) *gorm.DB type IUserQuery interface { GetUserDetail(ctx context.Context, opts ...DBOption) (*po.UserPO, error) GetUserList(ctx context.Context, opts ...DBOption) ([]po.UserPO, error) + GetUserCount(ctx context.Context, opts ...DBOption) (int64, error) + GetUserByID(ctx context.Context, userID int64) (*po.UserPO, error) GetUserByIDs(ctx context.Context, userIDs []int64) (map[int64]po.UserPO, error) + UpdateUserByID(ctx context.Context, user *po.UserPO) error WithID(id int64) DBOption WithEmail(email string) DBOption WithPassword(password string) DBOption CreateUser(ctx context.Context, email string, password string) (*po.UserPO, error) ResetUserPassword(ctx context.Context, userID int64, password string) error + WithLimit(limit int64) DBOption + WithOffset(offset int64) DBOption } type IUserProfileQuery interface { GetUserProfileByIDs(ctx context.Context, userIDs []int64) (map[int64]po.UserProfilePO, error) + GetUserProfileByID(ctx context.Context, userID int64) (*po.UserProfilePO, error) + GetUserProfileList(ctx context.Context, opts ...DBOption) ([]po.UserProfilePO, error) + GetUserProfileCount(ctx context.Context, opts ...DBOption) (int64, error) + UpdateUserProfileByID(ctx context.Context, userProfile *po.UserProfilePO) error + WithUserID(id int64) DBOption + WithLimit(limit int64) DBOption + WithOffset(offset int64) DBOption + //CreateUserProfile(ctx context.Context, userID int64) (*po.UserProfilePO, error) } type UserProfileQuery struct { @@ -46,6 +60,16 @@ func (u *UserProfileQuery) GetUserProfileByIDs(ctx context.Context, userIDs []in return userProfileMap, nil } +func (u *UserProfileQuery) GetUserProfileByID(ctx context.Context, userID int64) (*po.UserProfilePO, error) { + db := u.optionDB(ctx, u.WithUserID(userID)) + userProfilePO := po.UserProfilePO{} + result := db.Find(&userProfilePO) + if result.Error != nil { + return &userProfilePO, result.Error + } + return &userProfilePO, nil +} + func (u *UserProfileQuery) optionDB(ctx context.Context, opts ...DBOption) *gorm.DB { db := u.db.WithContext(ctx).Model(&po.UserProfilePO{}) for _, opt := range opts { @@ -60,6 +84,22 @@ func (u *UserProfileQuery) WithUserIDs(userIDs []int64) DBOption { } } +func (q *UserProfileQuery) WithUserID(id int64) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("user_id = ?", id) + } +} + +func (q *UserProfileQuery) WithLimit(limit int64) DBOption { + return func(db *gorm.DB) *gorm.DB { return db.Limit(int(limit)) } +} + +func (q *UserProfileQuery) WithOffset(offset int64) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Offset(int(offset)) + } +} + func NewUserProfileQuery() IUserProfileQuery { return &UserProfileQuery{db: dal.GetDBClient()} } @@ -70,6 +110,31 @@ func NewUserQuery() IUserQuery { } } +func (q *UserProfileQuery) GetUserProfileList(ctx context.Context, opts ...DBOption) ([]po.UserProfilePO, error) { + db := q.optionDB(ctx, opts...) + userProfilePOs := make([]po.UserProfilePO, 0) + result := db.Find(&userProfilePOs) + if result.Error != nil { + return userProfilePOs, result.Error + } + return userProfilePOs, nil +} + +func (q *UserProfileQuery) GetUserProfileCount(ctx context.Context, opts ...DBOption) (int64, error) { + db := q.optionDB(ctx, opts...) + count := int64(0) + result := db.Count(&count) + if result.Error != nil { + return 0, result.Error + } + return count, nil +} + +func (q *UserProfileQuery) UpdateUserProfileByID(ctx context.Context, userProfile *po.UserProfilePO) error { + result := q.optionDB(ctx, q.WithUserID(userProfile.UserID)).Save(&userProfile).Error + return result +} + type UserQuery struct { db *gorm.DB } @@ -88,6 +153,16 @@ func (q *UserQuery) GetUserByIDs(ctx context.Context, userIDs []int64) (map[int6 return userMap, nil } +func (q *UserQuery) GetUserByID(ctx context.Context, userID int64) (*po.UserPO, error) { + db := q.optionDB(ctx, q.WithID(userID)) + userPO := po.UserPO{} + result := db.Find(&userPO) + if result.Error != nil { + return &userPO, result.Error + } + return &userPO, nil +} + func (q *UserQuery) WithEmail(email string) DBOption { return func(db *gorm.DB) *gorm.DB { return db.Where("email = ?", email) @@ -137,6 +212,21 @@ func (q *UserQuery) GetUserList(ctx context.Context, opts ...DBOption) ([]po.Use return userPOs, nil } +func (q *UserQuery) GetUserCount(ctx context.Context, opts ...DBOption) (int64, error) { + db := q.optionDB(ctx, opts...) + count := int64(0) + result := db.Count(&count) + if result.Error != nil { + return 0, result.Error + } + return count, nil +} + +func (q *UserQuery) UpdateUserByID(ctx context.Context, user *po.UserPO) error { + result := q.optionDB(ctx, q.WithID(int64(user.ID))).Save(&user).Error + return result +} + func (q *UserQuery) CreateUser(ctx context.Context, email string, passwordStore string) (*po.UserPO, error) { user := po.UserPO{ Username: email, @@ -156,3 +246,13 @@ func (q *UserQuery) ResetUserPassword(ctx context.Context, userID int64, passwor result := q.optionDB(ctx, q.WithID(userID)).Debug().Update("password", passwordStore) return result.Error } + +func (q *UserQuery) WithLimit(limit int64) DBOption { + return func(db *gorm.DB) *gorm.DB { return db.Limit(int(limit)) } +} + +func (q *UserQuery) WithOffset(offset int64) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Offset(int(offset)) + } +} diff --git a/router.go b/router.go index 189da0a..33b920c 100644 --- a/router.go +++ b/router.go @@ -1,9 +1,10 @@ package main import ( - "github.com/gin-gonic/gin" "jcourse_go/handler" "jcourse_go/middleware" + + "github.com/gin-gonic/gin" ) func registerRouter(r *gin.Engine) { @@ -50,12 +51,17 @@ func registerRouter(r *gin.Engine) { userGroup := needAuthGroup.Group("/user") userGroup.GET("", handler.GetUserListHandler) userGroup.GET("/suggest", handler.GetSuggestedUserHandler) - userGroup.GET("/me", handler.GetCurrentUserHandler) - userGroup.GET("/:userID", handler.GetUserDetailHandler) + userGroup.GET("/:userID/summary", handler.GetUserSummaryHandler) + userGroup.GET("/:userID/detail", handler.GetUserDetailHandler) + userGroup.GET("/:userID/reviews", handler.GetUserReviewsHandler) + userGroup.POST("/:userID/watch", handler.WatchUserHandler) userGroup.POST("/:userID/unwatch", handler.UnWatchUserHandler) + userGroup.GET("/:userID/profile", handler.GetUserProfileHandler) + userGroup.PUT("/:userID/profile", handler.UpdateUserProfileHandler) adminGroup := needAuthGroup.Group("/admin") adminGroup.Use(middleware.RequireAdmin()) + adminGroup.GET("/user", handler.AdminGetUserList) adminGroup.GET("") } diff --git a/rpc/smtp.go b/rpc/smtp.go index fbb4742..83f51ab 100644 --- a/rpc/smtp.go +++ b/rpc/smtp.go @@ -7,6 +7,7 @@ import ( "strconv" "gopkg.in/gomail.v2" + "jcourse_go/util" ) diff --git a/service/auth.go b/service/auth.go index a0293ca..b7dc924 100644 --- a/service/auth.go +++ b/service/auth.go @@ -9,6 +9,7 @@ import ( "regexp" "github.com/SJTU-jCourse/password_hasher" + "jcourse_go/constant" "jcourse_go/model/converter" "jcourse_go/model/domain" @@ -109,7 +110,7 @@ func SendRegisterCodeEmail(ctx context.Context, email string) error { if err != nil { return err } - body := fmt.Sprintf(constant.EmailBodyVerifyCode, code) + body := fmt.Sprintf(constant.EmailBodyVerifyCode, code) // nolint: gosimple err = repository.StoreVerifyCode(ctx, email, code) if err != nil { return err @@ -125,7 +126,8 @@ func SendRegisterCodeEmail(ctx context.Context, email string) error { func ValidateEmail(email string) bool { // 1. validate basic email format regex := regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`) - if !regex.MatchString(email) { + + if !regex.MatchString(email) { // nolint: gosimple return false } diff --git a/service/auth_test.go b/service/auth_test.go index 19b556e..0d5665a 100644 --- a/service/auth_test.go +++ b/service/auth_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "jcourse_go/constant" ) diff --git a/service/user.go b/service/user.go index fb21f77..1e2272b 100644 --- a/service/user.go +++ b/service/user.go @@ -2,12 +2,28 @@ package service import ( "context" + "jcourse_go/model/dto" + "jcourse_go/model/po" + "jcourse_go/util" "jcourse_go/model/converter" "jcourse_go/model/domain" "jcourse_go/repository" ) +func GetUserSummaryByID(ctx context.Context, userID int64) (*dto.UserSummaryDTO, error) { + filter := domain.ReviewFilter{ + UserID: userID, + } + + total, _ := GetReviewCount(ctx, filter) + // 过滤非匿名点评 + + // 获取用户收到的赞数、被打赏积分数、关注的课程数 + + return converter.ConvertUserDomainToUserSummaryDTO(userID, total, 0, 0, 0), nil +} + func GetUserByIDs(ctx context.Context, userIDs []int64) (map[int64]domain.User, error) { result := make(map[int64]domain.User) if len(userIDs) == 0 { @@ -36,3 +52,108 @@ func GetUserByIDs(ctx context.Context, userIDs []int64) (map[int64]domain.User, } return result, nil } + +// 共用函数,用于获取用户基本信息和详细资料并组装成domain.User +func GetUserDomainByID(ctx context.Context, userID int64) (*domain.User, error) { + userQuery := repository.NewUserQuery() + userPO, err := userQuery.GetUserByID(ctx, userID) + if err != nil { + return nil, err + } + + userProfileQuery := repository.NewUserProfileQuery() + userProfilePO, err := userProfileQuery.GetUserProfileByID(ctx, userID) + if err != nil { + return nil, err + } + + user := converter.ConvertUserPOToDomain(*userPO) + converter.PackUserWithProfile(&user, *userProfilePO) + + return &user, nil +} + +func buildUserDBOptionFromFilter(query repository.IUserQuery, filter domain.UserFilter) []repository.DBOption { + opts := make([]repository.DBOption, 0) + if filter.PageSize > 0 { + opts = append(opts, query.WithLimit(filter.PageSize)) + } + if filter.Page > 0 { + opts = append(opts, query.WithOffset(util.CalcOffset(filter.Page, filter.PageSize))) + } + return opts +} + +func GetUserList(ctx context.Context, filter domain.UserFilter) ([]dto.UserDetailDTO, error) { + userQuery := repository.NewUserQuery() + userProfileQuery := repository.NewUserProfileQuery() + opts := buildUserDBOptionFromFilter(userQuery, filter) + userPOs, err := userQuery.GetUserList(ctx, opts...) + if err != nil { + return nil, err + } + userProfilePOs, err := userProfileQuery.GetUserProfileList(ctx, opts...) + if err != nil { + return nil, err + } + result := make([]dto.UserDetailDTO, 0) + + userProfileMap := make(map[int]*po.UserProfilePO) + for _, userProfilePO := range userProfilePOs { + userProfileMap[int(userProfilePO.UserID)] = &userProfilePO + } + + for _, userPO := range userPOs { + userDetailDTO := dto.UserDetailDTO{ + ID: int64(userPO.ID), + Username: userPO.Username, + Avatar: "", + Bio: "", + } + if userProfilePO, exists := userProfileMap[int(userPO.ID)]; exists { + userDetailDTO.Avatar = userProfilePO.Avatar + userDetailDTO.Bio = userProfilePO.Bio + } + result = append(result, userDetailDTO) + } + return result, nil +} + +func AdminGetUserList(ctx context.Context, filter domain.UserFilter) ([]dto.UserProfileDTO, error) { + // 视前端而定获取用户的哪些信息 + // E.g. UserProfileDTO + return nil, nil +} + +func GetUserCount(ctx context.Context, filter domain.UserFilter) (int64, error) { + userQuery := repository.NewUserQuery() + filter.Page, filter.PageSize = 0, 0 + opts := buildUserDBOptionFromFilter(userQuery, filter) + return userQuery.GetUserCount(ctx, opts...) +} + +func UpdateUserProfileByID(ctx context.Context, userProfileDTO *dto.UserProfileDTO) error { + userQuery := repository.NewUserQuery() + oldUserPO, errQuery := userQuery.GetUserByID(ctx, userProfileDTO.UserID) + if errQuery != nil { + return errQuery + } + newUserPO := converter.ConvertUpdateUserProfileDTOToUserPO(userProfileDTO, oldUserPO) + + errUpdate := userQuery.UpdateUserByID(ctx, &newUserPO) + if errUpdate != nil { + return errUpdate + } + + userProfileQuery := repository.NewUserProfileQuery() + oldUserProfilePO, errQuery2 := userProfileQuery.GetUserProfileByID(ctx, userProfileDTO.UserID) + if errQuery2 != nil { + return errQuery2 + } + newUserProfilePO := converter.ConvertUpdateUserProfileDTOToUsrProfilePO(userProfileDTO, oldUserProfilePO) + errUpdate2 := userProfileQuery.UpdateUserProfileByID(ctx, &newUserProfilePO) + if errUpdate2 != nil { + return errUpdate2 + } + return nil +}