【项目实践:位掩码状态设计方案】

前言

个人项目实践总结,分享位掩码状态设计的完整方案。

位掩码(Bitmask)状态设计方案

使用 2 的 n 次幂来设计类型、状态字段,通过位运算判断当前状态


一、什么是位掩码?

位掩码是一种利用二进制位来表示多种状态或权限的技术。每个状态使用一个独立的二进制位(1、2、4、8、16...),通过位运算进行组合和判断。

核心原理

十进制 二进制 含义
1 0001 待审核
2 0010 已通过
4 0100 已发布
8 1000 已删除

java

复制代码
// 一条数据 status = 3(二进制 0011)
// 表示同时拥有:待审核 + 已通过

// 判断是否已通过:(3 & 2) == 2 → true

二、优缺点分析

✅ 优点

优点 说明
存储高效 一个字段存储多个状态,节省数据库空间
查询高效 位运算比字符串匹配快得多
扩展方便 新增类型只需增加新位,不改变表结构
组合灵活 状态可自由组合,无限搭配

❌ 缺点

缺点 说明
可读性差 看到数字 3 不知道什么意思,需要查文档
查询不便 WHERE (status & 2) = 2 无法使用索引
维护困难 新人理解成本高,学习曲线陡峭
调试不便 日志中看到数字需要手动换算
扩展有限 数据库 int 最多 32 位,bigint 最多 64 位

⚠️ 索引问题

sql

复制代码
-- ❌ 位运算查询无法使用索引
SELECT * FROM task WHERE (status & 2) = 2;

-- ✅ 等值查询可以使用索引
SELECT * FROM task WHERE status = 3;

三、适用场景

场景 推荐度 原因
权限控制 ⭐⭐⭐⭐⭐ 多角色组合,查询少,状态多
配置开关 ⭐⭐⭐⭐⭐ 功能开关组合,如用户设置
业务状态流转 ⭐⭐ 状态通常互斥,不需要组合
频繁按状态查询 位运算导致索引失效

四、完整实现方案

1. 数据库层

方案 A:字段加注释(状态少)

sql

复制代码
CREATE TABLE task (
    id INT PRIMARY KEY,
    status INT DEFAULT 1 COMMENT '1:待审核, 2:已通过, 4:已发布, 8:已删除'
);
方案 B:字典表(状态多/动态)

sql

复制代码
-- 字典表
CREATE TABLE sys_dict (
    id INT PRIMARY KEY,
    dict_type VARCHAR(50) COMMENT '字典类型',
    dict_code INT COMMENT '字典值',
    dict_label VARCHAR(100) COMMENT '字典标签',
    sort INT COMMENT '排序'
);

-- 示例数据
INSERT INTO sys_dict VALUES (1, 'task_status', 1, '待审核', 1);
INSERT INTO sys_dict VALUES (2, 'task_status', 2, '已通过', 2);
INSERT INTO sys_dict VALUES (3, 'task_status', 4, '已发布', 3);
INSERT INTO sys_dict VALUES (4, 'task_status', 8, '已删除', 4);

2. Java 枚举实现

java

复制代码
public enum TaskStatus {
    PENDING(1, "待审核"),
    APPROVED(2, "已通过"),
    PUBLISHED(4, "已发布"),
    DELETED(8, "已删除");
    
    private final int code;
    private final String label;
    
    TaskStatus(int code, String label) {
        this.code = code;
        this.label = label;
    }
    
    public int getCode() { return code; }
    public String getLabel() { return label; }
    
    /**
     * 根据 code 获取枚举
     */
    public static TaskStatus fromCode(int code) {
        for (TaskStatus status : values()) {
            if (status.code == code) {
                return status;
            }
        }
        return null;
    }
    
    /**
     * 判断当前状态是否包含指定状态(位运算)
     */
    public boolean hasStatus(int flags) {
        return (flags & code) == code;
    }
    
    /**
     * 获取所有状态标签
     */
    public static List<String> getLabels(int flags) {
        List<String> labels = new ArrayList<>();
        for (TaskStatus status : values()) {
            if (status.hasStatus(flags)) {
                labels.add(status.getLabel());
            }
        }
        return labels;
    }
}

3. 实体类使用

java

复制代码
@Entity
public class Task {
    private int status;  // 数据库存储数字
    
    /**
     * 判断是否已通过
     */
    public boolean isApproved() {
        return TaskStatus.APPROVED.hasStatus(status);
    }
    
    /**
     * 添加状态
     */
    public void addStatus(TaskStatus status) {
        this.status |= status.getCode();
    }
    
    /**
     * 移除状态
     */
    public void removeStatus(TaskStatus status) {
        this.status &= ~status.getCode();
    }
    
    /**
     * 获取所有状态标签
     */
    public List<String> getStatusLabels() {
        return TaskStatus.getLabels(status);
    }
}

4. 后端 API 返回

java

复制代码
public class TaskDTO {
    private Integer status;          // 数字
    private String statusLabel;      // 显示用文字
    private List<String> statusLabels; // 多状态组合
    
    public static TaskDTO fromEntity(Task task) {
        TaskDTO dto = new TaskDTO();
        dto.setStatus(task.getStatus());
        dto.setStatusLabel(TaskStatus.fromCode(task.getStatus()).getLabel());
        dto.setStatusLabels(TaskStatus.getLabels(task.getStatus()));
        return dto;
    }
}

5. 前端显示

TypeScript / JavaScript

typescript

复制代码
// 方式1:前端硬编码
const statusMap = {
    1: '待审核',
    2: '已通过',
    4: '已发布',
    8: '已删除'
};

// 使用
<span>{{ statusMap[task.status] }}</span>
方式2:后端接口获取

typescript

复制代码
// 前端调用接口获取字典
const statusMap = await getDict('task_status');
const label = statusMap[task.status];

java

复制代码
// 后端提供字典接口
@GetMapping("/dict/task-status")
public ResultDto<Map<Integer, String>> getTaskStatusDict() {
    Map<Integer, String> dict = Arrays.stream(TaskStatus.values())
        .collect(Collectors.toMap(
            TaskStatus::getCode,
            TaskStatus::getLabel
        ));
    return ResultDto.success(dict);
}

五、各层职责

层级 存储 说明
数据库 数字 存储高效,减少空间
字段备注 文字 方便 DBA 维护
Java 枚举 数字 → 文字 代码层类型安全
前端 文字 显示给用户

text

复制代码
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   前端       │      │   后端       │      │   数据库     │
│             │      │             │      │             │
│ 显示: 已通过 │ ←──  │ 返回: 2     │ ←──  │ 存: 2       │
│             │      │ 映射: 2=已通过│      │             │
│ 不存数字    │      │ 使用枚举    │      │ 只存数字    │
└─────────────┘      └─────────────┘      └─────────────┘

前端只显示文字,永远不显示数字。


六、替代方案

方案 优点 缺点 适用场景
位掩码 存储小、性能高 可读性差、索引不友好 权限、配置
关联表 索引友好、可扩展 查询需 JOIN 多对多关系
JSON 字段 灵活、可读性好 查询复杂 少量灵活字段
多个布尔字段 可读性最好、索引友好 字段过多 互斥状态

推荐选择

  • 权限/配置开关 → 位掩码 ✅
  • 业务状态 → 枚举或关联表
  • 灵活扩展 → JSON 字段

七、最佳实践

✅ DO

  1. 状态固定时使用枚举,保证代码类型安全
  2. 数据库字段加注释,说明每个数字的含义
  3. 前端永远显示文字,不暴露数字
  4. 提供字典接口,支持动态维护
  5. 数字只作为存储方式,业务层都用枚举

❌ DON'T

  1. 不要在前端直接显示数字
  2. 不要硬编码数字在代码中(用枚举替代)
  3. 不要在频繁查询的字段上使用位运算(索引失效)
  4. 不要超过数据库位长度限制(int 32 位,bigint 64 位)

八、总结

位掩码设计是经典的计算机科学实践,在权限控制、配置开关等场景下非常高效。但在业务状态流转中,建议优先考虑可读性和可维护性。

设计原则:

数据层存数字 → 业务层用枚举 → 展示层显文字

让每一层做自己最擅长的事。


如果你觉得这篇文章有帮助,欢迎点赞、收藏、分享! 😊