Compare commits
3 Commits
0e4af79b53
...
12aa7a404a
| Author | SHA1 | Date | |
|---|---|---|---|
| 12aa7a404a | |||
| dbeb0a5733 | |||
| 22e288bf98 |
@@ -5,12 +5,11 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go.ipao.vip/gen/field"
|
||||||
"quyun/v2/app/middlewares"
|
"quyun/v2/app/middlewares"
|
||||||
"quyun/v2/app/requests"
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
|
|
||||||
"go.ipao.vip/gen/field"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
_ "go.ipao.vip/atom"
|
_ "go.ipao.vip/atom"
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"quyun/v2/app/services"
|
"quyun/v2/app/services"
|
||||||
|
"quyun/v2/providers/jwt"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @provider
|
// @provider
|
||||||
type auth struct{}
|
type auth struct {
|
||||||
|
jwt *jwt.JWT
|
||||||
|
}
|
||||||
|
|
||||||
// Phone
|
// Phone
|
||||||
//
|
//
|
||||||
// @Summary 手机验证
|
// @Summary 手机验证
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} requests.Pager{items=PostItem} "成功"
|
// @Param form body PhoneValidationForm true "手机号"
|
||||||
// @Router /v1/auth/:phone [post]
|
// @Success 200 {object} any "成功"
|
||||||
// @Bind phone path
|
// @Router /v1/auth/phone [post]
|
||||||
func (ctl *posts) Phone(ctx fiber.Ctx, phone string) error {
|
// @Bind form body
|
||||||
_, err := services.Users.FindByPhone(ctx, phone)
|
func (ctl *auth) Phone(ctx fiber.Ctx, form *PhoneValidationForm) error {
|
||||||
|
_, err := services.Users.FindByPhone(ctx, form.Phone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return errors.New("手机号未注册,请联系管理员开通")
|
return errors.New("手机号未注册,请联系管理员开通")
|
||||||
@@ -32,3 +34,40 @@ func (ctl *posts) Phone(ctx fiber.Ctx, phone string) error {
|
|||||||
// TODO: send sms
|
// TODO: send sms
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// @Param body body PhoneValidationForm true "请求体"
|
||||||
|
// @Success 200 {object} any "成功"
|
||||||
|
// @Router /v1/auth/validate [post]
|
||||||
|
// @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
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func (ctl *posts) List(
|
|||||||
}
|
}
|
||||||
|
|
||||||
postIds := lo.Map(pager.Items.([]*models.Post), func(item *models.Post, _ int) int64 { return item.ID })
|
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)
|
userBoughtIds, err := services.Users.BatchCheckHasBought(ctx, user.ID, postIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("BatchCheckHasBought err: %v", err)
|
log.WithError(err).Errorf("BatchCheckHasBought err: %v", err)
|
||||||
@@ -146,10 +146,14 @@ func (ctl *posts) Show(ctx fiber.Ctx, post *models.Post, user *models.User) (*Po
|
|||||||
return nil, fiber.ErrNotFound
|
return nil, fiber.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
bought, err := services.Users.HasBought(ctx, user.ID, post.ID)
|
var err error
|
||||||
|
bought := false
|
||||||
|
if user != nil {
|
||||||
|
bought, err = services.Users.HasBought(ctx, user.ID, post.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
medias, err := services.Posts.GetMediasByIds(ctx, post.HeadImages.Data())
|
medias, err := services.Posts.GetMediasByIds(ctx, post.HeadImages.Data())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -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",
|
// Url: "https://github.com/mediaelement/mediaelement-files/raw/refs/heads/master/big_buck_bunny.mp4",
|
||||||
// }, nil
|
// }, nil
|
||||||
|
|
||||||
preview := false
|
preview := true
|
||||||
bought, err := services.Users.HasBought(ctx, user.ID, post.ID)
|
bought, err := services.Users.HasBought(ctx, user.ID, post.ID)
|
||||||
if !bought || err != nil {
|
if err != nil {
|
||||||
preview = true
|
preview = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if bought {
|
||||||
|
preview = false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Fetching play URL for post ID: %d", post.ID)
|
log.Infof("Fetching play URL for post ID: %d", post.ID)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"quyun/v2/providers/ali"
|
"quyun/v2/providers/ali"
|
||||||
"quyun/v2/providers/app"
|
"quyun/v2/providers/app"
|
||||||
"quyun/v2/providers/job"
|
"quyun/v2/providers/job"
|
||||||
|
"quyun/v2/providers/jwt"
|
||||||
|
|
||||||
"go.ipao.vip/atom"
|
"go.ipao.vip/atom"
|
||||||
"go.ipao.vip/atom/container"
|
"go.ipao.vip/atom/container"
|
||||||
@@ -13,6 +14,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Provide(opts ...opt.Option) error {
|
func Provide(opts ...opt.Option) error {
|
||||||
|
if err := container.Container.Provide(func(
|
||||||
|
jwt *jwt.JWT,
|
||||||
|
) (*auth, error) {
|
||||||
|
obj := &auth{
|
||||||
|
jwt: jwt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj, nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := container.Container.Provide(func(
|
if err := container.Container.Provide(func(
|
||||||
app *app.Config,
|
app *app.Config,
|
||||||
job *job.Job,
|
job *job.Job,
|
||||||
@@ -29,11 +41,13 @@ func Provide(opts ...opt.Option) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := container.Container.Provide(func(
|
if err := container.Container.Provide(func(
|
||||||
|
auth *auth,
|
||||||
middlewares *middlewares.Middlewares,
|
middlewares *middlewares.Middlewares,
|
||||||
posts *posts,
|
posts *posts,
|
||||||
users *users,
|
users *users,
|
||||||
) (contracts.HttpRoute, error) {
|
) (contracts.HttpRoute, error) {
|
||||||
obj := &Routes{
|
obj := &Routes{
|
||||||
|
auth: auth,
|
||||||
middlewares: middlewares,
|
middlewares: middlewares,
|
||||||
posts: posts,
|
posts: posts,
|
||||||
users: users,
|
users: users,
|
||||||
|
|||||||
@@ -5,12 +5,11 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go.ipao.vip/gen/field"
|
||||||
"quyun/v2/app/middlewares"
|
"quyun/v2/app/middlewares"
|
||||||
"quyun/v2/app/requests"
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
|
|
||||||
"go.ipao.vip/gen/field"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
_ "go.ipao.vip/atom"
|
_ "go.ipao.vip/atom"
|
||||||
@@ -26,6 +25,7 @@ type Routes struct {
|
|||||||
log *log.Entry `inject:"false"`
|
log *log.Entry `inject:"false"`
|
||||||
middlewares *middlewares.Middlewares
|
middlewares *middlewares.Middlewares
|
||||||
// Controller instances
|
// Controller instances
|
||||||
|
auth *auth
|
||||||
posts *posts
|
posts *posts
|
||||||
users *users
|
users *users
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,17 @@ func (r *Routes) Name() string {
|
|||||||
// Register registers all HTTP routes with the provided fiber router.
|
// Register registers all HTTP routes with the provided fiber router.
|
||||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||||
func (r *Routes) Register(router fiber.Router) {
|
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
|
// Register routes for controller: posts
|
||||||
r.log.Debugf("Registering route: Get /v1/posts -> posts.List")
|
r.log.Debugf("Registering route: Get /v1/posts -> posts.List")
|
||||||
router.Get("/v1/posts"[len(r.Path()):], DataFunc3(
|
router.Get("/v1/posts"[len(r.Path()):], DataFunc3(
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ func (f *Middlewares) AuthFrontend(ctx fiber.Ctx) error {
|
|||||||
return ctx.Next()
|
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 {
|
if f.app.IsDevMode() && false {
|
||||||
user, err := services.Users.FindByID(ctx.Context(), 1001)
|
user, err := services.Users.FindByID(ctx.Context(), 1001)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,10 +46,9 @@ func (f *Middlewares) AuthFrontend(ctx fiber.Ctx) error {
|
|||||||
u.Path = "/v1/auth/phone"
|
u.Path = "/v1/auth/phone"
|
||||||
fullUrl = u.String()
|
fullUrl = u.String()
|
||||||
|
|
||||||
// check cookie exists
|
// 仅使用 Header 的 Bearer Token(前端 localStorage 存储,随请求透传)。
|
||||||
cookie := ctx.Cookies("token")
|
token := ctx.Get("Authorization")
|
||||||
log.Infof("cookie: %s", cookie)
|
if token == "" {
|
||||||
if cookie == "" {
|
|
||||||
log.Infof("auth redirect_uri: %s", fullUrl)
|
log.Infof("auth redirect_uri: %s", fullUrl)
|
||||||
if ctx.XHR() {
|
if ctx.XHR() {
|
||||||
return ctx.SendStatus(fiber.StatusUnauthorized)
|
return ctx.SendStatus(fiber.StatusUnauthorized)
|
||||||
@@ -47,10 +56,8 @@ func (f *Middlewares) AuthFrontend(ctx fiber.Ctx) error {
|
|||||||
return ctx.Redirect().To(fullUrl)
|
return ctx.Redirect().To(fullUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, err := f.jwt.Parse(cookie)
|
jwt, err := f.jwt.Parse(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// remove cookie
|
|
||||||
ctx.ClearCookie("token")
|
|
||||||
if ctx.XHR() {
|
if ctx.XHR() {
|
||||||
return ctx.SendStatus(fiber.StatusUnauthorized)
|
return ctx.SendStatus(fiber.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
@@ -59,8 +66,6 @@ func (f *Middlewares) AuthFrontend(ctx fiber.Ctx) error {
|
|||||||
|
|
||||||
user, err := services.Users.FindByID(ctx.Context(), jwt.UserID)
|
user, err := services.Users.FindByID(ctx.Context(), jwt.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// remove cookie
|
|
||||||
ctx.ClearCookie("token")
|
|
||||||
if ctx.XHR() {
|
if ctx.XHR() {
|
||||||
return ctx.SendStatus(fiber.StatusUnauthorized)
|
return ctx.SendStatus(fiber.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/auth/{phone}": {
|
"/v1/auth/phone": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -707,24 +707,49 @@ const docTemplate = `{
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "手机验证",
|
"summary": "手机验证",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "手机号",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/http.PhoneValidationForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "成功",
|
"description": "成功",
|
||||||
"schema": {
|
"schema": {}
|
||||||
"allOf": [
|
}
|
||||||
{
|
}
|
||||||
"$ref": "#/definitions/requests.Pager"
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/auth/validate": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "手机验证",
|
||||||
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "object",
|
"description": "请求体",
|
||||||
"properties": {
|
"name": "body",
|
||||||
"items": {
|
"in": "body",
|
||||||
"$ref": "#/definitions/http.PostItem"
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/http.PhoneValidationForm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
"responses": {
|
||||||
}
|
"200": {
|
||||||
|
"description": "成功",
|
||||||
|
"schema": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1280,6 +1305,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http.PhoneValidationForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"http.PlayUrl": {
|
"http.PlayUrl": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1379,10 +1415,7 @@ const docTemplate = `{
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"metas": {
|
"metas": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/types.JSONType-fields_MediaMetas"
|
||||||
"items": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"mime_type": {
|
"mime_type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1589,6 +1622,9 @@ const docTemplate = `{
|
|||||||
"types.JSONType-array_string": {
|
"types.JSONType-array_string": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"types.JSONType-fields_MediaMetas": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"types.JSONType-fields_OrderMeta": {
|
"types.JSONType-fields_OrderMeta": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -692,7 +692,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/auth/{phone}": {
|
"/v1/auth/phone": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -701,24 +701,49 @@
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "手机验证",
|
"summary": "手机验证",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "手机号",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/http.PhoneValidationForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "成功",
|
"description": "成功",
|
||||||
"schema": {
|
"schema": {}
|
||||||
"allOf": [
|
}
|
||||||
{
|
}
|
||||||
"$ref": "#/definitions/requests.Pager"
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/auth/validate": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "手机验证",
|
||||||
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "object",
|
"description": "请求体",
|
||||||
"properties": {
|
"name": "body",
|
||||||
"items": {
|
"in": "body",
|
||||||
"$ref": "#/definitions/http.PostItem"
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/http.PhoneValidationForm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
"responses": {
|
||||||
}
|
"200": {
|
||||||
|
"description": "成功",
|
||||||
|
"schema": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1274,6 +1299,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http.PhoneValidationForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"http.PlayUrl": {
|
"http.PlayUrl": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1373,10 +1409,7 @@
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"metas": {
|
"metas": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/types.JSONType-fields_MediaMetas"
|
||||||
"items": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"mime_type": {
|
"mime_type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1583,6 +1616,9 @@
|
|||||||
"types.JSONType-array_string": {
|
"types.JSONType-array_string": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"types.JSONType-fields_MediaMetas": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"types.JSONType-fields_OrderMeta": {
|
"types.JSONType-fields_OrderMeta": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -164,6 +164,13 @@ definitions:
|
|||||||
description: Valid is true if Time is not NULL
|
description: Valid is true if Time is not NULL
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
http.PhoneValidationForm:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
http.PlayUrl:
|
http.PlayUrl:
|
||||||
properties:
|
properties:
|
||||||
url:
|
url:
|
||||||
@@ -229,9 +236,7 @@ definitions:
|
|||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
metas:
|
metas:
|
||||||
items:
|
$ref: '#/definitions/types.JSONType-fields_MediaMetas'
|
||||||
type: integer
|
|
||||||
type: array
|
|
||||||
mime_type:
|
mime_type:
|
||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
@@ -367,6 +372,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
types.JSONType-array_string:
|
types.JSONType-array_string:
|
||||||
type: object
|
type: object
|
||||||
|
types.JSONType-fields_MediaMetas:
|
||||||
|
type: object
|
||||||
types.JSONType-fields_OrderMeta:
|
types.JSONType-fields_OrderMeta:
|
||||||
type: object
|
type: object
|
||||||
types.JSONType-fields_UserAuthToken:
|
types.JSONType-fields_UserAuthToken:
|
||||||
@@ -830,20 +837,39 @@ paths:
|
|||||||
summary: 调整用户余额
|
summary: 调整用户余额
|
||||||
tags:
|
tags:
|
||||||
- Admin Users
|
- Admin Users
|
||||||
/v1/auth/{phone}:
|
/v1/auth/phone:
|
||||||
post:
|
post:
|
||||||
|
parameters:
|
||||||
|
- description: 手机号
|
||||||
|
in: body
|
||||||
|
name: form
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/http.PhoneValidationForm'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: 成功
|
description: 成功
|
||||||
|
schema: {}
|
||||||
|
summary: 手机验证
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
/v1/auth/validate:
|
||||||
|
post:
|
||||||
|
parameters:
|
||||||
|
- description: 请求体
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
schema:
|
schema:
|
||||||
allOf:
|
$ref: '#/definitions/http.PhoneValidationForm'
|
||||||
- $ref: '#/definitions/requests.Pager'
|
produces:
|
||||||
- properties:
|
- application/json
|
||||||
items:
|
responses:
|
||||||
$ref: '#/definitions/http.PostItem'
|
"200":
|
||||||
type: object
|
description: 成功
|
||||||
|
schema: {}
|
||||||
summary: 手机验证
|
summary: 手机验证
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
|
|||||||
14
frontend/wechat/src/api/authApi.js
Normal file
14
frontend/wechat/src/api/authApi.js
Normal file
@@ -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(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
function getCurrentPathWithQueryHash() {
|
||||||
|
const { pathname, search, hash } = window.location;
|
||||||
|
return `${pathname}${search}${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Create axios instance with default config
|
// Create axios instance with default config
|
||||||
const client = axios.create({
|
const client = axios.create({
|
||||||
baseURL: '/v1',
|
baseURL: '/v1',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
withCredentials: true,
|
withCredentials: false,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
@@ -16,6 +21,11 @@ const client = axios.create({
|
|||||||
// Request interceptor
|
// Request interceptor
|
||||||
client.interceptors.request.use(
|
client.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers = config.headers || {};
|
||||||
|
config.headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
@@ -29,13 +39,20 @@ client.interceptors.response.use(
|
|||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
const noAuthRedirect = Boolean(error?.config?.meta?.noAuthRedirect);
|
||||||
// Handle HTTP errors here
|
// Handle HTTP errors here
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// Handle 401 Unauthorized error
|
// Handle 401 Unauthorized error
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
const redirectUrl = encodeURIComponent(window.location.href);
|
if (noAuthRedirect) {
|
||||||
window.location.href = `/v1/auth/wechat?redirect=${redirectUrl}`;
|
return Promise.reject(error);
|
||||||
return;
|
}
|
||||||
|
const redirectPath = encodeURIComponent(getCurrentPathWithQueryHash());
|
||||||
|
sessionStorage.setItem('post_auth_redirect', getCurrentPathWithQueryHash());
|
||||||
|
sessionStorage.removeItem('phone_verified');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = `/verify-phone?redirect=${redirectPath}`;
|
||||||
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
// Server responded with error status
|
// Server responded with error status
|
||||||
console.error('API Error:', error.response.status, error.response.data);
|
console.error('API Error:', error.response.status, error.response.data);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import client from './client';
|
|||||||
export const postApi = {
|
export const postApi = {
|
||||||
list({ page = 1, limit = 10, keyword = '' } = {}) {
|
list({ page = 1, limit = 10, keyword = '' } = {}) {
|
||||||
return client.get('/posts', {
|
return client.get('/posts', {
|
||||||
|
meta: { noAuthRedirect: true },
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
@@ -12,10 +13,10 @@ export const postApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
play(id) {
|
play(id) {
|
||||||
return client.get(`/posts/${id}/play`);
|
return client.get(`/posts/${id}/play`, { meta: { noAuthRedirect: true } });
|
||||||
},
|
},
|
||||||
show(id) {
|
show(id) {
|
||||||
return client.get(`/posts/${id}/show`);
|
return client.get(`/posts/${id}/show`, { meta: { noAuthRedirect: true } });
|
||||||
},
|
},
|
||||||
mine({ page = 1, limit = 10 } = {}) {
|
mine({ page = 1, limit = 10 } = {}) {
|
||||||
return client.get('/posts/mine', {
|
return client.get('/posts/mine', {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
const isAuthed = () => Boolean(localStorage.getItem('token'))
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: '列表', route: '/', icon: AiOutlineHome },
|
{ label: '列表', route: '/', icon: AiOutlineHome },
|
||||||
@@ -13,6 +14,11 @@ const tabs = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const switchTab = (index, route) => {
|
const switchTab = (index, route) => {
|
||||||
|
if ((route === '/purchased' || route === '/profile') && !isAuthed()) {
|
||||||
|
const redirect = encodeURIComponent(route)
|
||||||
|
router.replace(`/verify-phone?redirect=${redirect}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
activeTab.value = index
|
activeTab.value = index
|
||||||
router.replace(route)
|
router.replace(route)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ const routes = [
|
|||||||
path: '/posts/:id',
|
path: '/posts/:id',
|
||||||
name: 'article-detail',
|
name: 'article-detail',
|
||||||
component: () => import('@/views/ArticleDetail.vue')
|
component: () => import('@/views/ArticleDetail.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/verify-phone',
|
||||||
|
name: 'verify-phone',
|
||||||
|
component: () => import('@/views/VerifyPhone.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ const updateMediaSource = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBuy = async () => {
|
const handleBuy = async () => {
|
||||||
|
if (!localStorage.getItem('token')) {
|
||||||
|
const redirect = encodeURIComponent(router.currentRoute.value.fullPath);
|
||||||
|
router.replace(`/verify-phone?redirect=${redirect}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// confirm
|
// confirm
|
||||||
if (!confirm("确认购买该曲谱?")) {
|
if (!confirm("确认购买该曲谱?")) {
|
||||||
return;
|
return;
|
||||||
@@ -137,7 +142,7 @@ const handleBuy = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initiate payment:", error);
|
console.error("Failed to initiate payment:", error);
|
||||||
// alert("发起支付失败,请稍后重试");
|
// alert("发起支付失败,请稍后重试");
|
||||||
alert(error.response?.data || "发起支付失败,请稍后重试");
|
alert(error.response?.data?.message || "发起支付失败,请稍后重试");
|
||||||
} finally {
|
} finally {
|
||||||
buying.value = false;
|
buying.value = false;
|
||||||
}
|
}
|
||||||
@@ -209,7 +214,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div v-if="article && !article.bought" class="bg-white border-b border-gray-200">
|
<div v-if="article && !article.bought" class="bg-white border-b border-gray-200">
|
||||||
<div v-if="article.recharge_wechat" class="text-sm bg-orange-500 text-white px-4 py-2">
|
<div v-if="article.recharge_wechat" class="text-sm bg-orange-500 text-white px-4 py-2">
|
||||||
注意:未购买视频仅可预览 1 分钟,购买后可观看全集。账户充值购买联系微信:<span class="font-bold">{{ article.recharge_wechat }}</span>
|
注意:未购买视频仅可预览 1 分钟,购买后可观看全集。账户充值购买联系微信:<span class="font-bold">{{ article.recharge_wechat
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between max-w-md mx-auto p-4">
|
<div class="flex items-center justify-between max-w-md mx-auto p-4">
|
||||||
<div class="text-orange-600 text-2xl">
|
<div class="text-orange-600 text-2xl">
|
||||||
|
|||||||
129
frontend/wechat/src/views/VerifyPhone.vue
Normal file
129
frontend/wechat/src/views/VerifyPhone.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<script setup>
|
||||||
|
import { authApi } from "@/api/authApi";
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const phone = ref("");
|
||||||
|
const code = ref("");
|
||||||
|
const sending = ref(false);
|
||||||
|
const verifying = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let countdownTimer = null;
|
||||||
|
|
||||||
|
const canSend = computed(() => !sending.value && countdown.value <= 0 && phone.value.trim().length >= 6);
|
||||||
|
const canVerify = computed(() => !verifying.value && phone.value.trim() !== "" && code.value.trim() !== "");
|
||||||
|
|
||||||
|
const startCountdown = (seconds = 60) => {
|
||||||
|
countdown.value = seconds;
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value -= 1;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
countdownTimer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!canSend.value) return;
|
||||||
|
sending.value = true;
|
||||||
|
try {
|
||||||
|
await authApi.sendPhoneSms(phone.value);
|
||||||
|
startCountdown(60);
|
||||||
|
alert("验证码已发送");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
alert(error?.response?.data?.message || "发送验证码失败");
|
||||||
|
} finally {
|
||||||
|
sending.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRedirect = (value) => {
|
||||||
|
if (!value) return null;
|
||||||
|
try {
|
||||||
|
const decoded = decodeURIComponent(String(value));
|
||||||
|
if (decoded.startsWith("/")) return decoded;
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
if (!canVerify.value) return;
|
||||||
|
verifying.value = true;
|
||||||
|
try {
|
||||||
|
const resp = await authApi.validateSmsCode(phone.value, code.value);
|
||||||
|
const token = resp?.data?.token || resp?.data?.Token;
|
||||||
|
if (!token) {
|
||||||
|
alert("验证失败:未返回 token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem("token", String(token));
|
||||||
|
sessionStorage.setItem("phone_verified", "1");
|
||||||
|
const redirect =
|
||||||
|
normalizeRedirect(route.query.redirect) ||
|
||||||
|
normalizeRedirect(sessionStorage.getItem("post_auth_redirect")) ||
|
||||||
|
"/";
|
||||||
|
sessionStorage.removeItem("post_auth_redirect");
|
||||||
|
router.replace(redirect);
|
||||||
|
} catch (error) {
|
||||||
|
alert(error?.response?.data?.message || "验证失败");
|
||||||
|
} finally {
|
||||||
|
verifying.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const redirect = normalizeRedirect(route.query.redirect);
|
||||||
|
if (redirect) sessionStorage.setItem("post_auth_redirect", redirect);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 flex items-center justify-center px-4">
|
||||||
|
<div class="w-full max-w-[420px] bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">手机号验证</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">
|
||||||
|
未认证用户需要验证手机号后才能访问「已购买」「我的」并进行购买操作。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">手机号</label>
|
||||||
|
<input v-model="phone" inputmode="tel" autocomplete="tel" placeholder="请输入手机号"
|
||||||
|
class="w-full rounded-xl border border-gray-200 px-4 py-3 outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">验证码</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input v-model="code" inputmode="numeric" autocomplete="one-time-code" placeholder="请输入短信验证码"
|
||||||
|
class="flex-1 rounded-xl border border-gray-200 px-4 py-3 outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-300" />
|
||||||
|
<button :disabled="!canSend" @click="handleSend"
|
||||||
|
class="whitespace-nowrap rounded-xl px-4 py-3 text-sm font-medium border transition-colors"
|
||||||
|
:class="canSend ? 'bg-primary-600 text-white border-primary-600 hover:bg-primary-700 active:bg-primary-800' : 'bg-gray-100 text-gray-400 border-gray-200'">
|
||||||
|
<span v-if="countdown > 0">{{ countdown }}s</span>
|
||||||
|
<span v-else>发送验证码</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button :disabled="!canVerify" @click="handleVerify"
|
||||||
|
class="w-full rounded-xl px-4 py-3 font-semibold transition-colors"
|
||||||
|
:class="canVerify ? 'bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800' : 'bg-gray-100 text-gray-400'">
|
||||||
|
{{ verifying ? "验证中..." : "验证并继续" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="w-full text-sm text-gray-500 hover:text-gray-700" @click="router.replace('/')">
|
||||||
|
暂不验证,返回首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,10 @@ import { visualizer } from 'rollup-plugin-visualizer';
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import compression from 'vite-plugin-compression';
|
import compression from 'vite-plugin-compression';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(() => {
|
||||||
|
const backendUrl = process.env.VITE_BACKEND_URL || 'http://127.0.0.1:8088';
|
||||||
|
|
||||||
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
compression({
|
compression({
|
||||||
@@ -31,7 +34,11 @@ export default defineConfig({
|
|||||||
port: 3001,
|
port: 3001,
|
||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/v1': 'http://localhost:8088',
|
'/v1': {
|
||||||
|
target: backendUrl,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -53,4 +60,5 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user