c语言小项目分享
C语言项目实战:手把手实现【高并发积分竞猜与摇号结算系统】(附架构设计)
一、 项目背景与业务场景
在很多互联网产品的运营活动中,经常会遇到“积分竞猜”、“幸运摇号”、“盲盒抽取”等玩法。这类系统看似简单,实则对数据一致性、内存管理、并发状态控制以及金融级精度计算有很高的要求。
本项目使用纯 C 语言(C99标准)实现了一个积分竞猜与摇号结算系统的核心后端逻辑。系统涵盖了用户账户管理、多注竞猜、随机摇号算法(Fisher-Yates)、动态奖池分配以及本地文件持久化。非常适合用来深入理解 C 语言的指针操作、链表内存管理以及模块化架构设计。
二、 系统架构与模块划分
2.1 核心角色与权限
系统设计了三种角色,分别对应不同的操作权限:
| 角色 | 默认账号 | 权限说明 |
|---|---|---|
| 普通用户 | 自主注册 | 参与竞猜、查看记录、充值积分、修改密码、注销账号 |
| 管理员 | admin | 发起竞猜活动、查看所有记录、查询/清算数据、备份数据 |
| 结算员 | notary | 执行系统摇号、查看中奖记录、手动执行积分结算 |
2.2 模块化目录结构
项目采用严格的模块化设计,将“接口定义”与“逻辑实现”分离:
System/
├── include/ # 头文件(接口定义)
│ ├── common.h # 公共定义(数据结构、错误码)
│ ├── user.h # 用户模块接口
│ ├── lottery.h # 竞猜模块接口
│ ├── issue.h # 活动期数模块接口
│ ├── draw.h # 摇号与结算模块接口
│ ├── file.h # 文件持久化接口
│ └── util.h # 工具函数接口
├── src/ # 源文件(逻辑实现)
│ ├── main.c # 主程序入口
│ ├── user.c # 用户模块
│ ├── lottery.c # 竞猜模块
│ ├── draw.c # 摇号/结算模块
│ └── ...
└── data/ # 数据文件目录
设计思路:头文件声明“做什么”,源文件实现“怎么做”。模块之间通过 common.h 中定义的全局链表头节点进行数据共享。
三、 核心数据结构设计
系统使用单向链表作为核心数据结构,以下是几个关键节点的设计细节:
3.1 用户节点(防浮点数精度丢失)
typedef struct Player {
char account[32]; // 账号(全局唯一)
char password[32]; // 密码
long long balance; // 账户积分(单位:分)
int status; // 状态:0-正常,1-注销
struct Player *next;
} Player;
💡 亮点:金额/积分采用 long long 以“分”为单位存储,显示时除以 100 转换为元。这彻底避免了 C 语言中 float/double 带来的精度丢失问题,是金融/积分系统的标准做法。
3.2 竞猜凭证节点
typedef struct Lottery {
int issue_no; // 活动期号
char lottery_id[64]; // 凭证唯一标识
int base_nums[5][6]; // 基础号码(最多5注,每注6个)
int special_nums[5]; // 特别号码
int bet_count; // 购买注数(1~5)
char buyer_account[32]; // 购买者账号
int win_level; // 凭证总中奖等级
long long win_amount; // 凭证总奖励(分)
int paid; // 是否已结算
struct Lottery *next;
} Lottery;
3.3 全局状态与脏数据标记
Player *g_playerHead = NULL; // 用户链表头
Lottery *g_lotteryHead = NULL; // 竞猜凭证链表头
int g_dataDirty = 0; // 数据脏标记
💡 亮点:g_dataDirty 是一个极简但高效的工程优化。任何写操作(注册、购买、充值等)都会将其置为 1,保存后置为 0。程序退出时,只有当标记为 1 时才提示保存,有效减少了不必要的磁盘 I/O。
四、 核心业务逻辑剖析
4.1 随机摇号算法(Fisher-Yates 洗牌)
在生成基础号码时,如果使用简单的 rand() % 33 极易产生重复且分布不均。本项目采用了 Fisher-Yates 洗牌算法,确保每个号码被抽中的概率绝对均等:
void util_generateBaseNums(int nums[6]) {
int pool[33];
for (int i = 0; i < 33; i++) pool[i] = i + 1;
// Fisher-Yates 洗牌,取前6个
for (int i = 0; i < 6; i++) {
int j = i + rand() % (33 - i);
int temp = pool[i];
pool[i] = pool[j];
pool[j] = temp;
nums[i] = pool[i];
}
// 冒泡排序,保证输出美观
// ...
}
4.2 动态奖池分配算法(防击穿机制)
这是本系统最复杂的逻辑之一。当一期活动的“应发总奖励”超过了“当前奖池总额”时,系统不能超发,必须触发按比例缩水机制:
long long total_needed = float_total_1 + float_total_2 + fixedTotal;
if (total_needed > 0) {
// 计算缩水比例 scale
double scale = (pool < total_needed) ? (double)pool / total_needed : 1.0;
// 所有奖项乘以 scale 进行统一折算
if (issue->prize_count[1] > 0)
amt[1] = (long long)(float_total_1 * scale) / issue->prize_count[1];
if (scale < 1.0) {
printf("[注意] 奖池不足,固定奖项按 %.1f%% 比例折算发放\n", scale * 100);
}
}
这种设计保证了无论中奖人数多少,系统发出的积分永远不会超过奖池存量,防止了“奖池击穿”的严重业务事故。
4.3 统一错误码体系
为了便于后期维护和日志排查,项目定义了按模块划分的错误码体系:
// 用户模块 (-10 ~ -19)
#define RET_USER_NOT_FOUND -10
#define RET_USER_EXISTS -11
// 竞猜模块 (-20 ~ -29)
#define RET_BALANCE_INSUFFICIENT -21
// 文件模块 (-50 ~ -59)
#define RET_FILE_OPEN_ERR -50
五、 项目亮点与工程化总结
| 技术难点 | 实现方案 |
|---|---|
| 模块化架构 | 7个独立模块,头文件声明接口,源文件实现逻辑,降低耦合。 |
| 金融级精度 | 所有积分/金额使用 long long 以“分”为单位存储,杜绝浮点数误差。 |
| 奖池防击穿 | 当应发总额大于奖池时,自动触发全局 scale 缩水比例计算。 |
| 数据脏标记 | 引入 g_dataDirty 标记,退出时按需保存,优化磁盘 I/O 性能。 |
| 安全输入 | 密码输入使用 _getch() 实现无回显星号 masking,防偷窥。 |
| 内存管理 | 退出时遍历并 free 所有链表节点,确保零内存泄漏。 |
六、 写在最后
通过手写这个积分竞猜系统,能够极其深刻地体会到 C 语言在底层内存控制和业务逻辑抽象上的魅力。从链表节点的精准分配,到文件指针的格式化读写,再到指针数组的越界防护,每一个细节都是对基本功的考验。
希望这篇架构解析能为正在学习 C 语言系统编程、或者对控制台业务系统感兴趣的同学提供一些参考和启发。如果在编译或逻辑设计上有任何疑问,欢迎在评论区交流探讨!

浙公网安备 33010602011771号