《从零开发校园墙微信小程序》第 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;
}两点经验:
- 登录态直接用 openid 。后面 Sa-Token 里
StpUtil.login(openid),getLoginIdAsString()取到的就是它。这样「当前是谁」不用再查一次表。 - 不要把任何敏感凭据(如教务系统密码)和用户基础信息放同一张宽表里。课表绑定走独立的表(
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='管理端动态配置';几个值得学的细节:
CREATE TABLE IF NOT EXISTS:脚本可以在生产反复执行,不怕重复。updated_at ... ON UPDATE CURRENT_TIMESTAMP:更新时间交给数据库维护,代码里不用管。COLLATE utf8mb4_unicode_ci+utf8mb4:必须是utf8mb4,否则用户投稿里的 emoji 会存不进去。- 配置表「缺行用默认值」 :代码里有一份默认值注册表,数据库只存被运营改过的项,降低了出错面。
- 管理端所有变更操作(登录、回复、删除、封禁、改配置)写入
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) |
本篇的几条经验
- 建表前先列「名词」,一个名词一张表,再想关系;
- 镜像上游数据时,主键和上游保持一致 ;
- 「是否生效」类状态用字段而不是删行;
- 脚本幂等(
IF NOT EXISTS)+ 字段注释写清约束; - 一定用
utf8mb4。
下一篇,我们搭后端项目,并解决一个绕不开的问题:WebFlux 里怎么安全地调用阻塞代码 。