瀏覽代碼

✨ feat(api): 新增商品、物流、组织等8个业务模块

- 新增【商品管理】功能,支持商品分类、商品信息完整CRUD
- 新增【物流管理】功能,支持物流公司信息管理
- 新增【组织管理】功能,支持分行网点层级管理
- 新增【供应商管理】功能,支持供应商账号体系
- 新增【卡券管理】功能,支持盛京通卡和联名电子卡
- 新增【城市配置】功能,支持省市区县层级管理
- 新增【系统配置】功能,支持键值对配置管理
- 所有模块均集成认证中间件和创建人跟踪
yourname 4 月之前
父節點
當前提交
dafb3bd408
共有 35 個文件被更改,包括 1437 次插入1 次删除
  1. 41 1
      src/client/api.ts
  2. 24 0
      src/server/api.ts
  3. 21 0
      src/server/api/cards/index.ts
  4. 20 0
      src/server/api/cities/index.ts
  5. 20 0
      src/server/api/configs/index.ts
  6. 20 0
      src/server/api/express-companies/index.ts
  7. 21 0
      src/server/api/goods-categories/index.ts
  8. 21 0
      src/server/api/goods/index.ts
  9. 20 0
      src/server/api/organizations/index.ts
  10. 20 0
      src/server/api/suppliers/index.ts
  11. 9 0
      src/server/data-source.ts
  12. 36 0
      src/server/modules/card/card.entity.ts
  13. 91 0
      src/server/modules/card/card.schema.ts
  14. 9 0
      src/server/modules/card/card.service.ts
  15. 33 0
      src/server/modules/goods/goods-category.entity.ts
  16. 83 0
      src/server/modules/goods/goods-category.schema.ts
  17. 9 0
      src/server/modules/goods/goods-category.service.ts
  18. 96 0
      src/server/modules/goods/goods.entity.ts
  19. 257 0
      src/server/modules/goods/goods.schema.ts
  20. 9 0
      src/server/modules/goods/goods.service.ts
  21. 25 0
      src/server/modules/logistics/express-company.entity.ts
  22. 67 0
      src/server/modules/logistics/express-company.schema.ts
  23. 9 0
      src/server/modules/logistics/express-company.service.ts
  24. 25 0
      src/server/modules/organization/organization.entity.ts
  25. 67 0
      src/server/modules/organization/organization.schema.ts
  26. 9 0
      src/server/modules/organization/organization.service.ts
  27. 49 0
      src/server/modules/supplier/supplier.entity.ts
  28. 115 0
      src/server/modules/supplier/supplier.schema.ts
  29. 9 0
      src/server/modules/supplier/supplier.service.ts
  30. 28 0
      src/server/modules/system/city.entity.ts
  31. 79 0
      src/server/modules/system/city.schema.ts
  32. 9 0
      src/server/modules/system/city.service.ts
  33. 22 0
      src/server/modules/system/config.entity.ts
  34. 55 0
      src/server/modules/system/config.schema.ts
  35. 9 0
      src/server/modules/system/config.service.ts

+ 41 - 1
src/client/api.ts

@@ -5,6 +5,14 @@ import type { RoleRoutes } from '@/server/api'
 import type { FileRoutes } from '@/server/api'
 import type { AdvertisementRoutes } from '@/server/api'
 import type { AdvertisementTypeRoutes } from '@/server/api'
+import type { GoodsCategoryRoutes } from '@/server/api'
+import type { GoodsRoutes } from '@/server/api'
+import type { CityRoutes } from '@/server/api'
+import type { ConfigRoutes } from '@/server/api'
+import type { ExpressCompanyRoutes } from '@/server/api'
+import type { OrganizationRoutes } from '@/server/api'
+import type { SupplierRoutes } from '@/server/api'
+import type { CardRoutes } from '@/server/api'
 import { axiosFetch } from './utils/axios-fetch'
 
 // 创建客户端
@@ -30,4 +38,36 @@ export const advertisementClient = hc<AdvertisementRoutes>('/', {
 
 export const advertisementTypeClient = hc<AdvertisementTypeRoutes>('/', {
   fetch: axiosFetch,
-}).api.v1['advertisement-types']
+}).api.v1['advertisement-types']
+
+export const goodsCategoryClient = hc<GoodsCategoryRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['goods-categories']
+
+export const goodsClient = hc<GoodsRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.goods
+
+export const cityClient = hc<CityRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.cities
+
+export const configClient = hc<ConfigRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.configs
+
+export const expressCompanyClient = hc<ExpressCompanyRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['express-companies']
+
+export const organizationClient = hc<OrganizationRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.organizations
+
+export const supplierClient = hc<SupplierRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.suppliers
+
+export const cardClient = hc<CardRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.cards

+ 24 - 0
src/server/api.ts

@@ -7,6 +7,14 @@ import rolesRoute from './api/roles/index'
 import fileRoutes from './api/files/index'
 import advertisementRoutes from './api/advertisements/index'
 import advertisementTypeRoutes from './api/advertisement-types/index'
+import goodsCategoryRoutes from './api/goods-categories/index'
+import goodsRoutes from './api/goods/index'
+import cityRoutes from './api/cities/index'
+import configRoutes from './api/configs/index'
+import expressCompanyRoutes from './api/express-companies/index'
+import organizationRoutes from './api/organizations/index'
+import supplierRoutes from './api/suppliers/index'
+import cardRoutes from './api/cards/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -107,6 +115,14 @@ const roleRoutes = api.route('/api/v1/roles', rolesRoute)
 const fileApiRoutes = api.route('/api/v1/files', fileRoutes)
 const advertisementApiRoutes = api.route('/api/v1/advertisements', advertisementRoutes)
 const advertisementTypeApiRoutes = api.route('/api/v1/advertisement-types', advertisementTypeRoutes)
+const goodsCategoryApiRoutes = api.route('/api/v1/goods-categories', goodsCategoryRoutes)
+const goodsApiRoutes = api.route('/api/v1/goods', goodsRoutes)
+const cityApiRoutes = api.route('/api/v1/cities', cityRoutes)
+const configApiRoutes = api.route('/api/v1/configs', configRoutes)
+const expressCompanyApiRoutes = api.route('/api/v1/express-companies', expressCompanyRoutes)
+const organizationApiRoutes = api.route('/api/v1/organizations', organizationRoutes)
+const supplierApiRoutes = api.route('/api/v1/suppliers', supplierRoutes)
+const cardApiRoutes = api.route('/api/v1/cards', cardRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -114,6 +130,14 @@ export type RoleRoutes = typeof roleRoutes
 export type FileRoutes = typeof fileApiRoutes
 export type AdvertisementRoutes = typeof advertisementApiRoutes
 export type AdvertisementTypeRoutes = typeof advertisementTypeApiRoutes
+export type GoodsCategoryRoutes = typeof goodsCategoryApiRoutes
+export type GoodsRoutes = typeof goodsApiRoutes
+export type CityRoutes = typeof cityApiRoutes
+export type ConfigRoutes = typeof configApiRoutes
+export type ExpressCompanyRoutes = typeof expressCompanyApiRoutes
+export type OrganizationRoutes = typeof organizationApiRoutes
+export type SupplierRoutes = typeof supplierApiRoutes
+export type CardRoutes = typeof cardApiRoutes
 
 app.route('/', api)
 export default app

+ 21 - 0
src/server/api/cards/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Card } from '@/server/modules/card/card.entity';
+import { CardSchema, CreateCardDto, UpdateCardDto } from '@/server/modules/card/card.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const cardRoutes = createCrudRoutes({
+  entity: Card,
+  createSchema: CreateCardDto,
+  updateSchema: UpdateCardDto,
+  getSchema: CardSchema,
+  listSchema: CardSchema,
+  searchFields: ['card_no', 'card_type'],
+  relations: ['agent'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default cardRoutes;

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

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { City } from '@/server/modules/system/city.entity';
+import { CitySchema, CreateCityDto, UpdateCityDto } from '@/server/modules/system/city.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const cityRoutes = createCrudRoutes({
+  entity: City,
+  createSchema: CreateCityDto,
+  updateSchema: UpdateCityDto,
+  getSchema: CitySchema,
+  listSchema: CitySchema,
+  searchFields: ['name'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default cityRoutes;

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

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Config } from '@/server/modules/system/config.entity';
+import { ConfigSchema, CreateConfigDto, UpdateConfigDto } from '@/server/modules/system/config.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const configRoutes = createCrudRoutes({
+  entity: Config,
+  createSchema: CreateConfigDto,
+  updateSchema: UpdateConfigDto,
+  getSchema: ConfigSchema,
+  listSchema: ConfigSchema,
+  searchFields: ['key', 'value'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default configRoutes;

+ 20 - 0
src/server/api/express-companies/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { ExpressCompany } from '@/server/modules/logistics/express-company.entity';
+import { ExpressCompanySchema, CreateExpressCompanyDto, UpdateExpressCompanyDto } from '@/server/modules/logistics/express-company.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const expressCompanyRoutes = createCrudRoutes({
+  entity: ExpressCompany,
+  createSchema: CreateExpressCompanyDto,
+  updateSchema: UpdateExpressCompanyDto,
+  getSchema: ExpressCompanySchema,
+  listSchema: ExpressCompanySchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default expressCompanyRoutes;

+ 21 - 0
src/server/api/goods-categories/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { GoodsCategory } from '@/server/modules/goods/goods-category.entity';
+import { GoodsCategorySchema, CreateGoodsCategoryDto, UpdateGoodsCategoryDto } from '@/server/modules/goods/goods-category.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const goodsCategoryRoutes = createCrudRoutes({
+  entity: GoodsCategory,
+  createSchema: CreateGoodsCategoryDto,
+  updateSchema: UpdateGoodsCategoryDto,
+  getSchema: GoodsCategorySchema,
+  listSchema: GoodsCategorySchema,
+  searchFields: ['name'],
+  relations: ['imageFile'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default goodsCategoryRoutes;

+ 21 - 0
src/server/api/goods/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Goods } from '@/server/modules/goods/goods.entity';
+import { GoodsSchema, CreateGoodsDto, UpdateGoodsDto } from '@/server/modules/goods/goods.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const goodsRoutes = createCrudRoutes({
+  entity: Goods,
+  createSchema: CreateGoodsDto,
+  updateSchema: UpdateGoodsDto,
+  getSchema: GoodsSchema,
+  listSchema: GoodsSchema,
+  searchFields: ['name', 'instructions'],
+  relations: ['category1', 'category2', 'category3', 'supplier', 'imageFile'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default goodsRoutes;

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

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Organization } from '@/server/modules/organization/organization.entity';
+import { OrganizationSchema, CreateOrganizationDto, UpdateOrganizationDto } from '@/server/modules/organization/organization.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const organizationRoutes = createCrudRoutes({
+  entity: Organization,
+  createSchema: CreateOrganizationDto,
+  updateSchema: UpdateOrganizationDto,
+  getSchema: OrganizationSchema,
+  listSchema: OrganizationSchema,
+  searchFields: ['name'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default organizationRoutes;

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

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Supplier } from '@/server/modules/supplier/supplier.entity';
+import { SupplierSchema, CreateSupplierDto, UpdateSupplierDto } from '@/server/modules/supplier/supplier.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const supplierRoutes = createCrudRoutes({
+  entity: Supplier,
+  createSchema: CreateSupplierDto,
+  updateSchema: UpdateSupplierDto,
+  getSchema: SupplierSchema,
+  listSchema: SupplierSchema,
+  searchFields: ['name', 'username', 'realname'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default supplierRoutes;

+ 9 - 0
src/server/data-source.ts

@@ -8,6 +8,14 @@ import { Role } from "./modules/users/role.entity"
 import { File } from "./modules/files/file.entity"
 import { Advertisement } from "./modules/advertisements/advertisement.entity"
 import { AdvertisementType } from "./modules/advertisements/advertisement-type.entity"
+import { GoodsCategory } from "./modules/goods/goods-category.entity"
+import { Goods } from "./modules/goods/goods.entity"
+import { City } from "./modules/system/city.entity"
+import { Config } from "./modules/system/config.entity"
+import { ExpressCompany } from "./modules/logistics/express-company.entity"
+import { Organization } from "./modules/organization/organization.entity"
+import { Supplier } from "./modules/supplier/supplier.entity"
+import { Card } from "./modules/card/card.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -18,6 +26,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,
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 36 - 0
src/server/modules/card/card.entity.ts

@@ -0,0 +1,36 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { UserEntity as User } from '@/server/modules/users/user.entity';
+
+@Entity('card')
+export class Card {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'agent_id', type: 'int', unsigned: true, nullable: true, comment: '代理商ID' })
+  agentId!: number | null;
+
+  @Column({ name: 'card_type', type: 'tinyint', unsigned: true, comment: '1盛京通卡 2通用联名电子卡' })
+  cardType!: number;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 20, unique: true, comment: '卡号' })
+  cardNo!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1绑定 2解绑 通用联名电子卡不可解绑' })
+  state!: number;
+
+  @Column({ name: 'face_value', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '面值' })
+  faceValue!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User, { nullable: true })
+  @JoinColumn({ name: 'agent_id', referencedColumnName: 'id' })
+  agent!: User | null;
+}

+ 91 - 0
src/server/modules/card/card.schema.ts

@@ -0,0 +1,91 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@/server/modules/users/user.schema';
+
+export const CardSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '卡ID' }),
+  agentId: z.number().int().positive().nullable().openapi({
+    description: '代理商ID',
+    example: 1
+  }),
+  cardType: z.number().int().min(1).max(2).openapi({
+    description: '卡类型 1盛京通卡 2通用联名电子卡',
+    example: 1
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').openapi({
+    description: '密码',
+    example: 'hashed_password'
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1绑定 2解绑 通用联名电子卡不可解绑',
+    example: 1
+  }),
+  faceValue: z.coerce.number().multipleOf(0.01, '面值最多保留两位小数').min(0, '面值不能为负数').default(0).openapi({
+    description: '面值',
+    example: 100.00
+  }),
+  agent: UserSchema.nullable().optional().openapi({
+    description: '代理商信息'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  })
+});
+
+export const CreateCardDto = z.object({
+  cardType: z.number().int().min(1, '卡类型必须在1-2之间').max(2, '卡类型必须在1-2之间').openapi({
+    description: '卡类型 1盛京通卡 2通用联名电子卡',
+    example: 1
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  faceValue: z.coerce.number().multipleOf(0.01, '面值最多保留两位小数').min(0, '面值不能为负数').default(0).openapi({
+    description: '面值',
+    example: 100.00
+  }),
+  agentId: z.number().int().positive().nullable().optional().openapi({
+    description: '代理商ID',
+    example: 1
+  })
+});
+
+export const UpdateCardDto = z.object({
+  cardType: z.number().int().min(1).max(2).optional().openapi({
+    description: '卡类型 1盛京通卡 2通用联名电子卡',
+    example: 1
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').optional().openapi({
+    description: '卡号',
+    example: '1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').optional().openapi({
+    description: '密码',
+    example: 'hashed_password'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1绑定 2解绑 通用联名电子卡不可解绑',
+    example: 1
+  }),
+  faceValue: z.coerce.number().multipleOf(0.01, '面值最多保留两位小数').min(0, '面值不能为负数').optional().openapi({
+    description: '面值',
+    example: 100.00
+  }),
+  agentId: z.number().int().positive().nullable().optional().openapi({
+    description: '代理商ID',
+    example: 1
+  })
+});

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

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

+ 33 - 0
src/server/modules/goods/goods-category.entity.ts

@@ -0,0 +1,33 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { File } from '@/server/modules/files/file.entity';
+
+@Entity('goods_category')
+export class GoodsCategory {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '类别名称' })
+  name!: string;
+
+  @Column({ name: 'parent_id', type: 'int', unsigned: true, default: 0, comment: '上级id' })
+  parentId!: number;
+
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '分类图片文件ID' })
+  imageFileId!: number | null;
+
+  @Column({ name: 'level', type: 'int', unsigned: true, default: 0, comment: '层级' })
+  level!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用 2不可用' })
+  state!: number;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '创建时间' })
+  createdAt!: Date;
+
+  @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
+}

+ 83 - 0
src/server/modules/goods/goods-category.schema.ts

@@ -0,0 +1,83 @@
+import { z } from '@hono/zod-openapi';
+import { FileSchema } from '@/server/modules/files/file.schema';
+
+export const GoodsCategorySchema = z.object({
+  id: z.number().int().positive().openapi({ description: '类别ID' }),
+  name: z.string().min(1, '类别名称不能为空').max(255, '类别名称最多255个字符').openapi({
+    description: '类别名称',
+    example: '电子产品'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级分类ID',
+    example: 0
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '分类图片文件ID',
+    example: 1
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(0).openapi({
+    description: '分类层级',
+    example: 1
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  imageFile: FileSchema.nullable().optional().openapi({
+    description: '分类图片信息'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  })
+});
+
+export const CreateGoodsCategoryDto = z.object({
+  name: z.string().min(1, '类别名称不能为空').max(255, '类别名称最多255个字符').openapi({
+    description: '类别名称',
+    example: '电子产品'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级分类ID',
+    example: 0
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '分类图片文件ID',
+    example: 1
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(0).openapi({
+    description: '分类层级',
+    example: 1
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});
+
+export const UpdateGoodsCategoryDto = z.object({
+  name: z.string().min(1, '类别名称不能为空').max(255, '类别名称最多255个字符').optional().openapi({
+    description: '类别名称',
+    example: '电子产品'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').optional().openapi({
+    description: '上级分类ID',
+    example: 0
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '分类图片文件ID',
+    example: 1
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').optional().openapi({
+    description: '分类层级',
+    example: 1
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});

+ 9 - 0
src/server/modules/goods/goods-category.service.ts

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

+ 96 - 0
src/server/modules/goods/goods.entity.ts

@@ -0,0 +1,96 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { GoodsCategory } from './goods-category.entity';
+import { Supplier } from '@/server/modules/supplier/supplier.entity';
+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', unsigned: true, default: 0, comment: '销售数量' })
+  salesNum!: number;
+
+  @Column({ name: 'click_num', type: 'bigint', unsigned: true, default: 0, comment: '点击次数' })
+  clickNum!: number;
+
+  @Column({ name: 'category_id1', type: 'int', unsigned: true, default: 0, comment: '一级类别id' })
+  categoryId1!: number;
+
+  @Column({ name: 'category_id2', type: 'int', unsigned: true, default: 0, comment: '二级类别id' })
+  categoryId2!: number;
+
+  @Column({ name: 'category_id3', type: 'int', unsigned: true, default: 0, comment: '三级类别id' })
+  categoryId3!: number;
+
+  @Column({ name: 'goods_type', type: 'tinyint', unsigned: true, default: 1, comment: '订单类型 1实物产品 2虚拟产品' })
+  goodsType!: number;
+
+  @Column({ name: 'supplier_id', type: 'int', unsigned: true, nullable: true, comment: '所属供应商id' })
+  supplierId!: number | null;
+
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '商品主图文件ID' })
+  imageFileId!: number | null;
+
+  @Column({ name: 'slide_images', type: 'text', nullable: true, comment: '商品轮播图URL数组(JSON)' })
+  slideImages!: 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', unsigned: true, default: 0, comment: '排序' })
+  sort!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用 2不可用' })
+  state!: number;
+
+  @Column({ name: 'stock', type: 'bigint', unsigned: true, default: 0, comment: '库存' })
+  stock!: number;
+
+  @Column({ name: 'spu_id', type: 'int', unsigned: true, 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', unsigned: true, default: 1, comment: '最小起购量' })
+  lowestBuy!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => GoodsCategory, { nullable: true })
+  @JoinColumn({ name: 'category_id1', referencedColumnName: 'id' })
+  category1!: GoodsCategory | null;
+
+  @ManyToOne(() => GoodsCategory, { nullable: true })
+  @JoinColumn({ name: 'category_id2', referencedColumnName: 'id' })
+  category2!: GoodsCategory | null;
+
+  @ManyToOne(() => GoodsCategory, { nullable: true })
+  @JoinColumn({ name: 'category_id3', referencedColumnName: 'id' })
+  category3!: GoodsCategory | null;
+
+  @ManyToOne(() => Supplier, { nullable: true })
+  @JoinColumn({ name: 'supplier_id', referencedColumnName: 'id' })
+  supplier!: Supplier | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
+}

+ 257 - 0
src/server/modules/goods/goods.schema.ts

@@ -0,0 +1,257 @@
+import { z } from '@hono/zod-openapi';
+import { GoodsCategorySchema } from './goods-category.schema';
+import { SupplierSchema } from '@/server/modules/supplier/supplier.schema';
+import { FileSchema } from '@/server/modules/files/file.schema';
+
+const SlideImagesSchema = z.array(z.string()).nullable().optional().openapi({
+  description: '商品轮播图URL数组',
+  example: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg']
+});
+
+export const GoodsSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '商品ID' }),
+  name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').openapi({
+    description: '商品名称',
+    example: 'iPhone 15'
+  }),
+  price: z.coerce.number().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数').default(0).openapi({
+    description: '售卖价',
+    example: 5999.99
+  }),
+  costPrice: z.coerce.number().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').default(0).openapi({
+    description: '成本价',
+    example: 4999.99
+  }),
+  salesNum: z.coerce.number().int().nonnegative('销售数量必须为非负数').default(0).openapi({
+    description: '销售数量',
+    example: 100
+  }),
+  clickNum: z.coerce.number().int().nonnegative('点击次数必须为非负数').default(0).openapi({
+    description: '点击次数',
+    example: 1000
+  }),
+  categoryId1: z.number().int().nonnegative('一级类别ID必须为非负数').default(0).openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().nonnegative('二级类别ID必须为非负数').default(0).openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().nonnegative('三级类别ID必须为非负数').default(0).openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).default(1).openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  slideImages: SlideImagesSchema,
+  detail: z.string().nullable().optional().openapi({
+    description: '商品详情',
+    example: '这是商品详情内容'
+  }),
+  instructions: z.string().max(255, '简介最多255个字符').nullable().optional().openapi({
+    description: '简介',
+    example: '高品质智能手机'
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number().int().nonnegative('库存必须为非负数').default(0).openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().nonnegative('主商品ID必须为非负数').default(0).openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255, '主商品名称最多255个字符').nullable().optional().openapi({
+    description: '主商品名称',
+    example: 'iPhone系列'
+  }),
+  lowestBuy: z.number().int().positive('最小起购量必须为正整数').default(1).openapi({
+    description: '最小起购量',
+    example: 1
+  }),
+  category1: GoodsCategorySchema.nullable().optional().openapi({
+    description: '一级分类信息'
+  }),
+  category2: GoodsCategorySchema.nullable().optional().openapi({
+    description: '二级分类信息'
+  }),
+  category3: GoodsCategorySchema.nullable().optional().openapi({
+    description: '三级分类信息'
+  }),
+  supplier: SupplierSchema.nullable().optional().openapi({
+    description: '供应商信息'
+  }),
+  imageFile: FileSchema.nullable().optional().openapi({
+    description: '商品主图信息'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  })
+});
+
+export const CreateGoodsDto = z.object({
+  name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').openapi({
+    description: '商品名称',
+    example: 'iPhone 15'
+  }),
+  price: z.coerce.number().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数').default(0).openapi({
+    description: '售卖价',
+    example: 5999.99
+  }),
+  costPrice: z.coerce.number().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').default(0).openapi({
+    description: '成本价',
+    example: 4999.99
+  }),
+  categoryId1: z.number().int().nonnegative('一级类别ID必须为非负数').default(0).openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().nonnegative('二级类别ID必须为非负数').default(0).openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().nonnegative('三级类别ID必须为非负数').default(0).openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).default(1).openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().optional().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  slideImages: SlideImagesSchema,
+  detail: z.string().nullable().optional().openapi({
+    description: '商品详情',
+    example: '这是商品详情内容'
+  }),
+  instructions: z.string().max(255, '简介最多255个字符').nullable().optional().openapi({
+    description: '简介',
+    example: '高品质智能手机'
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number().int().nonnegative('库存必须为非负数').default(0).openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().nonnegative('主商品ID必须为非负数').default(0).openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255, '主商品名称最多255个字符').nullable().optional().openapi({
+    description: '主商品名称',
+    example: 'iPhone系列'
+  }),
+  lowestBuy: z.number().int().positive('最小起购量必须为正整数').default(1).openapi({
+    description: '最小起购量',
+    example: 1
+  })
+});
+
+export const UpdateGoodsDto = z.object({
+  name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').optional().openapi({
+    description: '商品名称',
+    example: 'iPhone 15'
+  }),
+  price: z.coerce.number().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数').optional().openapi({
+    description: '售卖价',
+    example: 5999.99
+  }),
+  costPrice: z.coerce.number().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').optional().openapi({
+    description: '成本价',
+    example: 4999.99
+  }),
+  categoryId1: z.number().int().nonnegative('一级类别ID必须为非负数').optional().openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().nonnegative('二级类别ID必须为非负数').optional().openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().nonnegative('三级类别ID必须为非负数').optional().openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).optional().openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().optional().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  slideImages: SlideImagesSchema,
+  detail: z.string().nullable().optional().openapi({
+    description: '商品详情',
+    example: '这是商品详情内容'
+  }),
+  instructions: z.string().max(255, '简介最多255个字符').nullable().optional().openapi({
+    description: '简介',
+    example: '高品质智能手机'
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').optional().openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number().int().nonnegative('库存必须为非负数').optional().openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().nonnegative('主商品ID必须为非负数').optional().openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255, '主商品名称最多255个字符').nullable().optional().openapi({
+    description: '主商品名称',
+    example: 'iPhone系列'
+  }),
+  lowestBuy: z.number().int().positive('最小起购量必须为正整数').optional().openapi({
+    description: '最小起购量',
+    example: 1
+  })
+});

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

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

+ 25 - 0
src/server/modules/logistics/express-company.entity.ts

@@ -0,0 +1,25 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('express_company')
+export class ExpressCompany {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 50, comment: '物流公司名称' })
+  name!: string;
+
+  @Column({ name: 'code', type: 'varchar', length: 20, comment: '物流编号' })
+  code!: string;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '使用状态 1可用 2禁用' })
+  state!: number;
+
+  @Column({ name: 'sort', type: 'int', unsigned: true, nullable: true, comment: '优先级 值越大越优先' })
+  sort!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+}

+ 67 - 0
src/server/modules/logistics/express-company.schema.ts

@@ -0,0 +1,67 @@
+import { z } from '@hono/zod-openapi';
+
+export const ExpressCompanySchema = z.object({
+  id: z.number().int().positive().openapi({ description: '快递公司ID' }),
+  name: z.string().min(1, '物流公司名称不能为空').max(50, '物流公司名称最多50个字符').openapi({
+    description: '物流公司名称',
+    example: '顺丰速运'
+  }),
+  code: z.string().min(1, '物流编号不能为空').max(20, '物流编号最多20个字符').openapi({
+    description: '物流编号',
+    example: 'SF'
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '使用状态 1可用 2禁用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').nullable().openapi({
+    description: '优先级 值越大越优先',
+    example: 100
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  })
+});
+
+export const CreateExpressCompanyDto = z.object({
+  name: z.string().min(1, '物流公司名称不能为空').max(50, '物流公司名称最多50个字符').openapi({
+    description: '物流公司名称',
+    example: '顺丰速运'
+  }),
+  code: z.string().min(1, '物流编号不能为空').max(20, '物流编号最多20个字符').openapi({
+    description: '物流编号',
+    example: 'SF'
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '使用状态 1可用 2禁用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').nullable().optional().openapi({
+    description: '优先级 值越大越优先',
+    example: 100
+  })
+});
+
+export const UpdateExpressCompanyDto = z.object({
+  name: z.string().min(1, '物流公司名称不能为空').max(50, '物流公司名称最多50个字符').optional().openapi({
+    description: '物流公司名称',
+    example: '顺丰速运'
+  }),
+  code: z.string().min(1, '物流编号不能为空').max(20, '物流编号最多20个字符').optional().openapi({
+    description: '物流编号',
+    example: 'SF'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '使用状态 1可用 2禁用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').nullable().optional().openapi({
+    description: '优先级 值越大越优先',
+    example: 100
+  })
+});

+ 9 - 0
src/server/modules/logistics/express-company.service.ts

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

+ 25 - 0
src/server/modules/organization/organization.entity.ts

@@ -0,0 +1,25 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('organization')
+export class Organization {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '分行网点名称' })
+  name!: string;
+
+  @Column({ name: 'parent_id', type: 'int', unsigned: true, default: 0, comment: '上级id' })
+  parentId!: number;
+
+  @Column({ name: 'level', type: 'int', unsigned: true, default: 4, comment: '层级' })
+  level!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用 2不可用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+}

+ 67 - 0
src/server/modules/organization/organization.schema.ts

@@ -0,0 +1,67 @@
+import { z } from '@hono/zod-openapi';
+
+export const OrganizationSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '分行网点ID' }),
+  name: z.string().min(1, '分行网点名称不能为空').max(255, '分行网点名称最多255个字符').openapi({
+    description: '分行网点名称',
+    example: '北京分行'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级网点ID',
+    example: 0
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(4).openapi({
+    description: '组织层级',
+    example: 4
+  }),
+  state: z.number().int().min(1).max(2).default(1).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'
+  })
+});
+
+export const CreateOrganizationDto = z.object({
+  name: z.string().min(1, '分行网点名称不能为空').max(255, '分行网点名称最多255个字符').openapi({
+    description: '分行网点名称',
+    example: '北京分行'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级网点ID',
+    example: 0
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(4).openapi({
+    description: '组织层级',
+    example: 4
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});
+
+export const UpdateOrganizationDto = z.object({
+  name: z.string().min(1, '分行网点名称不能为空').max(255, '分行网点名称最多255个字符').optional().openapi({
+    description: '分行网点名称',
+    example: '北京分行'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').optional().openapi({
+    description: '上级网点ID',
+    example: 0
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').optional().openapi({
+    description: '组织层级',
+    example: 4
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});

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

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

+ 49 - 0
src/server/modules/supplier/supplier.entity.ts

@@ -0,0 +1,49 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('supplier')
+export class Supplier {
+  @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: 'salt', type: 'char', length: 32, comment: '随机码' })
+  salt!: 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;
+}

+ 115 - 0
src/server/modules/supplier/supplier.schema.ts

@@ -0,0 +1,115 @@
+import { z } from '@hono/zod-openapi';
+
+export const SupplierSchema = 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: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  salt: z.string().length(32, '随机码必须为32位').openapi({
+    description: '随机码',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  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'
+  })
+});
+
+export const CreateSupplierDto = 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: 'supplier001'
+  }),
+  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 UpdateSupplierDto = 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: 'supplier001'
+  }),
+  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/supplier/supplier.service.ts

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

+ 28 - 0
src/server/modules/system/city.entity.ts

@@ -0,0 +1,28 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('city')
+export class City {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '地区名称' })
+  name!: string;
+
+  @Column({ name: 'level', type: 'int', unsigned: true, default: 0, comment: '层级 省市区县1,2,3,4' })
+  level!: number;
+
+  @Column({ name: 'parent_id', type: 'bigint', unsigned: true, default: 0, comment: '上级id' })
+  parentId!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用' })
+  state!: number;
+
+  @Column({ name: 'sort', type: 'int', unsigned: true, default: 0, comment: '排序数值越大越靠前' })
+  sort!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+}

+ 79 - 0
src/server/modules/system/city.schema.ts

@@ -0,0 +1,79 @@
+import { z } from '@hono/zod-openapi';
+
+export const CitySchema = z.object({
+  id: z.number().int().positive().openapi({ description: '地区ID' }),
+  name: z.string().min(1, '地区名称不能为空').max(255, '地区名称最多255个字符').openapi({
+    description: '地区名称',
+    example: '北京市'
+  }),
+  level: z.number().int().min(1).max(4).default(1).openapi({
+    description: '层级 省市区县1,2,3,4',
+    example: 1
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级地区ID',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(1).default(1).openapi({
+    description: '状态 1可用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序数值越大越靠前',
+    example: 0
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  })
+});
+
+export const CreateCityDto = z.object({
+  name: z.string().min(1, '地区名称不能为空').max(255, '地区名称最多255个字符').openapi({
+    description: '地区名称',
+    example: '北京市'
+  }),
+  level: z.number().int().min(1, '层级必须在1-4之间').max(4, '层级必须在1-4之间').default(1).openapi({
+    description: '层级 省市区县1,2,3,4',
+    example: 1
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级地区ID',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(1).default(1).openapi({
+    description: '状态 1可用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序数值越大越靠前',
+    example: 0
+  })
+});
+
+export const UpdateCityDto = z.object({
+  name: z.string().min(1, '地区名称不能为空').max(255, '地区名称最多255个字符').optional().openapi({
+    description: '地区名称',
+    example: '北京市'
+  }),
+  level: z.number().int().min(1, '层级必须在1-4之间').max(4, '层级必须在1-4之间').optional().openapi({
+    description: '层级 省市区县1,2,3,4',
+    example: 1
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').optional().openapi({
+    description: '上级地区ID',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(1).optional().openapi({
+    description: '状态 1可用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').optional().openapi({
+    description: '排序数值越大越靠前',
+    example: 0
+  })
+});

+ 9 - 0
src/server/modules/system/city.service.ts

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

+ 22 - 0
src/server/modules/system/config.entity.ts

@@ -0,0 +1,22 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('config')
+export class Config {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'key', type: 'varchar', length: 255, comment: '配置键名' })
+  key!: string;
+
+  @Column({ name: 'value', type: 'varchar', length: 255, comment: '配置值' })
+  value!: string;
+
+  @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;
+}

+ 55 - 0
src/server/modules/system/config.schema.ts

@@ -0,0 +1,55 @@
+import { z } from '@hono/zod-openapi';
+
+export const ConfigSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '配置ID' }),
+  key: z.string().min(1, '配置键名不能为空').max(255, '配置键名最多255个字符').openapi({
+    description: '配置键名',
+    example: 'site_name'
+  }),
+  value: z.string().min(1, '配置值不能为空').max(255, '配置值最多255个字符').openapi({
+    description: '配置值',
+    example: '站点名称'
+  }),
+  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'
+  })
+});
+
+export const CreateConfigDto = z.object({
+  key: z.string().min(1, '配置键名不能为空').max(255, '配置键名最多255个字符').openapi({
+    description: '配置键名',
+    example: 'site_name'
+  }),
+  value: z.string().min(1, '配置值不能为空').max(255, '配置值最多255个字符').openapi({
+    description: '配置值',
+    example: '站点名称'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1可用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateConfigDto = z.object({
+  key: z.string().min(1, '配置键名不能为空').max(255, '配置键名最多255个字符').optional().openapi({
+    description: '配置键名',
+    example: 'site_name'
+  }),
+  value: z.string().min(1, '配置值不能为空').max(255, '配置值最多255个字符').optional().openapi({
+    description: '配置值',
+    example: '站点名称'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2禁用',
+    example: 1
+  })
+});

+ 9 - 0
src/server/modules/system/config.service.ts

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