Przeglądaj źródła

✨ feat(user-card): add user card management module

- create user card entity, schema, service and API routes
- implement CRUD operations for user cards with auth middleware
- add user tracking for createdBy and updatedBy fields
- include relations with user and agent entities
- add search functionality for cardNo and sjtCardNo fields

✨ feat(user-card-balance): add user card balance record module

- create user card balance record entity, schema, service and API routes
- implement CRUD operations for balance records with auth middleware
- add user tracking for createdBy and updatedBy fields
- include relations with user and userCard entities
- add search functionality for cardNo, orderNo and remark fields

♻️ refactor(user): rename UserEntity export to User for consistency

- update export statement to use named export { UserEntity as User }
- ensure consistency with other entity exports in the project

🔧 chore(api): register new API routes and update client types

- add user-cards and user-card-balance-records routes to API
- update API type definitions for new routes
- create client API wrappers for userCardClient and userCardBalanceRecordClient
- add new entities to data source configuration
yourname 4 miesięcy temu
rodzic
commit
5a13866396

+ 11 - 1
src/client/api.ts

@@ -15,6 +15,8 @@ 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 type { UserCardRoutes } from '@/server/api'
+import type { UserCardBalanceRecordRoutes } from '@/server/api'
 import { axiosFetch } from './utils/axios-fetch'
 
 // 创建客户端
@@ -80,4 +82,12 @@ export const agentClient = hc<AgentRoutes>('/', {
 
 export const merchantClient = hc<MerchantRoutes>('/', {
   fetch: axiosFetch,
-}).api.v1.merchants
+}).api.v1.merchants
+
+export const userCardClient = hc<UserCardRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['user-cards']
+
+export const userCardBalanceRecordClient = hc<UserCardBalanceRecordRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['user-card-balance-records']

+ 6 - 0
src/server/api.ts

@@ -17,6 +17,8 @@ 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 userCardRoutes from './api/user-cards/index'
+import userCardBalanceRecordRoutes from './api/user-card-balance-records/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -127,6 +129,8 @@ 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)
+const userCardApiRoutes = api.route('/api/v1/user-cards', userCardRoutes)
+const userCardBalanceRecordApiRoutes = api.route('/api/v1/user-card-balance-records', userCardBalanceRecordRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -144,6 +148,8 @@ export type SupplierRoutes = typeof supplierApiRoutes
 export type CardRoutes = typeof cardApiRoutes
 export type AgentRoutes = typeof agentApiRoutes
 export type MerchantRoutes = typeof merchantApiRoutes
+export type UserCardRoutes = typeof userCardApiRoutes
+export type UserCardBalanceRecordRoutes = typeof userCardBalanceRecordApiRoutes
 
 app.route('/', api)
 export default app

+ 21 - 0
src/server/api/user-card-balance-records/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { UserCardBalanceRecord } from '@/server/modules/user-card-balance-records/user-card-balance-record.entity';
+import { UserCardBalanceRecordSchema, CreateUserCardBalanceRecordDto, UpdateUserCardBalanceRecordDto } from '@/server/modules/user-card-balance-records/user-card-balance-record.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const userCardBalanceRecordRoutes = createCrudRoutes({
+  entity: UserCardBalanceRecord,
+  createSchema: CreateUserCardBalanceRecordDto,
+  updateSchema: UpdateUserCardBalanceRecordDto,
+  getSchema: UserCardBalanceRecordSchema,
+  listSchema: UserCardBalanceRecordSchema,
+  searchFields: ['cardNo', 'orderNo', 'remark'],
+  relations: ['user', 'userCard'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default userCardBalanceRecordRoutes;

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

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { UserCard } from '@/server/modules/user-cards/user-card.entity';
+import { UserCardSchema, CreateUserCardDto, UpdateUserCardDto } from '@/server/modules/user-cards/user-card.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const userCardRoutes = createCrudRoutes({
+  entity: UserCard,
+  createSchema: CreateUserCardDto,
+  updateSchema: UpdateUserCardDto,
+  getSchema: UserCardSchema,
+  listSchema: UserCardSchema,
+  searchFields: ['cardNo', 'sjtCardNo'],
+  relations: ['user', 'agent'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default userCardRoutes;

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

@@ -18,6 +18,8 @@ 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"
+import { UserCard } from "./modules/user-cards/user-card.entity"
+import { UserCardBalanceRecord } from "./modules/user-card-balance-records/user-card-balance-record.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -29,6 +31,7 @@ export const AppDataSource = new DataSource({
   entities: [
     User, Role, File, Advertisement, AdvertisementType,
     GoodsCategory, Goods, City, Config, ExpressCompany, Organization, Supplier, Card, Agent, Merchant,
+    UserCard, UserCardBalanceRecord,
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 53 - 0
src/server/modules/user-card-balance-records/user-card-balance-record.entity.ts

@@ -0,0 +1,53 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+import { UserCard } from '@/server/modules/user-cards/user-card.entity';
+
+@Entity('user_card_balance_records')
+export class UserCardBalanceRecord {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户ID' })
+  userId!: number;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 20, comment: '卡号' })
+  cardNo!: string;
+
+  @Column({ name: 'amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '变动金额' })
+  amount!: number;
+
+  @Column({ name: 'amount_before', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '变动前金额' })
+  amountBefore!: number;
+
+  @Column({ name: 'amount_after', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '变动后金额' })
+  amountAfter!: number;
+
+  @Column({ name: 'order_no', type: 'varchar', length: 32, comment: '订单号' })
+  orderNo!: string;
+
+  @Column({ name: 'type', type: 'int', comment: '类型 1消费 2退款' })
+  type!: number;
+
+  @Column({ name: 'remark', type: 'varchar', length: 32, nullable: true, comment: '备注' })
+  remark!: string | null;
+
+  @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;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: User;
+
+  @ManyToOne(() => UserCard)
+  @JoinColumn({ name: 'card_no', referencedColumnName: 'cardNo' })
+  userCard!: UserCard;
+}

+ 121 - 0
src/server/modules/user-card-balance-records/user-card-balance-record.schema.ts

@@ -0,0 +1,121 @@
+import { z } from '@hono/zod-openapi';
+
+// 基础用户卡余额记录Schema
+const UserCardBalanceRecordSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '记录ID',
+    example: 1
+  }),
+  userId: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1001
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '12345678901234567890'
+  }),
+  amount: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).openapi({
+    description: '变动金额',
+    example: 50.50
+  }),
+  amountBefore: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).openapi({
+    description: '变动前金额',
+    example: 100.00
+  }),
+  amountAfter: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).openapi({
+    description: '变动后金额',
+    example: 150.50
+  }),
+  orderNo: z.string().min(1, '订单号不能为空').max(32, '订单号最多32个字符').openapi({
+    description: '订单号',
+    example: 'ORD20240101120000'
+  }),
+  type: z.coerce.number().int().min(1).max(2).openapi({
+    description: '类型 1消费 2退款',
+    example: 1
+  }),
+  remark: z.string().max(32, '备注最多32个字符').nullable().openapi({
+    description: '备注',
+    example: '消费充值'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新人ID',
+    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'
+  })
+});
+
+// 创建用户卡余额记录DTO
+export const CreateUserCardBalanceRecordDto = UserCardBalanceRecordSchema.pick({
+  userId: true,
+  cardNo: true,
+  amount: true,
+  amountBefore: true,
+  amountAfter: true,
+  orderNo: true,
+  type: true,
+  remark: true
+}).extend({
+  userId: z.coerce.number().int().positive('用户ID必须是正整数'),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符'),
+  amount: z.coerce.number().multipleOf(0.01).min(0).max(999999.99),
+  amountBefore: z.coerce.number().multipleOf(0.01).min(0).max(999999.99),
+  amountAfter: z.coerce.number().multipleOf(0.01).min(0).max(999999.99),
+  orderNo: z.string().min(1, '订单号不能为空').max(32, '订单号最多32个字符'),
+  type: z.coerce.number().int().min(1).max(2),
+  remark: z.string().max(32, '备注最多32个字符').nullable()
+});
+
+// 更新用户卡余额记录DTO
+export const UpdateUserCardBalanceRecordDto = UserCardBalanceRecordSchema.pick({
+  userId: true,
+  cardNo: true,
+  amount: true,
+  amountBefore: true,
+  amountAfter: true,
+  orderNo: true,
+  type: true,
+  remark: true
+}).partial().extend({
+  userId: z.coerce.number().int().positive('用户ID必须是正整数').optional(),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').optional(),
+  amount: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).optional(),
+  amountBefore: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).optional(),
+  amountAfter: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).optional(),
+  orderNo: z.string().min(1, '订单号不能为空').max(32, '订单号最多32个字符').optional(),
+  type: z.coerce.number().int().min(1).max(2).optional(),
+  remark: z.string().max(32, '备注最多32个字符').nullable().optional()
+});
+
+// 响应Schema(包含关联数据)
+export const UserCardBalanceRecordResponseSchema = UserCardBalanceRecordSchema.extend({
+  user: z.object({
+    id: z.number().int().positive(),
+    username: z.string(),
+    name: z.string().nullable(),
+    phone: z.string().nullable()
+  }).optional(),
+  userCard: z.object({
+    id: z.number().int().positive(),
+    cardNo: z.string(),
+    balance: z.number()
+  }).optional()
+});
+
+export type UserCardBalanceRecord = z.infer<typeof UserCardBalanceRecordSchema>;
+export type CreateUserCardBalanceRecordDto = z.infer<typeof CreateUserCardBalanceRecordDto>;
+export type UpdateUserCardBalanceRecordDto = z.infer<typeof UpdateUserCardBalanceRecordDto>;
+export type UserCardBalanceRecordResponse = z.infer<typeof UserCardBalanceRecordResponseSchema>;
+
+export { UserCardBalanceRecordSchema };

+ 9 - 0
src/server/modules/user-card-balance-records/user-card-balance-record.service.ts

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

+ 56 - 0
src/server/modules/user-cards/user-card.entity.ts

@@ -0,0 +1,56 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+import { Agent } from '@/server/modules/agent/agent.entity';
+
+@Entity('user_cards')
+export class UserCard {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户ID' })
+  userId!: number;
+
+  @Column({ name: 'agent_id', type: 'int', unsigned: true, nullable: true, comment: '代理商ID' })
+  agentId!: number | null;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 20, comment: '卡号' })
+  cardNo!: string;
+
+  @Column({ name: 'sjt_card_no', type: 'varchar', length: 20, nullable: true, comment: '盛京通卡卡号' })
+  sjtCardNo!: string | null;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'auth_code', type: 'varchar', length: 16, nullable: true, comment: '付款码70_75开头16位随机数' })
+  authCode!: string | null;
+
+  @Column({ name: 'state', type: 'int', unsigned: true, default: 1, comment: '状态 1绑定 2解绑 通用联名电子卡不可解绑' })
+  state!: number;
+
+  @Column({ name: 'balance', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '余额' })
+  balance!: number;
+
+  @Column({ name: 'is_default', type: 'int', default: 2, comment: '默认 1是 2否' })
+  isDefault!: number;
+
+  @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;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: User;
+
+  @ManyToOne(() => Agent)
+  @JoinColumn({ name: 'agent_id', referencedColumnName: 'id' })
+  agent!: Agent | null;
+}

+ 124 - 0
src/server/modules/user-cards/user-card.schema.ts

@@ -0,0 +1,124 @@
+import { z } from '@hono/zod-openapi';
+
+// 基础用户卡Schema
+const UserCardSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '用户卡ID',
+    example: 1
+  }),
+  userId: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1001
+  }),
+  agentId: z.number().int().positive().nullable().openapi({
+    description: '代理商ID',
+    example: 5
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '12345678901234567890'
+  }),
+  sjtCardNo: z.string().max(20, '盛京通卡卡号最多20个字符').nullable().openapi({
+    description: '盛京通卡卡号',
+    example: 'SJT1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').openapi({
+    description: '密码',
+    example: 'encrypted_password'
+  }),
+  authCode: z.string().max(16, '付款码最多16个字符').nullable().openapi({
+    description: '付款码70_75开头16位随机数',
+    example: '7012345678901234'
+  }),
+  state: z.coerce.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1绑定 2解绑 通用联名电子卡不可解绑',
+    example: 1
+  }),
+  balance: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).default(0).openapi({
+    description: '余额',
+    example: 100.50
+  }),
+  isDefault: z.coerce.number().int().min(1).max(2).default(2).openapi({
+    description: '默认 1是 2否',
+    example: 1
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新人ID',
+    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'
+  })
+});
+
+// 创建用户卡DTO
+export const CreateUserCardDto = UserCardSchema.pick({
+  userId: true,
+  agentId: true,
+  cardNo: true,
+  sjtCardNo: true,
+  password: true,
+  authCode: true,
+  state: true,
+  balance: true,
+  isDefault: true
+}).extend({
+  userId: z.coerce.number().int().positive('用户ID必须是正整数'),
+  agentId: z.coerce.number().int().positive('代理商ID必须是正整数').nullable(),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符'),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符'),
+  state: z.coerce.number().int().min(1).max(2).default(1),
+  balance: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).default(0),
+  isDefault: z.coerce.number().int().min(1).max(2).default(2)
+});
+
+// 更新用户卡DTO
+export const UpdateUserCardDto = UserCardSchema.pick({
+  userId: true,
+  agentId: true,
+  cardNo: true,
+  sjtCardNo: true,
+  password: true,
+  authCode: true,
+  state: true,
+  balance: true,
+  isDefault: true
+}).partial().extend({
+  userId: z.coerce.number().int().positive('用户ID必须是正整数').optional(),
+  agentId: z.coerce.number().int().positive('代理商ID必须是正整数').nullable().optional(),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').optional(),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').optional(),
+  state: z.coerce.number().int().min(1).max(2).optional(),
+  balance: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).optional(),
+  isDefault: z.coerce.number().int().min(1).max(2).optional()
+});
+
+// 响应Schema(包含关联数据)
+export const UserCardResponseSchema = UserCardSchema.extend({
+  user: z.object({
+    id: z.number().int().positive(),
+    username: z.string(),
+    name: z.string().nullable(),
+    phone: z.string().nullable()
+  }).optional(),
+  agent: z.object({
+    id: z.number().int().positive(),
+    name: z.string()
+  }).nullable().optional()
+});
+
+export type UserCard = z.infer<typeof UserCardSchema>;
+export type CreateUserCardDto = z.infer<typeof CreateUserCardDto>;
+export type UpdateUserCardDto = z.infer<typeof UpdateUserCardDto>;
+export type UserCardResponse = z.infer<typeof UserCardResponseSchema>;
+
+export { UserCardSchema };

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

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

+ 3 - 1
src/server/modules/users/user.entity.ts

@@ -61,4 +61,6 @@ export class UserEntity {
   constructor(partial?: Partial<UserEntity>) {
     Object.assign(this, partial);
   }
-}
+}
+
+export { UserEntity as User };