内部人员与活动管理平台:全栈复盘
项目概述
面向组织内部的 人员主数据、活动运营、知识资产与积分体系 的一体化平台,覆盖 Web 管理端 + 小程序/H5。典型能力包括:多字段人员档案、活动报名与签到、知识库上传与检索、积分排行与兑换、基于机构层级的 RBAC,以及 小程序与 Web 的联合登录体验(扫码登录 Web)。
技术栈:Vue 3、Ant Design Vue、微信小程序;后端 Spring Boot、MyBatis、MySQL(可按实际项目替换)。
重点模块与技术决策
1. 活动剪影:富文本编辑、存储与多端展示
需求:运营在 PC 侧编辑图文剪影;小程序侧一致展示;图片多、体积不确定。
方案对比与取舍:
| 方案 | 思路 | 优点 | 风险/成本 |
|---|---|---|---|
| A. HTML 字符串入库 | 正文 longtext + 图片独立上传 URL引用 | DB 体积小;单图可复用 | 孤儿文件需 GC;首屏请求多图 |
| B. 单文件 HTML | 图文合一(含 base64)上传对象存储 | 小程序 web-view 一条 URL 即可 | base64 膨胀约 33%+;大文章内存压力 |
落地:采用 方案 A 为主:wangEditor 等成熟编辑器 + 图片异步上传;配套 删除图片接口 与引用计数/定期清理策略,控制存储泄漏。
小程序展示:方案 A 下接口返回 HTML 字符串,经 rich-text /解析组件(或白名单标签策略)渲染;方案 B 可走 web-view + 业务域名白名单。
示例:wangEditor 5.x 自定义图片上传(走统一 OSS 接口;包名以官方为准)
import '@wangeditor/editor/dist/css/style.css';
// @wangeditor/editor-for-vue 版本需与 Vue 主版本对齐
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
import { api } from '@/api';
const editorConfig = {
MENU_CONF: {
uploadImage: {
async customUpload(file: File, insertFn: (url: string) => void) {
const form = new FormData();
form.append('file', file);
const { data } = await api.post('/upload/image', form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
insertFn(data.url); // 插入可访问 URL,正文里仅存 img src
},
},
},
};2. 文件上传、分片与预览
上传策略:以 10MB 为界——以下走 单文件直传;以上走 分片上传 + 合并,合并目录以 文件 MD5 为逻辑主键,保证断点续传与幂等。
工程细节:大文件 MD5 计算放入 Web Worker,避免长任务阻塞主线程(见站内 Web Worker 优化)。
示例:分片上传 + 断点续传主流程(与后端约定 hash / index / merge)
import { api } from '@/api';
const CHUNK_SIZE = 2 * 1024 * 1024;
export async function uploadBigFile(file: File, computeMd5: (f: File) => Promise<string>) {
const fileHash = await computeMd5(file); // 建议在 Worker 中计算
const { data: uploadedIndexes } = await api.get<number[]>(`/upload/chunks?hash=${fileHash}`);
const uploaded = new Set(uploadedIndexes ?? []);
for (let i = 0; i * CHUNK_SIZE < file.size; i++) {
if (uploaded.has(i)) continue;
const start = i * CHUNK_SIZE;
const chunk = file.slice(start, start + CHUNK_SIZE);
const form = new FormData();
form.append('hash', fileHash);
form.append('index', String(i));
form.append('file', chunk);
await api.post('/upload/chunk', form);
}
await api.post('/upload/merge', { hash: fileHash, filename: file.name });
}预览:
- 图片:原生
image。 - 视频:浏览器/H5 对 H.264 兼容最好;小程序侧规避 H.265。项目在 上传前用 MP4Box 做封装/编码探测,不符合则拦截,避免「存了不能播」。大文件预览配合后端 分段响应(如 Spring
ResourceHttpRequestHandlerRange),缩短首帧时间。 - Office/PDF:Web 端
vue-office;小程序web-view+ 域名白名单。
示例:上传前用 mp4box.js 判断是否 H.264(思路示例)
import MP4Box from 'mp4box';
export function probeMp4(file: File): Promise<boolean> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => {
const mp4 = MP4Box.createFile();
mp4.onReady = (info: { videoTracks: { codec: string }[] }) => {
const v = info.videoTracks[0];
// 常见 H.264 对应 avc1 / avc3 等,具体以 mp4box 输出为准
resolve(v ? v.codec.startsWith('avc') : false);
};
mp4.appendBuffer(new Uint8Array(reader.result as ArrayBuffer));
mp4.flush();
};
reader.readAsArrayBuffer(file);
});
}3. Web 端扫码登录(小程序身份授权 Web)
模式:仅允许已登录小程序用户扫码授权 Web 会话,降低账号密码泄露面。
实现路径:
- 轮询:Web 端持临时票据短轮询后端,直至小程序确认;实现简单,注意间隔与过期。
- WebSocket:小程序确认后服务端推送 Web 侧,实时性好,需处理断线与重连。
两端通过 同一后端会话颁发 统一身份凭证;二维码 一次性、短 TTL,防止重放。
示例:Web 端轮询扫码结果(Vue 组合式)
import { ref, onUnmounted } from 'vue';
import { api } from '@/api';
export function useScanLogin(pollMs = 1500) {
const ticket = ref<string | null>(null);
let timer: ReturnType<typeof setInterval> | null = null;
async function startPoll(onOk: (token: string) => void) {
if (!ticket.value) return;
timer = setInterval(async () => {
const { data } = await api.get(`/auth/scan/${ticket.value!}`);
if (data.status === 'CONFIRMED' && data.token) {
clearInterval(timer!);
timer = null;
onOk(data.token);
}
}, pollMs);
}
onUnmounted(() => timer && clearInterval(timer));
return { ticket, startPoll };
}4. 双地图 SDK集成(百度 + 高德)
场景:PC 活动选址需 POI 检索与坐标统一;小程序侧唤起导航。
难点:多 SDK 全局污染、命名冲突、重复初始化。
方案:JS 沙箱 / 动态加载隔离(参见 JS 沙箱),按路由或组件生命周期挂载/卸载地图实例,统一 GCJ-02/WGS-84 等业务约定(与后端存储坐标系对齐)。
5. 接口安全:非对称加密
公网暴露接口采用 RSA 等对敏感参数或签名进行保护,防篡改与中间人风险;配合 HTTPS、时间戳、Nonce 组成完整方案(详见 RSA 加密)。
示例:请求体字段级加密(前端 JSEncrypt,仅演示形态)
import JSEncrypt from 'jsencrypt';
export function encryptPayload(publicKeyPem: string, plain: object) {
const enc = new JSEncrypt();
enc.setPublicKey(publicKeyPem);
const json = JSON.stringify(plain);
// RSA 有长度上限,实战常配合 AES 会话密钥;此处仅表达「敏感字段不入明文 JSON」
const cipher = enc.encrypt(json);
return { alg: 'RSA', cipher };
}难点归纳
- 大文件与弱网:分片、断点、MD5 幂等与 Worker offload。
- 多端一致:富文本与 Office在小程序上的 能力边界(
rich-textvsweb-view)。 - 地图与登录:第三方 SDK 隔离与会话安全。
复盘小结
项目在 「内部工具类产品」 约束下,仍涉及 大文件工程化、跨端渲染差异、开放接口安全 等典型中高级全栈问题。文档化 方案对比与取舍理由,有利于后续面试与晋升材料中体现 架构思维,而非仅功能罗列。