Compare commits

...

10 Commits

46 changed files with 1207 additions and 149 deletions

1
.gitignore vendored

@ -72,3 +72,4 @@ CMakeLists.txt.user*
*.dll
*.exe
*.drawio

@ -51,8 +51,10 @@ DISTFILES += \
HF15693.dll \
HF15693.lib
# LIBS += -L$$PWD/. -LHF15693
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../release/ -lHF15693
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../debug/ -lHF15693
else:unix: LIBS += -L$$PWD/../ -lHF15693
LIBS += -L. -lHF15693
INCLUDEPATH += $$PWD/.
DEPENDPATH += $$PWD/.

163
README.md Normal file

@ -0,0 +1,163 @@
<h1><center>一卡通管理系统软件使用说明书</center></h1>
# 0 说明
本软件在 Windows 环境中运行,需要通过 USB 串口连接读卡器,对基于 ISO/IEC15693 协议的 RFID 芯片( NXP ICS20 或 SL2S2002 )进行卡内数据读写。
本软件使用数据库记录数据。一卡通系统管理者必须配置数据库,并在数据库中插入设备设置,具体方法见 [数据库配置方法](./database/README.md) 。
# 1 安装方法
点击[下载软件安装包](https://github.com/typingbugs/CardManageSystem/releases/download/v0.1.0/CardManageSystem-v0.1.0-setup-amd64.exe),双击安装包可执行文件,在弹出的请求管理员权限点击确认。
选择安装路径,点击下一步。
<img src="doc\image\installation1.png" alt="installation1" style="zoom:50%;" />
勾选创建桌面快捷方式,点击下一步。
<img src="doc\image\installation2.png" alt="installation2" style="zoom:50%;" />
等待应用安装完成。
<img src="doc\image\installation3.png" alt="installation3" style="zoom:50%;" />
点击完成。
<img src="doc\image\installation4.png" alt="installation4" style="zoom:50%;" />
自动弹出软件界面。
<img src="doc\image\installation5.png" alt="installation5" style="zoom:50%;" />
# 2 使用方法
软件界面左侧为工具栏,点击工具栏中的各个按钮可以切换至不同页面。
软件界面下方为状态栏,显示当前读卡器设置、数据库设置和设备设置。
## 2.1 配置读卡器和数据库连接
进行开卡等操作前,请按以下步骤配置读卡器和数据库连接。
### Step 1 连接读卡器
点击左侧工具栏的 `设置` 按钮转到设置页面。
在上方 `连接读卡器` 部分输入端口号(可以点击输入框的上下键增加或减少数字)。点击输入框下方的 `连接` 按钮,即可连接读卡器。
若连接成功,窗口下方状态栏左侧的勾选框将显示勾选,同时显示成功连接读卡器的 COM 口号。
<img src="doc\image\setting-step1.png" alt="setting-step1" style="zoom:50%;" />
### Step 2 连接数据库并认证设备名
点击左侧工具栏的 `设置` 按钮转到设置页面。
在下方 `连接数据库` 部分输入数据库的 IP 地址、数据库服务端口、数据库用户密码和设备名,点击下方的 `连接` 按钮,即可连接数据库并认证设备。
若连接成功,窗口下方状态栏右侧的勾选框将显示勾选,同时显示服务器的 IP 地址和服务端口;若设备认证成功,状态栏将显示设备名及其操作权限。
<img src="doc\image\setting-step2.png" alt="setting-step2" style="zoom:50%;" />
## 2.2 开卡
点击左侧工具栏的 `开卡` 按钮转到开卡页面。
> 请注意开卡需要设备有充值权限。
挂失卡复通和挂失卡移资功能也在这里。
首先点击 `查询` 按钮,卡号栏将显示读卡器扫描到的卡号。
**新用户开卡**
如果需要为新用户开卡,在卡号栏选择一张未启用的卡,在 `学/工号` 一栏填入新用户的学/工号,点击开卡。新用户开卡需要填写姓名、密码和二次确认密码进行注册,注册成功后开卡完成。
**挂失卡复通**
如果需要将已挂失的卡复通,在卡号栏选择该卡,点击开卡,在弹出的验证窗口输入用户密码即可将挂失卡复通。
**挂失卡移资**
如果需要将已挂失的卡移资到新卡,在卡号栏选择一张未启用的卡,在 `学/工号` 一栏填入挂失卡绑定的学/工号,点击开卡。在弹出的验证窗口输入用户密码即可将挂失卡移资到新卡。
<img src="doc\image\newCard.png" alt="newCard" style="zoom:50%;" />
## 2.3 充值
点击左侧工具栏的 `充值` 按钮转到充值页面。
> 请注意开卡需要设备有充值权限。
首先输入充值金额,卡内的余额不得超过 9999.99 元。
**远程充值**
在左侧学/工号栏中输入要充值卡绑定的学/工号,点击 `按学/工号充值` 按钮,可以为卡余额远程充值。
**线下充值**
点击右侧卡号栏右侧的 `查询` 按钮,在卡号栏中选择要充值的卡号,点击 `按卡号充值` 按钮,可以为该卡充值。
> 远程充值的交易记录不会存到卡内存储中。
<img src="doc\image\deposit.png" alt="deposit" style="zoom:50%;" />
## 2.4 消费
点击左侧工具栏的 `消费` 按钮转到消费页面。
> 消费只允许线下消费。
首先输入消费金额,不得超过 300.00 元。如果消费金额超过 50.00 元,消费时需要验证用户身份。
点击卡号栏右侧的 `查询` 按钮,选择消费卡号。点击 `消费` 按钮。
如果消费金额超过 50.00 元,需要在弹出的验证窗口中输入用户密码。
<img src="doc\image\consume.png" alt="consume" style="zoom:50%;" />
## 2.5 查询
点击左侧工具栏的 `查询` 按钮转到查询页面。
查询页面右上方显示卡的余额和状态,下方表格中显示该卡的交易记录,包括交易时间、类型(充值或消费)、交易金额、交易后余额、交易设备、交易号。
**按学工号远程查询**
在上方学/工号框中输入学工号,点击 `查询学/工号记录` 按钮,在弹出的验证窗口中输入用户密码,下方表格即可显示用户的所有交易记录,右上方显示卡余额和状态。
**线下查询卡内记录**
点击下方卡号栏右侧 `读卡` 按钮,在卡号栏中选择卡号,点击右侧 `查询卡内记录` ,下方表格即可显示卡内的所有交易记录,右上方显示卡余额和状态。
**线下查询卡绑定用户记录**
点击下方卡号栏右侧 `读卡` 按钮,在卡号栏中选择卡号,点击右侧 `查询所有记录` ,下方表格即可显示卡绑定用户的所有交易记录,右上方显示卡余额和状态。
> 未启用的卡不可被查询。
<img src="doc\image\query.png" alt="query" style="zoom: 50%;" />
## 2.6 挂失
点击左侧工具栏的 `挂失` 按钮转到挂失页面。
在学/工号框中输入学工号,点击 `挂失` 按钮,在弹出的验证窗口中输入用户密码即可挂失。用户的交易记录和卡记录还会保存在数据库中,直到用户将挂失卡移资到新卡。
> 未启用的卡不可被挂失;已挂失的卡不可被重复挂失。
<img src="doc\image\reportLoss.png" alt="reportLoss" style="zoom:50%;" />
## 2.7 退出
点击左侧工具栏的 `退出` 按钮转到退出页面。
点击`确定` 按钮即可退出应用程序。
<img src="doc\image\quitApp.png" alt="quitApp" style="zoom:50%;" />

@ -1,5 +1,6 @@
#include <cstdlib>
#include "VCDOurs.h"
#include "qlogging.h"
CVCDOurs::CVCDOurs(void)
{
@ -422,7 +423,7 @@ int CVCDOurs::writeBlocks( int nFirstBlock, int nBlockNum, uchar_t *pDat, uchar_
int k = 0;
for( int i = 0; i < nBlockNum; i++ )
{
k += writeBlock( nFirstBlock + i, pDat+i*8, pucUID );
k += writeBlock( nFirstBlock + i, pDat+i*4, pucUID );
}
return k;
}

@ -71,12 +71,12 @@ public:
virtual int readMultipleBlocks(
uchar_t *pucUID, // NULL表示select模式
uchar_t optional_flag, // optional bit = 1, 要求返回block的安全状态
uchar_t ucBlkno, // 开始block no
uchar_t blocknum, // 实际读的block是blocknum+1;
uchar_t ucBlkno, // 开始block no
uchar_t blocknum, // 实际读的block是blocknum+1;
uchar_t buf[], // 返回读取的数据
uchar_t aucSecurity[] // 如果optional_flag = 1, 返回该block的安全状态1表示locked.
)
{
{
t15bOption = optional_flag;
return readBlocks( ucBlkno, blocknum+1, buf, aucSecurity, pucUID );
}

@ -0,0 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="i18n">
<file alias="CardManageSystem_zh_CN.qm">D:/Courseware/Integrated_Practice_of_IoT_System/CardManageSystem/build/Desktop_Qt_6_7_2_MinGW_64_bit-Release/release/CardManageSystem_zh_CN.qm</file>
</qresource>
</RCC>

@ -172,6 +172,12 @@ void MainWindow::on_consumeButton_clicked()
return;
}
success = reader.insertRecord(recordId, cardId);
if (!success)
{
QMessageBox::warning(this, "提示", "消费完成。写卡失败。本交易不记录在卡上。");
}
QString consumeResultMessage = QString("消费成功:") + QString::number(deductValue) + QString("\n");
consumeResultMessage += QString("原余额:") + QString::number(originalBalance) + QString("\n");
consumeResultMessage += QString("消费后余额:") + QString::number(finalBalance) + QString("\n");
@ -263,8 +269,6 @@ bool MainWindow::deductCard(QString cardId, double deductValue, double &original
query.next();
finalBalance = query.value("@newBalance").toDouble();
/// @todo 写卡
return true;
}

5
database/README.md Normal file

@ -0,0 +1,5 @@
请在 MySQL 数据库中执行 [此初始化脚本](./initDb.sql) 。
请按照实际情况在设备表中插入设备信息。
软件访问数据库软件时,固定使用用户 `CardManageSystem` 和数据库 `CardManageSystem` ,如需更改,请对应修改源码 [databaseAPI.cpp](../databaseAPI.cpp) 。

154
database/initDb.sql Normal file

@ -0,0 +1,154 @@
DROP DATABASE IF EXISTS cardManageSystem;
CREATE DATABASE cardManageSystem;
ALTER DATABASE cardManageSystem CHARACTER SET utf8;
DROP USER IF EXISTS 'cardManageSystem'@'%';
CREATE USER 'cardManageSystem'@'%' IDENTIFIED BY 'RFID666';
USE cardManageSystem;
DROP PROCEDURE IF EXISTS sp_depositCard;
DROP PROCEDURE IF EXISTS sp_consumeCard;
CREATE TABLE device (
id INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) UNIQUE,
depositAllowed TINYINT NOT NULL,
CONSTRAINT chk_device_depositAllowed CHECK (depositAllowed IN (0, 1))
) AUTO_INCREMENT=1000;
CREATE TABLE `user` (
id INT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL
);
CREATE TABLE card (
id VARCHAR(16) PRIMARY KEY,
`status` TINYINT NOT NULL,
balance DECIMAL(6, 2) NOT NULL,
userId INT,
FOREIGN KEY (userId) REFERENCES `user`(id),
CONSTRAINT chk_card_status CHECK (`status` IN (-1, 0, 1)),
CONSTRAINT chk_card_balance CHECK (balance >= 0)
);
CREATE TABLE record (
id VARCHAR(255) PRIMARY KEY,
cardId VARCHAR(16) NOT NULL,
`time` DATETIME NOT NULL,
`type` TINYINT NOT NULL,
`value` DECIMAL(6, 2) NOT NULL,
originalBalance DECIMAL(6, 2) NOT NULL,
balance DECIMAL(6, 2) NOT NULL,
deviceId INT NOT NULL,
FOREIGN KEY (cardId) REFERENCES card(id),
FOREIGN KEY (deviceId) REFERENCES device(id),
CONSTRAINT chk_value CHECK (`value` >= -300),
CONSTRAINT chk_record CHECK (originalBalance + `value` = balance),
CONSTRAINT chk_record_originalBalance CHECK (originalBalance >= 0),
CONSTRAINT chk_record_balance CHECK (balance >= 0),
CONSTRAINT chk_record_type CHECK (`type` IN (0, 1))
);
DELIMITER //
CREATE PROCEDURE sp_depositCard(
IN p_cardId VARCHAR(16),
IN p_amount DECIMAL(6, 2),
IN p_recordId VARCHAR(255),
IN p_deviceId INT,
IN p_time DATETIME,
IN p_type TINYINT,
OUT p_newBalance DECIMAL(6, 2)
)
BEGIN
DECLARE v_originalBalance DECIMAL(6, 2);
SELECT balance INTO v_originalBalance
FROM card
WHERE id = p_cardId;
IF v_originalBalance IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Card not found';
END IF;
SET p_newBalance = v_originalBalance + p_amount;
UPDATE card
SET balance = p_newBalance
WHERE id = p_cardId;
INSERT INTO record (id, cardId, `time`, `type`, `value`, originalBalance, balance, deviceId)
VALUES (p_recordId, p_cardId, p_time, p_type, p_amount, v_originalBalance, p_newBalance, p_deviceId);
END //
CREATE PROCEDURE sp_consumeCard(
IN p_cardId VARCHAR(18),
IN p_amount DECIMAL(6, 2),
IN p_recordId VARCHAR(255),
IN p_deviceId INT,
IN p_time DATETIME,
IN p_type TINYINT,
OUT p_newBalance DECIMAL(6, 2)
)
BEGIN
DECLARE v_originalBalance DECIMAL(6, 2);
IF p_amount > 300 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid Amount';
END IF;
SELECT balance INTO v_originalBalance
FROM card
WHERE id = p_cardId;
IF v_originalBalance IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Card not found';
END IF;
SET p_newBalance = v_originalBalance - p_amount;
IF p_newBalance < 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient balance';
END IF;
UPDATE card
SET balance = p_newBalance
WHERE id = p_cardId;
INSERT INTO record (id, cardId, `time`, `type`, `value`, originalBalance, balance, deviceId)
VALUES (p_recordId, p_cardId, p_time, p_type, -p_amount, v_originalBalance, p_newBalance, p_deviceId);
END //
DELIMITER ;
CREATE VIEW record_view AS
SELECT time, type, value, balance, `name` AS device, record.id AS id, cardId
FROM record
JOIN device
ON deviceId = device.id
ORDER BY time DESC, type DESC;
INSERT INTO device (`name`, depositAllowed)
VALUES ('食堂1号机', 0), ('管理员1号机', 1);
GRANT ALL PRIVILEGES ON cardManageSystem.* TO 'cardManageSystem'@'%';
FLUSH PRIVILEGES;

@ -118,6 +118,12 @@ void MainWindow::on_depositByCardIdButton_clicked()
return;
}
success = reader.insertRecord(recordId, cardId);
if (!success)
{
QMessageBox::warning(this, "提示", "充值成功。写卡失败。本交易不记录在卡上。");
}
QString depositResultMessage = QString("充值成功:") + QString::number(topUpValue) + QString("\n");
depositResultMessage += QString("原余额:") + QString::number(originalBalance) + QString("\n");
depositResultMessage += QString("充值后余额:") + QString::number(finalBalance) + QString("\n");
@ -223,9 +229,10 @@ void MainWindow::on_depositByUserIdButton_clicked()
* @param finalBalance
* @param recordId
* @param info
* @return bool
* - true
* - false
* @return int
* - 0
* - 1
* - 2
* @details
*
*
@ -299,8 +306,6 @@ bool MainWindow::topUpCard(QString cardId, double topUpValue, double &originalBa
query.next();
finalBalance = query.value("@newBalance").toDouble();
/// @todo 写卡
return true;
}
@ -327,5 +332,5 @@ QString MainWindow::getRecordId(QDateTime currentTime, int userId, int recordTyp
QString typeStr = QString::number(recordType); // 第25位记录类型
QString randomHex = QString::number(QRandomGenerator::global()->bounded(0x10000), 16).rightJustified(4, '0'); // 第26-29位随机十六进制数
QString recordId = timeStr + userIdStr + typeStr + randomHex; // 共30位
return recordId;
return recordId.toUpper();
}

523
doc/README.md Normal file

@ -0,0 +1,523 @@
<h1><center>一卡通管理系统说明</center></h1>
# 1 系统概述
## 1.1 系统简介
本系统是一个一卡通管理系统,该系统面向学校应用、基于 RFID 技术的构建。该系统基于 ISO/IEC15693 协议的 RFID 芯片( NXP ICS20 或 SL2S2002 )进行卡内数据读写,并实现与数据库交互记录的功能。
## 1.2 系统组成
### 1.2.1 系统架构
<img src="image/architecture.svg" alt="architecture" style="zoom:50%;" />
本系统由一卡通管理系统软件、数据库以及读卡器硬件组成。
其中:
- 一卡通管理系统软件
- 为用户提供操作界面,实现读写数据库和硬件的逻辑;
- 运行在 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 进行连接,得到连接结果。如果连接不成功,弹出提示框。最后根据连接结果更新状态栏信息。
<img src="image/setting-setReader.svg" alt="setting-setReader" style="zoom:50%;" />
### 2.1.2 设置数据库连接
**内容**
该功能用于连接数据库,以及设备名认证。
> **设备名**用于在交易记录中记录操作设备的名称。
>
> 对于每个设备名,数据库中设置了其操作权限:
>
> - 可充值:使用该设备名的设备拥有开卡、充值和消费权限(在管理员 PC 中使用);
> - 仅可消费:使用该设备名的设备仅有消费权限(在食堂等消费场所的终端机中使用)。
用户首先要填写数据库的 IP 地址和端口,输入数据库的密码和要使用的设备名,点击连接按钮。
- 如果数据库连接不成功,软件弹出提示框,提示用户数据库连接失败,同时软件窗口下方任务栏的数据库连接状态勾选框取消勾选,显示“数据库未连接”、“未指定设备名”;
- 如果数据库连接成功,软件窗口下方任务栏的数据库连接状态勾选框显示勾选,同时显示连接的 IP 地址和端口;
连接成功后,将会进行设备名认证。
- 若设备名验证成功,软件窗口下方任务栏显示设备名及其操作权限;
- 若验证失败,软件弹出提示框,提示用户该设备名无效,同时软件窗口下方任务栏显示“未指定设备名”。
**流程**
从输入组件获取 IP 地址、端口、密码,连接数据库并更新任务栏显示信息。如果连接不成功,弹出提示框;如果连接成功,继续进行设备名认证。
查询数据库进行设备名认证,按认证结果更新任务栏显示信息。如果认证不成功,还要弹出提示框。
<img src="image/setting-setDatabase.svg" alt="setting-setDatabase" style="zoom:50%;" />
以下功能中,除了仅需输学/工号的功能仅需连接数据库外,其他所有功能都需同时连接读卡器和数据库。
## 2.2 开卡功能
### 2.2.1 新用户开卡
**内容**
本功能的使用条件是:用户使用了未启用卡和新学/工号。
> 本系统采用卡和用户一对一绑定的机制,即:一个学/工号在任一时刻只能绑定和使用 1 张卡。
> **卡**有三个状态:
>
> - 未启用:该卡未绑定任何用户,仅可用于开卡,不可用于查询、充值或消费。未启用的卡进行开卡操作后将绑定一个用户,进入“启用中”状态;
> - 启用中:该卡绑定了一个用户,可以用于查询、充值和消费,不可用于开卡。如果该卡绑定的用户进行了挂失操作,该卡进入“已被挂失”状态;
> - 已被挂失:该卡绑定了一个用户但在数据库中被标记为挂失,仅可用于查询和重开,不可用于充值或消费。如果绑定该卡的用户使用了另一卡号进行开卡,本卡在数据库中的余额和所有记录将会被移动到新卡,本卡在数据库中的数据被删除,再次被读取时将处于“未启用”状态。
>
> 实体卡内存储只存最多 6 条交易编号,其他信息全部存在数据库中。
>**用户**以学/工号唯一标识,拥有属性“姓名”和“密码”。
>
>- 学/工号支持4到8位数字
>- 密码在创建用户时要求输入并二次确认。用于查询用户的所有交易记录和出现大额交易时验证用户身份。
用户需要在有开卡(充值)权限的设备上操作。
用户点击“查询”按钮扫描读卡器上的卡片,输入学/工号,然后点击“开卡”。如果用户没有点击”查询“按钮扫描,点击”开卡“按钮时会弹出提示框。
如果卡号和学/工号满足本功能条件,则在弹出的窗口中输入姓名、密码和二次确认密码。信息无误后,用户信息和卡信息会被写入数据库,卡片会被初始化,开卡成功。此时卡内余额为零。
**流程**
当”开卡“按钮被点击,获取界面上选中的卡号和输入的学/工号。如果没有发现选中的卡号(用户没有点击”查询“按钮),则弹出提示框。
在数据库中查询卡号:
- 如果查询不到卡片,则在数据库中插入该卡片信息,设置为未启用状态并继续;
- 如果查询到卡片处于未启用状态,继续;
- 否则提示用户卡片已被启用并退出开卡功能,或进入挂失重开功能。
在数据库中查询学/工号:
- 如果查询不到用户信息(未注册),弹出信息输入框获取数据,并检查二次确认密码。将用户信息写入数据库并继续;
- 如果查询到用户,则在卡表中查询用户:
- 如果查询不到数据,则将刚才插入的卡片记录绑定上用户;
- 如果查询到数据,则视卡的状态进入挂失移资功能或提示用户已经绑定了卡,不可再开卡,并退出开卡功能。
流程图见 *2.2.3* 节。
### 2.2.2 重开卡
**内容**
本功能的使用条件是:卡号处于挂失状态。
用户需要在有开卡(充值)权限的设备上操作。
用户点击“查询”按钮扫描读卡器上的卡片,输入学/工号(尽管程序没有用到),然后点击“开卡”。如果用户没有点击”查询“按钮扫描,点击”开卡“按钮时会弹出提示框。
如果卡号满足本功能条件,则在弹出的窗口中输入密码验证用户身份。信息无误后,卡会被重新置为”启用中“状态,此时卡内余额为挂失前的余额。
**流程**
当”开卡“按钮被点击,获取界面上选中的卡号和输入的学/工号。如果没有发现选中的卡号(用户没有点击”查询“按钮),则弹出提示框。
在数据库中查询卡号:
- 如果已被挂失,查询数据库中绑定的用户学/工号和姓名。弹出输入窗口,显示绑定的用户姓名,询问用户是否输入密码并重新开卡:
- 如果用户选择取消则退出;
- 如果用户输入密码并点击确定,则验证用户:
- 验证成功则修改数据库,重新启用该卡;
- 验证失败则弹出提示框并退出。
- 如果是其他状态,则对应进入挂失卡移资或新用户开卡功能。
流程图见 *2.2.3* 节。
### 2.2.3 挂失卡移资
**内容**
本功能的使用条件是:卡号处于未启用状态,且用户学/工号绑定的卡处于挂失状态。
用户需要在有开卡(充值)权限的设备上操作。
用户点击“查询”按钮扫描读卡器上的卡片,输入学/工号,然后点击“开卡”。如果用户没有点击”查询“按钮扫描,点击”开卡“按钮时会弹出提示框。
如果卡号满足本功能条件,则在弹出的窗口中输入密码验证用户身份。信息无误后,用户挂失的旧卡的记录将会被移植给新卡,此时新卡处于”启用中“状态,卡内余额为旧卡挂失前的余额。旧卡记录会被删除。
**流程**
当”开卡“按钮被点击,获取界面上选中的卡号和输入的学/工号。如果没有发现选中的卡号(用户没有点击”查询“按钮),则弹出提示框。
在数据库中查询卡号:
- 如果查询不到卡片,则在数据库中插入该卡片信息,设置为未启用状态并继续;
- 如果查询到卡片处于未启用状态,继续;
- 否则提示用户卡片已被启用并退出开卡功能,或进入挂失重开功能。
在数据库中查询学/工号:
- 如果查询不到用户信息(未注册),则进入新用户开卡功能;
- 如果查询到用户,则继续在卡表中查询用户:
- 如果查询到用户,且用户绑定的卡处于挂失状态,则弹出输入框询问用户是否输入密码并重开新卡并移资。用户取消或验证失败则提示或退出;验证成功则修改数据库,将旧卡信息移植到新卡,删除旧卡信息。
- 如果查询不到用户则进入新用户开卡功能;如果查询到用户但用户绑定的卡处于启用状态则弹出提示框,退出。
整个开卡功能的流程图如下:
<img src="image/newCard.svg" alt="newCard" style="zoom:50%;" />
其中绿色部分对应新用户开卡功能,橙色部分对应重开卡部分,蓝色部分对应挂失卡移资部分。
## 2.3 充值功能
### 2.3.1 远程充值
**内容**
本功能用于远程为卡余额充值。
> **卡余额**为小数点后两位精度的小数。金额不得超过 9999.99 元,不得低于 0.00 元。
本功能使用学/工号为卡充值,无需使用实体卡。
用户需要在有充值权限的设备上操作。首先输入金额(限制大于 0.00 元且小于 9999.99 元),然后输入学/工号。如果学/工号对应的卡处于”已启用“状态,且充值金额加上原金额不超过余额限制 9999.99 元,则充值成功。否则弹窗提示。
**流程**
首先检查设备是否具有充值权限。如果没有,弹出提示窗口。
获取金额和学/工号,检查金额是否超出限制,若超出限制弹出提示窗口并退出。
在数据库中查询与该学/工号绑定的卡。如果未查询到记录或卡的状态不是”启用中“,弹出提示窗口并退出。
检查查询到卡的余额与金额之和是否超过余额限制,若超出限制弹出提示窗口并退出。
生成交易号,将交易记录插入数据库,将交易号写入卡中。
> **交易号**为 30 位十六进制数:
>
> - 第 0 - 3 位:随机数;
> - 第 4 位0 或 11 表示该交易类型为充值0 表示该交易类型为消费;
> - 第 5 - 15 位:学/工号,将学/工号(十进制)的每一位数字依次填入(不转换为十六进制);
> - 第 16 - 29 位:时间,从高到低分别是 yyyyMMddhhmmss
<img src="image/deposit-useId.svg" alt="deposit-useId" style="zoom:50%;" />
### 2.3.2 线下充值
**内容**
本功能用于使用卡片进行余额充值。
用户需要在有充值权限的设备上操作。首先输入金额(限制大于 0.00 元且小于 9999.99 元),然后点击”查询“扫描卡号并选择充值卡号。如果卡处于”已启用“状态,且充值金额加上原金额不超过余额限制 9999.99 元,则充值成功。否则弹窗提示。
**流程**
首先检查设备是否具有充值权限。如果没有,弹出提示窗口。
获取金额和卡号,检查金额是否超出限制,若超出限制弹出提示窗口并退出。
在数据库中查询卡号。如果未查询到记录或卡的状态不是”启用中“,弹出提示窗口并退出。
检查查询到卡的余额与金额之和是否超过余额限制,若超出限制弹出提示窗口并退出。
生成交易号,将交易记录插入数据库,将交易号写入卡中。
<img src="image/deposit-useCard.svg" alt="deposit-useCard" style="zoom:50%;" />
## 2.4 消费功能
**内容**
本功能用于使用卡片进行消费。
首先输入金额(限制不超过 300.00 元),然后点击”查询“扫描卡号并选择消费卡号。
- 如果卡处于”已启用“状态:
- 消费金额不超过不超过余额:
- 消费金额不超过 50.00 元,则消费成功;
- 消费金额超过 50.00 元且小于 300.00 元,需要验证用户身份。验证通过后消费成功;
- 消费金额超过限制 300.00 元,消费失败;
- 消费金额超出余额,消费失败;
- 卡片处于异常状态,消费失败。
**流程**
获取金额和卡号,在数据库中查询卡号。如果未查询到记录或卡的状态不是”启用中“,弹出提示窗口并退出。
检查金额是否超出余额、超出单次消费限制 300.00 元,若超出限制弹出提示窗口并退出。
检查金额是否超出 50.00 元,如果超出需要弹出输入窗口获取密码验证用户身份。
生成交易号,将交易记录插入数据库,将交易号写入卡中。
<img src="image/consume.svg" alt="consume" style="zoom:50%;" />
## 2.5 查询功能
### 2.5.1 查询卡内记录
**内容**
本功能用于查询卡内交易记录。
用户首先点击”读卡“并选择卡号,然后点击”查询卡内记录“按钮。
如果卡片处于未启用状态,则会弹出提示窗口。
如果卡片处于已启用或已被挂失状态,则余额框内显示余额,卡状态框内会显示卡状态,表格组件内显示卡内存储的交易编号(最多 6 条)对应的交易记录。
**流程**
程序获取卡号,查询数据库查看卡片状态。如果卡片未启用,弹出提示窗口并退出;
如果卡片已启用或已被挂失,在组件中显示余额、卡状态。
调用读卡器 API 读取卡内的交易编号,对每一条交易编号,查询数据库中的交易记录,并显示在表格组件中。
<img src="image/query-inCard.svg" alt="query-inCard" style="zoom:50%;" />
### 2.5.2 查询所有记录
**内容**
本功能用于查询用户所有交易记录。
用户可以通过两种方式触发此功能:
- 点击”读卡“并选择卡号,然后点击”查询所有记录“按钮;
- 输入学/工号,然后点击”查询学/工号记录“按钮;
如果卡片处于未启用状态,则会弹出提示窗口。
如果卡片处于已启用或已被挂失状态,则余额框内显示余额,卡状态框内会显示卡状态,表格组件内显示该用户的所有交易记录。
**流程**
程序获取卡号或用户学/工号(通过学/工号查到卡号),查询数据库查看卡片状态。如果卡片未启用,弹出提示窗口并退出;
如果卡片已启用或已被挂失,在组件中显示余额、卡状态。
查询该用户在数据库内的所有交易记录,并显示在表格组件中。
<img src="image/query-all.svg" alt="query-all" style="zoom:50%;" />
## 2.6 挂失功能
**内容**
本功能用于挂失用户的卡。
用户输入学/工号,然后点击”挂失“按钮;
如果学/工号未绑定卡或绑定的卡已经被挂失,则会弹出提示窗口并退出。
将该用户的卡设置为已被挂失状态。
**流程**
程序获取用户学/工号,查询数据库查看卡片状态。
如果学/工号未绑定卡或绑定的卡已经被挂失,弹出提示窗口并退出。
修改数据库,将学/工号绑定的卡设置为挂失状态。
<img src="image/reportLoss.svg" alt="reportLoss" style="zoom:50%;" />
## 2.7 退出功能
**内容**
本功能用于退出软件。
**流程**
程序关闭读卡器连接和数据库连接,最后关闭图形化界面后退出。
<img src="image/quitApp.svg" alt="quitApp" style="zoom:50%;" />
# 3 数据设计
## 3.1 磁盘文件设计
本系统使用数据库保存数据。
本系统涉及的实体有:
- 用户:以学/工号作为唯一标识符,具有属性姓名和学号;
- 卡:以卡号为唯一标识符,具有属性状态(未启用、已启用、已被挂失)和余额;
- 交易记录:以交易记录号为唯一标识符,具有属性交易时间、交易类型(充值、消费)、交易金额(充值为正数,消费为负数)、原金额、交易后余额;
- 设备:以设备编号作为唯一标识符,具有属性名称和充值权限。
实体-关系图如下:
<img src="image/ER.svg" alt="ER" style="zoom:50%;" />
因此在数据库中建立表:
- 用户(<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-45-89-1213-1617-2021-24 。
# 4 软件功能概要
本系统的功能包括:
- 设置连接和读写数据库、HF15693硬件
- 开卡:创建新用户、挂失重开卡、挂失移资新卡;
- 挂失卡;
- 充值:远程充值、线下充值;
- 消费;
- 查询:查询卡内记录及数据库内记录。
# 5 收获与建议
**收获**
- 在项目开发过程中我将课堂上学到的物联网理论知识应用到实际项目中深刻理解了RFID技术、数据库交互和Qt开发框架的原理和应用。
- 项目开发中遇到了诸多问题,如数据库连接、数据读写和硬件通信等。通过查阅资料、调试代码,我逐步解决了这些问题,提升了自己的问题解决能力。
- 通过项目我熟练掌握了Qt框架的使用增强了C++编程能力。同时对数据库设计和SQL查询也有了更深入的理解和实践。
- 在项目开发过程中,我学会了如何有效地管理项目进度,包括任务分解、时间安排和进度跟踪等。这些经验将对我未来的项目开发和管理大有裨益。
- 我学会了遵循代码规范,编写清晰、易维护的代码。同时,我也注重项目文档的编写,为项目后续的维护和迭代提供了有力支持。
**建议**
- 一开始我学习了 MFC 开发,但了解 MFC 后,我发现这个开发框架已经过时,存在许多缺点,不满足现代软件开发的需求,因此我选择了使用 Qt 开发。建议以后该课程可以更新学习的开发工具,比如教学使用 Qt 、Electron 等最新开发框架技术。

4
doc/image/ER.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 149 KiB

File diff suppressed because one or more lines are too long

After

(image error) Size: 166 KiB

BIN
doc/image/consume.png Normal file

Binary file not shown.

After

(image error) Size: 174 KiB

4
doc/image/consume.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 327 KiB

File diff suppressed because one or more lines are too long

After

(image error) Size: 300 KiB

File diff suppressed because one or more lines are too long

After

(image error) Size: 343 KiB

BIN
doc/image/deposit.png Normal file

Binary file not shown.

After

(image error) Size: 210 KiB

BIN
doc/image/installation1.png Normal file

Binary file not shown.

After

(image error) Size: 187 KiB

BIN
doc/image/installation2.png Normal file

Binary file not shown.

After

(image error) Size: 180 KiB

BIN
doc/image/installation3.png Normal file

Binary file not shown.

After

(image error) Size: 128 KiB

BIN
doc/image/installation4.png Normal file

Binary file not shown.

After

(image error) Size: 218 KiB

BIN
doc/image/installation5.png Normal file

Binary file not shown.

After

(image error) Size: 154 KiB

BIN
doc/image/newCard.png Normal file

Binary file not shown.

After

(image error) Size: 170 KiB

4
doc/image/newCard.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 562 KiB

4
doc/image/query-all.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 259 KiB

File diff suppressed because one or more lines are too long

After

(image error) Size: 197 KiB

BIN
doc/image/query.png Normal file

Binary file not shown.

After

(image error) Size: 300 KiB

BIN
doc/image/quitApp.png Normal file

Binary file not shown.

After

(image error) Size: 138 KiB

4
doc/image/quitApp.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 64 KiB

BIN
doc/image/reportLoss.png Normal file

Binary file not shown.

After

(image error) Size: 129 KiB

4
doc/image/reportLoss.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 166 KiB

File diff suppressed because one or more lines are too long

After

(image error) Size: 169 KiB

File diff suppressed because one or more lines are too long

After

(image error) Size: 100 KiB

BIN
doc/image/setting-step1.png Normal file

Binary file not shown.

After

(image error) Size: 193 KiB

BIN
doc/image/setting-step2.png Normal file

Binary file not shown.

After

(image error) Size: 209 KiB

@ -1,31 +0,0 @@
<mxfile host="Electron" modified="2024-07-28T12:00:05.441Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.6.1 Chrome/124.0.6367.207 Electron/30.0.6 Safari/537.36" etag="zDIfvHg48gbbwDQY4H_P" version="24.6.1" type="device">
<diagram name="第 1 页" id="8yjcYh25Fj_Vruqp0e9_">
<mxGraphModel dx="1434" dy="836" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="ikCD32b5rJxaaIhKNZvf-1" value="卡" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="400" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-2" value="用户" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="80" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-3" value="记录" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="400" y="440" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-4" value="学号" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="80" y="120" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-5" value="余额" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="560" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-6" value="卡号" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="400" y="120" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-7" value="状态" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="560" y="120" width="120" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

@ -1,147 +1,153 @@
<mxfile host="Electron" modified="2024-07-28T12:31:43.332Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.6.1 Chrome/124.0.6367.207 Electron/30.0.6 Safari/537.36" etag="HsDZe_ryQ9wAG3Kks_rW" version="24.6.1" type="device">
<mxfile host="Electron" modified="2024-08-02T10:23:27.443Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.6.1 Chrome/124.0.6367.207 Electron/30.0.6 Safari/537.36" etag="4ZaHNfXZlLg3Tqm3FgSD" version="24.6.1" type="device">
<diagram name="第 1 页" id="8yjcYh25Fj_Vruqp0e9_">
<mxGraphModel dx="1834" dy="836" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<mxGraphModel dx="1629" dy="899" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="ikCD32b5rJxaaIhKNZvf-11" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-1" target="ikCD32b5rJxaaIhKNZvf-5">
<mxCell id="ikCD32b5rJxaaIhKNZvf-11" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-1" target="ikCD32b5rJxaaIhKNZvf-5" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-18" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-1" target="ikCD32b5rJxaaIhKNZvf-16">
<mxCell id="ikCD32b5rJxaaIhKNZvf-18" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-1" target="ikCD32b5rJxaaIhKNZvf-16" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-1" value="卡" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-1" value="卡" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="400" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-8" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-2" target="ikCD32b5rJxaaIhKNZvf-4">
<mxCell id="ikCD32b5rJxaaIhKNZvf-8" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-2" target="ikCD32b5rJxaaIhKNZvf-4" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-14" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-2" target="ikCD32b5rJxaaIhKNZvf-13">
<mxCell id="ikCD32b5rJxaaIhKNZvf-14" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-2" target="ikCD32b5rJxaaIhKNZvf-13" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-2" value="用户" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="rXf3g1vUlJZsvpexLucj-2" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-2" target="rXf3g1vUlJZsvpexLucj-1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-2" value="用户" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="80" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-25" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;endArrow=none;endFill=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-21">
<mxCell id="ikCD32b5rJxaaIhKNZvf-25" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;endArrow=none;endFill=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-27" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-22">
<mxCell id="ikCD32b5rJxaaIhKNZvf-27" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-22" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-28" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-24">
<mxCell id="ikCD32b5rJxaaIhKNZvf-28" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-24" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-30" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-29">
<mxCell id="ikCD32b5rJxaaIhKNZvf-30" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-29" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-32" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-31">
<mxCell id="ikCD32b5rJxaaIhKNZvf-32" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-3" target="ikCD32b5rJxaaIhKNZvf-31" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-3" value="记录" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-3" value="记录" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="400" y="360" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-4" value="&lt;u&gt;学号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-4" value="&lt;u&gt;学/工号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="80" y="120" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-5" value="余额" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-5" value="余额" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="560" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-9" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-6" target="ikCD32b5rJxaaIhKNZvf-1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-9" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-6" target="ikCD32b5rJxaaIhKNZvf-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-6" value="&lt;u&gt;卡号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-6" value="&lt;u&gt;卡号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="400" y="120" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-10" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-7" target="ikCD32b5rJxaaIhKNZvf-1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-10" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-7" target="ikCD32b5rJxaaIhKNZvf-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-7" value="状态" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-7" value="状态" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="560" y="120" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-15" value="1" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-13" target="ikCD32b5rJxaaIhKNZvf-1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-15" value="1" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-13" target="ikCD32b5rJxaaIhKNZvf-1" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-13" value="绑定" style="rhombus;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-13" value="绑定" style="rhombus;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="240" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-17" value="n" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-16" target="ikCD32b5rJxaaIhKNZvf-3">
<mxCell id="ikCD32b5rJxaaIhKNZvf-17" value="n" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-16" target="ikCD32b5rJxaaIhKNZvf-3" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-16" value="存储" style="rhombus;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-16" value="存储" style="rhombus;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="400" y="280" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-20" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-19" target="ikCD32b5rJxaaIhKNZvf-3">
<mxCell id="ikCD32b5rJxaaIhKNZvf-20" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-19" target="ikCD32b5rJxaaIhKNZvf-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-19" value="&lt;u&gt;编号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-19" value="&lt;u&gt;编号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="240" y="280" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-21" value="金额" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-21" value="金额" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="240" y="440" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-22" value="原金额" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-22" value="原金额" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="400" y="440" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-24" value="类型" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-24" value="类型" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="560" y="360" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-29" value="余额" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-29" value="余额" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="560" y="440" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-31" value="时间" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-31" value="时间" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="560" y="280" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-46" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-35" target="ikCD32b5rJxaaIhKNZvf-44">
<mxCell id="ikCD32b5rJxaaIhKNZvf-46" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-35" target="ikCD32b5rJxaaIhKNZvf-44" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-35" value="设备名" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-35" value="设备名" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="80" y="360" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-37" value="n" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-36" target="ikCD32b5rJxaaIhKNZvf-3">
<mxCell id="ikCD32b5rJxaaIhKNZvf-37" value="n" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-36" target="ikCD32b5rJxaaIhKNZvf-3" edge="1">
<mxGeometry y="10" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-38" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-36" target="ikCD32b5rJxaaIhKNZvf-35">
<mxCell id="ikCD32b5rJxaaIhKNZvf-38" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-36" target="ikCD32b5rJxaaIhKNZvf-35" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-40" value="1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="ikCD32b5rJxaaIhKNZvf-38">
<mxCell id="ikCD32b5rJxaaIhKNZvf-40" value="1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="ikCD32b5rJxaaIhKNZvf-38" vertex="1" connectable="0">
<mxGeometry x="0.05" y="1" relative="1" as="geometry">
<mxPoint y="-11" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-36" value="产生" style="rhombus;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-36" value="产生" style="rhombus;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="240" y="360" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-42" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-41" target="ikCD32b5rJxaaIhKNZvf-35">
<mxCell id="ikCD32b5rJxaaIhKNZvf-42" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-41" target="ikCD32b5rJxaaIhKNZvf-35" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-41" value="&lt;u&gt;编号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-41" value="&lt;u&gt;编号&lt;/u&gt;" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="80" y="280" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-45" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-43" target="ikCD32b5rJxaaIhKNZvf-35">
<mxCell id="ikCD32b5rJxaaIhKNZvf-45" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-43" target="ikCD32b5rJxaaIhKNZvf-35" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-43" value="充值权限" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-43" value="充值权限" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="-80" y="360" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-44" value="名称" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-44" value="名称" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="80" y="440" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-48" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" edge="1" parent="1" source="ikCD32b5rJxaaIhKNZvf-47" target="ikCD32b5rJxaaIhKNZvf-2">
<mxCell id="ikCD32b5rJxaaIhKNZvf-48" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="ikCD32b5rJxaaIhKNZvf-47" target="ikCD32b5rJxaaIhKNZvf-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ikCD32b5rJxaaIhKNZvf-47" value="姓名" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxCell id="ikCD32b5rJxaaIhKNZvf-47" value="姓名" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="-80" y="200" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="rXf3g1vUlJZsvpexLucj-1" value="密码" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="240" y="120" width="120" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>

@ -83,7 +83,6 @@ MainWindow::MainWindow(QWidget *parent)
ui->queryResultTable->setColumnWidth(5, 240);
// 设置启动页面
ui->stackedWidget->setCurrentWidget(ui->settingPage);
}
@ -93,4 +92,3 @@ MainWindow::~MainWindow()
{
delete ui;
}

@ -71,6 +71,7 @@ private slots:
void on_queryInventoryButton_clicked();
void on_userRecordQueryButton_clicked();
void on_userIdRecordQueryButton_clicked();
void on_cardRecordQueryButton_clicked();
private:
Ui::MainWindow *ui;
@ -79,8 +80,6 @@ private:
Device device;
QStatusBar *statusBar;
QStackedWidget *stackedWidget;
QCheckBox *readerConnectStatusCheckBox;
QLabel *comNumberLabel;
QCheckBox *databaseConnectStatusCheckBox;

@ -137,7 +137,12 @@ void MainWindow::on_newCardButton_clicked()
"where id = :userId");
query.bindValue(":userId", cardUserId);
bool success = query.exec();
if (!success || !query.next())
if (!success)
{
QMessageBox::warning(this, "提示", QString("数据库异常。\n重开卡失败,请重试。"));
return;
}
if (!query.next())
{
QMessageBox::warning(this, "提示", QString("数据库异常。\n重开卡失败,请重试。"));
return;
@ -247,7 +252,7 @@ void MainWindow::on_newCardButton_clicked()
}
else if (userCardStatus == -1) // 用户有挂失卡,需要移资
{
/// @todo 弹出验证用户界面,要求用户输入密码;在数据库中将挂失卡的信息和消费记录移到新卡
// 弹出验证用户界面,要求用户输入密码;在数据库中将挂失卡的信息和消费记录移到新卡
QString info, prompt = QString("如需将挂失卡移资到本卡,请输入密码。");
bool success = verifyUser(userId, prompt, info);
if (!success)
@ -333,8 +338,6 @@ bool MainWindow::bindUserWithCard(int userId, QString cardId, QString &info)
return false;
}
/// @todo 写卡
return true;
}
@ -518,10 +521,15 @@ bool MainWindow::transferCard(int userId, QString newCardId, QString oldCardId,
// 查询旧卡余额
query.finish();
query.prepare("select balance from card "
"where userId = :userId;");
query.bindValue(":userId", oldCardId);
"where id = :cardId;");
query.bindValue(":cardId", oldCardId);
isExecuted = query.exec();
if (!isExecuted || query.next())
if (!isExecuted)
{
info = QString("数据库异常。");
return false;
}
if (!query.next())
{
info = QString("数据库异常。");
return false;
@ -570,8 +578,6 @@ bool MainWindow::transferCard(int userId, QString newCardId, QString oldCardId,
return false;
}
/// @todo 将数据库上的记录写到新卡上
return true;
}
@ -606,7 +612,5 @@ bool MainWindow::reopenCard(QString cardId, QString &info)
return false;
}
/// @todo 看看是否有写卡需求
return true;
}

@ -114,6 +114,11 @@ void MainWindow::on_userRecordQueryButton_clicked()
{
ui->queryCardStatusLabel->setText(QString("已被挂失"));
}
if (cardStatus == 1)
{
ui->queryCardStatusLabel->setText(QString("启用中"));
}
userId = query.value("userId").toInt();
balance = query.value("balance").toDouble();
ui->queryBalanceShowEdit->setText(QString::number(balance, 'f', 2));
@ -283,6 +288,138 @@ void MainWindow::on_userIdRecordQueryButton_clicked()
}
/**
* @brief
* @param void
* @return void
* @author
* @date 2024-07-31
*/
void MainWindow::on_cardRecordQueryButton_clicked()
{
if (!databaseReady())
{
QMessageBox::warning(this, QString("提示"), QString("数据库未连接,请设置。"));
if (ui->stackedWidget->currentWidget() != ui->settingPage)
{
ui->stackedWidget->setCurrentWidget(ui->settingPage);
}
return;
}
if (ui->queryCardIdBox->currentIndex() == -1)
{
QMessageBox::warning(this, "提示", "请放置卡片并点击查询按钮。");
return;
}
QString cardId = ui->queryCardIdBox->currentText();
int cardStatus;
double balance;
// 检查和获取绑定用户
QSqlQuery query(db->getDatabase());
query.prepare(QString("select userId, `status`, balance from card "
"where id = :cardId;"));
query.bindValue(":cardId", cardId);
bool success = false;
success = query.exec();
if (!success)
{
QMessageBox::warning(this, QString("提示"), QString("数据库异常。查询失败。"));
queryPageInitContent();
return;
}
if (query.next()) // 卡已被注册获取用户ID
{
cardStatus = query.value("status").toInt();
if (cardStatus == 0)
{
QMessageBox::warning(this, QString("提示"), QString("此卡未被启用。"));
queryPageInitContent();
return;
}
if (cardStatus == -1)
{
ui->queryCardStatusLabel->setText(QString("已被挂失"));
}
if (cardStatus == 1)
{
ui->queryCardStatusLabel->setText(QString("启用中"));
}
balance = query.value("balance").toDouble();
ui->queryBalanceShowEdit->setText(QString::number(balance, 'f', 2));
}
else // 卡没有注册
{
query.finish();
query.prepare(QString("insert into card "
"values (:cardId, 0, 0.0, null);"));
query.bindValue(":cardId", cardId);
success = query.exec();
if (!success)
{
QMessageBox::warning(this, QString("提示"), QString("数据库异常。查询失败。"));
queryPageInitContent();
return;
}
QMessageBox::warning(this, QString("提示"), QString("此卡未被启用。"));
queryPageInitContent();
return;
}
QStringList cardRecordIdList = reader.readAllRecords(cardId, success);
if (!success)
{
QMessageBox::warning(this, QString("提示"), QString("读卡器异常。查询失败。"));
queryPageInitContent();
return;
}
query.finish();
query.prepare(QString("select time, type, value, balance, device, id from record_view "
"where id = :recordId;"));
std::vector<QStringList> transactionRecordList;
for (int i = 0; i < cardRecordIdList.size(); i++)
{
QString recordId = cardRecordIdList[i];
query.bindValue(":recordId", recordId);
success = query.exec();
if (!success)
{
QMessageBox::warning(this, QString("提示"), QString("数据库异常。查询失败。"));
queryPageInitContent();
return;
}
if (!query.next()) {
// 原来的代码
// QMessageBox::warning(this, QString("提示"), QString("数据库异常。查询失败。"));
// queryPageInitContent();
// return;
// 这里本来应该报错退出但是读卡器经常发疯见Reader::readAllRecords(QString cardId, bool &ok)
// 所以发现有不在数据库中的记录号,直接跳过
continue;
}
QStringList transactionRecord = transactionRecord2QStringList
(
query.value("time").toDateTime(),
query.value("type").toInt(),
query.value("value").toDouble(),
query.value("balance").toDouble(),
query.value("device").toString(),
query.value("id").toString()
);
transactionRecordList.push_back(transactionRecord);
}
displayInTableWidget(transactionRecordList);
}
/**
* @brief QStringList
* `QStringList`便
@ -374,4 +511,3 @@ void MainWindow::displayInTableWidget(std::vector<QStringList> transactionRecord
}
}
}

@ -18,7 +18,8 @@ void MainWindow::on_quitAppAction_triggered()
/**
* @brief 退
* 退
* 退
*
* @param void
* @return void
* @author
@ -26,5 +27,7 @@ void MainWindow::on_quitAppAction_triggered()
*/
void MainWindow::on_confirmQuitButton_clicked()
{
if (reader.is_connected()) reader.disconnect();
free(db);
this->close();
}

@ -1,4 +1,5 @@
#include "readerAPI.h"
#include "qdebug.h"
/**
@ -29,7 +30,7 @@ bool Reader::is_connected()
*/
bool Reader::connect()
{
if (CVCDOurs::connectReaderByCOM(comNumber))
if (connectReaderByCOM(comNumber))
{
return true;
}
@ -41,6 +42,19 @@ bool Reader::connect()
}
/**
* @brief
* @param void
* @return void
* @author
* @date 2024-07-31
*/
void Reader::disconnect()
{
disconnectReaderByCOM();
}
/**
* @brief COM口号
* @param comNumber COM口号
@ -131,14 +145,13 @@ bool Reader::readRecordNumber(int &recordNum, int &recordIndex, QString cardId)
uchar_t recordIndexHex[4] = {0}; // 一个block有4个byte1个byte有两个hex会返回8个hex存在4个uchar_t中
uchar_t cardIdHex[8] = {0};
QByteArray ba = cardId.toLatin1();
StringToHex(ba.data(), cardIdHex);
StringToHex(cardId.toLatin1().data(), cardIdHex);
int hexNum = readBlocks(0, 1, recordIndexHex, nullptr, cardIdHex);
int hexNum = readSingleBlock(cardIdHex, 0, 0, recordIndexHex, nullptr);
if (hexNum == 0) return false;
recordNum = (int)(recordIndexHex[0] & 0x0F);
recordIndex = (int)(recordIndexHex[1] >> 4);
recordIndex = (int)(recordIndexHex[0] >> 4);
return true;
}
@ -171,10 +184,9 @@ bool Reader::writeRecordNumber(int recordNum, int recordIndex, QString cardId)
recordIndexStr[0] += (uchar_t)(recordNum);
uchar_t cardIdHex[8] = {0};
QByteArray ba = cardId.toLatin1();
StringToHex(ba.data(), cardIdHex);
StringToHex(cardId.toLatin1().data(), cardIdHex);
int success = writeBlock(0, recordIndexStr, cardIdHex);
int success = writeSingleBlock(cardIdHex, 0, 4, recordIndexStr);
return success == 1;
}
@ -207,14 +219,12 @@ bool Reader::insertRecord(QString record, QString cardId)
int blockIndex = 1 + 4 * recordIndex;
uchar_t cardIdHex[8] = {0};
QByteArray ba = cardId.toLatin1();
StringToHex(ba.data(), cardIdHex);
StringToHex(cardId.toLatin1().data(), cardIdHex);
uchar_t recordHex[4 * 4] = {0};
ba = record.toLatin1();
StringToHex(ba.data(), recordHex);
StringToHex(record.toLatin1().data(), recordHex);
int writeLineNumber = writeBlocks(blockIndex, 4, recordHex, cardIdHex);
int writeLineNumber = writeMultipleBlocks(cardIdHex, blockIndex, 4, 4, recordHex);
return writeLineNumber != 0;
}
@ -234,50 +244,57 @@ bool Reader::insertRecord(QString record, QString cardId)
* @author
* @date 2024-07-31
*/
QStringList Reader::getRecords(QString cardId, bool &ok)
QStringList Reader::readAllRecords(QString cardId, bool &ok)
{
QStringList recordList;
int recordNum, recordStartIndex;
bool success = readRecordNumber(recordNum, recordStartIndex, cardId);
int recordNum, recordIndex;
bool success = readRecordNumber(recordNum, recordIndex, cardId);
if (!success)
{
ok = false;
return recordList;
}
int recordStartIndex = ((recordIndex - recordNum + 1) % maxRecordNum + maxRecordNum) % maxRecordNum;
uchar_t cardIdHex[8] = {0};
QByteArray ba = cardId.toLatin1();
StringToHex(ba.data(), cardIdHex);
StringToHex(cardId.toLatin1().data(), cardIdHex);
for (int i = 0; i < recordNum; i++)
{
QString recordStr = "";
int recordIndex = 1 + 4 * ((recordStartIndex + i) % maxRecordNum);
for (int j = 0; j < 4; j++)
StringToHex(cardId.toLatin1().data(), cardIdHex); // readMultipleBlocks不知为何会改变cardIdHex的值要重新赋值
int recordBlockIndex = 1 + 4 * ((recordStartIndex + i) % maxRecordNum);
uchar_t recordHex[4 * 4] = {0};
int hexNum = readMultipleBlocks(cardIdHex, 0, recordBlockIndex, 4, recordHex, nullptr);
if (hexNum == 0)
{
uchar_t blockHex[4] = {0}; // 一个block有4个byte1个byte有两个hex会返回8个hex存在4个uchar_t中
int hexNum = readBlocks(recordIndex + j, 1, blockHex, nullptr, cardIdHex);
if (hexNum == 0)
{
ok = false;
return recordList;
}
// 原来的处理代码
// ok = false;
// return recordList;
char blockStr[9] = {0};
HexToString(blockHex, 4, blockStr);
recordStr += QString(blockStr);
// 这里经常出现hexNum == 0循环终止的情况原因是返回的reply为空值: “[]”
// 一旦出现[],后面的值大概率会出错。
// 出现主要集中在第二遍循环时。
// 尝试解决方案每一遍循环重新inventory重置一下环境怀疑某些变量被改了、每一遍循环睡眠2秒都没有解决这个问题
// 因此在发生错误时直接返回能够正确读取的记录ID。
break;
}
recordList.push_back(recordStr);
char recordStr[33] = {0};
HexToString(recordHex, 4 * 4, recordStr);
recordStr[30] = '\0';
QString qRecordStr = QString(recordStr).toUpper();
recordList.push_back(qRecordStr);
}
ok = true;
return recordList;
}
/**
* @brief
* 1block初始化为全0
* 1block的记录数量设置为0maxRecordNum-1
* @param cardId ID QString
* @return bool
* - true
@ -292,10 +309,10 @@ QStringList Reader::getRecords(QString cardId, bool &ok)
bool Reader::initCard(QString cardId)
{
uchar_t cardIdHex[8] = {0};
QByteArray ba = cardId.toLatin1();
StringToHex(ba.data(), cardIdHex);
StringToHex(cardId.toLatin1().data(), cardIdHex);
uchar_t allZeroHex[4] = {0};
int writeLineNumber = writeBlock(0, allZeroHex, cardIdHex);
uchar_t initHex[4] = {0};
initHex[0] = (char)(maxRecordNum - 1) << 4;
int writeLineNumber = writeSingleBlock(cardIdHex, 0, 4, initHex);
return writeLineNumber != 0;
}

@ -2,10 +2,10 @@
#define READERAPI_H
#include <HF15693.h>
#include <QString>
#include <QStringList>
#include <VCDOurs.h>
#include <QStringList>
typedef unsigned char uchar_t;
@ -29,13 +29,14 @@ public:
void setComNumber(int comNumber);
int getComNumber();
bool is_connected();
bool connect();
void disconnect();
QStringList inventory(int maxViccNum);
bool insertRecord(QString record, QString cardId);
bool writeRecords(QStringList recordList, QString cardId);
QStringList getRecords(QString cardId, bool &ok);
QStringList readAllRecords(QString cardId, bool &ok);
bool initCard(QString cardId);
};

@ -50,7 +50,7 @@ void MainWindow::on_reportLossButton_clicked()
// 查询学/工号是否存在
QSqlQuery query(db->getDatabase());
query.prepare(QString("select id from card "
query.prepare(QString("select `status` from card "
"where userId = :userId;"));
query.bindValue(":userId", userId);
bool success = query.exec();
@ -64,6 +64,20 @@ void MainWindow::on_reportLossButton_clicked()
QMessageBox::warning(this, "提示", "学/工号不存在,挂失失败。");
return;
}
int cardStatus = query.value("status").toInt();
if (cardStatus == -1)
{
QMessageBox::warning(this, "提示", "该卡已挂失,不可重复挂失。");
return;
}
QString info, prompt = QString("如需挂失该学/工号绑定的卡,请输入密码。");
success = verifyUser(userId, prompt, info);
if (!success)
{
QMessageBox::warning(this, "提示", info + QString("\n验证用户失败。挂失失败,请重试。"));
return;
}
// 将该学/工号的卡设置为挂失状态
query.prepare(QString("update card "