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 语言系统编程、或者对控制台业务系统感兴趣的同学提供一些参考和启发。如果在编译或逻辑设计上有任何疑问,欢迎在评论区交流探讨!

posted @ 2026-06-29 08:18  氛为人  阅读(6)  评论(0)    收藏  举报