coverage.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5. <title>handler: Go Coverage Report</title>
  6. <style>
  7. body {
  8. background: black;
  9. color: rgb(80, 80, 80);
  10. }
  11. body, pre, #legend span {
  12. font-family: Menlo, monospace;
  13. font-weight: bold;
  14. }
  15. #topbar {
  16. background: black;
  17. position: fixed;
  18. top: 0; left: 0; right: 0;
  19. height: 42px;
  20. border-bottom: 1px solid rgb(80, 80, 80);
  21. }
  22. #content {
  23. margin-top: 50px;
  24. }
  25. #nav, #legend {
  26. float: left;
  27. margin-left: 10px;
  28. }
  29. #legend {
  30. margin-top: 12px;
  31. }
  32. #nav {
  33. margin-top: 10px;
  34. }
  35. #legend span {
  36. margin: 0 5px;
  37. }
  38. .cov0 { color: rgb(192, 0, 0) }
  39. .cov1 { color: rgb(128, 128, 128) }
  40. .cov2 { color: rgb(116, 140, 131) }
  41. .cov3 { color: rgb(104, 152, 134) }
  42. .cov4 { color: rgb(92, 164, 137) }
  43. .cov5 { color: rgb(80, 176, 140) }
  44. .cov6 { color: rgb(68, 188, 143) }
  45. .cov7 { color: rgb(56, 200, 146) }
  46. .cov8 { color: rgb(44, 212, 149) }
  47. .cov9 { color: rgb(32, 224, 152) }
  48. .cov10 { color: rgb(20, 236, 155) }
  49. </style>
  50. </head>
  51. <body>
  52. <div id="topbar">
  53. <div id="nav">
  54. <select id="files">
  55. <option value="file0">github.com/go-nunu/nunu-layout-advanced/internal/handler/handler.go (80.0%)</option>
  56. <option value="file1">github.com/go-nunu/nunu-layout-advanced/internal/handler/user.go (55.6%)</option>
  57. <option value="file2">github.com/go-nunu/nunu-layout-advanced/internal/repository/repository.go (8.3%)</option>
  58. <option value="file3">github.com/go-nunu/nunu-layout-advanced/internal/repository/user.go (64.7%)</option>
  59. <option value="file4">github.com/go-nunu/nunu-layout-advanced/internal/service/service.go (100.0%)</option>
  60. <option value="file5">github.com/go-nunu/nunu-layout-advanced/internal/service/user.go (80.0%)</option>
  61. </select>
  62. </div>
  63. <div id="legend">
  64. <span>not tracked</span>
  65. <span class="cov0">not covered</span>
  66. <span class="cov8">covered</span>
  67. </div>
  68. </div>
  69. <div id="content">
  70. <pre class="file" id="file0" style="display: none">package handler
  71. import (
  72. "github.com/gin-gonic/gin"
  73. "github.com/go-nunu/nunu-layout-advanced/internal/middleware"
  74. "github.com/go-nunu/nunu-layout-advanced/pkg/log"
  75. )
  76. type Handler struct {
  77. logger *log.Logger
  78. }
  79. func NewHandler(logger *log.Logger) *Handler <span class="cov8" title="1">{
  80. return &amp;Handler{
  81. logger: logger,
  82. }
  83. }</span>
  84. func GetUserIdFromCtx(ctx *gin.Context) string <span class="cov8" title="1">{
  85. v, exists := ctx.Get("claims")
  86. if !exists </span><span class="cov0" title="0">{
  87. return ""
  88. }</span>
  89. <span class="cov8" title="1">return v.(*middleware.MyCustomClaims).UserId</span>
  90. }
  91. </pre>
  92. <pre class="file" id="file1" style="display: none">package handler
  93. import (
  94. "github.com/gin-gonic/gin"
  95. "github.com/go-nunu/nunu-layout-advanced/internal/service"
  96. "github.com/go-nunu/nunu-layout-advanced/pkg/helper/resp"
  97. "github.com/pkg/errors"
  98. "net/http"
  99. )
  100. type UserHandler interface {
  101. Register(ctx *gin.Context)
  102. Login(ctx *gin.Context)
  103. GetProfile(ctx *gin.Context)
  104. UpdateProfile(ctx *gin.Context)
  105. }
  106. type userHandler struct {
  107. *Handler
  108. userService service.UserService
  109. }
  110. func NewUserHandler(handler *Handler, userService service.UserService) UserHandler <span class="cov8" title="1">{
  111. return &amp;userHandler{
  112. Handler: handler,
  113. userService: userService,
  114. }
  115. }</span>
  116. func (h *userHandler) Register(ctx *gin.Context) <span class="cov8" title="1">{
  117. req := new(service.RegisterRequest)
  118. if err := ctx.ShouldBindJSON(req); err != nil </span><span class="cov0" title="0">{
  119. resp.HandleError(ctx, http.StatusBadRequest, 1, errors.Wrap(err, "invalid request").Error(), nil)
  120. return
  121. }</span>
  122. <span class="cov8" title="1">if err := h.userService.Register(ctx, req); err != nil </span><span class="cov0" title="0">{
  123. resp.HandleError(ctx, http.StatusBadRequest, 1, errors.Wrap(err, "invalid request").Error(), nil)
  124. return
  125. }</span>
  126. <span class="cov8" title="1">resp.HandleSuccess(ctx, nil)</span>
  127. }
  128. func (h *userHandler) Login(ctx *gin.Context) <span class="cov8" title="1">{
  129. var req service.LoginRequest
  130. if err := ctx.ShouldBindJSON(&amp;req); err != nil </span><span class="cov0" title="0">{
  131. resp.HandleError(ctx, http.StatusBadRequest, 1, errors.Wrap(err, "invalid request").Error(), nil)
  132. return
  133. }</span>
  134. <span class="cov8" title="1">token, err := h.userService.Login(ctx, &amp;req)
  135. if err != nil </span><span class="cov0" title="0">{
  136. resp.HandleError(ctx, http.StatusUnauthorized, 1, err.Error(), nil)
  137. return
  138. }</span>
  139. <span class="cov8" title="1">resp.HandleSuccess(ctx, gin.H{
  140. "accessToken": token,
  141. })</span>
  142. }
  143. func (h *userHandler) GetProfile(ctx *gin.Context) <span class="cov8" title="1">{
  144. userId := GetUserIdFromCtx(ctx)
  145. if userId == "" </span><span class="cov0" title="0">{
  146. resp.HandleError(ctx, http.StatusUnauthorized, 1, "unauthorized", nil)
  147. return
  148. }</span>
  149. <span class="cov8" title="1">user, err := h.userService.GetProfile(ctx, userId)
  150. if err != nil </span><span class="cov0" title="0">{
  151. resp.HandleError(ctx, http.StatusBadRequest, 1, err.Error(), nil)
  152. return
  153. }</span>
  154. <span class="cov8" title="1">resp.HandleSuccess(ctx, user)</span>
  155. }
  156. func (h *userHandler) UpdateProfile(ctx *gin.Context) <span class="cov8" title="1">{
  157. userId := GetUserIdFromCtx(ctx)
  158. var req service.UpdateProfileRequest
  159. if err := ctx.ShouldBindJSON(&amp;req); err != nil </span><span class="cov0" title="0">{
  160. resp.HandleError(ctx, http.StatusBadRequest, 1, errors.Wrap(err, "invalid request").Error(), nil)
  161. return
  162. }</span>
  163. <span class="cov8" title="1">if err := h.userService.UpdateProfile(ctx, userId, &amp;req); err != nil </span><span class="cov0" title="0">{
  164. resp.HandleError(ctx, http.StatusBadRequest, 1, err.Error(), nil)
  165. return
  166. }</span>
  167. <span class="cov8" title="1">resp.HandleSuccess(ctx, nil)</span>
  168. }
  169. </pre>
  170. <pre class="file" id="file2" style="display: none">package repository
  171. import (
  172. "context"
  173. "fmt"
  174. "github.com/go-nunu/nunu-layout-advanced/pkg/log"
  175. "github.com/redis/go-redis/v9"
  176. "github.com/spf13/viper"
  177. "gorm.io/driver/mysql"
  178. "gorm.io/gorm"
  179. "time"
  180. )
  181. type Repository struct {
  182. db *gorm.DB
  183. rdb *redis.Client
  184. logger *log.Logger
  185. }
  186. func NewRepository(db *gorm.DB, rdb *redis.Client, logger *log.Logger) *Repository <span class="cov8" title="1">{
  187. return &amp;Repository{
  188. db: db,
  189. rdb: rdb,
  190. logger: logger,
  191. }
  192. }</span>
  193. func NewDB(conf *viper.Viper) *gorm.DB <span class="cov0" title="0">{
  194. db, err := gorm.Open(mysql.Open(conf.GetString("data.mysql.user")), &amp;gorm.Config{})
  195. if err != nil </span><span class="cov0" title="0">{
  196. panic(err)</span>
  197. }
  198. <span class="cov0" title="0">return db</span>
  199. }
  200. func NewRedis(conf *viper.Viper) *redis.Client <span class="cov0" title="0">{
  201. rdb := redis.NewClient(&amp;redis.Options{
  202. Addr: conf.GetString("data.redis.addr"),
  203. Password: conf.GetString("data.redis.password"),
  204. DB: conf.GetInt("data.redis.db"),
  205. })
  206. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  207. defer cancel()
  208. _, err := rdb.Ping(ctx).Result()
  209. if err != nil </span><span class="cov0" title="0">{
  210. panic(fmt.Sprintf("redis error: %s", err.Error()))</span>
  211. }
  212. <span class="cov0" title="0">return rdb</span>
  213. }
  214. </pre>
  215. <pre class="file" id="file3" style="display: none">package repository
  216. import (
  217. "context"
  218. "github.com/go-nunu/nunu-layout-advanced/internal/model"
  219. "github.com/pkg/errors"
  220. "gorm.io/gorm"
  221. )
  222. type UserRepository interface {
  223. Create(ctx context.Context, user *model.User) error
  224. Update(ctx context.Context, user *model.User) error
  225. GetByID(ctx context.Context, id string) (*model.User, error)
  226. GetByUsername(ctx context.Context, username string) (*model.User, error)
  227. }
  228. type userRepository struct {
  229. *Repository
  230. }
  231. func NewUserRepository(r *Repository) UserRepository <span class="cov8" title="1">{
  232. return &amp;userRepository{
  233. Repository: r,
  234. }
  235. }</span>
  236. func (r *userRepository) Create(ctx context.Context, user *model.User) error <span class="cov8" title="1">{
  237. if err := r.db.Create(user).Error; err != nil </span><span class="cov0" title="0">{
  238. return errors.Wrap(err, "failed to create user")
  239. }</span>
  240. <span class="cov8" title="1">return nil</span>
  241. }
  242. func (r *userRepository) Update(ctx context.Context, user *model.User) error <span class="cov8" title="1">{
  243. if err := r.db.Save(user).Error; err != nil </span><span class="cov0" title="0">{
  244. return errors.Wrap(err, "failed to update user")
  245. }</span>
  246. <span class="cov8" title="1">return nil</span>
  247. }
  248. func (r *userRepository) GetByID(ctx context.Context, userId string) (*model.User, error) <span class="cov8" title="1">{
  249. var user model.User
  250. if err := r.db.Where("user_id = ?", userId).First(&amp;user).Error; err != nil </span><span class="cov0" title="0">{
  251. return nil, errors.Wrap(err, "failed to get user by ID")
  252. }</span>
  253. <span class="cov8" title="1">return &amp;user, nil</span>
  254. }
  255. func (r *userRepository) GetByUsername(ctx context.Context, username string) (*model.User, error) <span class="cov8" title="1">{
  256. var user model.User
  257. if err := r.db.Where("username = ?", username).First(&amp;user).Error; err != nil </span><span class="cov0" title="0">{
  258. if err == gorm.ErrRecordNotFound </span><span class="cov0" title="0">{
  259. return nil, nil
  260. }</span>
  261. <span class="cov0" title="0">return nil, errors.Wrap(err, "failed to get user by username")</span>
  262. }
  263. <span class="cov8" title="1">return &amp;user, nil</span>
  264. }
  265. </pre>
  266. <pre class="file" id="file4" style="display: none">package service
  267. import (
  268. "github.com/go-nunu/nunu-layout-advanced/internal/middleware"
  269. "github.com/go-nunu/nunu-layout-advanced/pkg/helper/sid"
  270. "github.com/go-nunu/nunu-layout-advanced/pkg/log"
  271. )
  272. type Service struct {
  273. logger *log.Logger
  274. sid *sid.Sid
  275. jwt *middleware.JWT
  276. }
  277. func NewService(logger *log.Logger, sid *sid.Sid, jwt *middleware.JWT) *Service <span class="cov8" title="1">{
  278. return &amp;Service{
  279. logger: logger,
  280. sid: sid,
  281. jwt: jwt,
  282. }
  283. }</span>
  284. </pre>
  285. <pre class="file" id="file5" style="display: none">package service
  286. import (
  287. "context"
  288. "github.com/go-nunu/nunu-layout-advanced/internal/model"
  289. "github.com/go-nunu/nunu-layout-advanced/internal/repository"
  290. "github.com/golang-jwt/jwt/v5"
  291. "github.com/pkg/errors"
  292. "golang.org/x/crypto/bcrypt"
  293. "time"
  294. )
  295. type RegisterRequest struct {
  296. Username string `json:"username" binding:"required"`
  297. Password string `json:"password" binding:"required"`
  298. Email string `json:"email" binding:"required,email"`
  299. }
  300. type LoginRequest struct {
  301. Username string `json:"username" binding:"required"`
  302. Password string `json:"password" binding:"required"`
  303. }
  304. type UpdateProfileRequest struct {
  305. Nickname string `json:"nickname"`
  306. Email string `json:"email" binding:"required,email"`
  307. Avatar string `json:"avatar"`
  308. }
  309. type ChangePasswordRequest struct {
  310. OldPassword string `json:"oldPassword" binding:"required"`
  311. NewPassword string `json:"newPassword" binding:"required"`
  312. }
  313. type UserService interface {
  314. Register(ctx context.Context, req *RegisterRequest) error
  315. Login(ctx context.Context, req *LoginRequest) (string, error)
  316. GetProfile(ctx context.Context, userId string) (*model.User, error)
  317. UpdateProfile(ctx context.Context, userId string, req *UpdateProfileRequest) error
  318. GenerateToken(ctx context.Context, userId string) (string, error)
  319. }
  320. type userService struct {
  321. userRepo repository.UserRepository
  322. *Service
  323. }
  324. func NewUserService(service *Service, userRepo repository.UserRepository) UserService <span class="cov8" title="1">{
  325. return &amp;userService{
  326. userRepo: userRepo,
  327. Service: service,
  328. }
  329. }</span>
  330. func (s *userService) Register(ctx context.Context, req *RegisterRequest) error <span class="cov8" title="1">{
  331. // 检查用户名是否已存在
  332. if user, err := s.userRepo.GetByUsername(ctx, req.Username); err == nil &amp;&amp; user != nil </span><span class="cov8" title="1">{
  333. return errors.New("username already exists")
  334. }</span>
  335. <span class="cov8" title="1">hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
  336. if err != nil </span><span class="cov0" title="0">{
  337. return errors.Wrap(err, "failed to hash password")
  338. }</span>
  339. // 生成用户ID
  340. <span class="cov8" title="1">userId, err := s.sid.GenString()
  341. if err != nil </span><span class="cov0" title="0">{
  342. return errors.Wrap(err, "failed to generate user ID")
  343. }</span>
  344. // 创建用户
  345. <span class="cov8" title="1">user := &amp;model.User{
  346. UserId: userId,
  347. Username: req.Username,
  348. Password: string(hashedPassword),
  349. Email: req.Email,
  350. }
  351. if err = s.userRepo.Create(ctx, user); err != nil </span><span class="cov0" title="0">{
  352. return errors.Wrap(err, "failed to create user")
  353. }</span>
  354. <span class="cov8" title="1">return nil</span>
  355. }
  356. func (s *userService) Login(ctx context.Context, req *LoginRequest) (string, error) <span class="cov8" title="1">{
  357. user, err := s.userRepo.GetByUsername(ctx, req.Username)
  358. if err != nil || user == nil </span><span class="cov8" title="1">{
  359. return "", errors.Wrap(err, "failed to get user by username")
  360. }</span>
  361. <span class="cov8" title="1">err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
  362. if err != nil </span><span class="cov0" title="0">{
  363. return "", errors.Wrap(err, "failed to hash password")
  364. }</span>
  365. // 生成JWT token
  366. <span class="cov8" title="1">token, err := s.GenerateToken(ctx, user.UserId)
  367. if err != nil </span><span class="cov0" title="0">{
  368. return "", errors.Wrap(err, "failed to generate JWT token")
  369. }</span>
  370. <span class="cov8" title="1">return token, nil</span>
  371. }
  372. func (s *userService) GetProfile(ctx context.Context, userId string) (*model.User, error) <span class="cov8" title="1">{
  373. user, err := s.userRepo.GetByID(ctx, userId)
  374. if err != nil </span><span class="cov0" title="0">{
  375. return nil, errors.Wrap(err, "failed to get user by ID")
  376. }</span>
  377. <span class="cov8" title="1">return user, nil</span>
  378. }
  379. func (s *userService) UpdateProfile(ctx context.Context, userId string, req *UpdateProfileRequest) error <span class="cov8" title="1">{
  380. user, err := s.userRepo.GetByID(ctx, userId)
  381. if err != nil </span><span class="cov8" title="1">{
  382. return errors.Wrap(err, "failed to get user by ID")
  383. }</span>
  384. <span class="cov8" title="1">user.Email = req.Email
  385. user.Nickname = req.Nickname
  386. if err = s.userRepo.Update(ctx, user); err != nil </span><span class="cov0" title="0">{
  387. return errors.Wrap(err, "failed to update user")
  388. }</span>
  389. <span class="cov8" title="1">return nil</span>
  390. }
  391. func (s *userService) GenerateToken(ctx context.Context, userId string) (string, error) <span class="cov8" title="1">{
  392. // 生成JWT token
  393. s.jwt.GenToken(userId, time.Now().Add(time.Hour*24*90))
  394. token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
  395. "userId": userId,
  396. "exp": time.Now().Add(time.Hour * 24).Unix(),
  397. }).SignedString([]byte("secret"))
  398. if err != nil </span><span class="cov0" title="0">{
  399. return "", errors.Wrap(err, "failed to generate JWT token")
  400. }</span>
  401. <span class="cov8" title="1">return token, nil</span>
  402. }
  403. </pre>
  404. </div>
  405. </body>
  406. <script>
  407. (function() {
  408. var files = document.getElementById('files');
  409. var visible;
  410. files.addEventListener('change', onChange, false);
  411. function select(part) {
  412. if (visible)
  413. visible.style.display = 'none';
  414. visible = document.getElementById(part);
  415. if (!visible)
  416. return;
  417. files.value = part;
  418. visible.style.display = 'block';
  419. location.hash = part;
  420. }
  421. function onChange() {
  422. select(files.value);
  423. window.scrollTo(0, 0);
  424. }
  425. if (location.hash != "") {
  426. select(location.hash.substr(1));
  427. }
  428. if (!visible) {
  429. select("file0");
  430. }
  431. })();
  432. </script>
  433. </html>