<h1><center>一卡通管理系统说明</center></h1>

# 1 系统概述

## 1.1 系统简介

本系统是一个一卡通管理系统，该系统面向学校应用、基于 RFID 技术的构建。该系统基于 ISO/IEC15693 协议的 RFID 芯片（ NXP ICS20 或 SL2S2002 ）进行卡内数据读写，并实现与数据库交互记录的功能。

## 1.2 系统组成

### 1.2.1 系统架构

 ![architecture](image\architecture.svg)

本系统由一卡通管理系统软件、数据库以及读卡器硬件组成。

其中：

- 一卡通管理系统软件
  - 为用户提供操作界面，实现读写数据库和硬件的逻辑；
  - 运行在 PC 机中，调用数据库连接插件与数据库通过网络交互，调用硬件驱动接口与读卡器进行交互；
- 数据库软件
  - 存储一卡通管理系统的数据；
  - 运行在服务器中，通过网络与 PC 机连接；
- 读卡器硬件
  - 读写卡片；
  - 通过 USB 串口与 PC 机连接。

使用软件前，用户需要将 PC 机连接网络，并将读卡器连接到 PC 机的 USB 口上。

### 1.2.2 系统功能

本系统的功能包括：

- 设置：连接和读写数据库、HF15693硬件；
- 开卡：创建新用户、挂失重开卡、挂失移资新卡；
- 挂失卡；
- 充值：远程充值、线下充值；
- 消费；
- 查询：查询卡内记录及数据库内记录。

## 1.3 运行环境及开发工具

本软件运行在 Windows 操作系统中。

本软件基于 Qt 6.7.2，使用 Qt Creator 14.0.0 开发，使用了 MySQL 8.0.37 作为数据库软件。

本项目使用 Git 作为开发版本管理工具。

在开发完毕后，使用 Inno Setup 软件将程序及其动态链接库文件打包为安装程序。



# 2 功能设计

## 2.1 设置功能

### 2.1.1 设置读卡器连接

**内容**

该功能用于连接读卡器硬件。用户在输入框内输入连接的 COM 口号，并点击“连接”按钮连接。

- 连接成功后，软件窗口下方任务栏的读卡器连接状态勾选框显示勾选，同时显示连接的串口号；

- 若连接不成功，软件弹出提示框，提示用户该串口上未识别到读卡器，同时软件窗口下方任务栏勾选框取消勾选，显示“当前无连接”。

**流程**

首先获取 COM 口输入框组件的 COM 口号，调用读卡器 API 进行连接，得到连接结果。如果连接不成功，弹出提示框。最后根据连接结果更新状态栏信息。

![setting-setReader](image\setting-setReader.svg)

### 2.1.2 设置数据库连接

**内容**

该功能用于连接数据库，以及设备名认证。

> **设备名**用于在交易记录中记录操作设备的名称。
>
> 对于每个设备名，数据库中设置了其操作权限：
>
> - 可充值：使用该设备名的设备拥有开卡、充值和消费权限（在管理员 PC 中使用）；
> - 仅可消费：使用该设备名的设备仅有消费权限（在食堂等消费场所的终端机中使用）。

用户首先要填写数据库的 IP 地址和端口，输入数据库的密码和要使用的设备名，点击连接按钮。

- 如果数据库连接不成功，软件弹出提示框，提示用户数据库连接失败，同时软件窗口下方任务栏的数据库连接状态勾选框取消勾选，显示“数据库未连接”、“未指定设备名”；

- 如果数据库连接成功，软件窗口下方任务栏的数据库连接状态勾选框显示勾选，同时显示连接的 IP 地址和端口；

连接成功后，将会进行设备名认证。

- 若设备名验证成功，软件窗口下方任务栏显示设备名及其操作权限；

- 若验证失败，软件弹出提示框，提示用户该设备名无效，同时软件窗口下方任务栏显示“未指定设备名”。

**流程**

从输入组件获取 IP 地址、端口、密码，连接数据库并更新任务栏显示信息。如果连接不成功，弹出提示框；如果连接成功，继续进行设备名认证。

查询数据库进行设备名认证，按认证结果更新任务栏显示信息。如果认证不成功，还要弹出提示框。

![setting-setDatabase](image\setting-setDatabase.svg)

以下功能中，除了仅需输学/工号的功能仅需连接数据库外，其他所有功能都需同时连接读卡器和数据库。

## 2.2 开卡功能

### 2.2.1 新用户开卡

**内容**

本功能的使用条件是：用户使用了未启用卡和新学/工号。

> 本系统采用卡和用户一对一绑定的机制，即：一个学/工号在任一时刻只能绑定和使用 1 张卡。

> **卡**有三个状态：
>
> - 未启用：该卡未绑定任何用户，仅可用于开卡，不可用于查询、充值或消费。未启用的卡进行开卡操作后将绑定一个用户，进入“启用中”状态；
> - 启用中：该卡绑定了一个用户，可以用于查询、充值和消费，不可用于开卡。如果该卡绑定的用户进行了挂失操作，该卡进入“已被挂失”状态；
> - 已被挂失：该卡绑定了一个用户但在数据库中被标记为挂失，仅可用于查询和重开，不可用于充值或消费。如果绑定该卡的用户使用了另一卡号进行开卡，本卡在数据库中的余额和所有记录将会被移动到新卡，本卡在数据库中的数据被删除，再次被读取时将处于“未启用”状态。
>
> 实体卡内存储只存最多 6 条交易编号，其他信息全部存在数据库中。

>**用户**以学/工号唯一标识，拥有属性“姓名”和“密码”。
>
>- 学/工号支持4到8位数字
>- 密码在创建用户时要求输入并二次确认。用于查询用户的所有交易记录和出现大额交易时验证用户身份。

用户需要在有开卡（充值）权限的设备上操作。

用户点击“查询”按钮扫描读卡器上的卡片，输入学/工号，然后点击“开卡”。如果用户没有点击”查询“按钮扫描，点击”开卡“按钮时会弹出提示框。

如果卡号和学/工号满足本功能条件，则在弹出的窗口中输入姓名、密码和二次确认密码。信息无误后，用户信息和卡信息会被写入数据库，卡片会被初始化，开卡成功。此时卡内余额为零。

**流程**

当”开卡“按钮被点击，获取界面上选中的卡号和输入的学/工号。如果没有发现选中的卡号（用户没有点击”查询“按钮），则弹出提示框。

在数据库中查询卡号：

- 如果查询不到卡片，则在数据库中插入该卡片信息，设置为未启用状态并继续；
- 如果查询到卡片处于未启用状态，继续；
- 否则提示用户卡片已被启用并退出开卡功能，或进入挂失重开功能。

在数据库中查询学/工号：

- 如果查询不到用户信息（未注册），弹出信息输入框获取数据，并检查二次确认密码。将用户信息写入数据库并继续；
- 如果查询到用户，则在卡表中查询用户：
  - 如果查询不到数据，则将刚才插入的卡片记录绑定上用户；
  - 如果查询到数据，则视卡的状态进入挂失移资功能或提示用户已经绑定了卡，不可再开卡，并退出开卡功能。

流程图见 *2.2.3* 节。

### 2.2.2 重开卡

**内容**

本功能的使用条件是：卡号处于挂失状态。

用户需要在有开卡（充值）权限的设备上操作。

用户点击“查询”按钮扫描读卡器上的卡片，输入学/工号（尽管程序没有用到），然后点击“开卡”。如果用户没有点击”查询“按钮扫描，点击”开卡“按钮时会弹出提示框。

如果卡号满足本功能条件，则在弹出的窗口中输入密码验证用户身份。信息无误后，卡会被重新置为”启用中“状态，此时卡内余额为挂失前的余额。

**流程**

当”开卡“按钮被点击，获取界面上选中的卡号和输入的学/工号。如果没有发现选中的卡号（用户没有点击”查询“按钮），则弹出提示框。

在数据库中查询卡号：

- 如果已被挂失，查询数据库中绑定的用户学/工号和姓名。弹出输入窗口，显示绑定的用户姓名，询问用户是否输入密码并重新开卡：
  - 如果用户选择取消则退出；
  - 如果用户输入密码并点击确定，则验证用户：
    - 验证成功则修改数据库，重新启用该卡；
    - 验证失败则弹出提示框并退出。
- 如果是其他状态，则对应进入挂失卡移资或新用户开卡功能。

流程图见 *2.2.3* 节。

### 2.2.3 挂失卡移资

**内容**

本功能的使用条件是：卡号处于未启用状态，且用户学/工号绑定的卡处于挂失状态。

用户需要在有开卡（充值）权限的设备上操作。

用户点击“查询”按钮扫描读卡器上的卡片，输入学/工号，然后点击“开卡”。如果用户没有点击”查询“按钮扫描，点击”开卡“按钮时会弹出提示框。

如果卡号满足本功能条件，则在弹出的窗口中输入密码验证用户身份。信息无误后，用户挂失的旧卡的记录将会被移植给新卡，此时新卡处于”启用中“状态，卡内余额为旧卡挂失前的余额。旧卡记录会被删除。

**流程**

当”开卡“按钮被点击，获取界面上选中的卡号和输入的学/工号。如果没有发现选中的卡号（用户没有点击”查询“按钮），则弹出提示框。

在数据库中查询卡号：

- 如果查询不到卡片，则在数据库中插入该卡片信息，设置为未启用状态并继续；
- 如果查询到卡片处于未启用状态，继续；
- 否则提示用户卡片已被启用并退出开卡功能，或进入挂失重开功能。

在数据库中查询学/工号：

- 如果查询不到用户信息（未注册），则进入新用户开卡功能；

- 如果查询到用户，则继续在卡表中查询用户：
  - 如果查询到用户，且用户绑定的卡处于挂失状态，则弹出输入框询问用户是否输入密码并重开新卡并移资。用户取消或验证失败则提示或退出；验证成功则修改数据库，将旧卡信息移植到新卡，删除旧卡信息。
  - 如果查询不到用户则进入新用户开卡功能；如果查询到用户但用户绑定的卡处于启用状态则弹出提示框，退出。

整个开卡功能的流程图如下：

![newCard](image\newCard.svg)

其中绿色部分对应新用户开卡功能，橙色部分对应重开卡部分，蓝色部分对应挂失卡移资部分。

## 2.3 充值功能

### 2.3.1 远程充值

**内容**

本功能用于远程为卡余额充值。

> **卡余额**为小数点后两位精度的小数。金额不得超过 9999.99 元，不得低于 0.00 元。

本功能使用学/工号为卡充值，无需使用实体卡。

用户需要在有充值权限的设备上操作。首先输入金额（限制大于 0.00 元且小于 9999.99 元），然后输入学/工号。如果学/工号对应的卡处于”已启用“状态，且充值金额加上原金额不超过余额限制 9999.99 元，则充值成功。否则弹窗提示。

**流程**

首先检查设备是否具有充值权限。如果没有，弹出提示窗口。

获取金额和学/工号，检查金额是否超出限制，若超出限制弹出提示窗口并退出。

在数据库中查询与该学/工号绑定的卡。如果未查询到记录或卡的状态不是”启用中“，弹出提示窗口并退出。

检查查询到卡的余额与金额之和是否超过余额限制，若超出限制弹出提示窗口并退出。

生成交易号，将交易记录插入数据库，将交易号写入卡中。

> **交易号**为 30 位十六进制数：
>
> - 第 0 - 3 位：随机数；
> - 第 4 位：0 或 1，1 表示该交易类型为充值，0 表示该交易类型为消费；
> - 第 5 - 15 位：学/工号，将学/工号（十进制）的每一位数字依次填入（不转换为十六进制）；
> - 第 16 - 29 位：时间，从高到低分别是 yyyyMMddhhmmss ；

![deposit-useId](image\deposit-useId.svg)

### 2.3.2 线下充值

**内容**

本功能用于使用卡片进行余额充值。

用户需要在有充值权限的设备上操作。首先输入金额（限制大于 0.00 元且小于 9999.99 元），然后点击”查询“扫描卡号并选择充值卡号。如果卡处于”已启用“状态，且充值金额加上原金额不超过余额限制 9999.99 元，则充值成功。否则弹窗提示。

**流程**

首先检查设备是否具有充值权限。如果没有，弹出提示窗口。

获取金额和卡号，检查金额是否超出限制，若超出限制弹出提示窗口并退出。

在数据库中查询卡号。如果未查询到记录或卡的状态不是”启用中“，弹出提示窗口并退出。

检查查询到卡的余额与金额之和是否超过余额限制，若超出限制弹出提示窗口并退出。

生成交易号，将交易记录插入数据库，将交易号写入卡中。

![deposit-useCard](image\deposit-useCard.svg)

## 2.4 消费功能

**内容**

本功能用于使用卡片进行消费。

首先输入金额（限制不超过 300.00 元），然后点击”查询“扫描卡号并选择消费卡号。

- 如果卡处于”已启用“状态：
  - 消费金额不超过不超过余额：
    - 消费金额不超过 50.00 元，则消费成功；
    - 消费金额超过 50.00 元且小于 300.00 元，需要验证用户身份。验证通过后消费成功；
    - 消费金额超过限制 300.00 元，消费失败；
  - 消费金额超出余额，消费失败；
- 卡片处于异常状态，消费失败。

**流程**

获取金额和卡号，在数据库中查询卡号。如果未查询到记录或卡的状态不是”启用中“，弹出提示窗口并退出。

检查金额是否超出余额、超出单次消费限制 300.00 元，若超出限制弹出提示窗口并退出。

检查金额是否超出 50.00 元，如果超出需要弹出输入窗口获取密码验证用户身份。

生成交易号，将交易记录插入数据库，将交易号写入卡中。

![consume](image\consume.svg)

## 2.5 查询功能

### 2.5.1 查询卡内记录

**内容**

本功能用于查询卡内交易记录。

用户首先点击”读卡“并选择卡号，然后点击”查询卡内记录“按钮。

如果卡片处于未启用状态，则会弹出提示窗口。

如果卡片处于已启用或已被挂失状态，则余额框内显示余额，卡状态框内会显示卡状态，表格组件内显示卡内存储的交易编号（最多 6 条）对应的交易记录。

**流程**

程序获取卡号，查询数据库查看卡片状态。如果卡片未启用，弹出提示窗口并退出；

如果卡片已启用或已被挂失，在组件中显示余额、卡状态。

调用读卡器 API 读取卡内的交易编号，对每一条交易编号，查询数据库中的交易记录，并显示在表格组件中。

![query-inCard](image\query-inCard.svg)

### 2.5.2 查询所有记录

**内容**

本功能用于查询用户所有交易记录。

用户可以通过两种方式触发此功能：

- 点击”读卡“并选择卡号，然后点击”查询所有记录“按钮；
- 输入学/工号，然后点击”查询学/工号记录“按钮；

如果卡片处于未启用状态，则会弹出提示窗口。

如果卡片处于已启用或已被挂失状态，则余额框内显示余额，卡状态框内会显示卡状态，表格组件内显示该用户的所有交易记录。

**流程**

程序获取卡号或用户学/工号（通过学/工号查到卡号），查询数据库查看卡片状态。如果卡片未启用，弹出提示窗口并退出；

如果卡片已启用或已被挂失，在组件中显示余额、卡状态。

查询该用户在数据库内的所有交易记录，并显示在表格组件中。

![query-all](image\query-all.svg)

## 2.6 挂失功能

**内容**

本功能用于挂失用户的卡。

用户输入学/工号，然后点击”挂失“按钮；

如果学/工号未绑定卡或绑定的卡已经被挂失，则会弹出提示窗口并退出。

将该用户的卡设置为已被挂失状态。

**流程**

程序获取用户学/工号，查询数据库查看卡片状态。

如果学/工号未绑定卡或绑定的卡已经被挂失，弹出提示窗口并退出。

修改数据库，将学/工号绑定的卡设置为挂失状态。

![reportLoss](image\reportLoss.svg)

## 2.7 退出功能

**内容**

本功能用于退出软件。

**流程**

程序关闭读卡器连接和数据库连接，最后关闭图形化界面后退出。

![quitApp](image\quitApp.svg)



# 3 数据设计

## 3.1 磁盘文件设计

本系统使用数据库保存数据。

本系统涉及的实体有：

- 用户：以学/工号作为唯一标识符，具有属性姓名和学号；
- 卡：以卡号为唯一标识符，具有属性状态（未启用、已启用、已被挂失）和余额；
- 交易记录：以交易记录号为唯一标识符，具有属性交易时间、交易类型（充值、消费）、交易金额（充值为正数，消费为负数）、原金额、交易后余额；
- 设备：以设备编号作为唯一标识符，具有属性名称和充值权限。

实体-关系图如下：

![ER](image\ER.svg)

因此在数据库中建立表：

- 用户（<u>学/工号</u>，姓名，密码）
  - 姓名、密码不能为空
- 卡片（<u>卡号</u>，状态，余额，用户的学/工号）
  - 卡状态、余额不能为空
  - 余额不能小于 0 ；状态必须为 -1 或 0 或 1 （分别表示已被挂失、未启用和启用中）
  - 用户的学/工号为外键，引用用户表的学/工号字段
- 交易记录（<u>交易记录号</u>，卡号，交易时间、交易类型、交易金额、原金额、交易后余额、设备编号）
  - 所有属性都不能为空
  - 卡号为外键，引用卡片表的卡号字段；设备编号为外键，引用设备表的编号字段
  - 交易金额不可小于 -300.00 ；原金额加上交易金额必须等于交易后金额；原金额和交易后金额都必须大于等于0 ；交易类型必须为 0 或 1 （分别表示消费和充值）
- 设备（<u>设备编号</u>，名称，充值权限）
  - 所有属性不能为空
  - 名称必须互不相同
  - 充值权限必须为 0 或 1 （分别表示仅可消费、可充值）

数据库初始化脚本见 [initDb.sql](..\database\initDb.sql) 。

本软件使用数据库固定使用 `cardManageSystem` 数据库和 `cardManageSystem` 用户。

## 3.2 程序内数据设计

### 3.2.1 全局变量定义及使用方法

由于本软件是基于 QT 的一个 MainWindow 组件类对象搭建，各个子界面都是 MainWindow 的子组件，因此所有的全局变量都被设置为 MainWindow 对象的属性，包括：

- reader：自定义 Reader 类（定义在 [readerAPI.h](..\readerAPI.h) 中）对象：
  - 变量： COM 口号和卡内最大的记录条数限制
  - 方法：
    - `void setComNumber(int comNumber)` 设置 COM 口号
    - `int getComNumber()` 获取当前COM口号
    - `bool is_connected()` 判断读卡器是否已连接
    - `bool connect()` 连接读卡器
    - `void disconnect()` 关闭连接
    - `QStringList inventory(int maxViccNum)` 获取读卡器中卡片的UID列表
    - `bool insertRecord(QString record, QString cardId)` 插入一条交易记录
    - `QStringList readAllRecords(QString cardId, bool &ok)` 获取全部记录
    - `bool initCard(QString cardId)`卡初始化
- db：自定义 Database 类（定义在 [databaseAPI.h](..\databaseAPI.h) 中）指针：
  - 变量：QT 的数据库对象、连接状态、固定的数据库名字符串和用户名字符串
  - 方法：
    - `Database(QSqlDatabase database)` 使用QSqlDatabase类初始化对象
    - `Database(QSqlDatabase database, QString hostName, int port, QString password)` 使用QSqlDatabase类、IP、端口、密码初始化对象
    - `void setHostName(QString hostName)` 设置hostName属性
    - `void setPort(int port)` 设置port属性
    - `void setPassword(QString password)` 设置password属性
    - `QSqlDatabase getDatabase()` 获取数据库对象属性
    - `int getPort()` 获取port属性
    - `QString getHostName()` 获取hostName属性
    - `bool is_connected()` 数据库是否已经连接
    - `bool open()` 连接数据库
- device：自定义 Device 类（定义在 [deviceAPI.h](..\deviceAPI.h) 中）对象：
  - 变量：设备名认证状态、设备充值权限、设备名称和设备编号
  - 方法：
    - `void setDevice(QString name, Database *db)` 设置并认证设备
    - `QString getName()` 获取设备名
    - `QString getNameAndDepositAllowed()` 获取设备名及其充值权限
    - `int getId()` 获取设备ID
    - `bool is_verified()` 设备是否已经认证
    - `bool is_depositAllowed()` 设备是否可充值
- 其他所有子组件的指针，通过 `ui->{组件名}` 的方法调用

### 3.2.2 交易记录定义

本系统中，交易记录主要存储在数据库中。交易记录作为变量出现在软件中的场景，是查询数据库得到某个用户的交易记录，显示在表格组件中。因此交易记录被定义为 QStringList 类对象（即 QString 类列表）。

使用 QSqlQuery 类方法查询到交易记录后，交易记录使用 QSqlQuery 类对象 query 的方法 value() 获取各个字段的值。获取后，将其转换为 QStringList 类对象，即可调用表格组件方法显示。该功能在 [queryPage.cpp](..\queryPage.cpp) 的 MainWindow::transactionRecord2QStringList() 方法中实现。



## 3.3 卡片内存储空间设计

卡片内存储 3 类数据：

|            内容            |                      长度                      |                  位置                  |
| :------------------------: | :--------------------------------------------: | :------------------------------------: |
| 卡内目前存储的交易记录数量 |                     4 bits                     |       block0 的 byte0 的 bit0-3        |
|   最近一条交易记录的下标   |                     4 bits                     |       block0 的 byte0 的 bit4-7        |
|          交易记录          | 每条记录 4 blocks，最多存 6 条记录占 24 blocks | block1-24，每连续 4 个block 为一条记录 |

每条交易记录只存储交易记录号（见 *2.3.1* 节）。

初始时，卡内目前存储的交易记录数量为 0 ，最近一条交易记录的下标为 5 。

写入记录时，程序首先读取最近一条交易记录的下标，然后将其加 1 模 6，在新下标的 blocks 中写入交易记录号，即循环写入，超过 6 条交易记录时，最新的交易记录会覆盖最早的交易记录。例如，最近一条交易记录的下标为 5 ，那么写入新记录时，将会到下标 (5 + 1) % 6 = 0 中写入，实际写入的 blocks 为 1 + 4 * 0 = 1 到 1 + 4 * (0 + 1) - 1 = 4 。

读取记录时，同样读取最近一条交易记录的下标，然后读取卡内目前存储的交易记录数量。接着循环读取，将按照读出的交易记录数量来读取交易记录。例如，最近一条交易记录的下标为 5 ，卡内目前存储的交易记录数量为 6 ，那么读取所有记录时，将会到最早的下标 (5 - 6 + 1) % 6 = 0 中开始读取，依次读取 blocks 为 1-4，5-8，9-12，13-16，17-20，21-24 。



# 4 软件功能概要

本系统的功能包括：

- 设置：连接和读写数据库、HF15693硬件；
- 开卡：创建新用户、挂失重开卡、挂失移资新卡；
- 挂失卡；
- 充值：远程充值、线下充值；
- 消费；
- 查询：查询卡内记录及数据库内记录。



# 5 收获与建议

**收获**

- 在项目开发过程中，我将课堂上学到的物联网理论知识应用到实际项目中，深刻理解了RFID技术、数据库交互和Qt开发框架的原理和应用。
- 项目开发中遇到了诸多问题，如数据库连接、数据读写和硬件通信等。通过查阅资料、调试代码，我逐步解决了这些问题，提升了自己的问题解决能力。
- 通过项目，我熟练掌握了Qt框架的使用，增强了C++编程能力。同时，对数据库设计和SQL查询也有了更深入的理解和实践。
- 在项目开发过程中，我学会了如何有效地管理项目进度，包括任务分解、时间安排和进度跟踪等。这些经验将对我未来的项目开发和管理大有裨益。
- 我学会了遵循代码规范，编写清晰、易维护的代码。同时，我也注重项目文档的编写，为项目后续的维护和迭代提供了有力支持。

**建议**

- 一开始我学习了 MFC 开发，但了解 MFC 后，我发现这个开发框架已经过时，存在许多缺点，不满足现代软件开发的需求，因此我选择了使用 Qt 开发。建议以后该课程可以更新学习的开发工具，比如教学使用 Qt 、Electron 等最新开发框架技术。