From f29f3eb8416c13e0be5863950282c992bd744248 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 31 Jul 2026 09:39:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=85=AC=E5=BC=80=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=AA=E8=A1=A8=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth.go | 20 -- backend/internal/router/router.go | 1 - backend/internal/service/auth.go | 31 --- backend/internal/service/fulfillment.go | 200 +++++++++++++++-- backend/internal/service/fulfillment_test.go | 58 +++++ frontend/src/App.tsx | 2 - frontend/src/api/index.ts | 4 - frontend/src/pages/Dashboard.tsx | 221 +++++++++++++++---- frontend/src/pages/Login.tsx | 10 +- frontend/src/pages/Register.tsx | 71 ------ frontend/src/types/index.ts | 19 ++ 11 files changed, 439 insertions(+), 198 deletions(-) delete mode 100644 frontend/src/pages/Register.tsx diff --git a/backend/internal/handler/auth.go b/backend/internal/handler/auth.go index 1af4e38..d8cf2df 100644 --- a/backend/internal/handler/auth.go +++ b/backend/internal/handler/auth.go @@ -35,26 +35,6 @@ func (h *AuthHandler) Login(c *gin.Context) { response.OK(c, result) } -type registerReq struct { - Username string `json:"username" binding:"required,min=3,max=32"` - Password string `json:"password" binding:"required,min=6,max=64"` - Nickname string `json:"nickname"` -} - -func (h *AuthHandler) Register(c *gin.Context) { - var req registerReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:用户名至少3位,密码至少6位") - return - } - user, err := h.svc.Register(req.Username, req.Password, req.Nickname) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, user) -} - func (h *AuthHandler) Profile(c *gin.Context) { user, err := h.svc.GetProfile(middleware.GetUserID(c)) if err != nil { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 13e579b..0318ec3 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -47,7 +47,6 @@ func Setup(h *Handlers) *gin.Engine { api := r.Group("/api") { api.POST("/auth/login", h.Auth.Login) - api.POST("/auth/register", h.Auth.Register) // 源头侧接口:上游发货平台查询订单并回传发货结果。 sourceOpen := api.Group("/open/v1") diff --git a/backend/internal/service/auth.go b/backend/internal/service/auth.go index 8ebd2e6..e1e8c67 100644 --- a/backend/internal/service/auth.go +++ b/backend/internal/service/auth.go @@ -46,37 +46,6 @@ func (s *AuthService) Login(username, password string) (*LoginResult, error) { return &LoginResult{Token: token, User: &user}, nil } -func (s *AuthService) Register(username, password, nickname string) (*model.User, error) { - var count int64 - s.db.Model(&model.User{}).Where("username = ?", username).Count(&count) - if count > 0 { - return nil, errors.New("用户名已存在") - } - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, err - } - user := &model.User{ - Username: username, - PasswordHash: string(hash), - Nickname: nickname, - Role: model.RoleMerchant, - Status: 1, - } - if user.Nickname == "" { - user.Nickname = username - } - if err := s.db.Create(user).Error; err != nil { - return nil, err - } - if s.tenant != nil { - if err := s.tenant.EnsureSelfMember(user.ID, model.MemberRoleOperator, user.Status); err != nil { - return nil, err - } - } - return user, nil -} - func (s *AuthService) GetProfile(userID uint) (*model.User, error) { var user model.User if err := s.db.First(&user, userID).Error; err != nil { diff --git a/backend/internal/service/fulfillment.go b/backend/internal/service/fulfillment.go index 6dcfa6a..4b44138 100644 --- a/backend/internal/service/fulfillment.go +++ b/backend/internal/service/fulfillment.go @@ -674,35 +674,189 @@ func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} { // DashboardStats 仪表盘聚合指标。 type DashboardStats struct { - ProductCount int64 `json:"product_count"` - MerchantCount int64 `json:"merchant_count"` - OrderCount int64 `json:"order_count"` - TotalSales int64 `json:"total_sales"` - TotalFees int64 `json:"total_fees"` - PendingOrderCount int64 `json:"pending_order_count"` + Scope string `json:"scope"` + CatalogProductCount int64 `json:"catalog_product_count"` + ProductCount int64 `json:"product_count"` + ActiveProductCount int64 `json:"active_product_count"` + MerchantCount int64 `json:"merchant_count"` + ActiveMerchantCount int64 `json:"active_merchant_count"` + UserCount int64 `json:"user_count"` + OrderCount int64 `json:"order_count"` + TodayOrderCount int64 `json:"today_order_count"` + TotalSales int64 `json:"total_sales"` + TodaySales int64 `json:"today_sales"` + TotalFees int64 `json:"total_fees"` + TodayFees int64 `json:"today_fees"` + PendingOrderCount int64 `json:"pending_order_count"` + ProcessingOrderCount int64 `json:"processing_order_count"` + SucceededOrderCount int64 `json:"succeeded_order_count"` + FailedOrderCount int64 `json:"failed_order_count"` + CancelledOrderCount int64 `json:"cancelled_order_count"` + WalletAvailableBalance int64 `json:"wallet_available_balance"` + WalletFrozenBalance int64 `json:"wallet_frozen_balance"` + APIClientCount int64 `json:"api_client_count"` + ActiveAPIClientCount int64 `json:"active_api_client_count"` + CallbackSubscriptionCount int64 `json:"callback_subscription_count"` + PendingCallbackCount int64 `json:"pending_callback_count"` + FailedCallbackCount int64 `json:"failed_callback_count"` } -// Dashboard 汇总商户维度的商品、商户、订单与金额统计。 +type dashboardStatusCount struct { + Status string + Count int64 +} + +// Dashboard 按角色汇总运营指标:平台管理员看全平台,商户账号看当前商户。 func (s *FulfillmentService) Dashboard(merchantID uint, isPlatformAdmin bool) (*DashboardStats, error) { - stats := &DashboardStats{} - s.db.Model(&model.MerchantProduct{}).Where("merchant_id = ?", merchantID).Count(&stats.ProductCount) + stats := &DashboardStats{Scope: "merchant"} if isPlatformAdmin { - s.db.Model(&model.Merchant{}).Where("status = ?", model.MerchantStatusActive).Count(&stats.MerchantCount) + stats.Scope = "platform" + } + now := time.Now() + year, month, day := now.Date() + todayStart := time.Date(year, month, day, 0, 0, 0, 0, now.Location()) + + productScope := func() *gorm.DB { + tx := s.db.Model(&model.MerchantProduct{}) + if !isPlatformAdmin { + tx = tx.Where("merchant_id = ?", merchantID) + } + return tx + } + orderScope := func() *gorm.DB { + tx := s.db.Model(&model.FulfillmentOrder{}) + if !isPlatformAdmin { + tx = tx.Where("merchant_id = ?", merchantID) + } + return tx + } + walletScope := func() *gorm.DB { + tx := s.db.Model(&model.WalletAccount{}) + if !isPlatformAdmin { + tx = tx.Where("merchant_id = ?", merchantID) + } + return tx + } + apiClientScope := func() *gorm.DB { + tx := s.db.Model(&model.APIClient{}) + if !isPlatformAdmin { + tx = tx.Where("merchant_id = ?", merchantID) + } + return tx + } + callbackScope := func() *gorm.DB { + tx := s.db.Model(&model.CallbackSubscription{}) + if !isPlatformAdmin { + tx = tx.Where("merchant_id = ?", merchantID) + } + return tx + } + callbackDeliveryScope := func() *gorm.DB { + tx := s.db.Model(&model.CallbackDelivery{}) + if !isPlatformAdmin { + tx = tx.Where("merchant_id = ?", merchantID) + } + return tx + } + + if err := s.db.Model(&model.Product{}).Count(&stats.CatalogProductCount).Error; err != nil { + return nil, err + } + if err := productScope().Count(&stats.ProductCount).Error; err != nil { + return nil, err + } + if err := productScope().Where("status = ?", model.ProductStatusActive).Count(&stats.ActiveProductCount).Error; err != nil { + return nil, err + } + if isPlatformAdmin { + if err := s.db.Model(&model.Merchant{}).Count(&stats.MerchantCount).Error; err != nil { + return nil, err + } + if err := s.db.Model(&model.Merchant{}).Where("status = ?", model.MerchantStatusActive).Count(&stats.ActiveMerchantCount).Error; err != nil { + return nil, err + } + if err := s.db.Model(&model.User{}).Count(&stats.UserCount).Error; err != nil { + return nil, err + } } else { - stats.MerchantCount = 1 + if err := s.db.Model(&model.Merchant{}).Where("id = ?", merchantID).Count(&stats.MerchantCount).Error; err != nil { + return nil, err + } + if err := s.db.Model(&model.Merchant{}).Where("id = ? AND status = ?", merchantID, model.MerchantStatusActive).Count(&stats.ActiveMerchantCount).Error; err != nil { + return nil, err + } + if err := s.db.Model(&model.User{}). + Joins("JOIN merchant_members ON merchant_members.user_id = users.id"). + Where("merchant_members.merchant_id = ?", merchantID). + Count(&stats.UserCount).Error; err != nil { + return nil, err + } + stats.CatalogProductCount = stats.ProductCount + } + if err := orderScope().Count(&stats.OrderCount).Error; err != nil { + return nil, err + } + if err := orderScope().Where("created_at >= ?", todayStart).Count(&stats.TodayOrderCount).Error; err != nil { + return nil, err + } + if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales).Error; err != nil { + return nil, err + } + if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + Where("created_at >= ?", todayStart). + Select("COALESCE(SUM(amount),0)").Scan(&stats.TodaySales).Error; err != nil { + return nil, err + } + if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TotalFees).Error; err != nil { + return nil, err + } + if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + Where("created_at >= ?", todayStart). + Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TodayFees).Error; err != nil { + return nil, err + } + var orderStatusCounts []dashboardStatusCount + if err := orderScope().Select("fulfillment_status AS status, COUNT(*) AS count"). + Group("fulfillment_status").Scan(&orderStatusCounts).Error; err != nil { + return nil, err + } + for _, item := range orderStatusCounts { + switch item.Status { + case model.FulfillmentStatusPending: + stats.PendingOrderCount = item.Count + case model.FulfillmentStatusProcessing: + stats.ProcessingOrderCount = item.Count + case model.FulfillmentStatusSucceeded: + stats.SucceededOrderCount = item.Count + case model.FulfillmentStatusFailed: + stats.FailedOrderCount = item.Count + case model.FulfillmentStatusCancelled: + stats.CancelledOrderCount = item.Count + } + } + if err := walletScope().Select("COALESCE(SUM(available_balance),0)").Scan(&stats.WalletAvailableBalance).Error; err != nil { + return nil, err + } + if err := walletScope().Select("COALESCE(SUM(frozen_balance),0)").Scan(&stats.WalletFrozenBalance).Error; err != nil { + return nil, err + } + if err := apiClientScope().Count(&stats.APIClientCount).Error; err != nil { + return nil, err + } + if err := apiClientScope().Where("status = ?", model.APIClientStatusActive).Count(&stats.ActiveAPIClientCount).Error; err != nil { + return nil, err + } + if err := callbackScope().Count(&stats.CallbackSubscriptionCount).Error; err != nil { + return nil, err + } + if err := callbackDeliveryScope().Where("status = ?", model.CallbackDeliveryPending).Count(&stats.PendingCallbackCount).Error; err != nil { + return nil, err + } + if err := callbackDeliveryScope().Where("status = ?", model.CallbackDeliveryFailed).Count(&stats.FailedCallbackCount).Error; err != nil { + return nil, err } - s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID).Count(&stats.OrderCount) - s.db.Model(&model.FulfillmentOrder{}). - Where("merchant_id = ? AND fulfillment_status = ?", merchantID, model.FulfillmentStatusPending). - Count(&stats.PendingOrderCount) - s.db.Model(&model.FulfillmentOrder{}). - Where("merchant_id = ?", merchantID). - Where("payment_status = ?", model.PaymentStatusPaid). - Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales) - s.db.Model(&model.FulfillmentOrder{}). - Where("merchant_id = ?", merchantID). - Where("payment_status = ?", model.PaymentStatusPaid). - Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TotalFees) return stats, nil } diff --git a/backend/internal/service/fulfillment_test.go b/backend/internal/service/fulfillment_test.go index 521a547..2b12cf9 100644 --- a/backend/internal/service/fulfillment_test.go +++ b/backend/internal/service/fulfillment_test.go @@ -118,6 +118,64 @@ func TestFulfillmentCreateOrderDebitsWalletAndIsIdempotent(t *testing.T) { } } +func TestDashboardScopesPlatformAndMerchantData(t *testing.T) { + db := newServiceTestDB(t) + merchantA, productA := seedFulfillmentMerchant(t, db, "dashboard-a", 1000, 5, 100) + merchantB, productB := seedFulfillmentMerchant(t, db, "dashboard-b", 2000, 5, 300) + svc := NewFulfillmentService(db, nil) + + if _, err := svc.CreateOrder(CreateFulfillmentOrderInput{ + MerchantID: merchantA, + APIClientID: 31, + ClientOrderNo: "dashboard-a-001", + SKU: productA.SKU, + }); err != nil { + t.Fatalf("create order a: %v", err) + } + if _, err := svc.CreateOrder(CreateFulfillmentOrderInput{ + MerchantID: merchantB, + APIClientID: 32, + ClientOrderNo: "dashboard-b-001", + SKU: productB.SKU, + }); err != nil { + t.Fatalf("create order b: %v", err) + } + + merchantStats, err := svc.Dashboard(merchantA, false) + if err != nil { + t.Fatalf("merchant dashboard: %v", err) + } + if merchantStats.Scope != "merchant" { + t.Fatalf("expected merchant scope, got %s", merchantStats.Scope) + } + if merchantStats.ProductCount != 1 || merchantStats.ActiveProductCount != 1 { + t.Fatalf("merchant product stats should only include current merchant, got %+v", merchantStats) + } + if merchantStats.OrderCount != 1 || merchantStats.PendingOrderCount != 1 || merchantStats.TotalSales != 100 { + t.Fatalf("merchant order stats should only include current merchant, got %+v", merchantStats) + } + if merchantStats.WalletAvailableBalance != 900 { + t.Fatalf("merchant wallet should only include current merchant, got %d", merchantStats.WalletAvailableBalance) + } + + platformStats, err := svc.Dashboard(merchantA, true) + if err != nil { + t.Fatalf("platform dashboard: %v", err) + } + if platformStats.Scope != "platform" { + t.Fatalf("expected platform scope, got %s", platformStats.Scope) + } + if platformStats.OrderCount != 2 || platformStats.PendingOrderCount != 2 || platformStats.TotalSales != 400 { + t.Fatalf("platform order stats should include all merchants, got %+v", platformStats) + } + if platformStats.WalletAvailableBalance < 2600 { + t.Fatalf("platform wallet should include all merchant wallets, got %d", platformStats.WalletAvailableBalance) + } + if platformStats.ProductCount <= merchantStats.ProductCount { + t.Fatalf("platform product stats should be broader than merchant stats, got platform=%d merchant=%d", platformStats.ProductCount, merchantStats.ProductCount) + } +} + func TestFulfillmentCreateOrderAppliesMerchantFeeRate(t *testing.T) { db := newServiceTestDB(t) merchantID, product := seedFulfillmentMerchant(t, db, "merchant-fee-rate", 1000, 5, 200) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae389f3..e77852e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,6 @@ import zhCN from 'antd/locale/zh_CN' import { AuthProvider, useAuth } from './store/auth' import MainLayout from './layouts/MainLayout' import Login from './pages/Login' -import Register from './pages/Register' import Dashboard from './pages/Dashboard' import OpenApiDocs from './pages/OpenApiDocs' import ApiDebugger from './pages/ApiDebugger' @@ -27,7 +26,6 @@ function AppRoutes() { return ( } /> - } /> { - const res = await request.post('/auth/register', data) - return res.data.data as User - }, profile: async () => { const res = await request.get('/auth/profile') return res.data.data as User diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index ccc358a..d83e2da 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,15 +1,77 @@ -import { useEffect, useState } from 'react' -import { Card, Col, Row, Statistic, Typography, Spin, message } from 'antd' +import { useEffect, useState, type ReactNode } from 'react' +import { Card, Col, Progress, Row, Space, Statistic, Typography, Spin, message } from 'antd' import { - ShopOutlined, - ShoppingOutlined, + ApiOutlined, + CheckCircleOutlined, + ClockCircleOutlined, + CloseCircleOutlined, + DatabaseOutlined, DollarOutlined, PercentageOutlined, - ClockCircleOutlined, + ShopOutlined, + ShoppingOutlined, + TeamOutlined, + WalletOutlined, } from '@ant-design/icons' import { dashboardApi } from '../api' import type { DashboardStats } from '../types' +const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN') + +function ratio(part: number, total: number) { + if (total <= 0) return 0 + return Math.round((part / total) * 100) +} + +function MetricCard({ + title, + value, + icon, + suffix, + color, +}: { + title: string + value: number + icon: ReactNode + suffix?: string + color?: string +}) { + return ( + + numberText(value)} + prefix={icon} + suffix={suffix} + valueStyle={color ? { color } : undefined} + /> + + ) +} + +function StatusLine({ + label, + value, + total, + color, +}: { + label: string + value: number + total: number + color: string +}) { + return ( +
+ + {label} + {numberText(value)} + + +
+ ) +} + export default function Dashboard() { const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) @@ -32,53 +94,132 @@ export default function Dashboard() { return (
- - 数据概览 - + +
+ + 数据概览 + + + {stats?.scope === 'platform' ? '平台全局运营数据' : '当前商户运营数据'} + +
+
+ - - } /> - + } + /> - - } /> - + } + /> - - } /> - + } /> - - } - suffix="积分" - /> - + } /> - - } - suffix="积分" - /> - + } /> - - } - valueStyle={{ color: (stats?.pending_order_count ?? 0) > 0 ? '#cf1322' : undefined }} - /> + } + color={(stats?.pending_order_count ?? 0) > 0 ? '#cf1322' : undefined} + /> + + + + + 积分与费用 + + + + } suffix="积分" /> + + + } suffix="积分" /> + + + } suffix="积分" /> + + + } suffix="积分" /> + + + + + + + + + + + + + + + + + + + + } + suffix={`/ ${numberText(stats?.api_client_count)}`} + /> + + + } /> + + + } /> + + + } + valueStyle={(stats?.failed_callback_count ?? 0) > 0 ? { color: '#cf1322' } : undefined} + /> + + diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index aca3966..b521a5d 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Card, Form, Input, Button, Typography, message, Space } from 'antd' import { UserOutlined, LockOutlined } from '@ant-design/icons' import { useAuth } from '../store/auth' @@ -53,11 +53,9 @@ export default function Login() { -
- - 还没有账号? 注册商户账号 - -
+ + 商户账号请联系平台管理员创建 + 默认管理员:admin / admin123 diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx deleted file mode 100644 index 518693a..0000000 --- a/frontend/src/pages/Register.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' -import { Card, Form, Input, Button, Typography, message, Space } from 'antd' -import { UserOutlined, LockOutlined } from '@ant-design/icons' -import { authApi } from '../api' - -export default function Register() { - const navigate = useNavigate() - const [loading, setLoading] = useState(false) - - const onFinish = async (values: { - username: string - password: string - nickname?: string - }) => { - setLoading(true) - try { - await authApi.register(values) - message.success('注册成功,请登录') - navigate('/login') - } catch (e) { - message.error(e instanceof Error ? e.message : '注册失败') - } finally { - setLoading(false) - } - } - - return ( -
- - -
- - 注册商户账号 - - 创建商户员工账号 -
-
- - } placeholder="用户名" size="large" /> - - - } placeholder="昵称(可选)" size="large" /> - - - } placeholder="密码" size="large" /> - - - - -
-
- - 已有账号? 去登录 - -
-
-
-
- ) -} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index d56497f..1f1c6c4 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -158,12 +158,31 @@ export interface CallbackCredential { } export interface DashboardStats { + scope: 'platform' | 'merchant' + catalog_product_count: number product_count: number + active_product_count: number merchant_count: number + active_merchant_count: number + user_count: number order_count: number + today_order_count: number total_sales: number + today_sales: number total_fees: number + today_fees: number pending_order_count: number + processing_order_count: number + succeeded_order_count: number + failed_order_count: number + cancelled_order_count: number + wallet_available_balance: number + wallet_frozen_balance: number + api_client_count: number + active_api_client_count: number + callback_subscription_count: number + pending_callback_count: number + failed_callback_count: number } export interface PageResult {