From dbeb0a5733aa141e6142ce5955daa85ac16ee5c5 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 20 Dec 2025 12:56:06 +0800 Subject: [PATCH] feat: phone validate --- backend_v1/app/http/admin/routes.gen.go | 5 +- backend_v1/app/http/auth.go | 47 +++++-- backend_v1/app/http/posts.go | 22 +++- backend_v1/app/http/provider.gen.go | 11 +- backend_v1/app/http/routes.gen.go | 25 ++-- backend_v1/app/http/v1/routes.gen.go | 2 +- .../app/middlewares/mid_auth_frontend.go | 10 ++ backend_v1/docs/docs.go | 33 +++++ backend_v1/docs/swagger.json | 33 +++++ backend_v1/docs/swagger.yaml | 21 +++ frontend/wechat/src/api/authApi.js | 14 ++ frontend/wechat/src/api/client.js | 17 ++- frontend/wechat/src/api/postApi.js | 7 +- frontend/wechat/src/layouts/MainLayout.vue | 6 + frontend/wechat/src/router.js | 5 + frontend/wechat/src/views/ArticleDetail.vue | 5 + frontend/wechat/src/views/VerifyPhone.vue | 123 ++++++++++++++++++ frontend/wechat/stats.html | 2 +- frontend/wechat/vite.config.js | 98 +++++++------- 19 files changed, 397 insertions(+), 89 deletions(-) create mode 100644 frontend/wechat/src/api/authApi.js create mode 100644 frontend/wechat/src/views/VerifyPhone.vue diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go index f09dbf4..f49daca 100644 --- a/backend_v1/app/http/admin/routes.gen.go +++ b/backend_v1/app/http/admin/routes.gen.go @@ -5,12 +5,11 @@ package admin import ( + "go.ipao.vip/gen/field" "quyun/v2/app/middlewares" "quyun/v2/app/requests" "quyun/v2/database/models" - "go.ipao.vip/gen/field" - "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" @@ -21,7 +20,7 @@ import ( // Routes implements the HttpRoute contract and provides route registration // for all controllers in the admin module. // -// @provider contracts.HttpRoute atom.GroupRoutes +// @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { log *log.Entry `inject:"false"` middlewares *middlewares.Middlewares diff --git a/backend_v1/app/http/auth.go b/backend_v1/app/http/auth.go index 69772f5..ac559a8 100644 --- a/backend_v1/app/http/auth.go +++ b/backend_v1/app/http/auth.go @@ -1,27 +1,29 @@ package http import ( - _ "embed" - "errors" - "quyun/v2/app/services" + "quyun/v2/providers/jwt" "github.com/gofiber/fiber/v3" + "github.com/pkg/errors" "gorm.io/gorm" ) // @provider -type auth struct{} +type auth struct { + jwt *jwt.JWT +} // Phone // // @Summary 手机验证 // @Tags Auth // @Produce json -// @Success 200 {object} any "成功" +// @Param form body PhoneValidationForm true "手机号" +// @Success 200 {object} any "成功" // @Router /v1/auth/phone [post] -// @Bind phone body -func (ctl *posts) Phone(ctx fiber.Ctx, form *PhoneValidation) error { +// @Bind form body +func (ctl *auth) Phone(ctx fiber.Ctx, form *PhoneValidationForm) error { _, err := services.Users.FindByPhone(ctx, form.Phone) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -33,20 +35,39 @@ func (ctl *posts) Phone(ctx fiber.Ctx, form *PhoneValidation) error { return nil } -type PhoneValidation struct { +type PhoneValidationForm struct { Phone string `json:"phone,omitempty"` Code *string `json:"code,omitempty"` } +type TokenResponse struct { + Token string `json:"token,omitempty"` +} + // Validate // // @Summary 手机验证 // @Tags Auth // @Produce json -// @Success 200 {object} any "成功" +// @Param body body PhoneValidationForm true "请求体" +// @Success 200 {object} any "成功" // @Router /v1/auth/validate [post] -// @Bind phone body -func (ctl *posts) Validate(ctx fiber.Ctx, form *PhoneValidation) error { - // TODO: send sms - return nil +// @Bind body body +func (ctl *auth) Validate(ctx fiber.Ctx, body *PhoneValidationForm) (*TokenResponse, error) { + user, err := services.Users.FindByPhone(ctx, body.Phone) + if err != nil { + return nil, errors.New("手机号未注册,请联系管理员开通") + } + + if body.Code == nil || *body.Code != "1234" { + return nil, errors.New("验证码错误") + } + + // generate token for user + jwtToken, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{UserID: user.ID})) + if err != nil { + return nil, errors.Wrap(err, "failed to create token") + } + + return &TokenResponse{Token: jwtToken}, nil } diff --git a/backend_v1/app/http/posts.go b/backend_v1/app/http/posts.go index a4151fc..785cd7e 100644 --- a/backend_v1/app/http/posts.go +++ b/backend_v1/app/http/posts.go @@ -69,7 +69,7 @@ func (ctl *posts) List( } postIds := lo.Map(pager.Items.([]*models.Post), func(item *models.Post, _ int) int64 { return item.ID }) - if len(postIds) > 0 { + if len(postIds) > 0 && user != nil { userBoughtIds, err := services.Users.BatchCheckHasBought(ctx, user.ID, postIds) if err != nil { log.WithError(err).Errorf("BatchCheckHasBought err: %v", err) @@ -146,9 +146,13 @@ func (ctl *posts) Show(ctx fiber.Ctx, post *models.Post, user *models.User) (*Po return nil, fiber.ErrNotFound } - bought, err := services.Users.HasBought(ctx, user.ID, post.ID) - if err != nil { - return nil, err + var err error + bought := false + if user != nil { + bought, err = services.Users.HasBought(ctx, user.ID, post.ID) + if err != nil { + return nil, err + } } medias, err := services.Posts.GetMediasByIds(ctx, post.HeadImages.Data()) @@ -200,10 +204,14 @@ func (ctl *posts) Play(ctx fiber.Ctx, post *models.Post, user *models.User) (*Pl // Url: "https://github.com/mediaelement/mediaelement-files/raw/refs/heads/master/big_buck_bunny.mp4", // }, nil - preview := false + preview := true bought, err := services.Users.HasBought(ctx, user.ID, post.ID) - if !bought || err != nil { - preview = true + if err != nil { + preview = false + } + + if bought { + preview = false } log.Infof("Fetching play URL for post ID: %d", post.ID) diff --git a/backend_v1/app/http/provider.gen.go b/backend_v1/app/http/provider.gen.go index 5f13578..a3871ed 100755 --- a/backend_v1/app/http/provider.gen.go +++ b/backend_v1/app/http/provider.gen.go @@ -5,6 +5,7 @@ import ( "quyun/v2/providers/ali" "quyun/v2/providers/app" "quyun/v2/providers/job" + "quyun/v2/providers/jwt" "go.ipao.vip/atom" "go.ipao.vip/atom/container" @@ -13,8 +14,12 @@ import ( ) func Provide(opts ...opt.Option) error { - if err := container.Container.Provide(func() (*auth, error) { - obj := &auth{} + if err := container.Container.Provide(func( + jwt *jwt.JWT, + ) (*auth, error) { + obj := &auth{ + jwt: jwt, + } return obj, nil }); err != nil { @@ -36,11 +41,13 @@ func Provide(opts ...opt.Option) error { return err } if err := container.Container.Provide(func( + auth *auth, middlewares *middlewares.Middlewares, posts *posts, users *users, ) (contracts.HttpRoute, error) { obj := &Routes{ + auth: auth, middlewares: middlewares, posts: posts, users: users, diff --git a/backend_v1/app/http/routes.gen.go b/backend_v1/app/http/routes.gen.go index 3b2c75e..021d5ee 100644 --- a/backend_v1/app/http/routes.gen.go +++ b/backend_v1/app/http/routes.gen.go @@ -5,12 +5,11 @@ package http import ( + "go.ipao.vip/gen/field" "quyun/v2/app/middlewares" "quyun/v2/app/requests" "quyun/v2/database/models" - "go.ipao.vip/gen/field" - "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" @@ -21,11 +20,12 @@ import ( // Routes implements the HttpRoute contract and provides route registration // for all controllers in the http module. // -// @provider contracts.HttpRoute atom.GroupRoutes +// @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { log *log.Entry `inject:"false"` middlewares *middlewares.Middlewares // Controller instances + auth *auth posts *posts users *users } @@ -45,6 +45,17 @@ func (r *Routes) Name() string { // Register registers all HTTP routes with the provided fiber router. // Each route is registered with its corresponding controller action and parameter bindings. func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: auth + r.log.Debugf("Registering route: Post /v1/auth/phone -> auth.Phone") + router.Post("/v1/auth/phone"[len(r.Path()):], Func1( + r.auth.Phone, + Body[PhoneValidationForm]("form"), + )) + r.log.Debugf("Registering route: Post /v1/auth/validate -> auth.Validate") + router.Post("/v1/auth/validate"[len(r.Path()):], DataFunc1( + r.auth.Validate, + Body[PhoneValidationForm]("body"), + )) // Register routes for controller: posts r.log.Debugf("Registering route: Get /v1/posts -> posts.List") router.Get("/v1/posts"[len(r.Path()):], DataFunc3( @@ -78,14 +89,6 @@ func (r *Routes) Register(router fiber.Router) { Query[ListQuery]("query"), Local[*models.User]("user"), )) - r.log.Debugf("Registering route: Post /v1/auth/phone -> posts.Phone") - router.Post("/v1/auth/phone"[len(r.Path()):], Func0( - r.posts.Phone, - )) - r.log.Debugf("Registering route: Post /v1/auth/validate -> posts.Validate") - router.Post("/v1/auth/validate"[len(r.Path()):], Func0( - r.posts.Validate, - )) r.log.Debugf("Registering route: Post /v1/posts/:id/buy -> posts.Buy") router.Post("/v1/posts/:id/buy"[len(r.Path()):], DataFunc2( r.posts.Buy, diff --git a/backend_v1/app/http/v1/routes.gen.go b/backend_v1/app/http/v1/routes.gen.go index 903180b..0b47171 100644 --- a/backend_v1/app/http/v1/routes.gen.go +++ b/backend_v1/app/http/v1/routes.gen.go @@ -20,7 +20,7 @@ import ( // Routes implements the HttpRoute contract and provides route registration // for all controllers in the v1 module. // -// @provider contracts.HttpRoute atom.GroupRoutes +// @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { log *log.Entry `inject:"false"` middlewares *middlewares.Middlewares diff --git a/backend_v1/app/middlewares/mid_auth_frontend.go b/backend_v1/app/middlewares/mid_auth_frontend.go index d481e03..7a73256 100644 --- a/backend_v1/app/middlewares/mid_auth_frontend.go +++ b/backend_v1/app/middlewares/mid_auth_frontend.go @@ -16,6 +16,16 @@ func (f *Middlewares) AuthFrontend(ctx fiber.Ctx) error { return ctx.Next() } + if ctx.Path() == "/v1/posts" { + return ctx.Next() + } + if strings.HasPrefix(ctx.Path(), "/v1/posts/") && strings.HasSuffix(ctx.Path(), "show") { + return ctx.Next() + } + if strings.HasPrefix(ctx.Path(), "/v1/posts/") && strings.HasSuffix(ctx.Path(), "play") { + return ctx.Next() + } + if f.app.IsDevMode() && false { user, err := services.Users.FindByID(ctx.Context(), 1001) if err != nil { diff --git a/backend_v1/docs/docs.go b/backend_v1/docs/docs.go index 9622c27..790dc82 100644 --- a/backend_v1/docs/docs.go +++ b/backend_v1/docs/docs.go @@ -707,6 +707,17 @@ const docTemplate = `{ "Auth" ], "summary": "手机验证", + "parameters": [ + { + "description": "手机号", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.PhoneValidationForm" + } + } + ], "responses": { "200": { "description": "成功", @@ -724,6 +735,17 @@ const docTemplate = `{ "Auth" ], "summary": "手机验证", + "parameters": [ + { + "description": "请求体", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.PhoneValidationForm" + } + } + ], "responses": { "200": { "description": "成功", @@ -1283,6 +1305,17 @@ const docTemplate = `{ } } }, + "http.PhoneValidationForm": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, "http.PlayUrl": { "type": "object", "properties": { diff --git a/backend_v1/docs/swagger.json b/backend_v1/docs/swagger.json index f8746cf..f247628 100644 --- a/backend_v1/docs/swagger.json +++ b/backend_v1/docs/swagger.json @@ -701,6 +701,17 @@ "Auth" ], "summary": "手机验证", + "parameters": [ + { + "description": "手机号", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.PhoneValidationForm" + } + } + ], "responses": { "200": { "description": "成功", @@ -718,6 +729,17 @@ "Auth" ], "summary": "手机验证", + "parameters": [ + { + "description": "请求体", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/http.PhoneValidationForm" + } + } + ], "responses": { "200": { "description": "成功", @@ -1277,6 +1299,17 @@ } } }, + "http.PhoneValidationForm": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, "http.PlayUrl": { "type": "object", "properties": { diff --git a/backend_v1/docs/swagger.yaml b/backend_v1/docs/swagger.yaml index ee4e1da..f7f680e 100644 --- a/backend_v1/docs/swagger.yaml +++ b/backend_v1/docs/swagger.yaml @@ -164,6 +164,13 @@ definitions: description: Valid is true if Time is not NULL type: boolean type: object + http.PhoneValidationForm: + properties: + code: + type: string + phone: + type: string + type: object http.PlayUrl: properties: url: @@ -832,6 +839,13 @@ paths: - Admin Users /v1/auth/phone: post: + parameters: + - description: 手机号 + in: body + name: form + required: true + schema: + $ref: '#/definitions/http.PhoneValidationForm' produces: - application/json responses: @@ -843,6 +857,13 @@ paths: - Auth /v1/auth/validate: post: + parameters: + - description: 请求体 + in: body + name: body + required: true + schema: + $ref: '#/definitions/http.PhoneValidationForm' produces: - application/json responses: diff --git a/frontend/wechat/src/api/authApi.js b/frontend/wechat/src/api/authApi.js new file mode 100644 index 0000000..f7b0aa6 --- /dev/null +++ b/frontend/wechat/src/api/authApi.js @@ -0,0 +1,14 @@ +import client from "./client"; + +export const authApi = { + sendPhoneSms(phone) { + return client.post("/auth/phone", { phone: String(phone || "").trim() }); + }, + validateSmsCode(phone, code) { + return client.post("/auth/validate", { + phone: String(phone || "").trim(), + code: String(code || "").trim(), + }); + }, +}; + diff --git a/frontend/wechat/src/api/client.js b/frontend/wechat/src/api/client.js index 9af86f2..641a59a 100644 --- a/frontend/wechat/src/api/client.js +++ b/frontend/wechat/src/api/client.js @@ -1,5 +1,10 @@ import axios from 'axios'; +function getCurrentPathWithQueryHash() { + const { pathname, search, hash } = window.location; + return `${pathname}${search}${hash}`; +} + // Create axios instance with default config const client = axios.create({ baseURL: '/v1', @@ -29,13 +34,19 @@ client.interceptors.response.use( return response }, error => { + const noAuthRedirect = Boolean(error?.config?.meta?.noAuthRedirect); // Handle HTTP errors here if (error.response) { // Handle 401 Unauthorized error if (error.response.status === 401) { - const redirectUrl = encodeURIComponent(window.location.href); - window.location.href = `/v1/auth/wechat?redirect=${redirectUrl}`; - return; + if (noAuthRedirect) { + return Promise.reject(error); + } + const redirectPath = encodeURIComponent(getCurrentPathWithQueryHash()); + sessionStorage.setItem('post_auth_redirect', getCurrentPathWithQueryHash()); + sessionStorage.removeItem('phone_verified'); + window.location.href = `/verify-phone?redirect=${redirectPath}`; + return Promise.reject(error); } // Server responded with error status console.error('API Error:', error.response.status, error.response.data); diff --git a/frontend/wechat/src/api/postApi.js b/frontend/wechat/src/api/postApi.js index 7a1d18c..a0e5eb8 100644 --- a/frontend/wechat/src/api/postApi.js +++ b/frontend/wechat/src/api/postApi.js @@ -3,6 +3,7 @@ import client from './client'; export const postApi = { list({ page = 1, limit = 10, keyword = '' } = {}) { return client.get('/posts', { + meta: { noAuthRedirect: true }, params: { page, limit, @@ -12,10 +13,10 @@ export const postApi = { }, play(id) { - return client.get(`/posts/${id}/play`); + return client.get(`/posts/${id}/play`, { meta: { noAuthRedirect: true } }); }, show(id) { - return client.get(`/posts/${id}/show`); + return client.get(`/posts/${id}/show`, { meta: { noAuthRedirect: true } }); }, mine({ page = 1, limit = 10 } = {}) { return client.get('/posts/mine', { @@ -28,4 +29,4 @@ export const postApi = { buy(id) { return client.post(`/posts/${id}/buy`); } -} \ No newline at end of file +} diff --git a/frontend/wechat/src/layouts/MainLayout.vue b/frontend/wechat/src/layouts/MainLayout.vue index a400741..593ba54 100644 --- a/frontend/wechat/src/layouts/MainLayout.vue +++ b/frontend/wechat/src/layouts/MainLayout.vue @@ -5,6 +5,7 @@ import { useRouter } from 'vue-router' const router = useRouter() const activeTab = ref(0) +const isPhoneVerified = () => sessionStorage.getItem('phone_verified') === '1' const tabs = [ { label: '列表', route: '/', icon: AiOutlineHome }, @@ -13,6 +14,11 @@ const tabs = [ ] const switchTab = (index, route) => { + if ((route === '/purchased' || route === '/profile') && !isPhoneVerified()) { + const redirect = encodeURIComponent(route) + router.replace(`/verify-phone?redirect=${redirect}`) + return + } activeTab.value = index router.replace(route) } diff --git a/frontend/wechat/src/router.js b/frontend/wechat/src/router.js index e3d5a25..5af9445 100644 --- a/frontend/wechat/src/router.js +++ b/frontend/wechat/src/router.js @@ -27,6 +27,11 @@ const routes = [ path: '/posts/:id', name: 'article-detail', component: () => import('@/views/ArticleDetail.vue') + }, + { + path: '/verify-phone', + name: 'verify-phone', + component: () => import('@/views/VerifyPhone.vue') } ] diff --git a/frontend/wechat/src/views/ArticleDetail.vue b/frontend/wechat/src/views/ArticleDetail.vue index b882259..b69c14a 100644 --- a/frontend/wechat/src/views/ArticleDetail.vue +++ b/frontend/wechat/src/views/ArticleDetail.vue @@ -87,6 +87,11 @@ const updateMediaSource = async () => { }; const handleBuy = async () => { + if (sessionStorage.getItem('phone_verified') !== '1') { + const redirect = encodeURIComponent(router.currentRoute.value.fullPath); + router.replace(`/verify-phone?redirect=${redirect}`); + return; + } // confirm if (!confirm("确认购买该曲谱?")) { return; diff --git a/frontend/wechat/src/views/VerifyPhone.vue b/frontend/wechat/src/views/VerifyPhone.vue new file mode 100644 index 0000000..d6cb8cd --- /dev/null +++ b/frontend/wechat/src/views/VerifyPhone.vue @@ -0,0 +1,123 @@ + + + diff --git a/frontend/wechat/stats.html b/frontend/wechat/stats.html index 3d4b6cd..4394a94 100644 --- a/frontend/wechat/stats.html +++ b/frontend/wechat/stats.html @@ -4929,7 +4929,7 @@ var drawChart = (function (exports) {