Browse Source

✨ feat(admin): 添加广告管理模块

- 新增广告管理页面,支持广告列表展示、创建、编辑和删除功能
- 新增广告类型管理页面,支持类型分类管理
- 在菜单导航中添加广告管理和广告类型入口
- 配置对应路由以支持新页面访问

✨ feat(product): 新增商品实体和Schema定义

- 创建商品数据库实体,包含完整商品信息字段
- 定义商品创建和更新的DTO验证Schema
- 支持商品图片、分类、库存等核心业务字段
yourname 4 months ago
parent
commit
ded811bbbd

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

@@ -8,7 +8,9 @@ import {
   LogOut,
   LogOut,
   BarChart3,
   BarChart3,
   LayoutDashboard,
   LayoutDashboard,
-  File
+  File,
+  Megaphone,
+  Tag
 } from 'lucide-react';
 } from 'lucide-react';
 
 
 export interface MenuItem {
 export interface MenuItem {
@@ -101,6 +103,20 @@ export const useMenu = () => {
       path: '/admin/analytics',
       path: '/admin/analytics',
       permission: 'analytics:view'
       permission: 'analytics:view'
     },
     },
+    {
+      key: 'advertisements',
+      label: '广告管理',
+      icon: <Megaphone className="h-4 w-4" />,
+      path: '/admin/advertisements',
+      permission: 'advertisement:manage'
+    },
+    {
+      key: 'advertisement-types',
+      label: '广告类型',
+      icon: <Tag className="h-4 w-4" />,
+      path: '/admin/advertisement-types',
+      permission: 'advertisement:manage'
+    },
     {
     {
       key: 'settings',
       key: 'settings',
       label: '系统设置',
       label: '系统设置',

+ 437 - 0
src/client/admin-shadcn/pages/AdvertisementTypes.tsx

@@ -0,0 +1,437 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Edit, Trash2, Search } from 'lucide-react';
+import { Input } from '@/client/components/ui/input';
+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 { Badge } from '@/client/components/ui/badge';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination';
+import { advertisementClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '@/server/modules/advertisements/advertisement-type.schema';
+
+type CreateRequest = InferRequestType<typeof advertisementClient.types.$post>['json'];
+type UpdateRequest = InferRequestType<typeof advertisementClient.types[':id']['$put']>['json'];
+type AdvertisementTypeResponse = InferResponseType<typeof advertisementClient.types.$get, 200>['data'][0];
+
+const createFormSchema = CreateAdvertisementTypeDto;
+const updateFormSchema = UpdateAdvertisementTypeDto;
+
+export const AdvertisementTypesPage = () => {
+  const queryClient = useQueryClient();
+  const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, search: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingType, setEditingType] = useState<AdvertisementTypeResponse | null>(null);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [typeToDelete, setTypeToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      name: '',
+      code: '',
+      remark: '',
+      status: 1
+    }
+  });
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {}
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['advertisement-types', searchParams],
+    queryFn: async () => {
+      const res = await advertisementClient.types.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      });
+      if (res.status !== 200) throw new Error('获取广告类型列表失败');
+      return await res.json();
+    }
+  });
+
+  // 创建广告类型
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await advertisementClient.types.$post({ json: data });
+      if (res.status !== 201) throw new Error('创建广告类型失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告类型创建成功');
+      setIsModalOpen(false);
+      createForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '创建广告类型失败');
+    }
+  });
+
+  // 更新广告类型
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await advertisementClient.types[':id'].$put({ 
+        param: { id: id.toString() },
+        json: data 
+      });
+      if (res.status !== 200) throw new Error('更新广告类型失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告类型更新成功');
+      setIsModalOpen(false);
+      setEditingType(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '更新广告类型失败');
+    }
+  });
+
+  // 删除广告类型
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await advertisementClient.types[':id'].$delete({ 
+        param: { id: id.toString() } 
+      });
+      if (res.status !== 200) throw new Error('删除广告类型失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告类型删除成功');
+      setDeleteDialogOpen(false);
+      setTypeToDelete(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '删除广告类型失败');
+    }
+  });
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+    refetch();
+  };
+
+  // 处理创建广告类型
+  const handleCreateType = () => {
+    setIsCreateForm(true);
+    setEditingType(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 处理编辑广告类型
+  const handleEditType = (type: AdvertisementTypeResponse) => {
+    setIsCreateForm(false);
+    setEditingType(type);
+    updateForm.reset({
+      name: type.name,
+      code: type.code,
+      remark: type.remark || undefined,
+      status: type.status
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理删除广告类型
+  const handleDeleteType = (id: number) => {
+    setTypeToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = () => {
+    if (typeToDelete) {
+      deleteMutation.mutate(typeToDelete);
+    }
+  };
+
+  // 处理表单提交
+  const handleSubmit = (data: CreateRequest | UpdateRequest) => {
+    if (isCreateForm) {
+      createMutation.mutate(data as CreateRequest);
+    } else if (editingType) {
+      updateMutation.mutate({ 
+        id: editingType.id, 
+        data: data as UpdateRequest 
+      });
+    }
+  };
+
+  // 渲染加载骨架
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <h1 className="text-2xl font-bold">广告类型管理</h1>
+          <Button disabled>
+            <Plus className="mr-2 h-4 w-4" />
+            创建类型
+          </Button>
+        </div>
+        
+        <Card>
+          <CardHeader>
+            <Skeleton className="h-6 w-1/4" />
+          </CardHeader>
+          <CardContent>
+            <Skeleton className="h-32 w-full" />
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">广告类型管理</h1>
+        <Button onClick={handleCreateType}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建类型
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>广告类型列表</CardTitle>
+          <CardDescription>管理广告的类型分类</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索类型名称或别名..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>ID</TableHead>
+                  <TableHead>类型名称</TableHead>
+                  <TableHead>调用别名</TableHead>
+                  <TableHead>备注</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data?.data.map((type) => (
+                  <TableRow key={type.id}>
+                    <TableCell>{type.id}</TableCell>
+                    <TableCell className="font-medium">{type.name}</TableCell>
+                    <TableCell>
+                      <code className="text-xs bg-muted px-1 rounded">{type.code}</code>
+                    </TableCell>
+                    <TableCell>
+                      {type.remark ? (
+                        <span className="text-sm">{type.remark}</span>
+                      ) : (
+                        <span className="text-muted-foreground text-xs">无备注</span>
+                      )}
+                    </TableCell>
+                    <TableCell>
+                      <Badge variant={type.status === 1 ? 'default' : 'secondary'}>
+                        {type.status === 1 ? '启用' : '禁用'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      {type.createTime ? new Date(type.createTime * 1000).toLocaleDateString() : '-'}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button 
+                          variant="ghost" 
+                          size="icon" 
+                          onClick={() => handleEditType(type)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button 
+                          variant="ghost" 
+                          size="icon" 
+                          onClick={() => handleDeleteType(type.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data?.data.length === 0 && !isLoading && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无广告类型数据</p>
+            </div>
+          )}
+
+          <DataTablePagination
+            current={searchParams.page}
+            pageSize={searchParams.limit}
+            total={data?.pagination.total || 0}
+            onChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建广告类型' : '编辑广告类型'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的广告类型' : '编辑现有广告类型信息'}
+            </DialogDescription>
+          </DialogHeader>
+          
+          <Form {...(isCreateForm ? createForm : updateForm)}>
+            <form onSubmit={(isCreateForm ? createForm : updateForm).handleSubmit(handleSubmit)} className="space-y-4">
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="name"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className="flex items-center">
+                      类型名称 <span className="text-red-500 ml-1">*</span>
+                    </FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入类型名称" {...field} />
+                    </FormControl>
+                    <FormDescription>广告类型的显示名称,最多50个字符</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="code"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className="flex items-center">
+                      调用别名 <span className="text-red-500 ml-1">*</span>
+                    </FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入调用别名" {...field} />
+                    </FormControl>
+                    <FormDescription>用于程序调用的唯一标识,最多20个字符</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="remark"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>备注</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入备注" {...field} />
+                    </FormControl>
+                    <FormDescription>广告类型的备注说明,最多100个字符</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="status"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>状态</FormLabel>
+                    <FormControl>
+                      <select 
+                        {...field} 
+                        className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                        value={field.value || 1}
+                        onChange={(e) => field.onChange(parseInt(e.target.value))}
+                      >
+                        <option value={1}>启用</option>
+                        <option value={0}>禁用</option>
+                      </select>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <DialogFooter>
+                <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                  取消
+                </Button>
+                <Button type="submit" disabled={(isCreateForm ? createMutation : updateMutation).isPending}>
+                  {isCreateForm ? '创建' : '更新'}
+                </Button>
+              </DialogFooter>
+            </form>
+          </Form>
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个广告类型吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button 
+              variant="destructive" 
+              onClick={confirmDelete}
+              disabled={deleteMutation.isPending}
+            >
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};
+
+// 简单的骨架屏组件
+const Skeleton = ({ className }: { className?: string }) => (
+  <div className={`animate-pulse rounded-md bg-muted ${className}`} />
+);

+ 563 - 0
src/client/admin-shadcn/pages/Advertisements.tsx

@@ -0,0 +1,563 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Edit, Trash2, Search, Eye } from 'lucide-react';
+import { Input } from '@/client/components/ui/input';
+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 { Badge } from '@/client/components/ui/badge';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination';
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+import { advertisementClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { CreateAdvertisementDto, UpdateAdvertisementDto } from '@/server/modules/advertisements/advertisement.schema';
+
+type CreateRequest = InferRequestType<typeof advertisementClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof advertisementClient[':id']['$put']>['json'];
+type AdvertisementResponse = InferResponseType<typeof advertisementClient.$get, 200>['data'][0];
+
+const createFormSchema = CreateAdvertisementDto;
+const updateFormSchema = UpdateAdvertisementDto;
+
+export const AdvertisementsPage = () => {
+  const queryClient = useQueryClient();
+  const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, search: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingAdvertisement, setEditingAdvertisement] = useState<AdvertisementResponse | null>(null);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [advertisementToDelete, setAdvertisementToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      title: '',
+      typeId: 1,
+      code: '',
+      url: '',
+      img: '',
+      sort: 0,
+      status: 1,
+      actionType: 1
+    }
+  });
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {}
+  });
+
+  // 获取广告类型列表
+  const { data: advertisementTypes } = useQuery({
+    queryKey: ['advertisement-types'],
+    queryFn: async () => {
+      const res = await advertisementClient.types.$get();
+      if (res.status !== 200) throw new Error('获取广告类型失败');
+      return await res.json();
+    }
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['advertisements', searchParams],
+    queryFn: async () => {
+      const res = await advertisementClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      });
+      if (res.status !== 200) throw new Error('获取广告列表失败');
+      return await res.json();
+    }
+  });
+
+  // 创建广告
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await advertisementClient.$post({ json: data });
+      if (res.status !== 201) throw new Error('创建广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告创建成功');
+      setIsModalOpen(false);
+      createForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '创建广告失败');
+    }
+  });
+
+  // 更新广告
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await advertisementClient[':id'].$put({ 
+        param: { id: id.toString() },
+        json: data 
+      });
+      if (res.status !== 200) throw new Error('更新广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告更新成功');
+      setIsModalOpen(false);
+      setEditingAdvertisement(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '更新广告失败');
+    }
+  });
+
+  // 删除广告
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await advertisementClient[':id'].$delete({ 
+        param: { id: id.toString() } 
+      });
+      if (res.status !== 200) throw new Error('删除广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告删除成功');
+      setDeleteDialogOpen(false);
+      setAdvertisementToDelete(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '删除广告失败');
+    }
+  });
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+    refetch();
+  };
+
+  // 处理创建广告
+  const handleCreateAdvertisement = () => {
+    setIsCreateForm(true);
+    setEditingAdvertisement(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 处理编辑广告
+  const handleEditAdvertisement = (advertisement: AdvertisementResponse) => {
+    setIsCreateForm(false);
+    setEditingAdvertisement(advertisement);
+    updateForm.reset({
+      title: advertisement.title || undefined,
+      typeId: advertisement.typeId || undefined,
+      code: advertisement.code || undefined,
+      url: advertisement.url || undefined,
+      img: advertisement.img || undefined,
+      sort: advertisement.sort || undefined,
+      status: advertisement.status || undefined,
+      actionType: advertisement.actionType || undefined
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理删除广告
+  const handleDeleteAdvertisement = (id: number) => {
+    setAdvertisementToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = () => {
+    if (advertisementToDelete) {
+      deleteMutation.mutate(advertisementToDelete);
+    }
+  };
+
+  // 处理表单提交
+  const handleSubmit = (data: CreateRequest | UpdateRequest) => {
+    if (isCreateForm) {
+      createMutation.mutate(data as CreateRequest);
+    } else if (editingAdvertisement) {
+      updateMutation.mutate({ 
+        id: editingAdvertisement.id, 
+        data: data as UpdateRequest 
+      });
+    }
+  };
+
+  // 渲染加载骨架
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <h1 className="text-2xl font-bold">广告管理</h1>
+          <Button disabled>
+            <Plus className="mr-2 h-4 w-4" />
+            创建广告
+          </Button>
+        </div>
+        
+        <Card>
+          <CardHeader>
+            <Skeleton className="h-6 w-1/4" />
+          </CardHeader>
+          <CardContent>
+            <Skeleton className="h-32 w-full" />
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">广告管理</h1>
+        <Button onClick={handleCreateAdvertisement}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建广告
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>广告列表</CardTitle>
+          <CardDescription>管理网站的所有广告内容</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索广告标题或别名..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>ID</TableHead>
+                  <TableHead>标题</TableHead>
+                  <TableHead>类型</TableHead>
+                  <TableHead>别名</TableHead>
+                  <TableHead>图片</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>排序</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data?.data.map((advertisement) => (
+                  <TableRow key={advertisement.id}>
+                    <TableCell>{advertisement.id}</TableCell>
+                    <TableCell>{advertisement.title || '-'}</TableCell>
+                    <TableCell>
+                      {advertisementTypes?.data.find(t => t.id === advertisement.typeId)?.name || '-'}
+                    </TableCell>
+                    <TableCell>
+                      <code className="text-xs bg-muted px-1 rounded">{advertisement.code || '-'}</code>
+                    </TableCell>
+                    <TableCell>
+                      {advertisement.img ? (
+                        <img 
+                          src={advertisement.img} 
+                          alt={advertisement.title || '广告图片'} 
+                          className="w-16 h-10 object-cover rounded"
+                          onError={(e) => {
+                            e.currentTarget.src = '/placeholder.png';
+                          }}
+                        />
+                      ) : (
+                        <span className="text-muted-foreground text-xs">无图片</span>
+                      )}
+                    </TableCell>
+                    <TableCell>
+                      <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
+                        {advertisement.status === 1 ? '启用' : '禁用'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>{advertisement.sort}</TableCell>
+                    <TableCell>
+                      {advertisement.createTime ? new Date(advertisement.createTime * 1000).toLocaleDateString() : '-'}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button 
+                          variant="ghost" 
+                          size="icon" 
+                          onClick={() => handleEditAdvertisement(advertisement)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button 
+                          variant="ghost" 
+                          size="icon" 
+                          onClick={() => handleDeleteAdvertisement(advertisement.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data?.data.length === 0 && !isLoading && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无广告数据</p>
+            </div>
+          )}
+
+          <DataTablePagination
+            current={searchParams.page}
+            pageSize={searchParams.limit}
+            total={data?.pagination.total || 0}
+            onChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建广告' : '编辑广告'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的广告' : '编辑现有广告信息'}
+            </DialogDescription>
+          </DialogHeader>
+          
+          <Form {...(isCreateForm ? createForm : updateForm)}>
+            <form onSubmit={(isCreateForm ? createForm : updateForm).handleSubmit(handleSubmit)} className="space-y-4">
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="title"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className="flex items-center">
+                      标题 <span className="text-red-500 ml-1">*</span>
+                    </FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入广告标题" {...field} />
+                    </FormControl>
+                    <FormDescription>广告显示的标题文本,最多30个字符</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="typeId"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className="flex items-center">
+                      广告类型 <span className="text-red-500 ml-1">*</span>
+                    </FormLabel>
+                    <FormControl>
+                      <select 
+                        {...field} 
+                        className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                        value={field.value || ''}
+                        onChange={(e) => field.onChange(parseInt(e.target.value))}
+                      >
+                        <option value="">请选择广告类型</option>
+                        {advertisementTypes?.data.map(type => (
+                          <option key={type.id} value={type.id}>{type.name}</option>
+                        ))}
+                      </select>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="code"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel className="flex items-center">
+                      调用别名 <span className="text-red-500 ml-1">*</span>
+                    </FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入调用别名" {...field} />
+                    </FormControl>
+                    <FormDescription>用于程序调用的唯一标识,最多20个字符</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="img"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>广告图片</FormLabel>
+                    <FormControl>
+                      <AvatarSelector
+                        value={field.value || undefined}
+                        onChange={field.onChange}
+                        maxSize={2}
+                        uploadPath="/advertisements"
+                        uploadButtonText="上传广告图片"
+                        previewSize="medium"
+                        placeholder="选择广告图片"
+                      />
+                    </FormControl>
+                    <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="url"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>跳转链接</FormLabel>
+                    <FormControl>
+                      <Input placeholder="请输入跳转链接" {...field} />
+                    </FormControl>
+                    <FormDescription>点击广告后跳转的URL地址</FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <div className="grid grid-cols-2 gap-4">
+                <FormField
+                  control={(isCreateForm ? createForm : updateForm).control}
+                  name="actionType"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>跳转类型</FormLabel>
+                      <FormControl>
+                        <select 
+                          {...field} 
+                          className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                          value={field.value || 1}
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={0}>不跳转</option>
+                          <option value={1}>Web页面</option>
+                          <option value={2}>小程序页面</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={(isCreateForm ? createForm : updateForm).control}
+                  name="sort"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>排序值</FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          placeholder="排序值" 
+                          {...field} 
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormDescription>数值越大排序越靠前</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+              </div>
+
+              <FormField
+                control={(isCreateForm ? createForm : updateForm).control}
+                name="status"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>状态</FormLabel>
+                    <FormControl>
+                      <select 
+                        {...field} 
+                        className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                        value={field.value || 1}
+                        onChange={(e) => field.onChange(parseInt(e.target.value))}
+                      >
+                        <option value={1}>启用</option>
+                        <option value={0}>禁用</option>
+                      </select>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <DialogFooter>
+                <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                  取消
+                </Button>
+                <Button type="submit" disabled={(isCreateForm ? createMutation : updateMutation).isPending}>
+                  {isCreateForm ? '创建' : '更新'}
+                </Button>
+              </DialogFooter>
+            </form>
+          </Form>
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个广告吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button 
+              variant="destructive" 
+              onClick={confirmDelete}
+              disabled={deleteMutation.isPending}
+            >
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};
+
+// 简单的骨架屏组件
+const Skeleton = ({ className }: { className?: string }) => (
+  <div className={`animate-pulse rounded-md bg-muted ${className}`} />
+);

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

@@ -8,6 +8,8 @@ import { DashboardPage } from './pages/Dashboard';
 import { UsersPage } from './pages/Users';
 import { UsersPage } from './pages/Users';
 import { LoginPage } from './pages/Login';
 import { LoginPage } from './pages/Login';
 import { FilesPage } from './pages/Files';
 import { FilesPage } from './pages/Files';
+import { AdvertisementsPage } from './pages/Advertisements';
+import { AdvertisementTypesPage } from './pages/AdvertisementTypes';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
   {
   {
@@ -45,6 +47,16 @@ export const router = createBrowserRouter([
         element: <FilesPage />,
         element: <FilesPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'advertisements',
+        element: <AdvertisementsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'advertisement-types',
+        element: <AdvertisementTypesPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: '*',
         path: '*',
         element: <NotFoundPage />,
         element: <NotFoundPage />,

+ 81 - 0
src/server/modules/products/goods.entity.ts

@@ -0,0 +1,81 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { File } from '@/server/modules/files/file.entity';
+
+@Entity('goods')
+export class Goods {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '商品名称' })
+  name!: string;
+
+  @Column({ name: 'price', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '售卖价' })
+  price!: number;
+
+  @Column({ name: 'cost_price', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '成本价' })
+  costPrice!: number;
+
+  @Column({ name: 'sales_num', type: 'bigint', default: 0, comment: '销售数量' })
+  salesNum!: number;
+
+  @Column({ name: 'click_num', type: 'bigint', default: 0, comment: '点击次数' })
+  clickNum!: number;
+
+  @Column({ name: 'category_id1', type: 'int', default: 0, comment: '一级类别id' })
+  categoryId1!: number;
+
+  @Column({ name: 'category_id2', type: 'int', default: 0, comment: '二级类别id' })
+  categoryId2!: number;
+
+  @Column({ name: 'category_id3', type: 'int', default: 0, comment: '三级类别id' })
+  categoryId3!: number;
+
+  @Column({ name: 'goods_type', type: 'int', default: 1, comment: '订单类型 1实物产品 2虚拟产品' })
+  goodsType!: number;
+
+  @Column({ name: 'supplier_id', type: 'int', nullable: true, comment: '所属供应商id' })
+  supplierId!: number | null;
+
+  @Column({ name: 'image', type: 'varchar', length: 255, nullable: true, comment: '商品主图' })
+  image!: string | null;
+
+  @Column({ name: 'slide_image', type: 'varchar', length: 2000, nullable: true, comment: '商品轮播(多个用,隔开)' })
+  slideImage!: string | null;
+
+  @Column({ name: 'detail', type: 'text', nullable: true, comment: '商品详情' })
+  detail!: string | null;
+
+  @Column({ name: 'instructions', type: 'varchar', length: 255, nullable: true, comment: '简介' })
+  instructions!: string | null;
+
+  @Column({ name: 'sort', type: 'int', default: 0, comment: '排序' })
+  sort!: number;
+
+  @Column({ name: 'state', type: 'int', default: 1, comment: '状态1可用2不可用' })
+  state!: number;
+
+  @Column({ name: 'stock', type: 'bigint', default: 0, comment: '库存' })
+  stock!: number;
+
+  @Column({ name: 'spu_id', type: 'int', default: 0, comment: '主商品ID' })
+  spuId!: number;
+
+  @Column({ name: 'spu_name', type: 'varchar', length: 255, nullable: true, comment: '主商品名称' })
+  spuName!: string | null;
+
+  @Column({ name: 'lowest_buy', type: 'int', default: 1, comment: '最小起购量' })
+  lowestBuy!: number;
+
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '商品主图文件ID' })
+  imageFileId!: number | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '创建时间' })
+  createdAt!: Date;
+
+  @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
+  updatedAt!: Date;
+}

+ 270 - 0
src/server/modules/products/goods.schema.ts

@@ -0,0 +1,270 @@
+import { z } from '@hono/zod-openapi';
+
+// 商品信息Schema
+export const GoodsSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '商品ID',
+    example: 1
+  }),
+  name: z.string().max(255).openapi({
+    description: '商品名称',
+    example: '精美商品'
+  }),
+  price: z.number().multipleOf(0.01).openapi({
+    description: '售卖价',
+    example: 99.99
+  }),
+  costPrice: z.number().multipleOf(0.01).openapi({
+    description: '成本价',
+    example: 50.00
+  }),
+  salesNum: z.number().int().min(0).openapi({
+    description: '销售数量',
+    example: 100
+  }),
+  clickNum: z.number().int().min(0).openapi({
+    description: '点击次数',
+    example: 1000
+  }),
+  categoryId1: z.number().int().min(0).openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().min(0).openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().min(0).openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  image: z.string().max(255).nullable().openapi({
+    description: '商品主图',
+    example: 'https://example.com/image.jpg'
+  }),
+  slideImage: z.string().max(2000).nullable().openapi({
+    description: '商品轮播图(多个用,隔开)',
+    example: 'image1.jpg,image2.jpg,image3.jpg'
+  }),
+  detail: z.string().nullable().openapi({
+    description: '商品详情',
+    example: '<p>商品详细描述</p>'
+  }),
+  instructions: z.string().max(255).nullable().openapi({
+    description: '简介',
+    example: '优质商品,值得信赖'
+  }),
+  sort: z.number().int().min(0).openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).openapi({
+    description: '状态1可用2不可用',
+    example: 1
+  }),
+  stock: z.number().int().min(0).openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().min(0).openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255).nullable().openapi({
+    description: '主商品名称',
+    example: null
+  }),
+  lowestBuy: z.number().int().min(1).openapi({
+    description: '最小起购量',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'product.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/product.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '商品主图文件信息'
+  }),
+  createdAt: z.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  })
+});
+
+// 创建商品DTO
+export const CreateGoodsDto = z.object({
+  name: z.string().max(255).openapi({
+    description: '商品名称',
+    example: '精美商品'
+  }),
+  price: z.coerce.number().multipleOf(0.01).openapi({
+    description: '售卖价',
+    example: 99.99
+  }),
+  costPrice: z.coerce.number().multipleOf(0.01).openapi({
+    description: '成本价',
+    example: 50.00
+  }),
+  categoryId1: z.coerce.number().int().min(0).openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.coerce.number().int().min(0).optional().openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.coerce.number().int().min(0).optional().openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.coerce.number().int().min(1).max(2).default(1).openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.coerce.number().int().positive().optional().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  image: z.string().max(255).optional().openapi({
+    description: '商品主图',
+    example: 'https://example.com/image.jpg'
+  }),
+  slideImage: z.string().max(2000).optional().openapi({
+    description: '商品轮播图(多个用,隔开)',
+    example: 'image1.jpg,image2.jpg,image3.jpg'
+  }),
+  detail: z.string().optional().openapi({
+    description: '商品详情',
+    example: '<p>商品详细描述</p>'
+  }),
+  instructions: z.string().max(255).optional().openapi({
+    description: '简介',
+    example: '优质商品,值得信赖'
+  }),
+  sort: z.coerce.number().int().min(0).default(0).openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.coerce.number().int().min(1).max(2).default(1).openapi({
+    description: '状态1可用2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number().int().min(0).default(0).openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.coerce.number().int().min(0).default(0).openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255).optional().openapi({
+    description: '主商品名称',
+    example: null
+  }),
+  lowestBuy: z.coerce.number().int().min(1).default(1).openapi({
+    description: '最小起购量',
+    example: 1
+  }),
+  imageFileId: z.coerce.number().int().positive().optional().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  })
+});
+
+// 更新商品DTO
+export const UpdateGoodsDto = z.object({
+  name: z.string().max(255).optional().openapi({
+    description: '商品名称',
+    example: '精美商品'
+  }),
+  price: z.coerce.number().multipleOf(0.01).optional().openapi({
+    description: '售卖价',
+    example: 99.99
+  }),
+  costPrice: z.coerce.number().multipleOf(0.01).optional().openapi({
+    description: '成本价',
+    example: 50.00
+  }),
+  categoryId1: z.coerce.number().int().min(0).optional().openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.coerce.number().int().min(0).optional().openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.coerce.number().int().min(0).optional().openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.coerce.number().int().min(1).max(2).optional().openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.coerce.number().int().positive().optional().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  image: z.string().max(255).optional().openapi({
+    description: '商品主图',
+    example: 'https://example.com/image.jpg'
+  }),
+  slideImage: z.string().max(2000).optional().openapi({
+    description: '商品轮播图(多个用,隔开)',
+    example: 'image1.jpg,image2.jpg,image3.jpg'
+  }),
+  detail: z.string().optional().openapi({
+    description: '商品详情',
+    example: '<p>商品详细描述</p>'
+  }),
+  instructions: z.string().max(255).optional().openapi({
+    description: '简介',
+    example: '优质商品,值得信赖'
+  }),
+  sort: z.coerce.number().int().min(0).optional().openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.coerce.number().int().min(1).max(2).optional().openapi({
+    description: '状态1可用2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number().int().min(0).optional().openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.coerce.number().int().min(0).optional().openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255).optional().openapi({
+    description: '主商品名称',
+    example: null
+  }),
+  lowestBuy: z.coerce.number().int().min(1).optional().openapi({
+    description: '最小起购量',
+    example: 1
+  }),
+  imageFileId: z.coerce.number().int().positive().optional().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  })
+});