Bläddra i källkod

✨ feat(membership): 添加会员套餐管理系统

- 创建会员套餐实体和服务层,支持CRUD操作
- 实现套餐管理后台页面,支持创建、编辑、删除套餐
- 在首页添加套餐展示和价格表
- 添加套餐API路由和客户端调用方法
- 初始化默认套餐数据:单次、单月、年、永久四种类型

🐛 fix(server): 优化错误处理逻辑
- 开发环境下返回详细错误堆栈信息
- 统一错误日志输出格式
yourname 3 månader sedan
förälder
incheckning
5245205dd9

+ 2 - 0
server.js

@@ -293,6 +293,8 @@ app.use(async (c) => {
   } catch (e) {
     if (!isProduction && vite) {
       vite.ssrFixStacktrace(e);
+      console.error('请求处理错误:', e.stack);
+      return c.text(e.stack, 500);
     }
     console.error('请求处理错误:', e.stack);
     return c.text('服务器内部错误', 500);

+ 9 - 1
src/client/admin-shadcn/menu.tsx

@@ -9,7 +9,8 @@ import {
   BarChart3,
   LayoutDashboard,
   File,
-  FileText
+  FileText,
+  CreditCard
 } from 'lucide-react';
 
 export interface MenuItem {
@@ -102,6 +103,13 @@ export const useMenu = () => {
       path: '/admin/analytics',
       permission: 'analytics:view'
     },
+    {
+      key: 'membership-plans',
+      label: '会员套餐',
+      icon: <CreditCard className="h-4 w-4" />,
+      path: '/admin/membership-plans',
+      permission: 'membership:manage'
+    },
     {
       key: 'settings',
       label: '系统设置',

+ 454 - 0
src/client/admin-shadcn/pages/MembershipPlans.tsx

@@ -0,0 +1,454 @@
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/client/components/ui/table';
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from '@/client/components/ui/dialog';
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from '@/client/components/ui/form';
+import { Input } from '@/client/components/ui/input';
+import { Textarea } from '@/client/components/ui/textarea';
+import { Switch } from '@/client/components/ui/switch';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/client/components/ui/select';
+import { z } from 'zod';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Plus, Edit, Trash2 } from 'lucide-react';
+import { client } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { toast } from 'sonner';
+
+type MembershipPlan = InferResponseType<typeof client.api.v1['membership-plans'].$get, 200>['data'][0];
+type CreateMembershipPlanRequest = InferRequestType<typeof client.api.v1['membership-plans'].$post>['json'];
+type UpdateMembershipPlanRequest = InferRequestType<typeof client.api.v1['membership-plans'][number]['$put']>['json'];
+
+const formSchema = z.object({
+  name: z.string().min(1, '套餐名称不能为空'),
+  type: z.enum(['single', 'monthly', 'yearly', 'lifetime']),
+  price: z.number().min(0, '价格必须大于等于0'),
+  durationDays: z.number().min(0, '有效期必须大于等于0'),
+  description: z.string().optional(),
+  features: z.string().optional(),
+  isActive: z.boolean(),
+  sortOrder: z.number().min(0),
+});
+
+export default function MembershipPlans() {
+  const queryClient = useQueryClient();
+  const [open, setOpen] = useState(false);
+  const [editingPlan, setEditingPlan] = useState<MembershipPlan | null>(null);
+
+  const { data: plans, isLoading } = useQuery({
+    queryKey: ['membership-plans'],
+    queryFn: async () => {
+      const response = await client.api.v1['membership-plans'].$get();
+      if (!response.ok) throw new Error('获取套餐失败');
+      const data = await response.json();
+      return data.data;
+    },
+  });
+
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateMembershipPlanRequest) => {
+      const response = await client.api.v1['membership-plans'].$post({ json: data });
+      if (!response.ok) throw new Error('创建套餐失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['membership-plans'] });
+      toast.success('套餐创建成功');
+      setOpen(false);
+    },
+    onError: (error) => {
+      toast.error(error.message);
+    },
+  });
+
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateMembershipPlanRequest }) => {
+      const response = await client.api.v1['membership-plans'][id].$put({ json: data });
+      if (!response.ok) throw new Error('更新套餐失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['membership-plans'] });
+      toast.success('套餐更新成功');
+      setOpen(false);
+    },
+    onError: (error) => {
+      toast.error(error.message);
+    },
+  });
+
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const response = await client.api.v1['membership-plans'][id].$delete();
+      if (!response.ok) throw new Error('删除套餐失败');
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['membership-plans'] });
+      toast.success('套餐删除成功');
+    },
+    onError: (error) => {
+      toast.error(error.message);
+    },
+  });
+
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      name: '',
+      type: 'single',
+      price: 0,
+      durationDays: 0,
+      description: '',
+      features: '',
+      isActive: true,
+      sortOrder: 0,
+    },
+  });
+
+  const handleCreate = () => {
+    setEditingPlan(null);
+    form.reset({
+      name: '',
+      type: 'single',
+      price: 27,
+      durationDays: 1,
+      description: '',
+      features: '',
+      isActive: true,
+      sortOrder: 0,
+    });
+    setOpen(true);
+  };
+
+  const handleEdit = (plan: MembershipPlan) => {
+    setEditingPlan(plan);
+    form.reset({
+      name: plan.name,
+      type: plan.type,
+      price: plan.price,
+      durationDays: plan.durationDays,
+      description: plan.description || '',
+      features: plan.features?.join(', ') || '',
+      isActive: plan.isActive === 1,
+      sortOrder: plan.sortOrder,
+    });
+    setOpen(true);
+  };
+
+  const handleSubmit = (values: z.infer<typeof formSchema>) => {
+    const data = {
+      ...values,
+      features: values.features?.split(',').map(f => f.trim()).filter(f => f) || [],
+      isActive: values.isActive ? 1 : 0,
+    };
+
+    if (editingPlan) {
+      updateMutation.mutate({ id: editingPlan.id, data });
+    } else {
+      createMutation.mutate(data);
+    }
+  };
+
+  const getTypeLabel = (type: string) => {
+    const labels = {
+      single: '单次',
+      monthly: '单月',
+      yearly: '年',
+      lifetime: '永久',
+    };
+    return labels[type as keyof typeof labels] || type;
+  };
+
+  return (
+    <div className="container mx-auto py-10">
+      <div className="flex justify-between items-center mb-6">
+        <h1 className="text-3xl font-bold">会员套餐管理</h1>
+        <Dialog open={open} onOpenChange={setOpen}>
+          <DialogTrigger asChild>
+            <Button onClick={handleCreate}>
+              <Plus className="w-4 h-4 mr-2" />
+              创建套餐
+            </Button>
+          </DialogTrigger>
+          <DialogContent className="max-w-2xl">
+            <DialogHeader>
+              <DialogTitle>{editingPlan ? '编辑套餐' : '创建套餐'}</DialogTitle>
+              <DialogDescription>
+                {editingPlan ? '修改套餐信息' : '创建新的会员套餐'}
+              </DialogDescription>
+            </DialogHeader>
+            <Form {...form}>
+              <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
+                <FormField
+                  control={form.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>套餐名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入套餐名称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                
+                <FormField
+                  control={form.control}
+                  name="type"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>套餐类型</FormLabel>
+                      <Select onValueChange={field.onChange} defaultValue={field.value}>
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="选择套餐类型" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value="single">单次(24小时)</SelectItem>
+                          <SelectItem value="monthly">单月</SelectItem>
+                          <SelectItem value="yearly">年</SelectItem>
+                          <SelectItem value="lifetime">永久</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="price"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>价格(元)</FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          step="0.01"
+                          placeholder="请输入价格"
+                          {...field}
+                          onChange={e => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="durationDays"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>有效期(天)</FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number"
+                          placeholder="请输入有效期天数,0表示永久"
+                          {...field}
+                          onChange={e => field.onChange(parseInt(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormDescription>0表示永久有效</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="description"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>套餐描述</FormLabel>
+                      <FormControl>
+                        <Textarea 
+                          placeholder="请输入套餐描述"
+                          className="resize-none"
+                          {...field}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="features"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>套餐功能</FormLabel>
+                      <FormControl>
+                        <Textarea 
+                          placeholder="请输入套餐功能,用逗号分隔"
+                          className="resize-none"
+                          {...field}
+                        />
+                      </FormControl>
+                      <FormDescription>多个功能用逗号分隔</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="sortOrder"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>排序</FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number"
+                          placeholder="请输入排序值"
+                          {...field}
+                          onChange={e => field.onChange(parseInt(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormDescription>数值越小排序越靠前</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="isActive"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">启用状态</FormLabel>
+                        <FormDescription>是否启用该套餐</FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value}
+                          onCheckedChange={field.onChange}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <div className="flex justify-end space-x-2">
+                  <Button type="button" variant="outline" onClick={() => setOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
+                    {editingPlan ? '更新' : '创建'}
+                  </Button>
+                </div>
+              </form>
+            </Form>
+          </DialogContent>
+        </Dialog>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>套餐列表</CardTitle>
+          <CardDescription>管理所有会员套餐</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <Table>
+            <TableHeader>
+              <TableRow>
+                <TableHead>套餐名称</TableHead>
+                <TableHead>类型</TableHead>
+                <TableHead>价格</TableHead>
+                <TableHead>有效期</TableHead>
+                <TableHead>状态</TableHead>
+                <TableHead>排序</TableHead>
+                <TableHead>创建时间</TableHead>
+                <TableHead>操作</TableHead>
+              </TableRow>
+            </TableHeader>
+            <TableBody>
+              {isLoading ? (
+                <TableRow>
+                  <TableCell colSpan={8} className="text-center">加载中...</TableCell>
+                </TableRow>
+              ) : (
+                plans?.map((plan) => (
+                  <TableRow key={plan.id}>
+                    <TableCell>{plan.name}</TableCell>
+                    <TableCell>{getTypeLabel(plan.type)}</TableCell>
+                    <TableCell>¥{plan.price}</TableCell>
+                    <TableCell>
+                      {plan.durationDays === 0 ? '永久' : `${plan.durationDays}天`}
+                    </TableCell>
+                    <TableCell>
+                      <span className={plan.isActive ? 'text-green-600' : 'text-red-600'}>
+                        {plan.isActive ? '启用' : '禁用'}
+                      </span>
+                    </TableCell>
+                    <TableCell>{plan.sortOrder}</TableCell>
+                    <TableCell>
+                      {new Date(plan.createdAt).toLocaleDateString()}
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex space-x-2">
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => handleEdit(plan)}
+                        >
+                          <Edit className="w-4 h-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => {
+                            if (window.confirm('确定要删除这个套餐吗?')) {
+                              deleteMutation.mutate(plan.id);
+                            }
+                          }}
+                        >
+                          <Trash2 className="w-4 h-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))
+              )}
+            </TableBody>
+          </Table>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

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

@@ -8,6 +8,7 @@ import { DashboardPage } from './pages/Dashboard';
 import { UsersPage } from './pages/Users';
 import { LoginPage } from './pages/Login';
 import { FilesPage } from './pages/Files';
+import MembershipPlans from './pages/MembershipPlans';
 
 export const router = createBrowserRouter([
   {
@@ -45,6 +46,11 @@ export const router = createBrowserRouter([
         element: <FilesPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'membership-plans',
+        element: <MembershipPlans />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 5 - 1
src/client/api.ts

@@ -2,7 +2,7 @@ import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
   AuthRoutes, UserRoutes, RoleRoutes,
-  FileRoutes
+  FileRoutes, MembershipPlanRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -75,3 +75,7 @@ export const roleClient = hc<RoleRoutes>('/', {
 export const fileClient = hc<FileRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.files;
+
+export const membershipPlanClient = hc<MembershipPlanRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['membership-plans'];

+ 129 - 2
src/client/home-shadcn/pages/HomePage.tsx

@@ -14,11 +14,18 @@ import {
   Package,
   Rocket,
   Shield,
-  User
+  User,
+  Check,
+  Clock,
+  Calendar,
+  Infinity
 } from 'lucide-react';
 import { useNavigate } from 'react-router-dom';
 import UserInfoModal from '@/client/home-shadcn/components/UserInfoModal';
 import { useAuth } from '@/client/home-shadcn/hooks/AuthProvider';
+import { useQuery } from '@tanstack/react-query';
+import { membershipPlanClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
 
 export default function HomePage() {
   const navigate = useNavigate();
@@ -26,6 +33,16 @@ export default function HomePage() {
   const [hoveredFeature, setHoveredFeature] = useState<number | null>(null);
   const [showUserModal, setShowUserModal] = useState(false);
 
+  const { data: membershipPlans } = useQuery({
+    queryKey: ['membership-plans-home'],
+    queryFn: async () => {
+      const response = await membershipPlanClient.$get();
+      if (!response.ok) throw new Error('获取套餐失败');
+      const data = await response.json();
+      return data.data.filter((plan: any) => plan.isActive === 1).sort((a: any, b: any) => a.sortOrder - b.sortOrder);
+    },
+  });
+
   const features = [
     {
       icon: <FileText className="h-8 w-8" />,
@@ -216,8 +233,118 @@ export default function HomePage() {
         </div>
       </div>
 
-      {/* How It Works */}
+      {/* Pricing Section */}
       <div className="py-20 bg-gradient-to-br from-gray-50 to-blue-50">
+        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+          <div className="text-center mb-16">
+            <h2 className="text-4xl font-bold text-gray-900 mb-4">
+              灵活的会员套餐
+            </h2>
+            <p className="text-xl text-gray-600">
+              选择最适合您的套餐,享受高效文档处理体验
+            </p>
+          </div>
+
+          <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
+            {membershipPlans?.map((plan) => (
+              <Card
+                key={plan.id}
+                className={`relative border-0 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1 ${
+                  plan.type === 'yearly' ? 'ring-2 ring-blue-500' : ''
+                }`}
+              >
+                {plan.type === 'yearly' && (
+                  <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
+                    <Badge className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
+                      推荐
+                    </Badge>
+                  </div>
+                )}
+                
+                <CardHeader>
+                  <div className="text-center">
+                    <h3 className="text-2xl font-bold mb-2">{plan.name}</h3>
+                    <div className="text-4xl font-bold text-blue-600 mb-2">
+                      ¥{plan.price}
+                    </div>
+                    <p className="text-gray-600">
+                      {plan.durationDays === 0 ? '永久有效' :
+                       plan.durationDays === 1 ? '24小时有效' :
+                       `${plan.durationDays}天有效`}
+                    </p>
+                  </div>
+                </CardHeader>
+                
+                <CardContent>
+                  <ul className="space-y-3">
+                    {plan.features?.map((feature: string, index: number) => (
+                      <li key={index} className="flex items-start">
+                        <Check className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
+                        <span className="text-gray-700 text-sm">{feature}</span>
+                      </li>
+                    )) || (
+                      <>
+                        <li className="flex items-start">
+                          <Check className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
+                          <span className="text-gray-700 text-sm">基础功能</span>
+                        </li>
+                      </>
+                    )}
+                  </ul>
+                  
+                  <Button
+                    className="w-full mt-6 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
+                    onClick={() => navigate('/pricing')}
+                  >
+                    立即选择
+                  </Button>
+                </CardContent>
+              </Card>
+            )) || (
+              // 默认套餐展示
+              <>
+                {[
+                  { name: '单次会员', price: 27, duration: '24小时有效', features: ['基础功能', '24小时使用'], icon: Clock, color: 'text-orange-600' },
+                  { name: '单月会员', price: 86, duration: '30天有效', features: ['全部功能', '无限制使用'], icon: Calendar, color: 'text-blue-600' },
+                  { name: '年会员', price: 286, duration: '365天有效', features: ['全部功能', '无限制使用', '优先支持'], icon: Calendar, color: 'text-green-600' },
+                  { name: '永久会员', price: 688, duration: '永久有效', features: ['全部功能', '永久使用', '终身更新'], icon: Infinity, color: 'text-purple-600' }
+                ].map((plan, index) => (
+                  <Card key={index} className="border-0 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
+                    <CardHeader>
+                      <div className="text-center">
+                        <h3 className="text-2xl font-bold mb-2">{plan.name}</h3>
+                        <div className="text-4xl font-bold text-blue-600 mb-2">
+                          ¥{plan.price}
+                        </div>
+                        <p className="text-gray-600">{plan.duration}</p>
+                      </div>
+                    </CardHeader>
+                    <CardContent>
+                      <ul className="space-y-3">
+                        {plan.features.map((feature, idx) => (
+                          <li key={idx} className="flex items-start">
+                            <Check className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
+                            <span className="text-gray-700 text-sm">{feature}</span>
+                          </li>
+                        ))}
+                      </ul>
+                      <Button
+                        className="w-full mt-6 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
+                        onClick={() => navigate('/pricing')}
+                      >
+                        立即选择
+                      </Button>
+                    </CardContent>
+                  </Card>
+                ))}
+              </>
+            )}
+          </div>
+        </div>
+      </div>
+
+      {/* How It Works */}
+      <div className="py-20 bg-white">
         <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
           <div className="text-center mb-16">
             <h2 className="text-4xl font-bold text-gray-900 mb-4">

+ 6 - 3
src/server/api.ts

@@ -4,7 +4,8 @@ import { errorHandler } from './utils/errorHandler'
 import usersRouter from './api/users/index'
 import authRoute from './api/auth/index'
 import rolesRoute from './api/roles/index'
-import fileRoutes from './api/files/index'
+import fileRoute from './api/files/index'
+import membershipPlanRoute from './api/membership-plans/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -102,12 +103,14 @@ if(1){
 const userRoutes = api.route('/api/v1/users', usersRouter)
 const authRoutes = api.route('/api/v1/auth', authRoute)
 const roleRoutes = api.route('/api/v1/roles', rolesRoute)
-const fileApiRoutes = api.route('/api/v1/files', fileRoutes)
+const fileRoutes = api.route('/api/v1/files', fileRoute)
+const membershipPlanRoutes = api.route('/api/v1/membership-plans', membershipPlanRoute)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
-export type FileRoutes = typeof fileApiRoutes
+export type FileRoutes = typeof fileRoutes
+export type MembershipPlanRoutes = typeof membershipPlanRoutes
 
 app.route('/', api)
 export default app

+ 16 - 0
src/server/api/membership-plans/index.ts

@@ -0,0 +1,16 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { MembershipPlan } from '@/server/modules/membership/membership-plan.entity';
+import { MembershipPlanSchema, CreateMembershipPlanDto, UpdateMembershipPlanDto } from '@/server/modules/membership/membership-plan.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const membershipPlanRoutes = createCrudRoutes({
+  entity: MembershipPlan,
+  createSchema: CreateMembershipPlanDto,
+  updateSchema: UpdateMembershipPlanDto,
+  getSchema: MembershipPlanSchema,
+  listSchema: MembershipPlanSchema,
+  searchFields: ['name', 'description'],
+  middleware: [] // 移除认证中间件,允许公开访问
+});
+
+export default membershipPlanRoutes;

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

@@ -7,6 +7,7 @@ import { UserEntity as User } from "./modules/users/user.entity"
 import { Role } from "./modules/users/role.entity"
 import { File } from "./modules/files/file.entity"
 import { PaymentEntity } from "./modules/payments/payment.entity"
+import { MembershipPlan } from "./modules/membership/membership-plan.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -16,7 +17,7 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role, File, PaymentEntity,
+    User, Role, File, PaymentEntity, MembershipPlan,
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 8 - 0
src/server/migrations/001-init-membership-plans.sql

@@ -0,0 +1,8 @@
+-- 初始化会员套餐数据
+-- 默认价格:单次27元,单月86元,年286元,永久688元
+
+INSERT INTO membership_plans (name, type, price, duration_days, description, features, is_active, sort_order, created_at, updated_at) VALUES
+('单次会员', 'single', 27.00, 1, '24小时有效,适合临时使用', '["基础功能", "24小时使用", "单次付费"]', 1, 1, NOW(), NOW()),
+('单月会员', 'monthly', 86.00, 30, '30天有效,适合短期项目', '["全部功能", "30天无限制使用", "优先支持", "数据导出"]', 1, 2, NOW(), NOW()),
+('年会员', 'yearly', 286.00, 365, '365天有效,性价比高', '["全部功能", "365天无限制使用", "优先支持", "数据导出", "专属客服", "定期更新"]', 1, 3, NOW(), NOW()),
+('永久会员', 'lifetime', 688.00, 0, '永久有效,一次付费终身使用', '["全部功能", "永久无限制使用", "优先支持", "数据导出", "专属客服", "终身更新", "VIP通道"]', 1, 4, NOW(), NOW());

+ 87 - 0
src/server/modules/membership/membership-plan.entity.ts

@@ -0,0 +1,87 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+export enum MembershipType {
+  SINGLE = 'single',
+  MONTHLY = 'monthly',
+  YEARLY = 'yearly',
+  LIFETIME = 'lifetime'
+}
+
+@Entity('membership_plans')
+export class MembershipPlan {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 100, comment: '套餐名称' })
+  name!: string;
+
+  @Column({ 
+    name: 'type', 
+    type: 'enum', 
+    enum: MembershipType, 
+    comment: '套餐类型:single-单次,monthly-单月,yearly-年,lifetime-永久' 
+  })
+  type!: MembershipType;
+
+  @Column({ name: 'price', type: 'decimal', precision: 10, scale: 2, comment: '价格' })
+  price!: number;
+
+  @Column({ name: 'duration_days', type: 'int', comment: '有效期天数(永久为0)' })
+  durationDays!: number;
+
+  @Column({ name: 'description', type: 'text', nullable: true, comment: '套餐描述' })
+  description!: string | null;
+
+  @Column({ name: 'features', type: 'json', comment: '套餐功能列表' })
+  features!: string[];
+
+  @Column({ name: 'is_active', type: 'tinyint', default: 1, comment: '是否启用' })
+  isActive!: number;
+
+  @Column({ name: 'sort_order', type: 'int', default: 0, comment: '排序' })
+  sortOrder!: number;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+
+// Zod Schema定义
+export const MembershipPlanSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '套餐ID', example: 1 }),
+  name: z.string().max(100).openapi({ description: '套餐名称', example: '单次会员' }),
+  type: z.nativeEnum(MembershipType).openapi({ description: '套餐类型', example: MembershipType.SINGLE }),
+  price: z.number().multipleOf(0.01).openapi({ description: '价格', example: 27.00 }),
+  durationDays: z.number().int().min(0).openapi({ description: '有效期天数', example: 1 }),
+  description: z.string().nullable().openapi({ description: '套餐描述', example: '24小时有效' }),
+  features: z.array(z.string()).openapi({ description: '套餐功能', example: ['基础功能', '24小时使用'] }),
+  isActive: z.number().int().min(0).max(1).openapi({ description: '是否启用', example: 1 }),
+  sortOrder: z.number().int().openapi({ description: '排序', example: 0 }),
+  createdAt: z.date().openapi({ description: '创建时间' }),
+  updatedAt: z.date().openapi({ description: '更新时间' })
+});
+
+export const CreateMembershipPlanDto = z.object({
+  name: z.string().max(100).openapi({ description: '套餐名称', example: '单次会员' }),
+  type: z.nativeEnum(MembershipType).openapi({ description: '套餐类型', example: MembershipType.SINGLE }),
+  price: z.number().multipleOf(0.01).positive().openapi({ description: '价格', example: 27.00 }),
+  durationDays: z.number().int().min(0).openapi({ description: '有效期天数', example: 1 }),
+  description: z.string().nullable().optional().openapi({ description: '套餐描述', example: '24小时有效' }),
+  features: z.array(z.string()).optional().default([]).openapi({ description: '套餐功能', example: ['基础功能'] }),
+  isActive: z.number().int().min(0).max(1).optional().default(1).openapi({ description: '是否启用', example: 1 }),
+  sortOrder: z.number().int().optional().default(0).openapi({ description: '排序', example: 0 })
+});
+
+export const UpdateMembershipPlanDto = z.object({
+  name: z.string().max(100).optional().openapi({ description: '套餐名称', example: '单次会员' }),
+  type: z.nativeEnum(MembershipType).optional().openapi({ description: '套餐类型', example: MembershipType.SINGLE }),
+  price: z.number().multipleOf(0.01).positive().optional().openapi({ description: '价格', example: 27.00 }),
+  durationDays: z.number().int().min(0).optional().openapi({ description: '有效期天数', example: 1 }),
+  description: z.string().nullable().optional().openapi({ description: '套餐描述', example: '24小时有效' }),
+  features: z.array(z.string()).optional().openapi({ description: '套餐功能', example: ['基础功能'] }),
+  isActive: z.number().int().min(0).max(1).optional().openapi({ description: '是否启用', example: 1 }),
+  sortOrder: z.number().int().optional().openapi({ description: '排序', example: 0 })
+});

+ 28 - 0
src/server/modules/membership/membership-plan.service.ts

@@ -0,0 +1,28 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { MembershipPlan } from './membership-plan.entity';
+
+export class MembershipPlanService extends GenericCrudService<MembershipPlan> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, MembershipPlan);
+  }
+
+  /**
+   * 获取启用的套餐列表,按排序升序排列
+   */
+  async getActivePlans(): Promise<MembershipPlan[]> {
+    const [plans] = await this.getList(1, 100, undefined, undefined, { isActive: 1 }, [], { sortOrder: 'ASC' });
+    return plans;
+  }
+
+  /**
+   * 根据类型获取套餐
+   */
+  async getPlanByType(type: string): Promise<MembershipPlan | null> {
+    const plans = await this.repository.findOne({
+      where: { type, isActive: 1 },
+      order: { sortOrder: 'ASC' }
+    });
+    return plans;
+  }
+}