关闭公开注册并优化仪表盘
This commit is contained in:
@@ -35,26 +35,6 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
|||||||
response.OK(c, result)
|
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) {
|
func (h *AuthHandler) Profile(c *gin.Context) {
|
||||||
user, err := h.svc.GetProfile(middleware.GetUserID(c))
|
user, err := h.svc.GetProfile(middleware.GetUserID(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ func Setup(h *Handlers) *gin.Engine {
|
|||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
{
|
{
|
||||||
api.POST("/auth/login", h.Auth.Login)
|
api.POST("/auth/login", h.Auth.Login)
|
||||||
api.POST("/auth/register", h.Auth.Register)
|
|
||||||
|
|
||||||
// 源头侧接口:上游发货平台查询订单并回传发货结果。
|
// 源头侧接口:上游发货平台查询订单并回传发货结果。
|
||||||
sourceOpen := api.Group("/open/v1")
|
sourceOpen := api.Group("/open/v1")
|
||||||
|
|||||||
@@ -46,37 +46,6 @@ func (s *AuthService) Login(username, password string) (*LoginResult, error) {
|
|||||||
return &LoginResult{Token: token, User: &user}, nil
|
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) {
|
func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := s.db.First(&user, userID).Error; err != nil {
|
if err := s.db.First(&user, userID).Error; err != nil {
|
||||||
|
|||||||
@@ -674,35 +674,189 @@ func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} {
|
|||||||
|
|
||||||
// DashboardStats 仪表盘聚合指标。
|
// DashboardStats 仪表盘聚合指标。
|
||||||
type DashboardStats struct {
|
type DashboardStats struct {
|
||||||
ProductCount int64 `json:"product_count"`
|
Scope string `json:"scope"`
|
||||||
MerchantCount int64 `json:"merchant_count"`
|
CatalogProductCount int64 `json:"catalog_product_count"`
|
||||||
OrderCount int64 `json:"order_count"`
|
ProductCount int64 `json:"product_count"`
|
||||||
TotalSales int64 `json:"total_sales"`
|
ActiveProductCount int64 `json:"active_product_count"`
|
||||||
TotalFees int64 `json:"total_fees"`
|
MerchantCount int64 `json:"merchant_count"`
|
||||||
PendingOrderCount int64 `json:"pending_order_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) {
|
func (s *FulfillmentService) Dashboard(merchantID uint, isPlatformAdmin bool) (*DashboardStats, error) {
|
||||||
stats := &DashboardStats{}
|
stats := &DashboardStats{Scope: "merchant"}
|
||||||
s.db.Model(&model.MerchantProduct{}).Where("merchant_id = ?", merchantID).Count(&stats.ProductCount)
|
|
||||||
if isPlatformAdmin {
|
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 {
|
} 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
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
func TestFulfillmentCreateOrderAppliesMerchantFeeRate(t *testing.T) {
|
||||||
db := newServiceTestDB(t)
|
db := newServiceTestDB(t)
|
||||||
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-fee-rate", 1000, 5, 200)
|
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-fee-rate", 1000, 5, 200)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import zhCN from 'antd/locale/zh_CN'
|
|||||||
import { AuthProvider, useAuth } from './store/auth'
|
import { AuthProvider, useAuth } from './store/auth'
|
||||||
import MainLayout from './layouts/MainLayout'
|
import MainLayout from './layouts/MainLayout'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Register from './pages/Register'
|
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
import OpenApiDocs from './pages/OpenApiDocs'
|
import OpenApiDocs from './pages/OpenApiDocs'
|
||||||
import ApiDebugger from './pages/ApiDebugger'
|
import ApiDebugger from './pages/ApiDebugger'
|
||||||
@@ -27,7 +26,6 @@ function AppRoutes() {
|
|||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ export const authApi = {
|
|||||||
const res = await request.post('/auth/login', { username, password })
|
const res = await request.post('/auth/login', { username, password })
|
||||||
return res.data.data as LoginResult
|
return res.data.data as LoginResult
|
||||||
},
|
},
|
||||||
register: async (data: { username: string; password: string; nickname?: string }) => {
|
|
||||||
const res = await request.post('/auth/register', data)
|
|
||||||
return res.data.data as User
|
|
||||||
},
|
|
||||||
profile: async () => {
|
profile: async () => {
|
||||||
const res = await request.get('/auth/profile')
|
const res = await request.get('/auth/profile')
|
||||||
return res.data.data as User
|
return res.data.data as User
|
||||||
|
|||||||
@@ -1,15 +1,77 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, type ReactNode } from 'react'
|
||||||
import { Card, Col, Row, Statistic, Typography, Spin, message } from 'antd'
|
import { Card, Col, Progress, Row, Space, Statistic, Typography, Spin, message } from 'antd'
|
||||||
import {
|
import {
|
||||||
ShopOutlined,
|
ApiOutlined,
|
||||||
ShoppingOutlined,
|
CheckCircleOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
DollarOutlined,
|
DollarOutlined,
|
||||||
PercentageOutlined,
|
PercentageOutlined,
|
||||||
ClockCircleOutlined,
|
ShopOutlined,
|
||||||
|
ShoppingOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { dashboardApi } from '../api'
|
import { dashboardApi } from '../api'
|
||||||
import type { DashboardStats } from '../types'
|
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 (
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title={title}
|
||||||
|
value={value}
|
||||||
|
formatter={() => numberText(value)}
|
||||||
|
prefix={icon}
|
||||||
|
suffix={suffix}
|
||||||
|
valueStyle={color ? { color } : undefined}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusLine({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
total,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
total: number
|
||||||
|
color: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
|
<Typography.Text>{label}</Typography.Text>
|
||||||
|
<Typography.Text strong>{numberText(value)}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Progress percent={ratio(value, total)} strokeColor={color} showInfo={false} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -32,53 +94,132 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }} align="start">
|
||||||
数据概览
|
<div>
|
||||||
</Typography.Title>
|
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 4 }}>
|
||||||
|
数据概览
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{stats?.scope === 'platform' ? '平台全局运营数据' : '当前商户运营数据'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card>
|
<MetricCard
|
||||||
<Statistic title="商户商品" value={stats?.product_count ?? 0} prefix={<ShoppingOutlined />} />
|
title={stats?.scope === 'platform' ? '可售商品' : '商户商品'}
|
||||||
</Card>
|
value={stats?.active_product_count ?? 0}
|
||||||
|
icon={<ShoppingOutlined />}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card>
|
<MetricCard
|
||||||
<Statistic title="平台商户" value={stats?.merchant_count ?? 0} prefix={<ShopOutlined />} />
|
title={stats?.scope === 'platform' ? '启用商户' : '当前商户'}
|
||||||
</Card>
|
value={stats?.active_merchant_count ?? 0}
|
||||||
|
icon={<ShopOutlined />}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card>
|
<MetricCard title="成员账号" value={stats?.user_count ?? 0} icon={<TeamOutlined />} />
|
||||||
<Statistic title="订单总数" value={stats?.order_count ?? 0} prefix={<ShoppingOutlined />} />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card>
|
<MetricCard title="订单总数" value={stats?.order_count ?? 0} icon={<DatabaseOutlined />} />
|
||||||
<Statistic
|
|
||||||
title="成交积分"
|
|
||||||
value={stats?.total_sales ?? 0}
|
|
||||||
prefix={<DollarOutlined />}
|
|
||||||
suffix="积分"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card>
|
<MetricCard title="今日订单" value={stats?.today_order_count ?? 0} icon={<ClockCircleOutlined />} />
|
||||||
<Statistic
|
|
||||||
title="平台手续费"
|
|
||||||
value={stats?.total_fees ?? 0}
|
|
||||||
prefix={<PercentageOutlined />}
|
|
||||||
suffix="积分"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card>
|
<MetricCard
|
||||||
<Statistic
|
title="待履约订单"
|
||||||
title="待处理订单"
|
value={stats?.pending_order_count ?? 0}
|
||||||
value={stats?.pending_order_count ?? 0}
|
icon={<ClockCircleOutlined />}
|
||||||
prefix={<ClockCircleOutlined />}
|
color={(stats?.pending_order_count ?? 0) > 0 ? '#cf1322' : undefined}
|
||||||
valueStyle={{ color: (stats?.pending_order_count ?? 0) > 0 ? '#cf1322' : undefined }}
|
/>
|
||||||
/>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 24 }}>
|
||||||
|
积分与费用
|
||||||
|
</Typography.Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<MetricCard title="成交积分" value={stats?.total_sales ?? 0} icon={<DollarOutlined />} suffix="积分" />
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<MetricCard title="今日成交" value={stats?.today_sales ?? 0} icon={<DollarOutlined />} suffix="积分" />
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<MetricCard title="平台手续费" value={stats?.total_fees ?? 0} icon={<PercentageOutlined />} suffix="积分" />
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<MetricCard title="可用余额" value={stats?.wallet_available_balance ?? 0} icon={<WalletOutlined />} suffix="积分" />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||||
|
<Col xs={24} lg={14}>
|
||||||
|
<Card title="订单状态">
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<StatusLine
|
||||||
|
label="已成功"
|
||||||
|
value={stats?.succeeded_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#52c41a"
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="待履约"
|
||||||
|
value={stats?.pending_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#faad14"
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="履约中"
|
||||||
|
value={stats?.processing_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#1677ff"
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="已失败"
|
||||||
|
value={stats?.failed_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#ff4d4f"
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="已取消"
|
||||||
|
value={stats?.cancelled_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#8c8c8c"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={10}>
|
||||||
|
<Card title="接口与回调">
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Statistic
|
||||||
|
title="API 客户端"
|
||||||
|
value={stats?.active_api_client_count ?? 0}
|
||||||
|
prefix={<ApiOutlined />}
|
||||||
|
suffix={`/ ${numberText(stats?.api_client_count)}`}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Statistic title="回调订阅" value={stats?.callback_subscription_count ?? 0} prefix={<CheckCircleOutlined />} />
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Statistic title="待投递回调" value={stats?.pending_callback_count ?? 0} prefix={<ClockCircleOutlined />} />
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Statistic
|
||||||
|
title="失败回调"
|
||||||
|
value={stats?.failed_callback_count ?? 0}
|
||||||
|
prefix={<CloseCircleOutlined />}
|
||||||
|
valueStyle={(stats?.failed_callback_count ?? 0) > 0 ? { color: '#cf1322' } : undefined}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
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 { Card, Form, Input, Button, Typography, message, Space } from 'antd'
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/auth'
|
||||||
@@ -53,11 +53,9 @@ export default function Login() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0, textAlign: 'center' }}>
|
||||||
<Typography.Text type="secondary">
|
商户账号请联系平台管理员创建
|
||||||
还没有账号? <Link to="/register">注册商户账号</Link>
|
</Typography.Paragraph>
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0, textAlign: 'center' }}>
|
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0, textAlign: 'center' }}>
|
||||||
默认管理员:admin / admin123
|
默认管理员:admin / admin123
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
minHeight: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card style={{ width: 400, boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
|
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
|
||||||
注册商户账号
|
|
||||||
</Typography.Title>
|
|
||||||
<Typography.Text type="secondary">创建商户员工账号</Typography.Text>
|
|
||||||
</div>
|
|
||||||
<Form layout="vertical" onFinish={onFinish}>
|
|
||||||
<Form.Item name="username" rules={[{ required: true, min: 3, message: '用户名至少3位' }]}>
|
|
||||||
<Input prefix={<UserOutlined />} placeholder="用户名" size="large" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="nickname">
|
|
||||||
<Input prefix={<UserOutlined />} placeholder="昵称(可选)" size="large" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="password" rules={[{ required: true, min: 6, message: '密码至少6位' }]}>
|
|
||||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" size="large" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" loading={loading} block size="large">
|
|
||||||
注册
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
已有账号? <Link to="/login">去登录</Link>
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -158,12 +158,31 @@ export interface CallbackCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardStats {
|
export interface DashboardStats {
|
||||||
|
scope: 'platform' | 'merchant'
|
||||||
|
catalog_product_count: number
|
||||||
product_count: number
|
product_count: number
|
||||||
|
active_product_count: number
|
||||||
merchant_count: number
|
merchant_count: number
|
||||||
|
active_merchant_count: number
|
||||||
|
user_count: number
|
||||||
order_count: number
|
order_count: number
|
||||||
|
today_order_count: number
|
||||||
total_sales: number
|
total_sales: number
|
||||||
|
today_sales: number
|
||||||
total_fees: number
|
total_fees: number
|
||||||
|
today_fees: number
|
||||||
pending_order_count: 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<T> {
|
export interface PageResult<T> {
|
||||||
|
|||||||
Reference in New Issue
Block a user