Nest.js:做个图书借阅系统(2) 数据库设计

275 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情 

本系列文章将涉及到的技术有Nest.js、Typescript、Vite、Vue、Antdv、MySql,有兴趣的可以关注一波,希望各位大大多多包涵,有什么建议可以提出来互相交流。


-【接单心得】

接单的时候一定要评估好预算,假设你的时薪是100,那么你就按你实际的工时来报价就好了,但是一般我们还需要在实际工时上浮个至少百分之二十,这样做是为了以防万一,当然如果对方要求不是很严格那就更好了。


-【需求分析】

根据之前获取的需求来看(请看上一篇)有一些隐藏的需求是没有明确说明的,这只能靠我们的专业来进行补充,至少登录权限这一块肯定是会有的,还有一些不太清楚的就只能问客户啦。

自从当面答应接单之后,之前聊得火热的前女友,突然感觉变得冷淡了起来,打电话不接,发微信也只是淡淡的回答几个嗯哦,女人都是如此吗? 在你冷淡的时候热情,在你热情的时候浇一盆水,把你拿捏的死死的?

过了几天才终于确认好几个问题,我重新整理后如下:

  • 登录验证权限
  • 会员系统
    • 注册充值开卡:半年卡,年卡
    • 续卡(续费可能优惠),退卡(退费暂时不管)
    • 根据会员卡号查询会员信息:包括借阅状态,过期时间
  • 入库,借阅,归还
    • 入库进货:录入图书信息
    • 图书查询归档:自定编号查询,在库和借出状态
    • 借出:减少库存
    • 归还:报损(照原价偿还),超时视作购买(你可以不来给钱,但是也不允许再借阅,如果要继续就得补上之前书的价钱差)

分析: 从功能上可以拆分成2大块,会员和借阅 因为用户存在单次消费不办卡的情况,但是我们借阅系统上不记账,所以不用管它。 续费的话到时候可能会出优惠活动,这个简单,我们加个字段记录一下办卡和续费金额就行。 退卡就有点复杂,目前还没有想好具体是哪一种,暂且先按购物网站的7天没使用可以无理由退。 借阅部分比较简单,查询数据,增减库存即可,就是归还的时候,如果有损失需要手动记上一笔


-【数据结构】

上一篇说到每次需要forFeature注册实体,其实这是Typeorm的存储库模式,这种模式往往在多表查询的时候不太直观也不够灵活,我们可以直接用EntityManager,这样在任意模块加载任意实体都可以。

EntityManager关联查询写法大概是下面这样

import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { User } from 'src/entity/user.entity';
import { Role } from 'src/entity/role.entity';
import { Rolemenu } from 'src/entity/rolemenu.entity';

@Injectable()
export class AppService {
  constructor(private entityManager: EntityManager) {}

  getHello(): string {
    return 'Hello World!';
  }

  getUser(): Promise<User[]> {
    const user = this.entityManager.getRepository(User);

    return user
      .createQueryBuilder('user')
      .leftJoinAndMapOne('user.role', Role, 'role', 'role.userId = user.id')
      .leftJoinAndMapOne(
        'role.rolemenu',
        Rolemenu,
        'rolemenu',
        'rolemenu.id = role.roleId',
      )
      .where('user.name = :name', { name: 'jack' })
      .select(
        ` 
          user.username as username,
          role.roleId as roleId,
          rolemenu.roleName as roleName
        `,
      )
      .getRawMany();
  }
}

// 输出 [{"username":"jack123","roleId":"1","roleName":"admin"}]

但是我相信有些小伙伴感觉,这写起来也太费劲了,确实如此,你也可以直接写sql语句,直接替换成query加上sql就好了。

return user.query(
      `SELECT a.username, b.roleId, c.rolename 
                        FROM bl_user a
                      LEFT JOIN bl_role b ON a.id = b.userId 
                      LEFT JOIN bl_rolemenu c ON c.id = b.roleId
                      WHERE a.name = ? `,
      ['jack'],
    );
// 遵循mysql语法

TypeORM官方文档中,实体关系实际上是通过mysql的外键实现的,多表关联查询时,必须先在entity实体代码上添加关系,再使用leftJoinAndSelect查询,但在实际开发中,外键因为有诸多限制不被推荐使用,大部分的都是无关系的表连接。经过反复的摸索,找到了更好的一种方法:如果要加载并映射单个实体,请使用leftJoinAndMapOne。 如果要加载和映射多个实体,请使用leftJoinAndMapMany

所以下面的表设计基本上都没有建立外键实体关系,直接在查询的时候建立关系就好,分享核心的会员和借阅部分的表设计

会员卡:一个会员同时只有一张卡,如果续卡,就在这个表里添加一条数据,并在原有的有效期截止日上再添加相应的时间,同时改变之前卡的状态。因为我们逻辑是年卡,借书直接跟会员关联,不需要去扣减会员卡的次数,因此可以简化设计。

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('membercard')
export class MemberCard {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ nullable: true })
  userId: number;

  @Column()
  cardtype: number;

  @Column({ nullable: true, type: 'decimal', scale: 2 })
  money: number;

  @Column({
    type: 'datetime',
  })
  createtime: Date;

  @Column({
    type: 'datetime',
  })
  overtime: Date;

  @Column()
  status: number;
}

图书库存: 主要是用来记录书的总数和当前在库数量,这样不用每次去从记录表里计算汇总,只需要在入库,借出和归还时修改字段值就好。Ps. 请注意,这里因为并发访问量不是很多,一般库存设计可以使用Redis缓存或者分表进行优化。

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('stock')
export class Stock {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  bookId: number;

  @Column({ comment: '总库存' })
  total: number;

  @Column({ comment: '当前库存' })
  current: number;
}

借阅记录表: 入库,借出,归还,损耗的历史全部直接记录在这个表里

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('lendrecord')
export class LendRecord {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  bookId: number;

  @Column()
  memberId: number;

  @Column({ nullable: true, length: 20, comment: '标记入库/借出/归还/损耗' })
  type: string;

  @Column({
    type: 'datetime',
    comment: '操作时间',
  })
  actiontime: Date;

  @Column({ comment: '操作员工编号' })
  actionUserId: number;

  @Column()
  status: number;

  @Column({ nullable: true, comment: '操作备注' })
  remark: string;
}

}


-【项目结构调整】

根据功能我们把项目结构弄成3个大块,登录,会员,借阅,直接使用nest cli的命令就可以了。 最后生成的目录如下:

微信截图_20220812101931.png

现在启动这个项目,数据库也会自动去创建对应的表和字段了,下一篇实现登录验证模块的功能。

这部分完整代码可以看我的github地址:点这里,点这里,点这里