修改页面
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
)
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
userSvc *services.UserService
|
||||
@@ -104,12 +105,44 @@
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetPoints(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
user, err := h.userSvc.GetByID(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusNotFound, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{"points": user.Points})
|
||||
}
|
||||
func (h *UserHandler) GetPoints(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
user, err := h.userSvc.GetByID(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusNotFound, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{"points": user.Points})
|
||||
}
|
||||
|
||||
// ========== 排行榜 ==========
|
||||
|
||||
func (h *UserHandler) GetLeaderboard(c *gin.Context) {
|
||||
limitStr := c.DefaultQuery("limit", "20")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
entries, err := h.userSvc.GetLeaderboardByPoints(limit)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, entries)
|
||||
}
|
||||
|
||||
// ========== 搜索用户 ==========
|
||||
|
||||
func (h *UserHandler) SearchUsers(c *gin.Context) {
|
||||
q := c.Query("q")
|
||||
if q == "" {
|
||||
middleware.Success(c, []interface{}{})
|
||||
return
|
||||
}
|
||||
users, err := h.userSvc.SearchUsers(q, 20)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, users)
|
||||
}
|
||||
|
||||
@@ -57,8 +57,12 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
user.PUT("/profile", userH.UpdateProfile)
|
||||
user.DELETE("/account", userH.DeleteAccount)
|
||||
user.GET("/points", userH.GetPoints)
|
||||
user.GET("/search", userH.SearchUsers)
|
||||
}
|
||||
|
||||
// 排行榜(公开数据,需登录)
|
||||
authed.GET("/leaderboard", userH.GetLeaderboard)
|
||||
|
||||
// 打卡模块
|
||||
checkin := authed.Group("/checkin")
|
||||
{
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CheckInServiceTestSuite struct {
|
||||
ServiceTestSuite
|
||||
}
|
||||
|
||||
func TestCheckInServiceSuite(t *testing.T) {
|
||||
suite.Run(t, new(CheckInServiceTestSuite))
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) setupUser() string {
|
||||
user, err := s.svc.RegisterByPhone("13800138100")
|
||||
s.Require().NoError(err)
|
||||
return user.ID
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestCreateItem() {
|
||||
userID := s.setupUser()
|
||||
|
||||
item := &models.CheckInItem{
|
||||
Name: "每日阅读",
|
||||
Category: models.CategoryStudy,
|
||||
CycleType: models.CycleDaily,
|
||||
Privacy: models.PrivacyFriendsOnly,
|
||||
DailyGoal: strPtr("读30页"),
|
||||
}
|
||||
err := s.svcCk.CreateItem(userID, item)
|
||||
s.Require().NoError(err)
|
||||
s.NotEmpty(item.ID)
|
||||
s.Equal(models.StatusActive, item.Status)
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestGetUserItems() {
|
||||
userID := s.setupUser()
|
||||
|
||||
item1 := &models.CheckInItem{Name: "跑步", Category: models.CategoryHealth, CycleType: models.CycleDaily, Privacy: models.PrivacyFriendsOnly}
|
||||
item2 := &models.CheckInItem{Name: "冥想", Category: models.CategoryHabit, CycleType: models.CycleDaily, Privacy: models.PrivacyFriendsOnly}
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID, item1))
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID, item2))
|
||||
|
||||
items, err := s.svcCk.GetUserItems(userID)
|
||||
s.Require().NoError(err)
|
||||
s.Len(items, 2)
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestGetItemByID() {
|
||||
userID := s.setupUser()
|
||||
|
||||
item := &models.CheckInItem{Name: "写作", Category: models.CategoryHobby, CycleType: models.CycleDaily, Privacy: models.PrivacyPublic}
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID, item))
|
||||
|
||||
found, err := s.svcCk.GetItemByID(item.ID, userID)
|
||||
s.Require().NoError(err)
|
||||
s.Equal("写作", found.Name)
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestGetItemByID_WrongUser() {
|
||||
userID1 := s.setupUser()
|
||||
userID2, _ := s.svc.RegisterByPhone("13800138101")
|
||||
s.Require().NotNil(userID2)
|
||||
|
||||
item := &models.CheckInItem{Name: "日记", Category: models.CategoryHabit, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID1, item))
|
||||
|
||||
_, err := s.svcCk.GetItemByID(item.ID, userID2.ID)
|
||||
s.Error(err) // should fail - wrong user
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestPauseAndResumeItem() {
|
||||
userID := s.setupUser()
|
||||
|
||||
item := &models.CheckInItem{Name: "瑜伽", Category: models.CategoryHealth, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID, item))
|
||||
|
||||
err := s.svcCk.PauseItem(item.ID, userID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
paused, err := s.svcCk.GetItemByID(item.ID, userID)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(models.StatusPaused, paused.Status)
|
||||
|
||||
err = s.svcCk.ResumeItem(item.ID, userID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
resumed, err := s.svcCk.GetItemByID(item.ID, userID)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(models.StatusActive, resumed.Status)
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestCreateRecord() {
|
||||
userID := s.setupUser()
|
||||
|
||||
item := &models.CheckInItem{Name: "背单词", Category: models.CategoryStudy, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID, item))
|
||||
|
||||
record, err := s.svcCk.CreateRecord(userID, item.ID, time.Now(), strPtr("背了50个"), nil)
|
||||
s.Require().NoError(err)
|
||||
s.NotEmpty(record.ID)
|
||||
s.Equal(models.RecordNormal, record.Status)
|
||||
}
|
||||
|
||||
func (s *CheckInServiceTestSuite) TestCreateRecord_DuplicateDate() {
|
||||
userID := s.setupUser()
|
||||
|
||||
item := &models.CheckInItem{Name: "喝水", Category: models.CategoryHealth, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
|
||||
s.Require().NoError(s.svcCk.CreateItem(userID, item))
|
||||
|
||||
_, err := s.svcCk.CreateRecord(userID, item.ID, time.Now(), nil, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = s.svcCk.CreateRecord(userID, item.ID, time.Now(), nil, nil)
|
||||
s.Error(err)
|
||||
s.Contains(err.Error(), "已打卡")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
// ServiceTestSuite 提供内存 SQLite 数据库用于服务层测试
|
||||
type ServiceTestSuite struct {
|
||||
suite.Suite
|
||||
db *gorm.DB
|
||||
svc *UserService
|
||||
svcCk *CheckInService
|
||||
svcPts *PointsService
|
||||
}
|
||||
|
||||
func (s *ServiceTestSuite) SetupSuite() {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(db.AutoMigrate(&models.User{}, &models.CheckInItem{}, &models.CheckInRecord{}, &models.Achievement{}, &models.Admin{}))
|
||||
s.db = db
|
||||
s.svc = NewUserService(db)
|
||||
s.svcCk = NewCheckInService(db)
|
||||
s.svcPts = NewPointsService(db)
|
||||
}
|
||||
|
||||
func (s *ServiceTestSuite) TearDownTest() {
|
||||
s.db.Exec("DELETE FROM users")
|
||||
s.db.Exec("DELETE FROM check_in_items")
|
||||
s.db.Exec("DELETE FROM check_in_records")
|
||||
s.db.Exec("DELETE FROM achievements")
|
||||
}
|
||||
@@ -85,6 +85,46 @@ func (s *UserService) DeleteAccount(id string) error {
|
||||
}).Error
|
||||
}
|
||||
|
||||
// LeaderboardEntry 排行榜条目
|
||||
type LeaderboardEntry struct {
|
||||
UserID string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
// GetLeaderboardByPoints 按积分排行
|
||||
func (s *UserService) GetLeaderboardByPoints(limit int) ([]LeaderboardEntry, error) {
|
||||
var users []models.User
|
||||
if err := s.db.Where("points > 0").Order("points desc").Limit(limit).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]LeaderboardEntry, 0, len(users))
|
||||
for _, u := range users {
|
||||
nick := ""
|
||||
if u.Nickname != nil {
|
||||
nick = *u.Nickname
|
||||
}
|
||||
entries = append(entries, LeaderboardEntry{
|
||||
UserID: u.ID,
|
||||
Nickname: nick,
|
||||
Value: u.Points,
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// SearchUsers 搜索用户(按昵称或手机号)
|
||||
func (s *UserService) SearchUsers(query string, limit int) ([]models.User, error) {
|
||||
var users []models.User
|
||||
q := "%" + query + "%"
|
||||
if err := s.db.Where("nickname LIKE ? OR phone LIKE ?", q, q).
|
||||
Limit(limit).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||
return string(bytes), err
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type UserServiceTestSuite struct {
|
||||
ServiceTestSuite
|
||||
}
|
||||
|
||||
func TestUserServiceSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserServiceTestSuite))
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestRegisterByPhone() {
|
||||
user, err := s.svc.RegisterByPhone("13800138000")
|
||||
s.Require().NoError(err)
|
||||
s.NotEmpty(user.ID)
|
||||
s.Equal("13800138000", *user.Phone)
|
||||
s.Equal(0, user.Points)
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestRegisterByPhone_Duplicate() {
|
||||
_, err := s.svc.RegisterByPhone("13800138001")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = s.svc.RegisterByPhone("13800138001")
|
||||
s.Error(err)
|
||||
s.Contains(err.Error(), "已注册")
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestGetByPhone() {
|
||||
created, err := s.svc.RegisterByPhone("13800138002")
|
||||
s.Require().NoError(err)
|
||||
|
||||
found, err := s.svc.GetByPhone("13800138002")
|
||||
s.Require().NoError(err)
|
||||
s.Equal(created.ID, found.ID)
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestGetByPhone_NotFound() {
|
||||
_, err := s.svc.GetByPhone("13800999999")
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestGetByID() {
|
||||
created, err := s.svc.RegisterByPhone("13800138003")
|
||||
s.Require().NoError(err)
|
||||
|
||||
found, err := s.svc.GetByID(created.ID)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(*created.Phone, *found.Phone)
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestUpdateProfile() {
|
||||
user, err := s.svc.RegisterByPhone("13800138004")
|
||||
s.Require().NoError(err)
|
||||
|
||||
updated, err := s.svc.UpdateProfile(user.ID, map[string]interface{}{
|
||||
"nickname": "TestUser",
|
||||
"bio": "Hello",
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.Equal("TestUser", *updated.Nickname)
|
||||
s.Equal("Hello", *updated.Bio)
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestUpdateProfile_RejectInvalidFields() {
|
||||
user, err := s.svc.RegisterByPhone("13800138005")
|
||||
s.Require().NoError(err)
|
||||
|
||||
updated, err := s.svc.UpdateProfile(user.ID, map[string]interface{}{
|
||||
"points": 9999, // points is not in the allowed list
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.Equal(0, updated.Points) // should not have changed
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestDeleteAccount() {
|
||||
user, err := s.svc.RegisterByPhone("13800138006")
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.svc.DeleteAccount(user.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// After delete, the user still exists but fields are cleared
|
||||
deleted, err := s.svc.GetByID(user.ID)
|
||||
s.Require().NoError(err)
|
||||
s.Nil(deleted.Phone)
|
||||
s.Nil(deleted.Nickname)
|
||||
}
|
||||
|
||||
func (s *UserServiceTestSuite) TestHashAndCheckPassword() {
|
||||
hash, err := HashPassword("my-secret-pwd")
|
||||
s.Require().NoError(err)
|
||||
s.NotEmpty(hash)
|
||||
|
||||
s.True(CheckPassword(hash, "my-secret-pwd"))
|
||||
s.False(CheckPassword(hash, "wrong-password"))
|
||||
}
|
||||
Reference in New Issue
Block a user