diff --git a/backend/app/http/admin/orders.go b/backend/app/http/admin/orders.go index 0006109..604bd5a 100644 --- a/backend/app/http/admin/orders.go +++ b/backend/app/http/admin/orders.go @@ -33,8 +33,9 @@ func (ctl *orders) List(ctx fiber.Ctx, pagination *requests.Pagination, query *O } // Refund -// @Router /admin/orders/:id/refund [post] -// @Bind id path +// +// @Router /admin/orders/:id/refund [post] +// @Bind id path func (ctl *orders) Refund(ctx fiber.Ctx, id int64) error { order, err := model.OrdersModel().GetByID(ctx.Context(), id) if err != nil { diff --git a/backend/app/http/admin/routes.gen.go b/backend/app/http/admin/routes.gen.go index dbfb2be..b91dfdf 100644 --- a/backend/app/http/admin/routes.gen.go +++ b/backend/app/http/admin/routes.gen.go @@ -3,15 +3,16 @@ package admin import ( + "quyun/app/requests" + "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" _ "go.ipao.vip/atom/contracts" . "go.ipao.vip/atom/fen" - "quyun/app/requests" ) -// @provider contracts.HttpRoute atom.GroupRoutes +// @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { log *log.Entry `inject:"false"` auth *auth diff --git a/backend/app/http/routes.gen.go b/backend/app/http/routes.gen.go index a63ae10..ff30fd1 100644 --- a/backend/app/http/routes.gen.go +++ b/backend/app/http/routes.gen.go @@ -3,16 +3,17 @@ package http import ( + "quyun/app/model" + "quyun/app/requests" + "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" _ "go.ipao.vip/atom/contracts" . "go.ipao.vip/atom/fen" - "quyun/app/model" - "quyun/app/requests" ) -// @provider contracts.HttpRoute atom.GroupRoutes +// @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { log *log.Entry `inject:"false"` auth *auth diff --git a/backend/app/http/wechats.go b/backend/app/http/wechats.go index bf374d4..78aa035 100644 --- a/backend/app/http/wechats.go +++ b/backend/app/http/wechats.go @@ -16,9 +16,10 @@ type wechats struct { } // GetJsSDK -// @Router /wechats/js-sdk [get] -// @Bind url query -// @Bind user local +// +// @Router /wechats/js-sdk [get] +// @Bind url query +// @Bind user local func (ctl *wechats) GetJsSDK(ctx fiber.Ctx, url string, user *model.Users) (*wechat.JsSDK, error) { if user.AuthToken.Data.StableExpiresAt.Before(time.Now()) { token, err := ctl.wechat.RefreshAccessToken(user.AuthToken.Data.RefreshToken) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index ea53106..acbe51e 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -23,7 +23,150 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {}, + "paths": { + "/admin/auth": { + "post": { + "responses": {} + } + }, + "/admin/medias": { + "get": { + "responses": {} + } + }, + "/admin/medias/{id}": { + "get": { + "responses": {} + }, + "delete": { + "responses": {} + } + }, + "/admin/orders": { + "get": { + "responses": {} + } + }, + "/admin/orders/{id}/refund": { + "post": { + "responses": {} + } + }, + "/admin/posts": { + "get": { + "responses": {} + }, + "post": { + "responses": {} + } + }, + "/admin/posts/{id}": { + "get": { + "responses": {} + }, + "put": { + "responses": {} + }, + "delete": { + "responses": {} + } + }, + "/admin/posts/{id}/send-to/{userId}": { + "post": { + "responses": {} + } + }, + "/admin/statistics": { + "get": { + "responses": {} + } + }, + "/admin/uploads/post-uploaded-action": { + "post": { + "responses": {} + } + }, + "/admin/uploads/pre-uploaded-check/{md5}.{ext}": { + "get": { + "responses": {} + } + }, + "/admin/users": { + "get": { + "responses": {} + } + }, + "/admin/users/{id}": { + "get": { + "responses": {} + } + }, + "/admin/users/{id}/articles": { + "get": { + "responses": {} + } + }, + "/admin/users/{id}/balance": { + "post": { + "responses": {} + } + }, + "/auth/login": { + "get": { + "responses": {} + } + }, + "/auth/wechat": { + "get": { + "responses": {} + } + }, + "/pay/callback/{channel}": { + "post": { + "responses": {} + } + }, + "/posts": { + "get": { + "responses": {} + } + }, + "/posts/mine": { + "get": { + "responses": {} + } + }, + "/posts/{id}/buy": { + "post": { + "responses": {} + } + }, + "/posts/{id}/play": { + "get": { + "responses": {} + } + }, + "/posts/{id}/show": { + "get": { + "responses": {} + } + }, + "/users/profile": { + "get": { + "responses": {} + } + }, + "/users/username": { + "put": { + "responses": {} + } + }, + "/wechats/js-sdk": { + "get": { + "responses": {} + } + } + }, "securityDefinitions": { "BasicAuth": { "type": "basic" diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index b5640f4..d36562c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -17,7 +17,150 @@ }, "host": "localhost:8080", "basePath": "/api/v1", - "paths": {}, + "paths": { + "/admin/auth": { + "post": { + "responses": {} + } + }, + "/admin/medias": { + "get": { + "responses": {} + } + }, + "/admin/medias/{id}": { + "get": { + "responses": {} + }, + "delete": { + "responses": {} + } + }, + "/admin/orders": { + "get": { + "responses": {} + } + }, + "/admin/orders/{id}/refund": { + "post": { + "responses": {} + } + }, + "/admin/posts": { + "get": { + "responses": {} + }, + "post": { + "responses": {} + } + }, + "/admin/posts/{id}": { + "get": { + "responses": {} + }, + "put": { + "responses": {} + }, + "delete": { + "responses": {} + } + }, + "/admin/posts/{id}/send-to/{userId}": { + "post": { + "responses": {} + } + }, + "/admin/statistics": { + "get": { + "responses": {} + } + }, + "/admin/uploads/post-uploaded-action": { + "post": { + "responses": {} + } + }, + "/admin/uploads/pre-uploaded-check/{md5}.{ext}": { + "get": { + "responses": {} + } + }, + "/admin/users": { + "get": { + "responses": {} + } + }, + "/admin/users/{id}": { + "get": { + "responses": {} + } + }, + "/admin/users/{id}/articles": { + "get": { + "responses": {} + } + }, + "/admin/users/{id}/balance": { + "post": { + "responses": {} + } + }, + "/auth/login": { + "get": { + "responses": {} + } + }, + "/auth/wechat": { + "get": { + "responses": {} + } + }, + "/pay/callback/{channel}": { + "post": { + "responses": {} + } + }, + "/posts": { + "get": { + "responses": {} + } + }, + "/posts/mine": { + "get": { + "responses": {} + } + }, + "/posts/{id}/buy": { + "post": { + "responses": {} + } + }, + "/posts/{id}/play": { + "get": { + "responses": {} + } + }, + "/posts/{id}/show": { + "get": { + "responses": {} + } + }, + "/users/profile": { + "get": { + "responses": {} + } + }, + "/users/username": { + "put": { + "responses": {} + } + }, + "/wechats/js-sdk": { + "get": { + "responses": {} + } + } + }, "securityDefinitions": { "BasicAuth": { "type": "basic" diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 9f48b7e..701e1ea 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -15,7 +15,93 @@ info: termsOfService: http://swagger.io/terms/ title: ApiDoc version: "1.0" -paths: {} +paths: + /admin/auth: + post: + responses: {} + /admin/medias: + get: + responses: {} + /admin/medias/{id}: + delete: + responses: {} + get: + responses: {} + /admin/orders: + get: + responses: {} + /admin/orders/{id}/refund: + post: + responses: {} + /admin/posts: + get: + responses: {} + post: + responses: {} + /admin/posts/{id}: + delete: + responses: {} + get: + responses: {} + put: + responses: {} + /admin/posts/{id}/send-to/{userId}: + post: + responses: {} + /admin/statistics: + get: + responses: {} + /admin/uploads/post-uploaded-action: + post: + responses: {} + /admin/uploads/pre-uploaded-check/{md5}.{ext}: + get: + responses: {} + /admin/users: + get: + responses: {} + /admin/users/{id}: + get: + responses: {} + /admin/users/{id}/articles: + get: + responses: {} + /admin/users/{id}/balance: + post: + responses: {} + /auth/login: + get: + responses: {} + /auth/wechat: + get: + responses: {} + /pay/callback/{channel}: + post: + responses: {} + /posts: + get: + responses: {} + /posts/{id}/buy: + post: + responses: {} + /posts/{id}/play: + get: + responses: {} + /posts/{id}/show: + get: + responses: {} + /posts/mine: + get: + responses: {} + /users/profile: + get: + responses: {} + /users/username: + put: + responses: {} + /wechats/js-sdk: + get: + responses: {} securityDefinitions: BasicAuth: type: basic diff --git a/backend_v1/app/http/admin/provider.gen.go b/backend_v1/app/http/admin/provider.gen.go index 4011648..53f02b6 100755 --- a/backend_v1/app/http/admin/provider.gen.go +++ b/backend_v1/app/http/admin/provider.gen.go @@ -50,12 +50,20 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func() (*smsCodeSends, error) { + obj := &smsCodeSends{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func( auth *auth, medias *medias, middlewares *middlewares.Middlewares, orders *orders, posts *posts, + smsCodeSends *smsCodeSends, statistics *statistics, uploads *uploads, users *users, @@ -66,6 +74,7 @@ func Provide(opts ...opt.Option) error { middlewares: middlewares, orders: orders, posts: posts, + smsCodeSends: smsCodeSends, statistics: statistics, uploads: uploads, users: users, diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go index 1266da8..3bd9def 100644 --- a/backend_v1/app/http/admin/routes.gen.go +++ b/backend_v1/app/http/admin/routes.gen.go @@ -30,6 +30,7 @@ type Routes struct { medias *medias orders *orders posts *posts + smsCodeSends *smsCodeSends statistics *statistics uploads *uploads users *users @@ -149,6 +150,12 @@ func (r *Routes) Register(router fiber.Router) { }, Body[PostForm]("form"), )) + // Register routes for controller: smsCodeSends + r.log.Debugf("Registering route: Get /admin/v1/sms-code-sends -> smsCodeSends.List") + router.Get("/admin/v1/sms-code-sends"[len(r.Path()):], DataFunc1( + r.smsCodeSends.List, + Query[dto.SmsCodeSendListQuery]("query"), + )) // Register routes for controller: statistics r.log.Debugf("Registering route: Get /admin/v1/statistics -> statistics.statistics") router.Get("/admin/v1/statistics"[len(r.Path()):], DataFunc0( diff --git a/backend_v1/app/http/admin/sms_code_sends.go b/backend_v1/app/http/admin/sms_code_sends.go new file mode 100644 index 0000000..8d7c427 --- /dev/null +++ b/backend_v1/app/http/admin/sms_code_sends.go @@ -0,0 +1,61 @@ +package admin + +import ( + "quyun/v2/app/http/dto" + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type smsCodeSends struct{} + +// List +// +// @Summary 短信验证码发送记录 +// @Tags Admin SMS +// @Produce json +// @Param query query dto.SmsCodeSendListQuery false "筛选条件" +// @Success 200 {object} requests.Pager{items=models.SmsCodeSend} "成功" +// @Router /admin/v1/sms-code-sends [get] +// @Bind query query +func (ctl *smsCodeSends) List(ctx fiber.Ctx, query *dto.SmsCodeSendListQuery) (*requests.Pager, error) { + if query.Pagination == nil { + query.Pagination = &requests.Pagination{} + } + query.Pagination.Format() + + db := services.DB() + if db == nil { + return &requests.Pager{Pagination: *query.Pagination, Total: 0, Items: []*models.SmsCodeSend{}}, nil + } + + q := db.WithContext(ctx.Context()).Model(&models.SmsCodeSend{}) + if query.Phone != nil && *query.Phone != "" { + q = q.Where("phone LIKE ?", database.WrapLike(*query.Phone)) + } + + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, err + } + + var items []*models.SmsCodeSend + if err := q. + Order("sent_at desc"). + Limit(int(query.Pagination.Limit)). + Offset(int(query.Pagination.Offset())). + Find(&items).Error; err != nil { + return nil, err + } + + return &requests.Pager{ + Pagination: *query.Pagination, + Total: total, + Items: items, + }, nil +} + diff --git a/backend_v1/app/http/dto/sms_code_send.go b/backend_v1/app/http/dto/sms_code_send.go new file mode 100644 index 0000000..0a84b89 --- /dev/null +++ b/backend_v1/app/http/dto/sms_code_send.go @@ -0,0 +1,10 @@ +package dto + +import "quyun/v2/app/requests" + +type SmsCodeSendListQuery struct { + *requests.Pagination + + Phone *string `query:"phone"` +} + diff --git a/backend_v1/app/services/db.go b/backend_v1/app/services/db.go new file mode 100644 index 0000000..c08edec --- /dev/null +++ b/backend_v1/app/services/db.go @@ -0,0 +1,6 @@ +package services + +import "gorm.io/gorm" + +func DB() *gorm.DB { return _db } + diff --git a/backend_v1/app/services/users.go b/backend_v1/app/services/users.go index a308542..2e77d58 100644 --- a/backend_v1/app/services/users.go +++ b/backend_v1/app/services/users.go @@ -392,14 +392,25 @@ func (m *users) SendPhoneCode(ctx context.Context, phone string) error { } // 生成/覆盖验证码:同一手机号再次发送时以最新验证码为准 + expiresAt := now.Add(5 * time.Minute) m.codeByPhone[phone] = phoneCodeEntry{ code: code, - expiresAt: now.Add(5 * time.Minute), + expiresAt: expiresAt, } m.lastSentAtByPhone[phone] = now // log phone and code log.Infof("SendPhoneCode to %s: code=%s", phone, code) + if _db != nil { + // 记录短信验证码发送日志(用于后台审计与排查)。 + _ = _db.WithContext(ctx).Create(&models.SmsCodeSend{ + Phone: phone, + Code: code, + SentAt: now, + ExpiresAt: expiresAt, + }).Error + } + return nil } diff --git a/backend_v1/database/migrations/20251223170000_create_sms_code_sends.sql b/backend_v1/database/migrations/20251223170000_create_sms_code_sends.sql new file mode 100644 index 0000000..8d7fc2e --- /dev/null +++ b/backend_v1/database/migrations/20251223170000_create_sms_code_sends.sql @@ -0,0 +1,18 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE sms_code_sends( + id SERIAL8 PRIMARY KEY, + phone varchar(20) NOT NULL, + code varchar(20) NOT NULL, + sent_at timestamp NOT NULL DEFAULT now(), + expires_at timestamp NOT NULL +); + +CREATE INDEX idx_sms_code_sends_phone ON sms_code_sends(phone); +CREATE INDEX idx_sms_code_sends_sent_at ON sms_code_sends(sent_at); +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +DROP TABLE sms_code_sends; +-- +goose StatementEnd + diff --git a/backend_v1/database/models/sms_code_send.go b/backend_v1/database/models/sms_code_send.go new file mode 100644 index 0000000..8cc2150 --- /dev/null +++ b/backend_v1/database/models/sms_code_send.go @@ -0,0 +1,17 @@ +package models + +import "time" + +const TableNameSmsCodeSend = "sms_code_sends" + +// SmsCodeSend mapped from table +type SmsCodeSend struct { + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` + Phone string `gorm:"column:phone;type:character varying(20);not null" json:"phone"` + Code string `gorm:"column:code;type:character varying(20);not null" json:"code"` + SentAt time.Time `gorm:"column:sent_at;type:timestamp without time zone;not null;default:now()" json:"sent_at"` + ExpiresAt time.Time `gorm:"column:expires_at;type:timestamp without time zone;not null" json:"expires_at"` +} + +func (*SmsCodeSend) TableName() string { return TableNameSmsCodeSend } + diff --git a/frontend/admin/src/App.vue b/frontend/admin/src/App.vue index add7ce5..55bf72a 100644 --- a/frontend/admin/src/App.vue +++ b/frontend/admin/src/App.vue @@ -33,6 +33,11 @@ const navItems = ref([ icon: 'pi pi-shopping-cart', command: () => router.push('/orders') }, + { + label: '短信认证', + icon: 'pi pi-comment', + command: () => router.push('/sms-code-sends') + }, { label: '设置', icon: 'pi pi-cog', diff --git a/frontend/admin/src/api/smsCodeSendService.js b/frontend/admin/src/api/smsCodeSendService.js new file mode 100644 index 0000000..92a4d85 --- /dev/null +++ b/frontend/admin/src/api/smsCodeSendService.js @@ -0,0 +1,14 @@ +import httpClient from './httpClient'; + +export const smsCodeSendService = { + get({ page = 1, limit = 10, phone = '' } = {}) { + return httpClient.get('/sms-code-sends', { + params: { + page, + limit, + phone: phone.trim() + } + }); + } +}; + diff --git a/frontend/admin/src/pages/SmsCodeSendPage.vue b/frontend/admin/src/pages/SmsCodeSendPage.vue new file mode 100644 index 0000000..5592e66 --- /dev/null +++ b/frontend/admin/src/pages/SmsCodeSendPage.vue @@ -0,0 +1,115 @@ + + + + diff --git a/frontend/admin/src/router.js b/frontend/admin/src/router.js index 8ea8d36..ff996c3 100644 --- a/frontend/admin/src/router.js +++ b/frontend/admin/src/router.js @@ -52,6 +52,11 @@ const routes = [ name: 'Orders', component: () => import('./pages/OrderPage.vue'), }, + { + path: '/sms-code-sends', + name: 'SmsCodeSends', + component: () => import('./pages/SmsCodeSendPage.vue'), + }, { path: '/login', name: 'Login',