Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292af904d4 | ||
|
|
9f381d91a8 |
@@ -3,38 +3,51 @@
|
|||||||
## 构建 & 运行
|
## 构建 & 运行
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c -o bank_system
|
gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c hash.c -o bank_system
|
||||||
./bank_system
|
./bank_system
|
||||||
```
|
```
|
||||||
|
|
||||||
`bank.h` 包含所有结构体定义、函数声明、全局常量。所有 `.c` 的编译顺序无要求。
|
`bank.h` 是唯一头文件,所有 `.c` 编译顺序无要求。
|
||||||
|
|
||||||
## 项目布局
|
## 项目布局
|
||||||
|
|
||||||
所有源文件在仓库根目录,无 `src/` 子目录。无包管理器、无构建系统、无测试框架。
|
所有源文件在根目录,无 `src/` 子目录。无包管理器、无构建系统、无测试框架。
|
||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `bank.c` | 主循环、菜单、权限路由 |
|
| `bank.c` | 主循环、菜单、权限路由 |
|
||||||
| `account.c` | 开户、销户、登录、查询全部账户 |
|
| `account.c` | 开户、销户、登录、修改密码、查询全部账户 |
|
||||||
| `transaction.c` | 存款、取款、查余额、转账 |
|
| `transaction.c` | 存款、取款、查余额、转账 |
|
||||||
| `file_io.c` | 二进制文件读写 (bank.dat) |
|
| `file_io.c` | 二进制文件读写 (bank.dat) |
|
||||||
| `utils.c` | 输入校验工具 |
|
| `utils.c` | 输入校验工具(`GetInputInt`/`GetInputDouble` 已定义但未被使用) |
|
||||||
| `log.c` | 环形队列日志,支持按关键词搜索 |
|
| `log.c` | 环形队列日志,支持 KMP 关键词搜索 |
|
||||||
| `graph.c` | 从转账日志建图,分析客户间资金来往关系 |
|
| `graph.c` | 从转账日志建邻接矩阵,分析客户间资金来往关系 |
|
||||||
|
| `hash.c` | 哈希表,映射账户ID到数组下标,线性探测+墓碑标记 |
|
||||||
| `bank.h` | 唯一头文件,声明所有公有函数和类型 |
|
| `bank.h` | 唯一头文件,声明所有公有函数和类型 |
|
||||||
|
|
||||||
## 重要约束
|
## 重要数据/约束
|
||||||
|
|
||||||
- **Max 50 账户** (`MAX_ACCOUNTS`),全局数组 `g_astAccounts`,不使用 `malloc`。
|
- **Max 50 账户** (`MAX_ACCOUNTS`),全局数组 `g_astAccounts`,不使用 `malloc`。
|
||||||
- **默认管理员**: ID=1000, name="admin", password=123456, permission=1。系统首次运行时自动创建。
|
- **`g_iAccCount` 初始值=1**(非0),槽位 0 预留给管理员。
|
||||||
- **密码**: 纯 6 位数字。
|
- **默认管理员**: ID=1000, name="admin", password=123456, quanxian=1。首次运行时自动创建。
|
||||||
- **权限**: 1=管理员,2=普通用户。管理员多出显示全部、删账户、创管理员、看日志功能。
|
- **密码**: 纯 6 位数字(100000-999999)。
|
||||||
- **数据持久化**: 二进制文件 `bank.dat`(已加入 `.gitignore`)。
|
- **权限**: 1=管理员,2=普通用户。
|
||||||
- **全局日志队列**: 环形队列 `g_logQueue`,最多 100 条 (`MAX_LOGS`),溢出覆盖旧记录。
|
- **数据持久化**: 二进制文件 `bank.dat`(已加入 `.gitignore`)。`SaveData()` 总是写入恰好 50 条记录。
|
||||||
|
- **全局日志队列**: 环形队列 `g_logQueue`,最多 100 条 (`MAX_LOGS`),溢出时队头出队覆盖旧记录。
|
||||||
|
- **哈希表**: `HASH_SIZE=101`(质数),开放寻址法+线性探测,删除用墓碑标记(id=-2)。
|
||||||
|
|
||||||
## 代码约定
|
## 代码约定
|
||||||
|
|
||||||
- 中文注释和中文菜单字符串。
|
- 中文注释和中文菜单字符串。
|
||||||
- 全局变量命名前缀 `g_`(如 `g_iAccCount`, `g_astAccounts`)。
|
- 全局变量命名前缀 `g_`(如 `g_iAccCount`, `g_astAccounts`)。
|
||||||
- 无测试;曾有测试文件但已被删除(commit `ce8abe3`)。
|
- 无测试。
|
||||||
|
|
||||||
|
## 已知陷阱
|
||||||
|
|
||||||
|
- **`InitAdminAccount()` 双重调用导致密码重置** — `LoadData()` 内无文件时调用一次,`main()` 第 21 行无条件再调用一次。管理员密码修改在重启后必然丢失。
|
||||||
|
- **`SaveData()` 内部调用 `LoadData()`**(`file_io.c:22`),写盘后立即重读以重新计数 `g_iAccCount`。注意这会导致刚保存的数组被覆盖读回。
|
||||||
|
- **`GenerateDummyData()`(graph.c)会清空全部日志并修改账户余额**,仅用于测试演示,非生产操作。
|
||||||
|
- **`DeleteAccount()` 不递减 `g_iAccCount`** — 递减由调用方 `bank.c:84` 负责。函数内 `for (; i < g_iAccCount-i; i++)` 边界有误(应为 `g_iAccCount-1`)。
|
||||||
|
- **`exit(0)` 短路** — `bank.c:53` 直接 `exit(0)`,不会执行 `main()` 末尾的清理代码(当前无实质性清理,但未来加逻辑时需注意)。
|
||||||
|
- **`ChangePassword()` 已定义但未接入菜单**(`account.c:159`),无法从界面调用。
|
||||||
|
- **`utils.c` 的 `GetInputInt`/`GetInputDouble` 已定义但未被任何调用方使用**,全部输入仍用裸 `scanf`。
|
||||||
|
|||||||
@@ -60,11 +60,17 @@
|
|||||||
- 日志记录和存储(环形队列)
|
- 日志记录和存储(环形队列)
|
||||||
|
|
||||||
8. **资金关系图分析(graph.c)**
|
8. **资金关系图分析(graph.c)**
|
||||||
- 从转账日志构建邻接矩阵
|
- 从转账日志构建邻接矩阵
|
||||||
- 显示全部转账关系图
|
- 显示全部转账关系图
|
||||||
- 查看指定账户关联关系
|
- 查看指定账户关联关系
|
||||||
- 最活跃账户排名(度中心性)
|
- 最活跃账户排名(度中心性)
|
||||||
- BFS最短转账路径查找
|
- BFS最短转账路径查找
|
||||||
|
|
||||||
|
9. **哈希表模块(hash.c)**
|
||||||
|
- 映射账户ID到数组下标,O(1)快速查找
|
||||||
|
- 开放寻址法,线性探测解决冲突
|
||||||
|
- 墓碑标记处理删除操作
|
||||||
|
- 支持从数组重建哈希表
|
||||||
|
|
||||||
### 数据结构
|
### 数据结构
|
||||||
```c
|
```c
|
||||||
@@ -81,7 +87,7 @@ typedef struct strAccount
|
|||||||
## 使用说明
|
## 使用说明
|
||||||
### 编译
|
### 编译
|
||||||
```bash
|
```bash
|
||||||
gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c -o bank_system
|
gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c hash.c -o bank_system
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行
|
### 运行
|
||||||
@@ -127,7 +133,7 @@ gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c -o bank_syste
|
|||||||
- **密码规则**: 6位数字(100000-999999)
|
- **密码规则**: 6位数字(100000-999999)
|
||||||
- **数据存储**: 二进制文件(bank.dat)
|
- **数据存储**: 二进制文件(bank.dat)
|
||||||
- **输入验证**: 所有数值输入都包含错误处理
|
- **输入验证**: 所有数值输入都包含错误处理
|
||||||
- **内存管理**: 使用全局数组存储账户数据
|
- **内存管理**: 使用全局数组存储账户数据,哈希表加速ID查找
|
||||||
|
|
||||||
## 文件说明
|
## 文件说明
|
||||||
- `bank.c` - 主程序文件
|
- `bank.c` - 主程序文件
|
||||||
@@ -137,6 +143,7 @@ gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c -o bank_syste
|
|||||||
- `utils.c` - 工具函数实现
|
- `utils.c` - 工具函数实现
|
||||||
- `log.c` - 日志记录和存储(环形队列)
|
- `log.c` - 日志记录和存储(环形队列)
|
||||||
- `graph.c` - 资金关系图分析模块
|
- `graph.c` - 资金关系图分析模块
|
||||||
|
- `hash.c` - 哈希表模块,映射账户ID到数组下标
|
||||||
- `bank.h` - 头文件,包含结构体定义和函数声明
|
- `bank.h` - 头文件,包含结构体定义和函数声明
|
||||||
- `bank.dat` - 数据存储文件(运行时自动生成)
|
- `bank.dat` - 数据存储文件(运行时自动生成)
|
||||||
|
|
||||||
@@ -149,7 +156,7 @@ gcc bank.c account.c transaction.c file_io.c utils.c log.c graph.c -o bank_syste
|
|||||||
|
|
||||||
## 扩展建议
|
## 扩展建议
|
||||||
- ~~添加交易记录功能~~
|
- ~~添加交易记录功能~~
|
||||||
- 实现账户排序和搜索
|
- ~~实现账户快速搜索~~(已通过哈希表实现)
|
||||||
- 增加更多权限级别
|
- 增加更多权限级别
|
||||||
- 改进用户界面(如使用图形界面)
|
- 改进用户界面(如使用图形界面)
|
||||||
- 添加数据加密功能
|
- 添加数据加密功能
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ void CreateAccount(int quanxian)
|
|||||||
g_astAccounts[g_iAccCount] = stNewAcc;
|
g_astAccounts[g_iAccCount] = stNewAcc;
|
||||||
|
|
||||||
//把记录账户数量的全局变量自增。加过之后自增,所以下标0占用,数量是1个
|
//把记录账户数量的全局变量自增。加过之后自增,所以下标0占用,数量是1个
|
||||||
|
HashInsert(stNewAcc.id, g_iAccCount);
|
||||||
g_iAccCount++;
|
g_iAccCount++;
|
||||||
|
|
||||||
printf("开户成功! 账户ID: %d\n", stNewAcc.id);
|
printf("开户成功! 账户ID: %d\n", stNewAcc.id);
|
||||||
@@ -133,12 +134,7 @@ int LoginAccount(int temp_id) {
|
|||||||
*/
|
*/
|
||||||
int FindAccount(int id)
|
int FindAccount(int id)
|
||||||
{
|
{
|
||||||
for(int i = 0; i < g_iAccCount; i++)
|
return HashSearch(id);
|
||||||
{
|
|
||||||
if(g_astAccounts[i].id == id) return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
//删除账户
|
//删除账户
|
||||||
void DeleteAccount(int id) {
|
void DeleteAccount(int id) {
|
||||||
@@ -151,6 +147,7 @@ void DeleteAccount(int id) {
|
|||||||
printf("正在删除一下账户...");
|
printf("正在删除一下账户...");
|
||||||
printf("%-8d %-30s %-10.2f %-3d\n",
|
printf("%-8d %-30s %-10.2f %-3d\n",
|
||||||
g_astAccounts[i].id, g_astAccounts[i].name, g_astAccounts[i].balance,g_astAccounts[i].quanxian);
|
g_astAccounts[i].id, g_astAccounts[i].name, g_astAccounts[i].balance,g_astAccounts[i].quanxian);
|
||||||
|
HashDelete(id);
|
||||||
for (; i < g_iAccCount-i; i++)
|
for (; i < g_iAccCount-i; i++)
|
||||||
{
|
{
|
||||||
g_astAccounts[i]=g_astAccounts[i+1];
|
g_astAccounts[i]=g_astAccounts[i+1];
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ int main(void)
|
|||||||
printf("=== 简易银行账户管理系统 ===\n");
|
printf("=== 简易银行账户管理系统 ===\n");
|
||||||
printf("欢迎使用! 系统已加载%d个账户\n", g_iAccCount);
|
printf("欢迎使用! 系统已加载%d个账户\n", g_iAccCount);
|
||||||
InitAdminAccount();//初始化管理员账号
|
InitAdminAccount();//初始化管理员账号
|
||||||
|
RebuildHashTable();
|
||||||
InitLogQueue();
|
InitLogQueue();
|
||||||
int iChoice=-1; //选择标志
|
int iChoice=-1; //选择标志
|
||||||
int pChoice=-1;
|
int pChoice=-1;
|
||||||
@@ -81,6 +82,7 @@ int main(void)
|
|||||||
scanf("%d", &d_id);
|
scanf("%d", &d_id);
|
||||||
DeleteAccount(d_id);
|
DeleteAccount(d_id);
|
||||||
g_iAccCount--;
|
g_iAccCount--;
|
||||||
|
RebuildHashTable();
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
CreateAccount(1);
|
CreateAccount(1);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#define MAX_LOGS 100 // 日志队列最大容量,防止无限增长
|
#define MAX_LOGS 100 // 日志队列最大容量,防止无限增长
|
||||||
#define MAX_ACCOUNTS 50 //系统规格为50个账户
|
#define MAX_ACCOUNTS 50 //系统规格为50个账户
|
||||||
#define NAME_LEN 31 //用户名字最长30字符,因此需要多1个字符串结束符
|
#define NAME_LEN 31 //用户名字最长30字符,因此需要多1个字符串结束符
|
||||||
|
#define HASH_SIZE 101 //哈希表大小,取质数以减少冲突
|
||||||
|
|
||||||
// 账户信息,每个账户id对应了名字、账户余额等信息,是不是用数据结构定义比较合适?
|
// 账户信息,每个账户id对应了名字、账户余额等信息,是不是用数据结构定义比较合适?
|
||||||
typedef struct strAccount
|
typedef struct strAccount
|
||||||
@@ -16,6 +17,21 @@ typedef struct strAccount
|
|||||||
|
|
||||||
// strAccount STACCOUNT;
|
// strAccount STACCOUNT;
|
||||||
|
|
||||||
|
// 哈希表条目,映射账户ID到g_astAccounts数组下标
|
||||||
|
typedef struct {
|
||||||
|
int id; // 账户ID,-1表示空槽位
|
||||||
|
int index; // g_astAccounts中的下标
|
||||||
|
} HashEntry;
|
||||||
|
|
||||||
|
extern HashEntry g_hashTable[HASH_SIZE];
|
||||||
|
|
||||||
|
// 哈希表操作
|
||||||
|
void InitHashTable();
|
||||||
|
int HashInsert(int id, int index);
|
||||||
|
int HashSearch(int id);
|
||||||
|
void HashDelete(int id);
|
||||||
|
void RebuildHashTable();
|
||||||
|
|
||||||
// 函数声明,main函数中调用的各模块函数需要在程序公用的.h中声明
|
// 函数声明,main函数中调用的各模块函数需要在程序公用的.h中声明
|
||||||
// 公开出来被其他模块调用的函数,也应该在公用的.h中声明
|
// 公开出来被其他模块调用的函数,也应该在公用的.h中声明
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include "bank.h"
|
||||||
|
|
||||||
|
extern STACCOUNT g_astAccounts[];
|
||||||
|
extern int g_iAccCount;
|
||||||
|
|
||||||
|
HashEntry g_hashTable[HASH_SIZE];
|
||||||
|
|
||||||
|
static int HashFunc(int id)
|
||||||
|
{
|
||||||
|
return id % HASH_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitHashTable()
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < HASH_SIZE; i++) {
|
||||||
|
g_hashTable[i].id = -1;
|
||||||
|
g_hashTable[i].index = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int HashInsert(int id, int index)
|
||||||
|
{
|
||||||
|
int pos = HashFunc(id);
|
||||||
|
int first_tombstone = -1;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < HASH_SIZE; i++) {
|
||||||
|
int probe = (pos + i) % HASH_SIZE;
|
||||||
|
if (g_hashTable[probe].id == id) {
|
||||||
|
g_hashTable[probe].index = index;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (g_hashTable[probe].id == -1) {
|
||||||
|
if (first_tombstone != -1) {
|
||||||
|
probe = first_tombstone;
|
||||||
|
}
|
||||||
|
g_hashTable[probe].id = id;
|
||||||
|
g_hashTable[probe].index = index;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (g_hashTable[probe].id == -2 && first_tombstone == -1) {
|
||||||
|
first_tombstone = probe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int HashSearch(int id)
|
||||||
|
{
|
||||||
|
int pos = HashFunc(id);
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < HASH_SIZE; i++) {
|
||||||
|
int probe = (pos + i) % HASH_SIZE;
|
||||||
|
if (g_hashTable[probe].id == id) {
|
||||||
|
return g_hashTable[probe].index;
|
||||||
|
}
|
||||||
|
if (g_hashTable[probe].id == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HashDelete(int id)
|
||||||
|
{
|
||||||
|
int pos = HashFunc(id);
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < HASH_SIZE; i++) {
|
||||||
|
int probe = (pos + i) % HASH_SIZE;
|
||||||
|
if (g_hashTable[probe].id == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_hashTable[probe].id == id) {
|
||||||
|
g_hashTable[probe].id = -2;
|
||||||
|
g_hashTable[probe].index = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RebuildHashTable()
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
InitHashTable();
|
||||||
|
for (i = 0; i < g_iAccCount; i++) {
|
||||||
|
if (g_astAccounts[i].id != 0) {
|
||||||
|
HashInsert(g_astAccounts[i].id, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user