Procházet zdrojové kódy

✨ feat(admin): 新增代理商管理功能

- 添加代理商管理菜单项,使用 UserCheck 图标
- 创建完整的代理商管理页面,支持增删改查操作
- 集成代理商 API 客户端到前端请求体系
- 后端新增 Agent 实体、schema 定义及 CRUD API
- 支持代理商状态管理(启用/禁用)和搜索功能
- 使用 DataTablePagination 实现分页查询
yourname před 4 měsíci
rodič
revize
6b40b7f752

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

@@ -13,7 +13,8 @@ import {
   Tag,
   Tag,
   Package,
   Package,
   Truck,
   Truck,
-  Building
+  Building,
+  UserCheck
 } from 'lucide-react';
 } from 'lucide-react';
 
 
 export interface MenuItem {
 export interface MenuItem {
@@ -159,6 +160,13 @@ export const useMenu = () => {
       path: '/admin/suppliers',
       path: '/admin/suppliers',
       permission: 'supplier:manage'
       permission: 'supplier:manage'
     },
     },
+    {
+      key: 'agents',
+      label: '代理商管理',
+      icon: <UserCheck className="h-4 w-4" />,
+      path: '/admin/agents',
+      permission: 'agent:manage'
+    },
     {
     {
       key: 'settings',
       key: 'settings',
       label: '系统设置',
       label: '系统设置',

+ 578 - 0
src/client/admin-shadcn/pages/Agents.tsx

@@ -0,0 +1,578 @@
+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 { agentClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+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 { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { Switch } from '@/client/components/ui/switch';
+import { CreateAgentDto, UpdateAgentDto } from '@/server/modules/agent/agent.schema';
+
+// 使用RPC方式提取类型
+type CreateAgentRequest = InferRequestType<typeof agentClient.$post>['json'];
+type UpdateAgentRequest = InferRequestType<typeof agentClient[':id']['$put']>['json'];
+type AgentResponse = InferResponseType<typeof agentClient.$get, 200>['data'][0];
+
+// 直接使用后端定义的 schema
+const createAgentFormSchema = CreateAgentDto;
+const updateAgentFormSchema = UpdateAgentDto;
+
+type CreateAgentFormData = CreateAgentRequest;
+type UpdateAgentFormData = UpdateAgentRequest;
+
+export const AgentsPage = () => {
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    search: ''
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingAgent, setEditingAgent] = useState<any>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [agentToDelete, setAgentToDelete] = useState<number | null>(null);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  
+  const createForm = useForm<CreateAgentFormData>({
+    resolver: zodResolver(createAgentFormSchema),
+    defaultValues: {
+      username: '',
+      name: null,
+      phone: null,
+      realname: null,
+      password: '',
+      state: 2,
+    },
+  });
+
+  const updateForm = useForm<UpdateAgentFormData>({
+    resolver: zodResolver(updateAgentFormSchema),
+    defaultValues: {
+      username: undefined,
+      name: undefined,
+      phone: null,
+      realname: null,
+      password: undefined,
+      state: undefined,
+    },
+  });
+
+  const { data: agentsData, isLoading, refetch } = useQuery({
+    queryKey: ['agents', searchParams],
+    queryFn: async () => {
+      const res = await agentClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      });
+      if (res.status !== 200) {
+        throw new Error('获取代理商列表失败');
+      }
+      return await res.json();
+    }
+  });
+
+  const agents = agentsData?.data || [];
+  const totalCount = agentsData?.pagination?.total || 0;
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 处理分页
+  const handlePageChange = (page: number, limit: number) => {
+    setSearchParams(prev => ({ ...prev, page, limit }));
+  };
+
+  // 打开创建代理商对话框
+  const handleCreateAgent = () => {
+    setEditingAgent(null);
+    setIsCreateForm(true);
+    createForm.reset({
+      username: '',
+      name: null,
+      phone: null,
+      realname: null,
+      password: '',
+      state: 2,
+    });
+    setIsModalOpen(true);
+  };
+
+  // 打开编辑代理商对话框
+  const handleEditAgent = (agent: AgentResponse) => {
+    setEditingAgent(agent);
+    setIsCreateForm(false);
+    updateForm.reset({
+      username: agent.username,
+      name: agent.name,
+      phone: agent.phone,
+      realname: agent.realname,
+      state: agent.state,
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理创建表单提交
+  const handleCreateSubmit = async (data: CreateAgentFormData) => {
+    try {
+      const res = await agentClient.$post({
+        json: data
+      });
+      if (res.status !== 201) {
+        throw new Error('创建代理商失败');
+      }
+      toast.success('代理商创建成功');
+      setIsModalOpen(false);
+      refetch();
+    } catch (error) {
+      console.error('创建代理商失败:', error);
+      toast.error('创建失败,请重试');
+    }
+  };
+
+  // 处理更新表单提交
+  const handleUpdateSubmit = async (data: UpdateAgentFormData) => {
+    if (!editingAgent) return;
+    
+    try {
+      const res = await agentClient[':id']['$put']({
+        param: { id: editingAgent.id },
+        json: data
+      });
+      if (res.status !== 200) {
+        throw new Error('更新代理商失败');
+      }
+      toast.success('代理商更新成功');
+      setIsModalOpen(false);
+      refetch();
+    } catch (error) {
+      console.error('更新代理商失败:', error);
+      toast.error('更新失败,请重试');
+    }
+  };
+
+  // 处理删除代理商
+  const handleDeleteAgent = (id: number) => {
+    setAgentToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  const confirmDelete = async () => {
+    if (!agentToDelete) return;
+    
+    try {
+      const res = await agentClient[':id']['$delete']({
+        param: { id: agentToDelete }
+      });
+      if (res.status !== 204) {
+        throw new Error('删除代理商失败');
+      }
+      toast.success('代理商删除成功');
+      refetch();
+    } catch (error) {
+      console.error('删除代理商失败:', error);
+      toast.error('删除失败,请重试');
+    } finally {
+      setDeleteDialogOpen(false);
+      setAgentToDelete(null);
+    }
+  };
+
+  // 渲染加载骨架
+  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>
+            <div className="space-y-2">
+              <Skeleton className="h-4 w-full" />
+              <Skeleton className="h-4 w-full" />
+              <Skeleton className="h-4 w-full" />
+            </div>
+          </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={handleCreateAgent}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建代理商
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>代理商列表</CardTitle>
+          <CardDescription>
+            管理系统中的所有代理商,共 {totalCount} 位代理商
+          </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>用户名</TableHead>
+                  <TableHead>代理商名称</TableHead>
+                  <TableHead>姓名</TableHead>
+                  <TableHead>手机号</TableHead>
+                  <TableHead>登录次数</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {agents.map((agent) => (
+                  <TableRow key={agent.id}>
+                    <TableCell className="font-medium">{agent.username}</TableCell>
+                    <TableCell>{agent.name || '-'}</TableCell>
+                    <TableCell>{agent.realname || '-'}</TableCell>
+                    <TableCell>{agent.phone || '-'}</TableCell>
+                    <TableCell>{agent.loginNum}</TableCell>
+                    <TableCell>
+                      <Badge
+                        variant={agent.state === 1 ? 'default' : 'secondary'}
+                      >
+                        {agent.state === 1 ? '启用' : '禁用'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      {format(new Date(agent.createdAt), 'yyyy-MM-dd HH:mm')}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEditAgent(agent)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDeleteAgent(agent.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          <DataTablePagination
+            currentPage={searchParams.page}
+            totalCount={totalCount}
+            pageSize={searchParams.limit}
+            onPageChange={handlePageChange}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑代理商对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>
+              {editingAgent ? '编辑代理商' : '创建代理商'}
+            </DialogTitle>
+            <DialogDescription>
+              {editingAgent ? '编辑现有代理商信息' : '创建一个新的代理商账户'}
+            </DialogDescription>
+          </DialogHeader>
+          
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户名
+                        <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入用户名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>代理商名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入代理商名称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="realname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>姓名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入姓名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        密码
+                        <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="请输入密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="state"
+                  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 === 1}
+                          onCheckedChange={(checked) => field.onChange(checked ? 1 : 2)}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    创建代理商
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户名
+                        <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入用户名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>代理商名称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入代理商名称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="realname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>姓名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入姓名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>新密码</FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="留空则不修改密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="state"
+                  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 === 1}
+                          onCheckedChange={(checked) => field.onChange(checked ? 1 : 2)}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    更新代理商
+                  </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}>
+              删除
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};

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

@@ -14,6 +14,7 @@ import { GoodsCategories } from './pages/GoodsCategories';
 import { GoodsPage } from './pages/Goods';
 import { GoodsPage } from './pages/Goods';
 import { ExpressCompaniesPage } from './pages/ExpressCompanies';
 import { ExpressCompaniesPage } from './pages/ExpressCompanies';
 import { SuppliersPage } from './pages/Suppliers';
 import { SuppliersPage } from './pages/Suppliers';
+import { AgentsPage } from './pages/Agents';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
   {
   {
@@ -81,6 +82,11 @@ export const router = createBrowserRouter([
         element: <SuppliersPage />,
         element: <SuppliersPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'agents',
+        element: <AgentsPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: '*',
         path: '*',
         element: <NotFoundPage />,
         element: <NotFoundPage />,

+ 6 - 1
src/client/api.ts

@@ -13,6 +13,7 @@ import type { ExpressCompanyRoutes } from '@/server/api'
 import type { OrganizationRoutes } from '@/server/api'
 import type { OrganizationRoutes } from '@/server/api'
 import type { SupplierRoutes } from '@/server/api'
 import type { SupplierRoutes } from '@/server/api'
 import type { CardRoutes } from '@/server/api'
 import type { CardRoutes } from '@/server/api'
+import type { AgentRoutes } from '@/server/api'
 import { axiosFetch } from './utils/axios-fetch'
 import { axiosFetch } from './utils/axios-fetch'
 
 
 // 创建客户端
 // 创建客户端
@@ -70,4 +71,8 @@ export const supplierClient = hc<SupplierRoutes>('/', {
 
 
 export const cardClient = hc<CardRoutes>('/', {
 export const cardClient = hc<CardRoutes>('/', {
   fetch: axiosFetch,
   fetch: axiosFetch,
-}).api.v1.cards
+}).api.v1.cards
+
+export const agentClient = hc<AgentRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.agents

+ 3 - 0
src/server/api.ts

@@ -15,6 +15,7 @@ import expressCompanyRoutes from './api/express-companies/index'
 import organizationRoutes from './api/organizations/index'
 import organizationRoutes from './api/organizations/index'
 import supplierRoutes from './api/suppliers/index'
 import supplierRoutes from './api/suppliers/index'
 import cardRoutes from './api/cards/index'
 import cardRoutes from './api/cards/index'
+import agentRoutes from './api/agents/index'
 import { AuthContext } from './types/context'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
 import { Hono } from 'hono'
@@ -123,6 +124,7 @@ const expressCompanyApiRoutes = api.route('/api/v1/express-companies', expressCo
 const organizationApiRoutes = api.route('/api/v1/organizations', organizationRoutes)
 const organizationApiRoutes = api.route('/api/v1/organizations', organizationRoutes)
 const supplierApiRoutes = api.route('/api/v1/suppliers', supplierRoutes)
 const supplierApiRoutes = api.route('/api/v1/suppliers', supplierRoutes)
 const cardApiRoutes = api.route('/api/v1/cards', cardRoutes)
 const cardApiRoutes = api.route('/api/v1/cards', cardRoutes)
+const agentApiRoutes = api.route('/api/v1/agents', agentRoutes)
 
 
 export type AuthRoutes = typeof authRoutes
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type UserRoutes = typeof userRoutes
@@ -138,6 +140,7 @@ export type ExpressCompanyRoutes = typeof expressCompanyApiRoutes
 export type OrganizationRoutes = typeof organizationApiRoutes
 export type OrganizationRoutes = typeof organizationApiRoutes
 export type SupplierRoutes = typeof supplierApiRoutes
 export type SupplierRoutes = typeof supplierApiRoutes
 export type CardRoutes = typeof cardApiRoutes
 export type CardRoutes = typeof cardApiRoutes
+export type AgentRoutes = typeof agentApiRoutes
 
 
 app.route('/', api)
 app.route('/', api)
 export default app
 export default app

+ 20 - 0
src/server/api/agents/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Agent } from '@/server/modules/agent/agent.entity';
+import { AgentSchema, CreateAgentDto, UpdateAgentDto } from '@/server/modules/agent/agent.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const agentRoutes = createCrudRoutes({
+  entity: Agent,
+  createSchema: CreateAgentDto,
+  updateSchema: UpdateAgentDto,
+  getSchema: AgentSchema,
+  listSchema: AgentSchema,
+  searchFields: ['name', 'username', 'realname', 'phone'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default agentRoutes;

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

@@ -16,6 +16,7 @@ import { ExpressCompany } from "./modules/logistics/express-company.entity"
 import { Organization } from "./modules/organization/organization.entity"
 import { Organization } from "./modules/organization/organization.entity"
 import { Supplier } from "./modules/supplier/supplier.entity"
 import { Supplier } from "./modules/supplier/supplier.entity"
 import { Card } from "./modules/card/card.entity"
 import { Card } from "./modules/card/card.entity"
+import { Agent } from "./modules/agent/agent.entity"
 
 
 export const AppDataSource = new DataSource({
 export const AppDataSource = new DataSource({
   type: "mysql",
   type: "mysql",
@@ -26,7 +27,7 @@ export const AppDataSource = new DataSource({
   database: process.env.DB_DATABASE || "d8dai",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
   entities: [
     User, Role, File, Advertisement, AdvertisementType,
     User, Role, File, Advertisement, AdvertisementType,
-    GoodsCategory, Goods, City, Config, ExpressCompany, Organization, Supplier, Card,
+    GoodsCategory, Goods, City, Config, ExpressCompany, Organization, Supplier, Card, Agent,
   ],
   ],
   migrations: [],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 52 - 0
src/server/modules/agent/agent.entity.ts

@@ -0,0 +1,52 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('agent')
+export class Agent {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '代理商名称' })
+  name!: string | null;
+
+  @Column({ name: 'username', type: 'varchar', length: 20, unique: true, comment: '用户名' })
+  username!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'phone', type: 'char', length: 11, nullable: true, comment: '手机号码' })
+  phone!: string | null;
+
+  @Column({ name: 'realname', type: 'varchar', length: 20, nullable: true, comment: '姓名' })
+  realname!: string | null;
+
+  @Column({ name: 'login_num', type: 'int', unsigned: true, default: 0, comment: '登录次数' })
+  loginNum!: number;
+
+  @Column({ name: 'login_time', type: 'int', unsigned: true, default: 0, comment: '登录时间' })
+  loginTime!: number;
+
+  @Column({ name: 'login_ip', type: 'varchar', length: 15, nullable: true, comment: '登录IP' })
+  loginIp!: string | null;
+
+  @Column({ name: 'last_login_time', type: 'int', unsigned: true, default: 0, comment: '上次登录时间' })
+  lastLoginTime!: number;
+
+  @Column({ name: 'last_login_ip', type: 'varchar', length: 15, nullable: true, comment: '上次登录IP' })
+  lastLoginIp!: string | null;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 2, comment: '状态 1启用 2禁用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 119 - 0
src/server/modules/agent/agent.schema.ts

@@ -0,0 +1,119 @@
+import { z } from '@hono/zod-openapi';
+
+export const AgentSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '代理商ID' }),
+  name: z.string().min(1, '代理商名称不能为空').max(255, '代理商名称最多255个字符').nullable().openapi({
+    description: '代理商名称',
+    example: '代理商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'agent001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateAgentDto = z.object({
+  name: z.string().min(1, '代理商名称不能为空').max(255, '代理商名称最多255个字符').nullable().optional().openapi({
+    description: '代理商名称',
+    example: '代理商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'agent001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateAgentDto = z.object({
+  name: z.string().min(1, '代理商名称不能为空').max(255, '代理商名称最多255个字符').nullable().optional().openapi({
+    description: '代理商名称',
+    example: '代理商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').optional().openapi({
+    description: '用户名',
+    example: 'agent001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});

+ 9 - 0
src/server/modules/agent/agent.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Agent } from './agent.entity';
+
+export class AgentService extends GenericCrudService<Agent> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Agent);
+  }
+}