Browse Source

✨ feat(admin): 新增用户注册功能和管理设置

- 在用户管理菜单中添加创建用户子菜单项,包含用户列表和创建用户页面
- 新增注册页面组件,支持用户名、密码、邮箱等字段的表单验证和提交
- 在用户管理页面添加首页注册功能开关,可控制是否允许首页注册
- 新增系统设置API模块,包含设置项的增删改查和批量操作功能
- 创建系统设置数据库实体和迁移脚本,初始化首页注册功能设置
- 在首页根据设置状态动态显示或隐藏注册按钮
- 添加公共设置API端点供前端获取注册功能状态

【架构】
- 新增SystemSetting实体和对应的服务层、数据访问层
- 添加系统设置相关的API路由和OpenAPI文档
- 扩展客户端API配置以支持设置相关接口调用

【样式】
- 使用Lucide React图标库添加UserPlus图标
- 优化表单布局和输入框样式,添加图标装饰
yourname 3 months ago
parent
commit
64e4a5b607

+ 18 - 3
src/client/admin/menu.tsx

@@ -12,7 +12,8 @@ import {
   FileText,
   CreditCard,
   Wallet,
-  LayoutTemplate
+  LayoutTemplate,
+  UserPlus
 } from 'lucide-react';
 
 export interface MenuItem {
@@ -88,8 +89,22 @@ export const useMenu = () => {
       key: 'users',
       label: '用户管理',
       icon: <Users className="h-4 w-4" />,
-      path: '/admin/users',
-      permission: 'user:manage'
+      permission: 'user:manage',
+      children: [
+        {
+          key: 'users-list',
+          label: '用户列表',
+          path: '/admin/users',
+          permission: 'user:view'
+        },
+        {
+          key: 'users-register',
+          label: '创建用户',
+          icon: <UserPlus className="h-4 w-4" />,
+          path: '/admin/register',
+          permission: 'user:create'
+        }
+      ]
     },
     {
       key: 'files',

+ 195 - 0
src/client/admin/pages/Register.tsx

@@ -0,0 +1,195 @@
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { useNavigate } from 'react-router';
+import { toast } from 'sonner';
+import { authClient } from '@/client/api';
+
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Eye, EyeOff, User, Lock, Mail } from 'lucide-react';
+
+const registerSchema = z.object({
+  username: z.string()
+    .min(3, '用户名至少3个字符')
+    .max(20, '用户名不能超过20个字符')
+    .regex(/^[a-zA-Z0-9_-]+$/, '用户名只能包含字母、数字、下划线和连字符'),
+  password: z.string()
+    .min(6, '密码至少6个字符')
+    .max(30, '密码不能超过30个字符'),
+  confirmPassword: z.string(),
+  email: z.string().email('请输入有效的邮箱地址').optional(),
+}).refine((data) => data.password === data.confirmPassword, {
+  message: '两次密码输入不一致',
+  path: ['confirmPassword'],
+});
+
+type RegisterFormData = z.infer<typeof registerSchema>;
+
+const RegisterPage: React.FC = () => {
+  const navigate = useNavigate();
+  
+  const form = useForm<RegisterFormData>({
+    resolver: zodResolver(registerSchema),
+    defaultValues: {
+      username: '',
+      password: '',
+      confirmPassword: '',
+      email: '',
+    },
+  });
+
+  const onSubmit = async (data: RegisterFormData) => {
+    try {
+      const response = await authClient.register.$post({
+        json: {
+          username: data.username,
+          password: data.password,
+          email: data.email,
+        }
+      });
+      
+      if (response.status !== 201) {
+        const result = await response.json();
+        throw new Error(result.message || '注册失败');
+      }
+      
+      toast.success('用户注册成功!');
+      navigate('/admin/users');
+    } catch (error) {
+      toast.error(error instanceof Error ? error.message : '注册失败,请稍后重试');
+    }
+  };
+
+  return (
+    <div className="min-h-screen flex items-center justify-center bg-background px-4 py-12">
+      <Card className="w-full max-w-md border">
+        <CardHeader className="space-y-1">
+          <CardTitle className="text-2xl font-bold text-center">创建用户账号</CardTitle>
+          <CardDescription className="text-center">
+            在管理后台创建新用户账号
+          </CardDescription>
+        </CardHeader>
+        
+        <CardContent>
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+              <FormField
+                control={form.control}
+                name="username"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>用户名</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          placeholder="请输入用户名"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="email"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>邮箱(可选)</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          type="email"
+                          placeholder="请输入邮箱地址"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="password"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>密码</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          type="password"
+                          placeholder="请输入密码"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="confirmPassword"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>确认密码</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          type="password"
+                          placeholder="请再次输入密码"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <Button 
+                type="submit" 
+                className="w-full"
+                disabled={form.formState.isSubmitting}
+              >
+                {form.formState.isSubmitting ? '创建中...' : '创建用户'}
+              </Button>
+            </form>
+          </Form>
+        </CardContent>
+
+        <CardFooter className="flex flex-col space-y-4">
+          <div className="text-sm text-center">
+            <span className="text-muted-foreground">返回</span>
+            <Button
+              variant="link"
+              className="px-1"
+              onClick={() => navigate('/admin/users')}
+            >
+              用户管理
+            </Button>
+          </div>
+        </CardFooter>
+      </Card>
+    </div>
+  );
+};
+
+export default RegisterPage;

+ 62 - 6
src/client/admin/pages/Users.tsx

@@ -1,8 +1,8 @@
 import React, { useState } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { format } from 'date-fns';
-import { Plus, Search, Edit, Trash2 } from 'lucide-react';
-import { userClient } from '@/client/api';
+import { Plus, Search, Edit, Trash2, Settings } from 'lucide-react';
+import { userClient, settingsClient } from '@/client/api';
 import type { InferRequestType, InferResponseType } from 'hono/client';
 import { Button } from '@/client/components/ui/button';
 import { Input } from '@/client/components/ui/input';
@@ -46,6 +46,8 @@ export const UsersPage = () => {
   // Avatar selector is now integrated, no separate state needed
 
   const [isCreateForm, setIsCreateForm] = useState(true);
+  const [registerSetting, setRegisterSetting] = useState<boolean>(true);
+  const [isSettingLoading, setIsSettingLoading] = useState(false);
   
   const createForm = useForm<CreateUserFormData>({
     resolver: zodResolver(createUserFormSchema),
@@ -90,6 +92,25 @@ export const UsersPage = () => {
     }
   });
 
+  // 获取首页注册设置状态
+  useQuery({
+    queryKey: ['register-setting'],
+    queryFn: async () => {
+      try {
+        const res = await settingsClient[':key']['$get']({
+          param: { key: 'home_register_enabled' }
+        });
+        if (res.status === 200) {
+          const data = await res.json();
+          setRegisterSetting(data.settingValue === 'true');
+        }
+      } catch (error) {
+        console.error('获取注册设置失败:', error);
+      }
+      return null;
+    }
+  });
+
   const users = usersData?.data || [];
   const totalCount = usersData?.pagination?.total || 0;
 
@@ -202,6 +223,30 @@ export const UsersPage = () => {
     }
   };
 
+  // 切换首页注册功能状态
+  const toggleRegisterSetting = async () => {
+    setIsSettingLoading(true);
+    try {
+      const newValue = !registerSetting;
+      const res = await settingsClient[':key']['$put']({
+        param: { key: 'home_register_enabled' },
+        json: { settingValue: newValue.toString() }
+      });
+      
+      if (res.status === 200) {
+        setRegisterSetting(newValue);
+        toast.success(newValue ? '已开启首页注册功能' : '已关闭首页注册功能');
+      } else {
+        throw new Error('更新设置失败');
+      }
+    } catch (error) {
+      console.error('切换注册设置失败:', error);
+      toast.error('操作失败,请重试');
+    } finally {
+      setIsSettingLoading(false);
+    }
+  };
+
   // 渲染加载骨架
   if (isLoading) {
     return (
@@ -234,10 +279,21 @@ export const UsersPage = () => {
     <div className="space-y-4">
       <div className="flex justify-between items-center">
         <h1 className="text-2xl font-bold">用户管理</h1>
-        <Button onClick={handleCreateUser}>
-          <Plus className="mr-2 h-4 w-4" />
-          创建用户
-        </Button>
+        <div className="flex gap-2">
+          <Button
+            variant="outline"
+            onClick={toggleRegisterSetting}
+            disabled={isSettingLoading}
+            className="flex items-center gap-2"
+          >
+            <Settings className="h-4 w-4" />
+            {registerSetting ? '关闭首页注册' : '开启首页注册'}
+          </Button>
+          <Button onClick={handleCreateUser}>
+            <Plus className="mr-2 h-4 w-4" />
+            创建用户
+          </Button>
+        </div>
       </div>
 
       <Card>

+ 6 - 0
src/client/admin/routes.tsx

@@ -11,6 +11,7 @@ import { FilesPage } from './pages/Files';
 import MembershipPlans from './pages/MembershipPlans';
 import { PaymentsPage } from './pages/Payments';
 import Templates from './pages/Templates';
+import RegisterPage from './pages/Register';
 
 export const router = createBrowserRouter([
   {
@@ -63,6 +64,11 @@ export const router = createBrowserRouter([
         element: <Templates />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'register',
+        element: <RegisterPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 12 - 2
src/client/api.ts

@@ -2,7 +2,7 @@ import { hc } from 'hono/client'
 import type {
   AuthRoutes, UserRoutes, RoleRoutes,
   FileRoutes, MembershipPlanRoutes, PaymentRoutes,
-  TemplateRoutes, PublicTemplateRoutes
+  TemplateRoutes, PublicTemplateRoutes, SettingsRoutes, PublicSettingsRoutes
 } from '@/server/api';
 import { axiosFetch } from './utils/axios-fetch';
 
@@ -38,4 +38,14 @@ export const templateClient = hc<TemplateRoutes>('/', {
 // 公共模板客户端(无需认证)
 export const publicTemplateClient = hc<PublicTemplateRoutes>('/', {
   fetch: axiosFetch,
-}).api.v1.public.templates;
+}).api.v1.public.templates;
+
+// 系统设置客户端(需要认证)
+export const settingsClient = hc<SettingsRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.settings;
+
+// 公共设置客户端(无需认证)
+export const publicSettingsClient = hc<PublicSettingsRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.public.settings;

+ 27 - 7
src/client/home/pages/HomePage.tsx

@@ -24,7 +24,7 @@ import { useNavigate } from 'react-router-dom';
 import UserInfoModal from '@/client/home/components/UserInfoModal';
 import { useAuth } from '@/client/home/hooks/AuthProvider';
 import { useQuery } from '@tanstack/react-query';
-import { membershipPlanClient } from '@/client/api';
+import { membershipPlanClient, publicSettingsClient } from '@/client/api';
 import type { InferResponseType } from 'hono/client';
 
 export default function HomePage() {
@@ -32,6 +32,7 @@ export default function HomePage() {
   const { user, isAuthenticated } = useAuth();
   const [hoveredFeature, setHoveredFeature] = useState<number | null>(null);
   const [showUserModal, setShowUserModal] = useState(false);
+  const [registerEnabled, setRegisterEnabled] = useState(true);
 
   const { data: membershipPlans } = useQuery({
     queryKey: ['membership-plans-home'],
@@ -43,6 +44,23 @@ export default function HomePage() {
     },
   });
 
+  // 获取首页注册设置状态
+  useQuery({
+    queryKey: ['register-setting-public'],
+    queryFn: async () => {
+      try {
+        const res = await publicSettingsClient.registerStatus.$get();
+        if (res.status === 200) {
+          const data = await res.json();
+          setRegisterEnabled(data.enabled);
+        }
+      } catch (error) {
+        console.error('获取注册设置失败:', error);
+      }
+      return null;
+    }
+  });
+
   const features = [
     {
       icon: <FileText className="h-8 w-8" />,
@@ -116,12 +134,14 @@ export default function HomePage() {
                   >
                     登录
                   </button>
-                  <button
-                    onClick={() => navigate('/register')}
-                    className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors font-medium"
-                  >
-                    注册
-                  </button>
+                  {registerEnabled && (
+                    <button
+                      onClick={() => navigate('/register')}
+                      className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors font-medium"
+                    >
+                      注册
+                    </button>
+                  )}
                 </>
               )}
             </nav>

+ 6 - 0
src/server/api.ts

@@ -9,6 +9,8 @@ import membershipPlanRoute from './api/membership-plans/index'
 import paymentRoute from './api/payments/index'
 import templateRoute from './api/templates/index'
 import publicTemplateRoute from './api/public/templates/index'
+import publicSettingsRoute from './api/public/settings/index'
+import settingsRoute from './api/settings/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -111,6 +113,8 @@ const membershipPlanRoutes = api.route('/api/v1/membership-plans', membershipPla
 const paymentRoutes = api.route('/api/v1/payments', paymentRoute)
 const templateRoutes = api.route('/api/v1/templates', templateRoute)
 const publicTemplateRoutes = api.route('/api/v1/public/templates', publicTemplateRoute)
+const publicSettingsRoutes = api.route('/api/v1/public/settings', publicSettingsRoute)
+const settingsRoutes = api.route('/api/v1/settings', settingsRoute)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -120,6 +124,8 @@ export type MembershipPlanRoutes = typeof membershipPlanRoutes
 export type PaymentRoutes = typeof paymentRoutes
 export type TemplateRoutes = typeof templateRoutes
 export type PublicTemplateRoutes = typeof publicTemplateRoutes
+export type PublicSettingsRoutes = typeof publicSettingsRoutes
+export type SettingsRoutes = typeof settingsRoutes
 
 app.route('/', api)
 export default app

+ 7 - 0
src/server/api/public/settings/index.ts

@@ -0,0 +1,7 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import getRegisterStatusRoute from './register-status';
+
+const app = new OpenAPIHono()
+  .route('/', getRegisterStatusRoute);
+
+export default app;

+ 44 - 0
src/server/api/public/settings/register-status.ts

@@ -0,0 +1,44 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { SystemSettingService } from '@/server/modules/settings/system-setting.service';
+
+// 响应Schema
+const RegisterStatusResponse = z.object({
+  enabled: z.boolean().openapi({
+    description: '首页注册功能是否启用',
+    example: true
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/register-status',
+  responses: {
+    200: {
+      description: '成功获取首页注册状态',
+      content: { 'application/json': { schema: RegisterStatusResponse } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono().openapi(routeDef, async (c) => {
+  try {
+    const service = new SystemSettingService(AppDataSource);
+    const enabled = await service.getBooleanValue('home_register_enabled');
+    
+    return c.json({ enabled }, 200);
+  } catch (error) {
+    const { code = 500, message = '获取注册状态失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 70 - 0
src/server/api/settings/[key]/get.ts

@@ -0,0 +1,70 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { SettingValueResponse } from '@/server/modules/settings/system-setting.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { SystemSettingService } from '@/server/modules/settings/system-setting.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 路径参数Schema
+const GetParams = z.object({
+  key: z.string().openapi({
+    param: { name: 'key', in: 'path' },
+    example: 'home_register_enabled',
+    description: '设置键名'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{key}',
+  middleware: [authMiddleware],
+  request: {
+    params: GetParams
+  },
+  responses: {
+    200: {
+      description: '成功获取系统设置值',
+      content: { 'application/json': { schema: SettingValueResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '设置不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { key } = c.req.valid('param');
+    const service = new SystemSettingService(AppDataSource);
+    const setting = await service.findByKey(key);
+    
+    if (!setting) {
+      return c.json({ code: 404, message: `设置 ${key} 不存在` }, 404);
+    }
+
+    return c.json({
+      settingKey: setting.settingKey,
+      settingValue: setting.settingValue,
+      description: setting.description,
+      settingType: setting.settingType
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '获取系统设置失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 76 - 0
src/server/api/settings/[key]/put.ts

@@ -0,0 +1,76 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { SystemSettingSchema } from '@/server/modules/settings/system-setting.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { SystemSettingService } from '@/server/modules/settings/system-setting.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 路径参数Schema
+const UpdateParams = z.object({
+  key: z.string().openapi({
+    param: { name: 'key', in: 'path' },
+    example: 'home_register_enabled',
+    description: '设置键名'
+  })
+});
+
+// 请求体Schema
+const UpdateValueRequest = z.object({
+  settingValue: z.string().nullable().openapi({
+    description: '设置值',
+    example: 'false'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{key}',
+  middleware: [authMiddleware],
+  request: {
+    params: UpdateParams,
+    body: {
+      content: {
+        'application/json': { schema: UpdateValueRequest }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功更新系统设置',
+      content: { 'application/json': { schema: SystemSettingSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '设置不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { key } = c.req.valid('param');
+    const { settingValue } = await c.req.json();
+    
+    const service = new SystemSettingService(AppDataSource);
+    const setting = await service.updateValue(key, settingValue);
+    
+    return c.json(setting, 200);
+  } catch (error) {
+    const { code = 500, message = '更新系统设置失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 62 - 0
src/server/api/settings/batch-put.ts

@@ -0,0 +1,62 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { SystemSettingSchema } from '@/server/modules/settings/system-setting.schema';
+import { BatchUpdateSettingsDto } from '@/server/modules/settings/system-setting.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { SystemSettingService } from '@/server/modules/settings/system-setting.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 响应Schema
+const BatchUpdateResponse = z.object({
+  data: z.array(SystemSettingSchema),
+  updated: z.number().openapi({ example: 3, description: '成功更新的设置数量' })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'put',
+  path: '/batch',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: BatchUpdateSettingsDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功批量更新系统设置',
+      content: { 'application/json': { schema: BatchUpdateResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { settings } = await c.req.json();
+    
+    const service = new SystemSettingService(AppDataSource);
+    const results = await service.batchUpdate(settings);
+    
+    return c.json({
+      data: results,
+      updated: results.length
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '批量更新系统设置失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 53 - 0
src/server/api/settings/get.ts

@@ -0,0 +1,53 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { SystemSettingSchema } from '@/server/modules/settings/system-setting.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { SystemSettingService } from '@/server/modules/settings/system-setting.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 响应Schema
+const ListResponse = z.object({
+  data: z.array(SystemSettingSchema),
+  total: z.number().openapi({ example: 10, description: '总记录数' })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  responses: {
+    200: {
+      description: '成功获取系统设置列表',
+      content: { 'application/json': { schema: ListResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const service = new SystemSettingService(AppDataSource);
+    const data = await service.findAll();
+    
+    return c.json({
+      data,
+      total: data.length
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '获取系统设置列表失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 13 - 0
src/server/api/settings/index.ts

@@ -0,0 +1,13 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import listRoute from './get';
+import getByKeyRoute from './[key]/get';
+import updateRoute from './[key]/put';
+import batchUpdateRoute from './batch-put';
+
+const app = new OpenAPIHono()
+  .route('/', listRoute)
+  .route('/', getByKeyRoute)
+  .route('/', updateRoute)
+  .route('/', batchUpdateRoute);
+
+export default app;

+ 6 - 2
src/server/data-source.ts

@@ -9,6 +9,8 @@ import { File } from "./modules/files/file.entity"
 import { PaymentEntity } from "./modules/payments/payment.entity"
 import { MembershipPlan } from "./modules/membership/membership-plan.entity"
 import { Template } from "./modules/templates/template.entity"
+import { InitSystemSettings1735900000000 } from "./migrations/1735900000000-init-system-settings"
+import { SystemSetting } from "./modules/settings/system-setting.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -18,9 +20,11 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role, File, PaymentEntity, MembershipPlan, Template,
+    User, Role, File, PaymentEntity, MembershipPlan, Template, SystemSetting,
+  ],
+  migrations: [
+    InitSystemSettings1735900000000,
   ],
-  migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",
   logging: process.env.DB_LOGGING === "true",
 });

+ 21 - 0
src/server/migrations/1735900000000-init-system-settings.ts

@@ -0,0 +1,21 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class InitSystemSettings1735900000000 implements MigrationInterface {
+    name = 'InitSystemSettings1735900000000'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        // 插入默认系统设置
+        await queryRunner.query(`
+            INSERT INTO system_settings (setting_key, setting_value, description, setting_type, created_at, updated_at)
+            VALUES 
+            ('home_register_enabled', 'true', '首页注册功能开关', 'boolean', NOW(), NOW())
+        `);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        // 删除插入的系统设置
+        await queryRunner.query(`
+            DELETE FROM system_settings WHERE setting_key = 'home_register_enabled'
+        `);
+    }
+}

+ 49 - 0
src/server/modules/settings/system-setting.entity.ts

@@ -0,0 +1,49 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+@Entity('system_settings')
+export class SystemSetting {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ 
+    name: 'setting_key', 
+    type: 'varchar', 
+    length: 100, 
+    unique: true,
+    comment: '设置键名' 
+  })
+  settingKey!: string;
+
+  @Column({ 
+    name: 'setting_value', 
+    type: 'text', 
+    nullable: true,
+    comment: '设置值' 
+  })
+  settingValue!: string | null;
+
+  @Column({ 
+    name: 'description', 
+    type: 'varchar', 
+    length: 255, 
+    nullable: true,
+    comment: '设置描述' 
+  })
+  description!: string | null;
+
+  @Column({ 
+    name: 'setting_type', 
+    type: 'varchar', 
+    length: 50, 
+    default: 'string',
+    comment: '设置类型: string, number, boolean, json' 
+  })
+  settingType!: string;
+
+  @CreateDateColumn({ name: 'created_at', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', comment: '更新时间' })
+  updatedAt!: Date;
+}

+ 113 - 0
src/server/modules/settings/system-setting.schema.ts

@@ -0,0 +1,113 @@
+import { z } from '@hono/zod-openapi';
+
+// 系统设置完整Schema
+export const SystemSettingSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '系统设置ID',
+    example: 1
+  }),
+  settingKey: z.string().max(100).openapi({
+    description: '设置键名',
+    example: 'home_register_enabled'
+  }),
+  settingValue: z.string().nullable().openapi({
+    description: '设置值',
+    example: 'true'
+  }),
+  description: z.string().max(255).nullable().openapi({
+    description: '设置描述',
+    example: '首页注册功能开关'
+  }),
+  settingType: z.string().max(50).openapi({
+    description: '设置类型',
+    example: 'boolean'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  })
+});
+
+// 创建系统设置DTO
+export const CreateSystemSettingDto = z.object({
+  settingKey: z.string().min(1, '设置键名不能为空').max(100, '设置键名最多100个字符').openapi({
+    description: '设置键名',
+    example: 'home_register_enabled'
+  }),
+  settingValue: z.string().nullable().openapi({
+    description: '设置值',
+    example: 'true'
+  }),
+  description: z.string().max(255, '描述最多255个字符').nullable().optional().openapi({
+    description: '设置描述',
+    example: '首页注册功能开关'
+  }),
+  settingType: z.string().max(50, '设置类型最多50个字符').default('string').openapi({
+    description: '设置类型',
+    example: 'boolean'
+  })
+});
+
+// 更新系统设置DTO
+export const UpdateSystemSettingDto = z.object({
+  settingKey: z.string().min(1, '设置键名不能为空').max(100, '设置键名最多100个字符').optional().openapi({
+    description: '设置键名',
+    example: 'home_register_enabled'
+  }),
+  settingValue: z.string().nullable().optional().openapi({
+    description: '设置值',
+    example: 'false'
+  }),
+  description: z.string().max(255, '描述最多255个字符').nullable().optional().openapi({
+    description: '设置描述',
+    example: '首页注册功能开关'
+  }),
+  settingType: z.string().max(50, '设置类型最多50个字符').optional().openapi({
+    description: '设置类型',
+    example: 'boolean'
+  })
+});
+
+// 批量更新设置DTO
+export const BatchUpdateSettingsDto = z.object({
+  settings: z.array(z.object({
+    settingKey: z.string().min(1, '设置键名不能为空').max(100),
+    settingValue: z.string().nullable(),
+    description: z.string().max(255).nullable().optional(),
+    settingType: z.string().max(50).optional()
+  })).openapi({
+    description: '批量设置列表',
+    example: [
+      {
+        settingKey: 'home_register_enabled',
+        settingValue: 'true',
+        description: '首页注册功能开关',
+        settingType: 'boolean'
+      }
+    ]
+  })
+});
+
+// 获取设置值响应DTO
+export const SettingValueResponse = z.object({
+  settingKey: z.string().openapi({
+    description: '设置键名',
+    example: 'home_register_enabled'
+  }),
+  settingValue: z.string().nullable().openapi({
+    description: '设置值',
+    example: 'true'
+  }),
+  description: z.string().nullable().openapi({
+    description: '设置描述',
+    example: '首页注册功能开关'
+  }),
+  settingType: z.string().openapi({
+    description: '设置类型',
+    example: 'boolean'
+  })
+});

+ 149 - 0
src/server/modules/settings/system-setting.service.ts

@@ -0,0 +1,149 @@
+import { DataSource, Repository } from 'typeorm';
+import { SystemSetting } from './system-setting.entity';
+import { CreateSystemSettingDto, UpdateSystemSettingDto } from './system-setting.schema';
+
+export class SystemSettingService {
+  private repository: Repository<SystemSetting>;
+  
+  constructor(dataSource: DataSource) {
+    this.repository = dataSource.getRepository(SystemSetting);
+  }
+
+  /**
+   * 获取所有系统设置
+   */
+  async findAll(): Promise<SystemSetting[]> {
+    return this.repository.find({ order: { settingKey: 'ASC' } });
+  }
+
+  /**
+   * 根据键名获取系统设置
+   */
+  async findByKey(settingKey: string): Promise<SystemSetting | null> {
+    return this.repository.findOne({ where: { settingKey } });
+  }
+
+  /**
+   * 获取设置值
+   */
+  async getValue(settingKey: string): Promise<string | null> {
+    const setting = await this.findByKey(settingKey);
+    return setting?.settingValue || null;
+  }
+
+  /**
+   * 获取布尔值设置
+   */
+  async getBooleanValue(settingKey: string): Promise<boolean> {
+    const value = await this.getValue(settingKey);
+    return value === 'true';
+  }
+
+  /**
+   * 获取数字值设置
+   */
+  async getNumberValue(settingKey: string): Promise<number> {
+    const value = await this.getValue(settingKey);
+    return value ? Number(value) : 0;
+  }
+
+  /**
+   * 创建系统设置
+   */
+  async create(data: CreateSystemSettingDto): Promise<SystemSetting> {
+    const existing = await this.findByKey(data.settingKey);
+    if (existing) {
+      throw new Error(`设置键名 ${data.settingKey} 已存在`);
+    }
+
+    const setting = this.repository.create(data);
+    return this.repository.save(setting);
+  }
+
+  /**
+   * 更新系统设置
+   */
+  async update(settingKey: string, data: UpdateSystemSettingDto): Promise<SystemSetting> {
+    const setting = await this.findByKey(settingKey);
+    if (!setting) {
+      throw new Error(`设置 ${settingKey} 不存在`);
+    }
+
+    Object.assign(setting, data);
+    return this.repository.save(setting);
+  }
+
+  /**
+   * 更新设置值
+   */
+  async updateValue(settingKey: string, settingValue: string | null): Promise<SystemSetting> {
+    const setting = await this.findByKey(settingKey);
+    if (!setting) {
+      throw new Error(`设置 ${settingKey} 不存在`);
+    }
+
+    setting.settingValue = settingValue;
+    return this.repository.save(setting);
+  }
+
+  /**
+   * 批量更新设置
+   */
+  async batchUpdate(settings: Array<{ settingKey: string; settingValue: string | null }>): Promise<SystemSetting[]> {
+    const results: SystemSetting[] = [];
+    
+    for (const item of settings) {
+      try {
+        const setting = await this.findByKey(item.settingKey);
+        if (setting) {
+          setting.settingValue = item.settingValue;
+          const updated = await this.repository.save(setting);
+          results.push(updated);
+        } else {
+          // 如果设置不存在,创建新的
+          const newSetting = this.repository.create({
+            settingKey: item.settingKey,
+            settingValue: item.settingValue,
+            settingType: 'string',
+            description: `系统设置: ${item.settingKey}`
+          });
+          const created = await this.repository.save(newSetting);
+          results.push(created);
+        }
+      } catch (error) {
+        console.error(`更新设置 ${item.settingKey} 失败:`, error);
+      }
+    }
+    
+    return results;
+  }
+
+  /**
+   * 删除系统设置
+   */
+  async delete(settingKey: string): Promise<boolean> {
+    const result = await this.repository.delete({ settingKey });
+    return result.affected !== null && result.affected > 0;
+  }
+
+  /**
+   * 初始化默认设置
+   */
+  async initDefaultSettings(): Promise<void> {
+    const defaultSettings = [
+      {
+        settingKey: 'home_register_enabled',
+        settingValue: 'true',
+        description: '首页注册功能开关',
+        settingType: 'boolean'
+      }
+    ];
+
+    for (const setting of defaultSettings) {
+      const existing = await this.findByKey(setting.settingKey);
+      if (!existing) {
+        await this.create(setting);
+      }
+    }
+  }
+}