修改页面
CI / Server (Go) (push) Has been cancelled
CI / Web (Next.js) (push) Has been cancelled
CI / Mobile (React Native) (push) Has been cancelled
CI / Admin (React) (push) Has been cancelled
CI / Docker Build (push) Has been cancelled

This commit is contained in:
zeronline
2026-06-25 16:33:32 +08:00
parent c5cccd346b
commit 545329ac40
63 changed files with 2560 additions and 161 deletions
BIN
View File
Binary file not shown.
+14 -2
View File
@@ -5,11 +5,13 @@ go 1.22
require (
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.23.0
gorm.io/driver/mysql v1.5.6
gorm.io/gorm v1.25.10
gorm.io/gorm v1.30.0
)
require (
@@ -17,13 +19,17 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -34,12 +40,18 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
+29 -7
View File
@@ -10,6 +10,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
@@ -18,6 +20,10 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -35,6 +41,10 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -64,6 +74,9 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -76,8 +89,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -93,10 +107,10 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -108,7 +122,15 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+51 -18
View File
@@ -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)
}
+4
View File
@@ -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")
}
+40
View File
@@ -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"))
}