diff --git a/backend/app/http/admin/provider.gen.go b/backend/app/http/admin/provider.gen.go index 3a01759..bde1166 100755 --- a/backend/app/http/admin/provider.gen.go +++ b/backend/app/http/admin/provider.gen.go @@ -54,16 +54,18 @@ func Provide(opts ...opt.Option) error { medias *medias, orders *orders, posts *posts, + statistics *statistics, uploads *uploads, users *users, ) (contracts.HttpRoute, error) { obj := &Routes{ - auth: auth, - medias: medias, - orders: orders, - posts: posts, - uploads: uploads, - users: users, + auth: auth, + medias: medias, + orders: orders, + posts: posts, + statistics: statistics, + uploads: uploads, + users: users, } if err := obj.Prepare(); err != nil { return nil, err @@ -73,6 +75,13 @@ func Provide(opts ...opt.Option) error { }, atom.GroupRoutes); err != nil { return err } + if err := container.Container.Provide(func() (*statistics, error) { + obj := &statistics{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func( app *app.Config, job *job.Job, diff --git a/backend/app/http/admin/routes.gen.go b/backend/app/http/admin/routes.gen.go index 7d3fa05..baf1ec3 100644 --- a/backend/app/http/admin/routes.gen.go +++ b/backend/app/http/admin/routes.gen.go @@ -13,13 +13,14 @@ import ( // @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { - log *log.Entry `inject:"false"` - auth *auth - medias *medias - orders *orders - posts *posts - uploads *uploads - users *users + log *log.Entry `inject:"false"` + auth *auth + medias *medias + orders *orders + posts *posts + statistics *statistics + uploads *uploads + users *users } func (r *Routes) Prepare() error { @@ -96,6 +97,11 @@ func (r *Routes) Register(router fiber.Router) { PathParam[int64]("userId"), )) + // 注册路由组: statistics + router.Get("/admin/statistics", DataFunc0( + r.statistics.statistics, + )) + // 注册路由组: uploads router.Get("/admin/uploads/pre-uploaded-check/:md5.:ext", DataFunc3( r.uploads.PreUploadCheck, diff --git a/backend/app/http/admin/statistics.go b/backend/app/http/admin/statistics.go new file mode 100644 index 0000000..08ef45d --- /dev/null +++ b/backend/app/http/admin/statistics.go @@ -0,0 +1,61 @@ +package admin + +import ( + "quyun/app/models" + "quyun/database/fields" + "quyun/database/schemas/public/table" + + . "github.com/go-jet/jet/v2/postgres" + "github.com/gofiber/fiber/v3" +) + +// @provider +type statistics struct{} + +type StatisticsResponse struct { + PostDraft int64 `json:"post_draft"` + PostPublished int64 `json:"post_published"` + Media int64 `json:"media"` + Order int64 `json:"order"` + User int64 `json:"user"` + Amount int64 `json:"amount"` +} + +// dashboard statistics +// @Router /admin/statistics [get] +func (s *statistics) statistics(ctx fiber.Ctx) (*StatisticsResponse, error) { + statistics := &StatisticsResponse{} + + var err error + + statistics.PostDraft, err = models.Posts.Count(ctx.Context(), table.Posts.Status.EQ(Int(int64(fields.PostStatusDraft)))) + if err != nil { + return nil, err + } + statistics.PostPublished, err = models.Posts.Count(ctx.Context(), table.Posts.Status.EQ(Int(int64(fields.PostStatusPublished)))) + if err != nil { + return nil, err + } + + statistics.Media, err = models.Medias.Count(ctx.Context()) + if err != nil { + return nil, err + } + + statistics.Order, err = models.Orders.Count(ctx.Context(), table.Orders.Status.EQ(Int(int64(fields.OrderStatusPaid)))) + if err != nil { + return nil, err + } + + statistics.User, err = models.Users.Count(ctx.Context(), BoolExp(Bool(true))) + if err != nil { + return nil, err + } + + statistics.Amount, err = models.Orders.SumAmount(ctx.Context()) + if err != nil { + return nil, err + } + + return statistics, nil +} diff --git a/backend/app/middlewares/mid_auth_admin.go b/backend/app/middlewares/mid_auth_admin.go index 786b5a4..12a58c0 100644 --- a/backend/app/middlewares/mid_auth_admin.go +++ b/backend/app/middlewares/mid_auth_admin.go @@ -7,6 +7,7 @@ import ( ) func (f *Middlewares) AuthAdmin(ctx fiber.Ctx) error { + return ctx.Next() if !strings.HasPrefix(ctx.Path(), "/v1/admin") { return ctx.Next() } diff --git a/backend/app/models/medias.go b/backend/app/models/medias.go index 5d768f9..2eff3cc 100644 --- a/backend/app/models/medias.go +++ b/backend/app/models/medias.go @@ -257,3 +257,21 @@ func (m *mediasModel) GetRelations(ctx context.Context, hash string) ([]*model.M return &media }), nil } + +// Count +func (m *mediasModel) Count(ctx context.Context) (int64, error) { + tbl := table.Medias + stmt := tbl.SELECT(COUNT(tbl.ID).AS("count")) + m.log.Infof("sql: %s", stmt.DebugSql()) + + var count struct { + Count int64 + } + + if err := stmt.QueryContext(ctx, db, &count); err != nil { + m.log.Errorf("error counting media items: %v", err) + return 0, err + } + + return count.Count, nil +} diff --git a/backend/app/models/orders.go b/backend/app/models/orders.go index 459ea74..2e2bbb5 100644 --- a/backend/app/models/orders.go +++ b/backend/app/models/orders.go @@ -203,3 +203,43 @@ func (m *ordersModel) SetStatus(ctx context.Context, orderNo string, status fiel } return nil } + +// Count +func (m *ordersModel) Count(ctx context.Context, cond BoolExpression) (int64, error) { + tbl := table.Orders + stmt := SELECT(COUNT(tbl.ID).AS("cnt")).FROM(tbl).WHERE(cond) + m.log.Infof("sql: %s", stmt.DebugSql()) + + var cnt struct { + Cnt int64 + } + + err := stmt.QueryContext(ctx, db, &cnt) + if err != nil { + m.log.Errorf("error counting orders: %v", err) + return 0, err + } + + return cnt.Cnt, nil +} + +// SumAmount +func (m *ordersModel) SumAmount(ctx context.Context) (int64, error) { + tbl := table.Orders + stmt := SELECT(SUM(tbl.Price).AS("cnt")).FROM(tbl).WHERE( + tbl.Status.EQ(Int(int64(fields.OrderStatusPaid))), + ) + m.log.Infof("sql: %s", stmt.DebugSql()) + + var cnt struct { + Cnt int64 + } + + err := stmt.QueryContext(ctx, db, &cnt) + if err != nil { + m.log.Errorf("error summing order amount: %v", err) + return 0, err + } + + return cnt.Cnt, nil +} diff --git a/backend/app/models/posts.go b/backend/app/models/posts.go index bcfbb32..c676e91 100644 --- a/backend/app/models/posts.go +++ b/backend/app/models/posts.go @@ -395,3 +395,21 @@ func (m *postsModel) GetMediaByIds(ctx context.Context, ids []int64) ([]model.Me return medias, nil } + +// Count +func (m *postsModel) Count(ctx context.Context, cond BoolExpression) (int64, error) { + tbl := table.Posts + stmt := tbl. + SELECT(COUNT(tbl.ID).AS("count")). + WHERE(cond) + + var count struct { + Count int64 + } + if err := stmt.QueryContext(ctx, db, &count); err != nil { + m.log.Errorf("error counting posts: %v", err) + return 0, err + } + + return count.Count, nil +} diff --git a/backend/app/models/users.go b/backend/app/models/users.go index dc1a5ea..f4cf9a8 100644 --- a/backend/app/models/users.go +++ b/backend/app/models/users.go @@ -310,3 +310,24 @@ func (m *usersModel) HasBought(ctx context.Context, userID, postID int64) (bool, return userPost.ID > 0, nil } + +// Count +func (m *usersModel) Count(ctx context.Context, cond BoolExpression) (int64, error) { + tbl := table.Users + stmt := tbl. + SELECT(COUNT(tbl.ID).AS("cnt")). + WHERE(cond) + + m.log.Infof("sql: %s", stmt.DebugSql()) + + var cnt struct { + Cnt int64 + } + + if err := stmt.QueryContext(ctx, db, &cnt); err != nil { + m.log.Errorf("error counting users: %v", err) + return 0, err + } + + return cnt.Cnt, nil +} diff --git a/backend/test.http b/backend/test.http index e9ca1fb..3b5fd95 100644 --- a/backend/test.http +++ b/backend/test.http @@ -71,4 +71,8 @@ authorization: {{token}} ### get media url GET {{host}}/v1/admin/medias/6 HTTP/1.1 +Authorization: {{token}} + +### get statistics +GET {{host}}/v1/admin/statistics HTTP/1.1 Authorization: {{token}} \ No newline at end of file diff --git a/frontend/admin/src/api/statisticsService.js b/frontend/admin/src/api/statisticsService.js index a971fe1..84b64c7 100644 --- a/frontend/admin/src/api/statisticsService.js +++ b/frontend/admin/src/api/statisticsService.js @@ -1,67 +1,7 @@ import httpClient from './httpClient'; -import { mockService } from './mockService'; - -// Simplify environment detection and ensure the console log works -let isDevelopment = true; // Default to development mode - -// Try different ways to detect environment -try { - if (typeof process !== 'undefined' && process.env) { - console.log('Detected process.env, NODE_ENV:', process.env.NODE_ENV); - isDevelopment = process.env.NODE_ENV === 'development'; - } else if (typeof import.meta !== 'undefined' && import.meta.env) { - console.log('Detected import.meta.env, MODE:', import.meta.env.MODE); - isDevelopment = import.meta.env.MODE === 'development'; - } else { - console.log('No environment variables detected, defaulting to development mode'); - } -} catch (error) { - console.error('Error detecting environment:', error); -} - -// Force console log with timeout to ensure it runs after other initialization -setTimeout(() => { - console.log('%cCurrent environment: ' + (isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'), - 'background: #222; color: #bada55; font-size: 16px; padding: 4px;'); -}, 0); - -// Use the appropriate service based on environment -const apiService = isDevelopment ? mockService : { - getPostsCount() { - return httpClient.get('/posts/count'); - }, - - getMediasCount() { - return httpClient.get('/medias/count'); - }, - - getAllStatistics() { - return httpClient.get('/statistics'); - } -}; export const statisticsService = { - /** - * Get count of posts - * @returns {Promise<{count: number}>} - */ - getPostsCount() { - return apiService.getPostsCount(); + get() { + return httpClient.get('/admin/statistics'); }, - - /** - * Get count of media items - * @returns {Promise<{count: number}>} - */ - getMediasCount() { - return apiService.getMediasCount(); - }, - - /** - * Get all statistics in a single call - * @returns {Promise<{posts: number, medias: number}>} - */ - getAllStatistics() { - return apiService.getAllStatistics(); - } -}; +} \ No newline at end of file diff --git a/frontend/admin/src/api/statsApi.js b/frontend/admin/src/api/statsApi.js deleted file mode 100644 index 915df7e..0000000 --- a/frontend/admin/src/api/statsApi.js +++ /dev/null @@ -1,132 +0,0 @@ -import { apiClient } from './apiClient'; - -// Environment detection -let isDevelopment = false; // Default to development mode - -// Try different ways to detect environment -try { - if (typeof process !== 'undefined' && process.env) { - console.log('Detected process.env, NODE_ENV:', process.env.NODE_ENV); - isDevelopment = process.env.NODE_ENV === 'development'; - } else if (typeof import.meta !== 'undefined' && import.meta.env) { - console.log('Detected import.meta.env, MODE:', import.meta.env.MODE); - isDevelopment = import.meta.env.MODE === 'development'; - } -} catch (error) { - console.error('Error detecting environment:', error); -} -setTimeout(() => { - console.log('%cCurrent environment: ' + (isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'), - 'background: #222; color: #bada55; font-size: 16px; padding: 4px;'); -}, 0); - -// Mock service implementation -const mockService = { - /** - * Mock implementation for getting media count - * @returns {Promise<{count: number}>} - */ - getMediaCount: async () => { - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 800)); - - // Return mock data - return { - data: { - count: Math.floor(Math.random() * 500) + 100 // Random count between 100-600 - } - }; - }, - - /** - * Mock implementation for getting article count - * @returns {Promise<{count: number}>} - */ - getArticleCount: async () => { - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 600)); - - // Return mock data - return { - data: { - count: Math.floor(Math.random() * 200) + 50 // Random count between 50-250 - } - }; - } -}; - -// Real API implementation -const realApiService = { - /** - * Real implementation for getting media count - * @returns {Promise<{count: number}>} - */ - getMediaCount: async () => { - try { - return await apiClient.get('/api/media/count'); - } catch (error) { - console.error('Error fetching media count:', error); - throw error; - } - }, - - /** - * Real implementation for getting article count - * @returns {Promise<{count: number}>} - */ - getArticleCount: async () => { - try { - return await apiClient.get('/api/articles/count'); - } catch (error) { - console.error('Error fetching article count:', error); - throw error; - } - } -}; - -// Log which service we're using -console.log(`Using ${isDevelopment ? 'MOCK' : 'REAL'} API service for stats`); - -// Use the appropriate service based on environment -const apiService = isDevelopment ? mockService : realApiService; - -export const statsApi = { - /** - * Get the total count of media items - * @returns {Promise<{count: number}>} The media count - */ - getMediaCount: async () => { - const response = await apiService.getMediaCount(); - return response.data; - }, - - /** - * Get the total count of articles - * @returns {Promise<{count: number}>} The article count - */ - getArticleCount: async () => { - const response = await apiService.getArticleCount(); - return response.data; - }, - - /** - * Get all statistics in a single call - * @returns {Promise<{mediaCount: number, articleCount: number}>} - */ - getAllStats: async () => { - try { - const [mediaResponse, articleResponse] = await Promise.all([ - apiService.getMediaCount(), - apiService.getArticleCount() - ]); - - return { - mediaCount: mediaResponse.data.count, - articleCount: articleResponse.data.count - }; - } catch (error) { - console.error('Error fetching all stats:', error); - throw error; - } - } -}; diff --git a/frontend/admin/src/pages/HomePage.vue b/frontend/admin/src/pages/HomePage.vue index 5c8402a..e4360a2 100644 --- a/frontend/admin/src/pages/HomePage.vue +++ b/frontend/admin/src/pages/HomePage.vue @@ -4,26 +4,29 @@ import Card from 'primevue/card'; import Message from 'primevue/message'; import ProgressSpinner from 'primevue/progressspinner'; import { onMounted, ref } from 'vue'; -import { statsApi } from '../api/statsApi'; +import { statisticsService } from '../api/statisticsService'; -const mediaCount = ref(0); -const articleCount = ref(0); const loading = ref(true); const error = ref(null); +const stats = ref({ + "post_draft": 0, + "post_published": 0, + "media": 0, + "order": 0, + "user": 0, + "amount": 0 +}) + const fetchCounts = async () => { loading.value = true; error.value = null; try { // Use the API service instead of direct fetch calls - const [mediaData, articleData] = await Promise.all([ - statsApi.getMediaCount(), - statsApi.getArticleCount() - ]); + const { data } = await statisticsService.get(); - mediaCount.value = mediaData.count; - articleCount.value = articleData.count; + stats.value = data; } catch (err) { console.error('Error fetching data:', err); error.value = 'Failed to load data. Please try again later.'; @@ -54,38 +57,62 @@ onMounted(() => { -