跳到正文
微校 WeSmile

技术架构

四端同源是怎么做到的

只有一个答案:判据全在服务端,四个端都只是它的显示器。下面是这句话落到工程上的样子。

技术选型

服务端
Spring Boot 4 · Spring Data JPA · Redis · MySQL 8 · JDK 25
网页与管理台
Next.js 16 App Router · shadcn/ui · Tailwind CSS 4 · TanStack Query
小程序
微信原生框架(非 uni-app / Taro)· TypeScript 逻辑层
存储
MySQL 8 · Redis · 对象存储(客户端直传)

按业务域纵向切分

不是按 controller / service / repository 分层——那样一个目录会装近百个文件,改一处业务要同时打开四个目录。现在改树洞只需要打开一个目录。

  • admin126

    管理端的全部接口:审核、运营配置、订单、权限

  • user69

    资料、关注、隐私、签到、举报、角色

  • message65

    系统消息、私聊额度、WebSocket、订阅消息

  • ugc63

    动态、话题、点赞收藏分享、搜索

  • campus48

    学校、学院、校区、地点

  • showcase39

    广告位、表情、诗词、轮播、能力标签

  • form35

    问卷表单、题目、CP 匹配

  • student34

    学生身份认证(邮箱 / 照片 / 答题)

  • activity27

    活动、场次、报名、活动评论

  • score23

    微校分、充值、兑换、支付

  • organization22

    社团组织与身份

  • course21

    课程、教师、教室、课程评价

  • treehole16

    匿名树洞

  • tracking14

    打开事件与行为埋点

  • attachment14

    附件(多域共用,独立成域)

  • wechat9

    小程序扫码登录与内部回调

  • idle11

    闲置交易

  • ai11

    AI 供应商与内容初筛

  • comment7

    评论(从 ugc 拆出,四类内容共用)

右上角的数字是该域的文件数,条形按最大的那个域等比缩放。跨域共用的东西只放 shared,且必须真的跨域。

浏览器里的那两个端

「学生在浏览器里也能用」「校级管理员自己就能用起来」——这两句光靠文字说都没有说服力。

网页版

与小程序共用同一套后端、同一批账号与内容。

  • 新鲜事:桌面三栏,右上角是浏览范围
  • 动态详情与楼中楼评论
  • 私信:与同学的一对一会话
  • 小程序扫码确认:当前账号清楚显示,确认后网页才登录

运营管理台

审核、运营配置、题库、配对、订单都在这里。

  • 数据概览:校级管理员看到的是本校的数字
  • 学生认证审核:AI 初筛给的理由摆在显眼处,人做最后一判(证件照已打码)
  • 内容审核:删除是软删,勾「含已删除」可回看并一键恢复
  • CP 配对工作台:待匹配与已配对一目了然,人来拍板
  • 投放范围:同一条轮播可以只给一所学校、甚至只给一个校区看
  • 通用订单:买的是什么是一个字段,不是写死的「充值」

接口契约

这五条是写死的。四个端各自实现一次客户端,靠的就是它们不变。

  1. 01HTTP 状态码表意

    成功就是 2xx 且响应体直接是数据,没有外层包裹;失败是对应的状态码,响应体固定为 {error, message}。分支判断只看 error 常量,永远不比对 message——文案会改,还会随语言变。

  2. 02401 分三态

    TOKEN_EXPIRED 触发单飞刷新后重放原请求;TOKEN_MISSING 与 TOKEN_INVALID 直接回登录态。三态不分开的话,一次过期就会把用户踢回登录页。

  3. 03游标分页

    {start, limit} 换 {elements, total, hasMore, nextStart}。start 是不透明游标,续页只能用上一页给的 nextStart——自己算 start + limit 会串页。

  4. 04空字段被省略

    服务端 JSON 不下发 null。所以读任何可空字段之前先回答一句:这个键不存在的时候算什么?把它写进代码注释里,比记在脑子里可靠。

  5. 05错误文案有语言

    服务端按 Accept-Language 返回对应语言的错误文案。所以客户端固定发自己界面的语言,而不是透传浏览器的——否则中文界面里会冒出英文提示。

认证与令牌

  • 学生侧

    自建不透明令牌存在 Redis 里。访问令牌 4 小时且每次请求滑动续期,刷新令牌 7 天且一次性使用。同一用户重复登录会吊销之前的令牌。Redis 里存的只是一个 User:123 的引用,用户信息每次读取时回查——所以处罚、认证状态一改立刻生效,不用等令牌到期;也没有一份序列化的用户对象躺在缓存里。

  • 两个登录入口

    小程序走微信 code 登录;网页展示打开确认页的小程序码。扫码后小程序先用自己的令牌确认当前用户,用户明确点确认,浏览器轮询拿一次性 Web 令牌。身份直接来自小程序会话。

  • 管理侧

    与学生是两套完全独立的账号体系,令牌互不通用。访问令牌是 30 分钟的 JWT,服务端登记 jti 所以能即时吊销;刷新令牌不透明、7 天、用完即焚——因此客户端的单飞刷新是必需品而不是优化。

四个端各自的取舍

这些检查是拿故障换来的

这个项目最常见的故障有一个共同点。它不报错:没有异常、没有红日志、请求照样 200 —— 只是那件事没发生。这类问题 review 看不出来(代码本身没毛病),单测也测不了(测什么?测一个字符串等于另一个字符串?),只能一类一类单独拦一道。下面每一条都对应一次真的踩过。

  • 模板绑到了不存在的东西

    小程序对悬空绑定是静默的:bindtap 指向一个不存在的方法,点上去什么都不发生;wx:if 里引用一个不存在的字段,条件恒假,那一块永远不出现。重命名时只改了一边就是这种症状 —— 页面空着,控制台一行日志都没有。

  • 图标类名写错了一个词

    图标是纯 CSS 背景图,类名写错时那个位置只是空着 —— 而「空着」和「这里本来就没图标」在界面上一模一样。网页登录确认页重写时写了个 icon-info-muted,图标表里只有 icon-close-muted 那一族,结果态的圆圈里什么都没有。是逐个核对图标表才发现的,不是页面告诉我的。

  • 推送模板 id 被写死在客户端

    十几处调用点各自硬编码一串模板 id,而服务端配额认的是另一套码表,两边谁也对不上谁。授权框照弹、用户照点、接口照返回 200,只是推送永远不到。这是这个项目上一个真实故障的根因。

  • 漏翻的 key 被原样发给用户

    服务端返回的是文案 key,翻译表里没有就原样下发。学生撤回一条不存在的入驻申请时,弹窗里写的是 entity.school_application。而它只在「那条 404 真的发生」的那一刻现形 —— 接口通、功能对、平时一切正常。

  • 拦截名单靠字符串对上

    受限动作名单里放的是路径字符串,和 controller 上的注解没有任何编译期联系。谁把 /dm/message 改个名,那一条就再也匹配不上 —— 被禁言的用户从那一刻起能发私信,而没有任何测试会红、任何日志会响。放行名单失配只是白放一条;拦截名单失配是放开一条。

  • 响应体里塞了整个实体

    把 JPA 实体直接放进 VO,实体上有什么就发什么。闲置详情里的卖家字段就这么带出了 openid、unionId、手机号、学生邮箱、地址 —— 列表页一次是一整页的量。讽刺的是那 8 个隐私开关管的是 QQ、微信号、生日,比这些轻得多,还有专门的单测盯着。

用棘轮,不用清单

还没清干净的那些(比如仍写死在服务端的中文提示)不列成一份「待迁清单」—— 清单是第二个真相来源:会忘记更新、会过期,而且没人去对账。改成让测试自己说还差哪些文件:新增一处,构建当场红;转掉一处就从名单里删掉。名单只会变短。

会误报的检查比没有检查更糟

先被无视,然后被绕过,最后连它抓到的真问题也一起没人看了。所以每条检查的判据都刻意收窄,只报确定性错误:字段的深层属性不查(那要类型信息),wxs 语法只收实机真撞出过编译错误的写法,导航栏底色按解析出来的 token 判而不是按类名判 —— 按类名判会把七八个正常页面全部误报。

121
服务端测试类
11
小程序静态检查

部署需要准备什么

系统本身自部署。真正的前置条件是这几个外部账号,其中两个的申请周期比写代码长。

所有凭据从环境变量注入,缺一个应用在启动期就会失败并指出缺哪一项——不会带着未解析的占位符跑起来。