Sfoglia il codice sorgente

✨ feat(merchant): add merchant module

- create merchant entity with fields including name, username, password and other basic information
- implement merchant service using generic CRUD service
- define merchant schema with validation rules for create, update and query operations
- add merchant API routes and client for /api/v1/merchants endpoint
- register merchant routes in the main API configuration
- enable CRUD operations with auth middleware and user tracking
yourname 4 mesi fa
parent
commit
9389ea2b4b

+ 6 - 1
src/client/api.ts

@@ -14,6 +14,7 @@ import type { OrganizationRoutes } from '@/server/api'
 import type { SupplierRoutes } from '@/server/api'
 import type { CardRoutes } from '@/server/api'
 import type { AgentRoutes } from '@/server/api'
+import type { MerchantRoutes } from '@/server/api'
 import { axiosFetch } from './utils/axios-fetch'
 
 // 创建客户端
@@ -75,4 +76,8 @@ export const cardClient = hc<CardRoutes>('/', {
 
 export const agentClient = hc<AgentRoutes>('/', {
   fetch: axiosFetch,
-}).api.v1.agents
+}).api.v1.agents
+
+export const merchantClient = hc<MerchantRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.merchants

+ 3 - 0
src/server/api.ts

@@ -16,6 +16,7 @@ import organizationRoutes from './api/organizations/index'
 import supplierRoutes from './api/suppliers/index'
 import cardRoutes from './api/cards/index'
 import agentRoutes from './api/agents/index'
+import merchantRoutes from './api/merchants/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -125,6 +126,7 @@ const organizationApiRoutes = api.route('/api/v1/organizations', organizationRou
 const supplierApiRoutes = api.route('/api/v1/suppliers', supplierRoutes)
 const cardApiRoutes = api.route('/api/v1/cards', cardRoutes)
 const agentApiRoutes = api.route('/api/v1/agents', agentRoutes)
+const merchantApiRoutes = api.route('/api/v1/merchants', merchantRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -141,6 +143,7 @@ export type OrganizationRoutes = typeof organizationApiRoutes
 export type SupplierRoutes = typeof supplierApiRoutes
 export type CardRoutes = typeof cardApiRoutes
 export type AgentRoutes = typeof agentApiRoutes
+export type MerchantRoutes = typeof merchantApiRoutes
 
 app.route('/', api)
 export default app

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

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

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

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

+ 58 - 0
src/server/modules/merchant/merchant.entity.ts

@@ -0,0 +1,58 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('merchant')
+export class Merchant {
+  @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;
+
+  @Column({ name: 'rsa_public_key', type: 'varchar', length: 2000, nullable: true, comment: '公钥' })
+  rsaPublicKey!: string | null;
+
+  @Column({ name: 'aes_key', type: 'varchar', length: 32, nullable: true, comment: 'aes秘钥' })
+  aesKey!: string | null;
+
+  @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;
+}

+ 143 - 0
src/server/modules/merchant/merchant.schema.ts

@@ -0,0 +1,143 @@
+import { z } from '@hono/zod-openapi';
+
+export const MerchantSchema = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  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 CreateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});
+
+export const UpdateMerchantDto = 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: 'merchant001'
+  }),
+  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
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});

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

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