【C 语言项目实战】基于链表与文件操作的标准化彩票管理系统设计与实现
一、项目背景与技术架构
在职坐标平台的C语言的学习旅程中,我们往往止步于语法基础和简单的算法练习。然而,如何将链表、文件操作、模块化设计结合,构建一个真实的业务系统,是很多初学者面临的瓶颈。
本系统基于C 语言(C99 标准)编写,采用单向链表作为核心数据结构,并结合二进制文件进行数据持久化。系统严格遵循模块化设计原则,实现了用户管理、彩票发行、投注购买、开奖兑奖等完整业务闭环。
核心亮点:
- 多角色权限控制:支持彩民、管理员、公证员三类角色的差异化操作。
- 双色球中奖算法:实现复杂的奖项等级判定逻辑(6 红球 +1 蓝球)。
- 动态奖池机制:支持奖池累加、滚存及按比例分配。
- 数据安全:采用二进制文件存储,确保掉电后数据不丢失。
二、系统架构与需求概览
根据需求文档,本系统面向彩民、管理员、公证员三类角色,严格遵循四层架构(表示层、业务逻辑层、数据访问层、基础设施层)。
2.1 核心业务流程
系统的核心在于“发行-购买-开奖-兑奖”的闭环。
- 管理员:负责添加期号并发行彩票。
- 彩民:登录购买,支持多注、多倍投注。
- 公证员:独立执行开奖(设置号码)和兑奖(资金发放),确保公平性。
2.2 开发环境与工具链
本系统主要在 Windows 环境下开发,使用了轻量级的 GCC 编译器。
| 维度 | 选型 | 说明 |
|---|---|---|
| 开发语言 | C 语言 | 遵循 C99 标准 |
| 编译器 | GCC (MinGW-w64) | 高效稳定的编译环境 |
| 构建工具 | Make | 通过 Makefile 管理编译流程 |
| 数据结构 | 单向链表 | 内存中高效管理用户、彩票及投注记录 |
| 存储方式 | 二进制文件 (.bin) | 保证数据安全性与读写效率 |
三、系统架构设计
系统采用了清晰的四层分层架构,确保了代码的可维护性和扩展性。
- 表示层 (UI):负责所有界面渲染与菜单显示。
- 业务逻辑层:核心处理层,包含用户、彩票、公证等模块。
- 数据访问层 (File):负责链表与二进制文件之间的读写交互。
- 基础设施层 (Utils/Common):提供工具函数、宏定义及枚举类型。
模块依赖关系:
main.c作为程序入口,调度system模块。system模块根据角色分发至user、admin或notary模块。- 各业务模块通过
file模块与磁盘数据交互,并依赖utils进行内存管理和输入验证。
四、核心数据结构设计
数据结构是系统的骨架。根据SRS文档,我们设计了三个核心结构体,完美映射业务实体。
4.1 彩民信息 (User)
支持多角色(彩民/管理员/公证员)和余额管理。
1typedef struct User { 2 int userId; // 全局自增ID 3 char account[MAX_ACCOUNT_LEN]; // 账号 4 char password[MAX_PASSWORD_LEN]; // 密码 5 double balance; // 余额 6 int role; // 角色枚举 7 struct User *next; 8} User;4.2 彩票期号 (Lottery)
管理每一期的状态与奖池。特别注意,奖池基础值为100万(DEFAULT_PRIZE_POOL)。
1typedef struct Lottery { 2 int issueNumber; // 期号 3 int issueStatus; // 发行状态 4 int drawStatus; // 开奖状态 5 int winningNumbers[NUMBERS_PER_BET]; // 开奖号码 (7个) 6 int totalSold; // 售出总注数 7 double prizePool; // 奖池总额 8 struct Lottery *next; 9} Lottery;4.3 购买记录 (Ticket)
这是最复杂的结构,需要记录多注号码、倍数及中奖详情。
1typedef struct Ticket { 2 long long ticketId; // 彩票唯一ID 3 int issueNumber; // 所属期号 4 int betCount; // 注数 (1-5) 5 // 二维数组存储: 5注 x 8位 (6红+1蓝+1倍数) 6 int selectedNumbers[MAX_BET_COUNT][NUMBERS_PER_BET + 1]; 7 char buyerAccount[MAX_ACCOUNT_LEN]; // 买家账号 8 double price; // 总价格 9 int winStatus; // 中奖状态 10 int prizeLevel[MAX_BET_COUNT]; // 每注的奖项等级 11 double winAmount; // 中奖金额 12 struct Ticket *next; 13} Ticket;五、核心业务逻辑实现
5.1 多角色权限控制
系统启动后,根据登录账号的角色(role字段),分发至不同的操作菜单。这是通过main函数中的switch-case实现的:
1// 伪代码示例 2User* currentUser = userLogin(userList); 3switch(currentUser->role) { 4 case ROLE_ADMIN: 5 adminMenu(); 6 break; 7 case ROLE_NOTARY: 8 notaryMenu(); 9 break; 10 default: 11 userMenu(); // 彩民 12}5.2 彩票发行与奖池规则
- 发行流程:管理员需先“添加期号”(奖池初始化为 100 万),再“发行彩票”。
- 奖池累加:若上一期未发行,其奖池将自动累加到本期。
- 销售规则:单价硬编码为 2 元/倍,支持 1~10 倍投注。
5.3 双色球中奖算法 (重点)
这是本系统的难点之一。系统采用双色球模式(6 红球 +1 蓝球)进行判定。
| 奖项等级 | 判定条件 (红球 + 蓝球) | 奖金分配比例 |
|---|---|---|
| 一等奖 | 6 + 1 | 奖池的 50% |
| 二等奖 | 6 + 0 或 5 + 1 | 奖池的 25% |
| 三等奖 | 5 + 0 或 4 + 1 | 奖池的 15% |
| 四等奖 | 4 + 0 或 3 + 1 | 奖池的 10% |
算法实现逻辑 (isWinningLottery):
- 统计每注红球匹配数量(红球已自动排序)。
- 判断蓝球是否匹配。
- 根据红球匹配数和蓝球状态,查表确定奖项等级。
代码实现逻辑:
1// notary.c - 中奖判定核心逻辑 2int isWinningLottery(User *userHead, Ticket *ticket, Lottery *lottery) { 3 // 1. 防御性检查:用户是否已注销 4 if (userIsDeleted(userHead, ticket->buyerAccount)) { 5 ticket->winStatus = WIN_STATUS_INVALID; 6 return 0; 7 } 8 9 // 2. 遍历每一注 10 for (int i = 0; i < ticket->betCount; i++) { 11 int redMatch = 0; 12 // 红球匹配 (前6个) 13 for (int j = 0; j < 6; j++) { 14 for (int k = 0; k < 6; k++) { 15 if (ticket->selectedNumbers[i][j] == lottery->winningNumbers[k]) { 16 redMatch++; 17 break; 18 } 19 } 20 } 21 // 蓝球匹配 (第7个) 22 int blueMatch = (ticket->selectedNumbers[i][6] == lottery->winningNumbers[6]); 23 24 // 3. 判定等级 25 if (redMatch == 6 && blueMatch) { 26 ticket->prizeLevel[i] = PRIZE_LEVEL_FIRST; 27 } else if ((redMatch == 6 && !blueMatch) || (redMatch == 5 && blueMatch)) { 28 ticket->prizeLevel[i] = PRIZE_LEVEL_SECOND; 29 } 30 // ... 其他等级判定 31 } 32 return 1; 33}5.4 兑奖流程 (Cash Prizes)
兑奖与开奖分离,确保流程严谨:
- 累加奖池:将当期所有彩票的
price累加至奖池。 - 统计倍数:遍历所有彩票,统计每个奖项等级的总倍数。
- 计算奖金:
每倍奖金 = 奖池 × 比例 ÷ 总倍数。 - 发放奖金:遍历中奖彩票,
中奖金额 = 每倍奖金 × 该注倍数,并更新用户余额。
六、数据持久化与链表管理
系统采用二进制文件进行存储,确保断电数据不丢失。
6.1 文件操作封装
在file.c中,我们封装了通用的读写逻辑。以保存用户为例:
1int fileSaveUsers(User *head) { 2 FILE *fp = fopen(USERS_FILE_PATH, "wb"); 3 if (!fp) return 0; 4 5 User *current = head->next; // 跳过头节点 6 while (current != NULL) { 7 fwrite(current, sizeof(User), 1, fp); 8 current = current->next; 9 } 10 fclose(fp); 11 return 1; 12}6.2 内存安全
严格遵守C语言内存管理规范:
- 初始化:所有链表节点
next指针置为NULL。 - 释放:程序退出时,调用
utilsFreeXxxList释放所有链表节点。 - 检查:
malloc后立即检查NULL,防止野指针。
七、总结与展望
通过本次项目实战,我深入理解了 C 语言在大型项目中的模块化设计思想。该系统不仅实现了基本的 CRUD 操作,还解决了复杂的业务逻辑(如奖池分配、多维数组排序等)。
项目亮点:
- 高内聚低耦合:9个模块分工明确,
main.c仅作为调度中心。 - 业务逻辑严谨:严格区分了“开奖”与“兑奖”两个步骤,符合实际业务场景。
- 健壮性:完善的输入校验和内存管理机制。
未来优化方向:
- 增加数据变更标记机制,仅在数据变更时保存,提升性能。
- 实现图形化界面(GUI)版本。