《从零开发校园墙微信小程序》第 1 篇。这一篇不写业务代码,只做一件事:把数据模型想清楚。后面所有功能都是建立在这些表之上的。

先想清楚:校园墙里有哪些「东西」

不要一上来就建表。先把业务里的名词列出来:

  • 人 :谁在用?谁被封号了?
  • 投稿 :一条投稿有文字、图片、时间、点赞评论数;
  • 评论 :对投稿的评论与回复;
  • 图片 :上传到哪里,谁传的;
  • 规则 :敏感词、封禁、频率限制;
  • 运营 :反馈、管理员、配置、审计。

每个名词基本对应一张表。下面按这个顺序讲。

一、用户:为什么用 openid

小程序里用户没有账号密码,微信给每个「用户 × 小程序」一个唯一标识 openid 。所以 user 表这样设计:

@TableName("user")
public class User {
    @TableId
    private String id;
    private String openid;          // 微信 openid:登录身份的唯一依据
    private String phone;
    @Column("nikeName")
    private String nikename;        // 昵称(注意:历史字段名里 nike 拼写如此)
    private String gender;
    @Column("avatarUrl")
    private String avatarurl;
    private String city;
    private String country;
    @Column("unionId")
    private Integer unionid;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

两点经验:

  1. 登录态直接用 openid 。后面 Sa-Token 里 StpUtil.login(openid),getLoginIdAsString() 取到的就是它。这样「当前是谁」不用再查一次表。
  2. 不要把任何敏感凭据(如教务系统密码)和用户基础信息放同一张宽表里。课表绑定走独立的表(user_student),权限也拆成了 roles / user_roles / user_permissions。

二、投稿:把微博「镜像」成自己的表

墙建在微博上(见第 0 篇),所以投稿对应的表就是微博状态的镜像 weibos_status。字段基本照搬微博接口的返回:

@TableName("weibos_status")
public class Status {
    @TableId  private String id;        // 微博 id
    private String mid;
    private String idstr;
    private String text;                // 正文
    private String source;              // 来源
    private String thumbnailPic;        // 缩略图
    private String bmiddlePic;          // 中图
    private String originalPic;         // 原图
    private String userId;
    private Integer repostsCount;       // 转发数
    private Integer commentsCount;      // 评论数
    private Integer attitudesCount;     // 点赞数
    // ……
}

为什么不自己另起一套字段?因为镜像的意义就是「和上游对得上」 :同一条微博,你库里的 id 和微博的 id 一致,删除、更新、关联评论都能用同一个键。
还有一张 status_user 记录「这条投稿是谁发的」——微博上只会显示墙号,真正的提交人 openid 只存在你自己的库里 ,这是后面封禁、限流、追责的依据。

三、评论、图片

@TableName("weibos_comments")
public class Comments {
    @TableId private Long id;
    private LocalDateTime createdAt;
    private String text;
    private String statusId;   // 属于哪条微博
    private String rootid;     // 回复链根评论
    private String userId;
    private String name;
    @TableField(exist = false)
    private Boolean miniProgram; // 是否来自小程序(不入库,查询时计算)
}

@TableName("image")
public class Image {
    @TableId(type = IdType.AUTO) private Integer id;
    private String status;
    private String filename;
    private String openid;     // 谁上传的
    private String url;        // MinIO 访问地址
    private LocalDateTime uploadTime;
}

注意 @TableField(exist = false):这是非表字段 ,只是给前端用的派生值,MyBatis-Plus 会忽略它。

四、规则:敏感词与封禁

@TableName("sensitive_word")
public class SensitiveWord {
    @TableId(type = IdType.AUTO) private Integer id;
    private String word;
    private Integer level;     // 1 = 拦截,2 = 替换
    private String replace;    // level=2 时替换成什么
}

@TableName("user_ban")
public class UserBan {
    @TableId private String userid;
    private String cause;      // 封禁原因
    @Column("ban_date") private LocalDateTime banDate;
    private Integer isWork;    // 是否生效(软封禁,可解封)
}

两个设计取舍:

  • 敏感词分两级 :一级直接拦截并记录,二级自动替换成指定文字。这比「一刀切拒绝」体验好得多。
  • 封禁用isWork 而不是删除行:解封只要改一个字段,历史记录还在,审计时有据可查。

另外还有一张 sensitive_word_log 记录每次命中(词、内容、用户、时间),「多次记录会被封号」就是靠它统计的。

五、运营:反馈、管理员、配置

这些是后来才加的,用「纯新增、可重复执行」的 SQL 脚本管理,直接贴一段 sql/admin.sql 的风格:

-- 动态配置:管理页可调的运行参数;缺行时使用代码内注册表默认值
CREATE TABLE IF NOT EXISTS sys_config (
  config_key   VARCHAR(128) NOT NULL,
  config_value VARCHAR(512) NOT NULL DEFAULT '',
  description  VARCHAR(255) NOT NULL DEFAULT '',
  updated_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (config_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理端动态配置';

几个值得学的细节:

  1. CREATE TABLE IF NOT EXISTS :脚本可以在生产反复执行,不怕重复。
  2. updated_at ... ON UPDATE CURRENT_TIMESTAMP :更新时间交给数据库维护,代码里不用管。
  3. COLLATE utf8mb4_unicode_ci + utf8mb4:必须是 utf8mb4,否则用户投稿里的 emoji 会存不进去。
  4. 配置表「缺行用默认值」 :代码里有一份默认值注册表,数据库只存被运营改过的项,降低了出错面。
  5. 管理端所有变更操作(登录、回复、删除、封禁、改配置)写入 admin_audit_log,谁在什么时候对什么做了什么 都有留痕。

反馈表同理,feedback 里有一个小细节值得抄:

openid VARCHAR(128) NOT NULL DEFAULT '' COMMENT '提交人 openid(来自服务端 token,不信任客户端)',
content TEXT NOT NULL COMMENT '反馈内容(入库前已做 HTML 转义,防 XSS)',

「不信任客户端」「入库前转义」直接写进字段注释,是给未来的自己(和接手的人)最好的提醒。

小结:一张表速查

表作用
user用户,openid 为登录身份
weibos_status / weibos_comments墙上的投稿与评论(微博镜像)
status_user投稿 ↔ 真实提交人
image上传图片记录
sensitive_word / sensitive_word_log敏感词与命中日志
user_ban封禁
feedback / feedback_images用户反馈
admin_user / admin_audit_log / sys_config管理后台
articles公告文章(存 Markdown 原文 + 渲染后的 HTML)

本篇的几条经验

  1. 建表前先列「名词」,一个名词一张表,再想关系;
  2. 镜像上游数据时,主键和上游保持一致 ;
  3. 「是否生效」类状态用字段而不是删行;
  4. 脚本幂等(IF NOT EXISTS)+ 字段注释写清约束;
  5. 一定用 utf8mb4。

下一篇,我们搭后端项目,并解决一个绕不开的问题:WebFlux 里怎么安全地调用阻塞代码 。

最后修改:2026 年 11 月 12 日
如果觉得我的文章对你有用,请随意赞赏