基于 React + Go + PostgreSQL + Redis 的管理系统开发框架

siqian-admin 开发框架

https://github.com/KakaCheng2010/siqian-admin.git

基于 React + Go + PostgreSQL + Redis 的管理系统开发框架

🚀 技术栈

后端

  • Go 1.21+ - 主要后端语言
  • Gin - Web框架
  • GORM - ORM框架
  • PostgreSQL - 主数据库
  • Redis - 缓存和会话存储
  • JWT - 身份认证
  • Viper - 配置管理

前端

  • React 18 - 前端框架
  • TypeScript - 类型安全
  • Ant Design - UI组件库
  • React Router - 路由管理
  • Axios - HTTP客户端
  • Zustand - 状态管理

📋 功能模块

系统管理模块 (sys/)

  • 用户管理 - 用户CRUD、状态管理、角色分配
  • 组织管理 - 树形组织结构、层级管理
  • 角色管理 - 角色定义、权限分配
  • 菜单管理 - 动态菜单、权限控制
  • 字典管理 - 系统字典、数据字典项

认证模块 (auth/)

  • 用户认证 - JWT令牌认证
  • 权限控制 - 基于角色的访问控制
  • 会话管理 - Redis会话存储

🏗️ 项目结构

复制代码
siqian-admin/
├── backend/                 # Go后端
│   ├── cmd/                # 应用入口
│   │   └── main.go
│   ├── internal/           # 内部包
│   │   ├── api/           # API层
│   │   ├── config/        # 配置管理
│   │   ├── database/      # 数据库连接
│   │   ├── middleware/    # 中间件
│   │   ├── router/        # 路由配置
│   │   ├── service/       # 业务逻辑层
│   │   ├── sys/          # 系统管理模块
│   │   │   ├── api/       # 系统管理API
│   │   │   ├── model/     # 数据模型
│   │   │   └── service/   # 业务逻辑
│   │   └── utils/         # 工具函数
│   ├── config.yaml        # 配置文件
│   ├── Dockerfile
│   ├── go.mod
│   └── go.sum
├── frontend/              # React前端
│   ├── src/
│   │   ├── components/    # 公共组件
│   │   ├── pages/         # 页面组件
│   │   ├── services/      # API服务
│   │   ├── store/         # 状态管理
│   │   ├── router/        # 路由配置
│   │   ├── hooks/         # 自定义Hooks
│   │   └── utils/         # 工具函数
│   ├── package.json
│   └── Dockerfile
└── README.md

🚀 快速开始

环境要求

  • Go 1.21+
  • Node.js 18+
  • Docker & Docker Compose
  • Git

配置文件

数据库文件位于 backend/scripts/siqian-admin.sql

项目配置文件位于 backend/config.yaml

yaml 复制代码
server:
  port: "8080"        # 服务器端口
  mode: "debug"       # 运行模式

database:
  host: "localhost"   # 数据库主机
  port: 5432          # 数据库端口
  user: "postgres"    # 数据库用户名
  password: "password" # 数据库密码
  dbname: "go_admin"  # 数据库名称
  sslmode: "disable"  # SSL模式

redis:
  host: "localhost"   # Redis主机
  port: 6379         # Redis端口
  password: ""       # Redis密码
  db: 0             # Redis数据库

jwt:
  secret: "your-secret-key" # JWT密钥
  expire_time: 24           # 过期时间(小时)

启动项目

bash 复制代码
# 1. 启动数据库和Redis
docker-compose up -d postgres redis

# 2. 启动后端
cd backend
go mod tidy
go run cmd/main.go

# 3. 启动前端
cd frontend
npm install
npm run dev

访问地址

🛠️ 新功能开发指南

完整开发流程:从后端到前端

以添加"产品管理"模块为例,展示完整的开发流程:

1. 后端开发

步骤1:创建数据模型

go 复制代码
// backend/internal/sys/model/product.go
package model

import (
    "time"
    "gorm.io/gorm"
)

type Product struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    Name        string         `json:"name" gorm:"not null" binding:"required"`
    Code        string         `json:"code" gorm:"uniqueIndex;not null" binding:"required"`
    Price       float64        `json:"price" gorm:"not null"`
    Category    string         `json:"category"`
    Status      int            `json:"status" gorm:"default:1"` // 1:正常 0:禁用
    Description string         `json:"description"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
}

func (Product) TableName() string {
    return "sys_products"
}

步骤2:创建业务逻辑层

go 复制代码
// backend/internal/sys/service/product.go
package service

import (
    "siqian-admin/internal/sys/model"
    "gorm.io/gorm"
)

type ProductService struct {
    db *gorm.DB
}

func NewProductService(db *gorm.DB) *ProductService {
    return &ProductService{db: db}
}

func (s *ProductService) CreateProduct(product *model.Product) error {
    return s.db.Create(product).Error
}

func (s *ProductService) GetProductByID(id uint) (*model.Product, error) {
    var product model.Product
    err := s.db.First(&product, id).Error
    return &product, err
}

func (s *ProductService) UpdateProduct(product *model.Product) error {
    return s.db.Save(product).Error
}

func (s *ProductService) DeleteProduct(id uint) error {
    return s.db.Delete(&model.Product{}, id).Error
}

func (s *ProductService) ListProducts(page, pageSize int) ([]model.Product, int64, error) {
    var products []model.Product
    var total int64

    offset := (page - 1) * pageSize

    err := s.db.Model(&model.Product{}).Count(&total).Error
    if err != nil {
        return nil, 0, err
    }

    err = s.db.Offset(offset).Limit(pageSize).Find(&products).Error
    return products, total, err
}

步骤3:创建API层

go 复制代码
// backend/internal/sys/api/product.go
package api

import (
    "siqian-admin/internal/sys/model"
    "siqian-admin/internal/sys/service"
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
)

type ProductHandler struct {
    productService *service.ProductService
}

func NewProductHandler(productService *service.ProductService) *ProductHandler {
    return &ProductHandler{productService: productService}
}

type CreateProductRequest struct {
    Name        string  `json:"name" binding:"required"`
    Code        string  `json:"code" binding:"required"`
    Price       float64 `json:"price" binding:"required"`
    Category    string  `json:"category"`
    Status      int     `json:"status"`
    Description string  `json:"description"`
}

func (h *ProductHandler) CreateProduct(c *gin.Context) {
    var req CreateProductRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    product := &model.Product{
        Name:        req.Name,
        Code:        req.Code,
        Price:       req.Price,
        Category:    req.Category,
        Status:      req.Status,
        Description: req.Description,
    }

    if err := h.productService.CreateProduct(product); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, gin.H{"message": "产品创建成功", "product": product})
}

func (h *ProductHandler) GetProduct(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的产品ID"})
        return
    }

    product, err := h.productService.GetProductByID(uint(id))
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "产品不存在"})
        return
    }

    c.JSON(http.StatusOK, product)
}

func (h *ProductHandler) ListProducts(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))

    products, total, err := h.productService.ListProducts(page, pageSize)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "获取产品列表失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "products":  products,
        "total":     total,
        "page":      page,
        "page_size": pageSize,
    })
}

func (h *ProductHandler) UpdateProduct(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的产品ID"})
        return
    }

    var req CreateProductRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    product, err := h.productService.GetProductByID(uint(id))
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "产品不存在"})
        return
    }

    product.Name = req.Name
    product.Code = req.Code
    product.Price = req.Price
    product.Category = req.Category
    product.Status = req.Status
    product.Description = req.Description

    if err := h.productService.UpdateProduct(product); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "更新成功", "product": product})
}

func (h *ProductHandler) DeleteProduct(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的产品ID"})
        return
    }

    if err := h.productService.DeleteProduct(uint(id)); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

步骤4:注册路由

go 复制代码
// backend/internal/router/router.go
// 在路由配置中添加产品管理路由
func SetupRoutes(r *gin.Engine, db *gorm.DB, rdb *redis.Client) {
    // ... 其他路由配置

    // 产品管理
    productService := sysservice.NewProductService(db)
    productHandler := sysapi.NewProductHandler(productService)

    products := authorized.Group("/products")
    {
        products.POST("", productHandler.CreateProduct)
        products.GET("", productHandler.ListProducts)
        products.GET("/:id", productHandler.GetProduct)
        products.PUT("/:id", productHandler.UpdateProduct)
        products.DELETE("/:id", productHandler.DeleteProduct)
    }
}
2. 前端开发

步骤1:创建API服务

typescript 复制代码
// frontend/src/services/product.ts
import api from './api';

export interface Product {
  id: number;
  name: string;
  code: string;
  price: number;
  category: string;
  status: number;
  description: string;
  created_at: string;
  updated_at: string;
}

export interface CreateProductRequest {
  name: string;
  code: string;
  price: number;
  category: string;
  status: number;
  description: string;
}

export interface ProductListResponse {
  products: Product[];
  total: number;
  page: number;
  page_size: number;
}

export const productService = {
  getProducts: async (page = 1, pageSize = 10): Promise<ProductListResponse> => {
    const response = await api.get('/products', {
      params: { page, page_size: pageSize }
    });
    return response.data;
  },

  getProduct: async (id: string): Promise<Product> => {
    const response = await api.get(`/products/${id}`);
    return response.data;
  },

  createProduct: async (data: CreateProductRequest): Promise<Product> => {
    const response = await api.post('/products', data);
    return response.data.product;
  },

  updateProduct: async (id: string, data: CreateProductRequest): Promise<Product> => {
    const response = await api.put(`/products/${id}`, data);
    return response.data.product;
  },

  deleteProduct: async (id: string): Promise<void> => {
    await api.delete(`/products/${id}`);
  },
};

步骤2:创建页面组件

typescript 复制代码
// frontend/src/pages/product/ProductManagement.tsx
import React, { useState, useEffect } from 'react';
import { Card, Table, Button, Space, Tag, message, Popconfirm, Modal, Form, Input, InputNumber, Select } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
import { productService, Product, CreateProductRequest } from '../../services/product';

const ProductManagement: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(false);
  const [createVisible, setCreateVisible] = useState(false);
  const [editVisible, setEditVisible] = useState(false);
  const [editingProduct, setEditingProduct] = useState<Product | null>(null);
  const [form] = Form.useForm<CreateProductRequest>();
  const [editForm] = Form.useForm<CreateProductRequest>();

  // 加载产品列表
  const loadProducts = async () => {
    setLoading(true);
    try {
      const data = await productService.getProducts();
      setProducts(data.products);
    } catch (error) {
      message.error('加载产品列表失败');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadProducts();
  }, []);

  // 打开新增弹窗
  const openCreateModal = () => {
    form.resetFields();
    form.setFieldsValue({ status: 1 });
    setCreateVisible(true);
  };

  // 提交新增
  const handleCreate = async () => {
    try {
      const values = await form.validateFields();
      await productService.createProduct(values);
      message.success('创建成功');
      setCreateVisible(false);
      loadProducts();
    } catch (error: any) {
      if (error?.errorFields) return;
      message.error(error?.response?.data?.error || '创建失败');
    }
  };

  // 打开编辑弹窗
  const openEditModal = (product: Product) => {
    setEditingProduct(product);
    editForm.setFieldsValue({
      name: product.name,
      code: product.code,
      price: product.price,
      category: product.category,
      status: product.status,
      description: product.description,
    });
    setEditVisible(true);
  };

  // 提交编辑
  const handleEdit = async () => {
    if (!editingProduct) return;
    try {
      const values = await editForm.validateFields();
      await productService.updateProduct(editingProduct.id.toString(), values);
      message.success('更新成功');
      setEditVisible(false);
      loadProducts();
    } catch (error: any) {
      if (error?.errorFields) return;
      message.error(error?.response?.data?.error || '更新失败');
    }
  };

  // 删除产品
  const handleDelete = async (id: string) => {
    try {
      await productService.deleteProduct(id);
      message.success('删除成功');
      loadProducts();
    } catch (error: any) {
      message.error(error?.response?.data?.error || '删除失败');
    }
  };

  const columns = [
    {
      title: '产品名称',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: '产品编码',
      dataIndex: 'code',
      key: 'code',
    },
    {
      title: '价格',
      dataIndex: 'price',
      key: 'price',
      render: (price: number) => `¥${price.toFixed(2)}`,
    },
    {
      title: '分类',
      dataIndex: 'category',
      key: 'category',
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (status: number) => (
        <Tag color={status === 1 ? 'green' : 'red'}>
          {status === 1 ? '正常' : '禁用'}
        </Tag>
      ),
    },
    {
      title: '操作',
      key: 'action',
      render: (_, record: Product) => (
        <Space>
          <Button
            type="link"
            icon={<EditOutlined />}
            onClick={() => openEditModal(record)}
          >
            编辑
          </Button>
          <Popconfirm
            title="确定要删除这个产品吗?"
            onConfirm={() => handleDelete(record.id.toString())}
            okText="确定"
            cancelText="取消"
          >
            <Button type="link" danger icon={<DeleteOutlined />}>
              删除
            </Button>
          </Popconfirm>
        </Space>
      ),
    },
  ];

  return (
    <Card title="产品管理">
      <div style={{ marginBottom: 16 }}>
        <Space>
          <Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
            新增产品
          </Button>
          <Button icon={<ReloadOutlined />} onClick={loadProducts}>
            刷新
          </Button>
        </Space>
      </div>

      <Table
        columns={columns}
        dataSource={products}
        rowKey="id"
        loading={loading}
        pagination={{
          showSizeChanger: true,
          showQuickJumper: true,
          showTotal: (total) => `共 ${total} 条记录`,
        }}
      />

      {/* 新增弹窗 */}
      <Modal
        title="新增产品"
        open={createVisible}
        onOk={handleCreate}
        onCancel={() => setCreateVisible(false)}
        okText="确定"
        cancelText="取消"
      >
        <Form form={form} layout="vertical">
          <Form.Item name="name" label="产品名称" rules={[{ required: true, message: '请输入产品名称' }]}>
            <Input placeholder="请输入产品名称" />
          </Form.Item>
          <Form.Item name="code" label="产品编码" rules={[{ required: true, message: '请输入产品编码' }]}>
            <Input placeholder="请输入产品编码" />
          </Form.Item>
          <Form.Item name="price" label="价格" rules={[{ required: true, message: '请输入价格' }]}>
            <InputNumber placeholder="请输入价格" style={{ width: '100%' }} min={0} />
          </Form.Item>
          <Form.Item name="category" label="分类">
            <Input placeholder="请输入分类" />
          </Form.Item>
          <Form.Item name="status" label="状态">
            <Select>
              <Select.Option value={1}>正常</Select.Option>
              <Select.Option value={0}>禁用</Select.Option>
            </Select>
          </Form.Item>
          <Form.Item name="description" label="描述">
            <Input.TextArea placeholder="请输入描述" rows={3} />
          </Form.Item>
        </Form>
      </Modal>

      {/* 编辑弹窗 */}
      <Modal
        title="编辑产品"
        open={editVisible}
        onOk={handleEdit}
        onCancel={() => setEditVisible(false)}
        okText="确定"
        cancelText="取消"
      >
        <Form form={editForm} layout="vertical">
          <Form.Item name="name" label="产品名称" rules={[{ required: true, message: '请输入产品名称' }]}>
            <Input placeholder="请输入产品名称" />
          </Form.Item>
          <Form.Item name="code" label="产品编码" rules={[{ required: true, message: '请输入产品编码' }]}>
            <Input placeholder="请输入产品编码" />
          </Form.Item>
          <Form.Item name="price" label="价格" rules={[{ required: true, message: '请输入价格' }]}>
            <InputNumber placeholder="请输入价格" style={{ width: '100%' }} min={0} />
          </Form.Item>
          <Form.Item name="category" label="分类">
            <Input placeholder="请输入分类" />
          </Form.Item>
          <Form.Item name="status" label="状态">
            <Select>
              <Select.Option value={1}>正常</Select.Option>
              <Select.Option value={0}>禁用</Select.Option>
            </Select>
          </Form.Item>
          <Form.Item name="description" label="描述">
            <Input.TextArea placeholder="请输入描述" rows={3} />
          </Form.Item>
        </Form>
      </Modal>
    </Card>
  );
};

export default ProductManagement;

步骤3:添加路由

在菜单管理配置菜单并赋给角色,重新登录后即可访问

🔐 权限控制使用指南

后端权限控制

1. 中间件权限控制

go 复制代码
// 在路由中使用权限中间件
users.GET("", middleware.AuthMiddleware(rdb, []string{"user:list"}), userHandler.ListUsers)
users.POST("", middleware.AuthMiddleware(rdb, []string{"user:create"}), userHandler.CreateUser)
users.PUT("/:id", middleware.AuthMiddleware(rdb, []string{"user:update"}), userHandler.UpdateUser)
users.DELETE("/:id", middleware.AuthMiddleware(rdb, []string{"user:delete"}), userHandler.DeleteUser)

2. 权限标识规范

  • 格式:模块:操作
  • 示例:user:listuser:createuser:updateuser:delete
  • 菜单权限:user:menu

前端权限控制

typescript 复制代码
// 在组件中使用权限控制
import { usePermission } from '../utils/permission';

const UserManagement: React.FC = () => {
  const canCreate = usePermission('user:create');
  const canUpdate = usePermission('user:update');
  const canDelete = usePermission('user:delete');

  return (
    <div>
      {canCreate && (
        <Button type="primary" onClick={handleCreate}>
          新增用户
        </Button>
      )}

      <Table
        columns={[
          // ... 其他列
          {
            title: '操作',
            render: (_, record) => (
              <Space>
                {canUpdate && (
                  <Button onClick={() => handleEdit(record)}>编辑</Button>
                )}
                {canDelete && (
                  <Button danger onClick={() => handleDelete(record)}>删除</Button>
                )}
              </Space>
            ),
          },
        ]}
      />
    </div>
  );
};

3. 路由权限控制

typescript 复制代码
// 在路由配置中使用权限控制
import { usePermission } from '../utils/permission';

const ProtectedRoute: React.FC<{ permission: string; children: React.ReactNode }> = ({
  permission,
  children,
}) => {
  const hasPermission = usePermission(permission);

  if (!hasPermission) {
    return <div>无权限访问</div>;
  }

  return <>{children}</>;
};

// 使用示例
<Route 
  path="/users" 
  element={
    <ProtectedRoute permission="user:menu">
      <UserManagement />
    </ProtectedRoute>
  } 
/>

前端字典使用

1. 在组件中使用字典

typescript 复制代码
// 使用字典数据
import { useDict } from '../hooks/useDict';
import { useDictStore } from '../store/dictStore';

const UserForm: React.FC = () => {
  // 方式1:使用Hook
  const { dictOptions: statusOptions } = useDict('user_status');

  // 方式2:直接从Store获取
  const { getDictOptions } = useDictStore();
  const genderOptions = getDictOptions('user_gender');

  return (
    <Form>
      <Form.Item name="status" label="状态">
        <Select options={statusOptions} placeholder="请选择状态" />
      </Form.Item>

      <Form.Item name="gender" label="性别">
        <Select options={genderOptions} placeholder="请选择性别" />
      </Form.Item>
    </Form>
  );
};

🧩 组件使用指南

1. OrganizationTree组件

typescript 复制代码
// 使用组织树组件
import OrganizationTree from '../components/OrganizationTree';

const MyComponent: React.FC = () => {
  const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);

  return (
    <OrganizationTree
      selectedOrgId={selectedOrgId}
      onSelect={setSelectedOrgId}
      defaultExpandAll={true}
      title="选择组织"
      size="default"
    />
  );
};

2. UserSelector组件

typescript 复制代码
// 使用用户选择器组件
import UserSelector from '../components/UserSelector';

const MyComponent: React.FC = () => {
  const [userSelectorVisible, setUserSelectorVisible] = useState(false);
  const [selectedUserKeys, setSelectedUserKeys] = useState<React.Key[]>([]);

  const handleUserSelect = (userIds: string[]) => {
    console.log('选中的用户ID:', userIds);
    setUserSelectorVisible(false);
  };

  return (
    <div>
      <Button onClick={() => setUserSelectorVisible(true)}>
        选择用户
      </Button>

      <UserSelector
        visible={userSelectorVisible}
        title="选择用户"
        selectedUserKeys={selectedUserKeys}
        onCancel={() => setUserSelectorVisible(false)}
        onOk={handleUserSelect}
      />
    </div>
  );
};

🖼️ 效果展示








相关推荐
脚踏实地的大梦想家3 小时前
【Go】P6 Golang 基础:流程控制
开发语言·golang
QX_hao4 小时前
【Go】--数组和切片
后端·golang·restful
-睡到自然醒~4 小时前
提升应用性能:Go中的同步与异步处理
开发语言·后端·golang
只吃不吃香菜4 小时前
Go WebSocket 协程泄漏问题分析与解决方案
开发语言·websocket·golang
神的孩子都在歌唱4 小时前
PostgreSQL 向量检索方式(pgvector)
数据库·人工智能·postgresql
ChineHe4 小时前
Golang并发编程篇001_并发编程相关概念解释
开发语言·后端·golang
知识分享小能手5 小时前
微信小程序入门学习教程,从入门到精通,项目实战:美妆商城小程序 —— 知识点详解与案例代码 (18)
前端·学习·react.js·微信小程序·小程序·vue·前端技术
DoraBigHead5 小时前
React 中的代数效应:从概念到 Fiber 架构的落地
前端·javascript·react.js
今天头发还在吗5 小时前
【框架演进】Vue与React的跨越性变革:从Vue2到Vue3,从Class到Hooks
javascript·vue.js·react.js