Vue3+Express+MySQL+Docker实现明日方舟社区网站全栈开发 在视频平台上经常能看到开发者用几十小时搭建一个主题网站的分享。这类项目最终未必会成为大流量产品但过程中涉及的需求拆解、后端设计、前端联调、部署排错恰好覆盖了一名玩家型开发者转独立开发会遇到的大部分问题。围绕“明日方舟”主题搭建一个社区网站就是一个非常典型的案例用户需要注册登录、发帖展示攻略和创作、评论互动、按关键词搜索内容还希望网站能稳定上线访问。这篇文章从这类项目出发梳理如何用 Vue 3 Express MySQL Docker 从零搭建一个可运行、可扩展的明日方舟社区网站并给出每一步的关键代码和排查思路。如果已经有 Node.js 和 Vue 的基础这套流程可以在 1 到 2 天内跑通。如果是从零开始学也可以把它当成一个综合练手项目按模块逐个实现。开发时先做最小可用版本后续再考虑优化性能、增加实时聊天和完善安全策略。1. 明日方舟社区网站要做什么技术栈怎么选1.1 先把“社区网站”拆成具体功能“社区网站”是一个很宽泛的说法。明日方舟玩家需要的不只是内容展示而是一个能讨论干员强度、分享攻略、发布同人创作、互相评论点赞的地方。落实到开发上至少需要这些能力用户注册、登录和身份校验不能每个人发帖都匿名。帖子发布和列表展示支持文本、标题和图片。帖子详情页和评论功能让用户围绕内容互动。点赞功能让优质内容更容易被看到。搜索功能按标题或内容找到相关帖子。后台/审核机制避免垃圾信息和违规内容失控。这些功能并不是一次性全部做完。和时间紧张的项目一样应该先保证“用户能注册、能发帖、能看帖、能评论”这条主链路跑通再逐步加上点赞、搜索、排行榜和实时聊天。1.2 功能模块优先级速查表下面这张表可以用于规划开发顺序。优先级 P0 是必须项P1 是上线前建议完成项P2 和 P3 可以根据精力分批做。功能模块优先级说明用户注册/登录P0使用 JWT 保持登录态密码哈希存储帖子发布/列表/详情P0文本内容、标题、图片、分页评论功能P0登录用户可评论帖子详情页展示点赞功能P1同一用户只能点赞一次需要防重复关键词搜索P1先使用 MySQL 的 LIKE再评估全文索引排行榜/热门内容P2按点赞数或浏览数排序实时聊天P3后续用 WebSocket 或第三方服务扩展内容审核/举报P1至少实现帖子删除和用户封禁接口1.3 为什么选择 Vue 3 Express MySQL Docker这套组合不是唯一选择但对独立开发者和从零开始的玩家型开发者比较友好。Vue 3 Vite 提供了清晰的前端组件化开发方式社区中的 UI 库和导航库丰富能快速搭出页面框架。Express 是 Node.js 生态中最轻量的后端框架之一注册路由、挂载中间件、处理 JSON 请求都很直观。MySQL 适合存储结构化社区数据。帖子、用户、评论、点赞之间有关系关系型数据库写 SQL 查询比文档型数据库更容易理解。Docker Compose 可以把 MySQL、后端和前端一起编排起来避免环境不一致的问题。本地开发和服务器部署可以使用同一套配置。后端也可以换成 Java Spring Boot 或 Go但对只熟悉 JavaScript 的开发者来说Express 的学习曲线更低。数据库也可以换成 PostgreSQL不过 MySQL 的文档和部署资料更多遇到问题更容易搜索到答案。2. 环境准备和项目初始化2.1 本地开发环境要求建议先确认本机软件版本避免出现“代码没写错但环境不兼容”的问题。软件建议版本用途Node.js18 及以上运行 Express 后端和 Vite 前端npm9 及以上安装 JavaScript 依赖MySQL8.0 及以上存储社区业务数据Docker20.10 及以上本地一键启动整套服务Git2.30 及以上管理项目版本检查命令node -v npm -v mysql --version docker --version如果还没有安装 MySQL直接用 Docker 启动一个临时实例更省事docker run -d --name ark-mysql \ -e MYSQL_ROOT_PASSWORDark-password \ -e MYSQL_DATABASEarknights_community \ -p 3306:3306 \ mysql:8.0注意本地开发时密码可以简单但生产环境必须使用强密码并且不要把密码提交到 Git。2.2 创建前后端项目目录整个项目采用 monorepo 风格一个仓库下包含server和client两个目录。mkdir arknights-community cd arknights-community mkdir server cd server npm init -y npm install express mysql2 cors jsonwebtoken bcryptjs multer dotenv依赖说明expressWeb 框架。mysql2连接 MySQL支持 Promise。cors解决浏览器前后端跨域问题。jsonwebtoken生成和校验 JWT。bcryptjs对用户密码做哈希。multer处理图片上传。dotenv读取.env配置文件。然后在项目根目录初始化前端cd .. npm create vitelatest client -- --template vue cd client npm install npm install axios vue-router pinia这里使用 Vite 官方脚手架创建 Vue 3 项目Vite 默认支持热更新非常适合调试社区页面。2.3 创建项目基础结构前后端目录最终大致如下arknights-community/ ├── server/ │ ├── src/ │ │ ├── routes/ │ │ ├── controllers/ │ │ ├── db.js │ │ └── index.js │ ├── uploads/ │ └── .env ├── client/ │ ├── src/ │ │ ├── api/ │ │ ├── views/ │ │ ├── router/ │ │ └── components/ │ └── vite.config.js └── docker-compose.yml实际编码时不需要一步到位先创建目录再按功能模块填充文件即可。关键是.env文件要和源码区分开里面存放数据库连接信息、JWT 密钥、监听端口等敏感配置。3. 数据库设计社区网站的核心表要提前定好3.1 用户、帖子、评论、点赞四张表社区网站最核心的四个实体是用户、帖子、评论、点赞。先看用户表CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(100) NOT NULL, avatar VARCHAR(255) DEFAULT , role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;用户表中不要直接存明文密码而是存bcrypt生成的哈希字符串。role字段用来区分用户和管理员社区后期需要管理员删帖时很有用。帖子表CREATE TABLE post ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, title VARCHAR(120) NOT NULL, content TEXT NOT NULL, category VARCHAR(20) DEFAULT , cover VARCHAR(255) DEFAULT , view_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_category (category), CONSTRAINT fk_post_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;category可以存放“干员评测”“剧情讨论”“同人创作”“新手求助”等分类。cover存帖子封面图路径浏览时会比从正文中解析图片更高效。评论表CREATE TABLE comment ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, post_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, content VARCHAR(1000) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_post_id (post_id), CONSTRAINT fk_comment_post FOREIGN KEY (post_id) REFERENCES post(id) ON DELETE CASCADE, CONSTRAINT fk_comment_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;删除帖子时应该连同评论一起删除。设置ON DELETE CASCADE后MySQL 会在删除主表记录时自动删除关联评论避免遗留脏数据。点赞表CREATE TABLE post_like ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, post_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_post_user (post_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;点赞表的关键是post_id和user_id组成唯一索引。这样即使用户快速点击两次接口数据库也会拒绝第二条重复数据。3.2 是否需要真正使用外键上面的 SQL 中使用了外键FOREIGN KEY。这种方式在小项目里能保证数据一致性但在高并发大流量场景下外键会带来额外的锁开销和约束检查成本。推荐做法是开发阶段或项目初期使用外键避免出现孤立数据。如果后续流量变大需要拆库拆表再考虑去掉外键通过应用层和定时任务做一致性处理。查询时使用JOIN依然可以关联用户信息不需要依赖外键。3.3 写入一批测试数据建完表后先手动插入几条测试数据方便前端联调接口。INSERT INTO user (username, password_hash) VALUES (admin, $2a$10$dummyhash), (player01, $2a$10$dummyhash); INSERT INTO post (user_id, title, content, category) VALUES (1, 新手开荒干员推荐, 前期比较推荐使用近卫和狙击干员……, 新手求助), (2, 危机合约阵容分享, 这期合约打法比较考验地面阻挡……, 攻略分享);注意这里的password_hash不能真的用明文。开发时应该通过注册接口写入或者用脚本先生成哈希再插入。4. 后端接口实现注册登录、发帖和评论4.1 数据库连接和基础入口编写server/src/db.js创建 MySQL 连接池const mysql require(mysql2/promise); require(dotenv).config(); const pool mysql.createPool({ host: process.env.DB_HOST || localhost, user: process.env.DB_USER || root, password: process.env.DB_PASSWORD || , database: process.env.DB_NAME || arknights_community, waitForConnections: true, connectionLimit: 10, }); module.exports pool;mysql2/promise的每个查询都会返回 Promise方便在async/await中使用。生产环境中连接池数量不要设置过小否则高并发时请求会排队。然后创建server/src/index.jsconst express require(express); const cors require(cors); require(dotenv).config(); const authRouter require(./routes/auth); const postRouter require(./routes/post); const commentRouter require(./routes/comment); const app express(); app.use(cors()); app.use(express.json()); app.use(/static, express.static(uploads)); app.use(/api/auth, authRouter); app.use(/api/posts, postRouter); app.use(/api/comments, commentRouter); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(Server running on port ${PORT}); });/static路径用于访问上传的图片uploads目录会自动创建但不要忘记在项目初始化时创建。4.2 注册登录接口和 JWT 身份认证注册接口要做两件事密码哈希入库、返回 JWT 给前端保存。// server/src/routes/auth.js const express require(express); const bcrypt require(bcryptjs); const jwt require(jsonwebtoken); const pool require(../db); const router express.Router(); const JWT_SECRET process.env.JWT_SECRET || dev-secret; router.post(/register, async (req, res) { const { username, password } req.body; if (!username || !password) { return res.status(400).json({ message: 用户名和密码不能为空 }); } const [rows] await pool.query(SELECT id FROM user WHERE username ?, [username]); if (rows.length 0) { return res.status(400).json({ message: 用户名已存在 }); } const hash await bcrypt.hash(password, 10); const [result] await pool.query( INSERT INTO user (username, password_hash) VALUES (?, ?), [username, hash] ); const token jwt.sign({ id: result.insertId, username }, JWT_SECRET, { expiresIn: 7d, }); res.json({ token, user: { id: result.insertId, username } }); }); router.post(/login, async (req, res) { const { username, password } req.body; const [rows] await pool.query(SELECT * FROM user WHERE username ?, [username]); if (rows.length 0) { return res.status(400).json({ message: 用户名或密码错误 }); } const user rows[0]; const ok await bcrypt.compare(password, user.password_hash); if (!ok) { return res.status(400).json({ message: 用户名或密码错误 }); } const token jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: 7d, }); res.json({ token, user: { id: user.id, username: user.username } }); }); module.exports router;代码中有几个关键点bcrypt.hash(password, 10)的第二个参数是成本因子越大越安全但耗时也越长。开发时 10 是合理值。JWT 的有效期设置为 7 天前端在网站请求中带上Authorization: Bearer token。登录成功后不应该返回password_hash所以这里只选了id和username。注意JWT 的开发密钥不能写死在代码里生产环境必须通过.env或配置中心注入。发布帖子之前需要实现一个简单的 JWT 中间件function auth(req, res, next) { const header req.headers.authorization || ; const token header.startsWith(Bearer ) ? header.slice(7) : null; if (!token) { return res.status(401).json({ message: 未登录 }); } try { const payload jwt.verify(token, JWT_SECRET); req.user payload; next(); } catch (e) { return res.status(401).json({ message: 登录已过期请重新登录 }); } }后续需要登录才能操作的接口直接在路由中传入auth中间件。4.3 帖子发布和图片上传帖子接口使用multer处理图片。先配置上传目录和文件过滤// server/src/config/upload.js const multer require(multer); const path require(path); const storage multer.diskStorage({ destination(req, file, cb) { cb(null, uploads/); }, filename(req, file, cb) { const ext path.extname(file.originalname); cb(null, ${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}); }, }); const upload multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter(req, file, cb) { if (!/^image\/(jpeg|png|webp|gif)$/.test(file.mimetype)) { return cb(new Error(仅支持图片文件)); } cb(null, true); }, }); module.exports upload;帖子创建接口// server/src/routes/post.js router.post(/, auth, upload.single(cover), async (req, res) { const { title, content, category } req.body; const userId req.user.id; if (!title || !content) { return res.status(400).json({ message: 标题和内容不能为空 }); } const cover req.file ? /static/${req.file.filename} : ; const [result] await pool.query( INSERT INTO post (user_id, title, content, category, cover) VALUES (?, ?, ?, ?, ?), [userId, title, content, category, cover] ); res.status(201).json({ id: result.insertId }); });图片上传成功后会返回/static/xxx.jpg这样的访问路径。因为 Express 已经挂载了express.static(uploads)浏览器可以直接访问这个路径。需要注意multer遇到非图片文件时会报错需要在全局错误处理中间件中捕获否则返回给前端的错误可能是 HTML 而不是 JSONapp.use((err, req, res, next) { res.status(400).json({ message: err.message || 服务器错误 }); });4.4 评论接口和帖子详情评论接口需要登录逻辑相对简单// server/src/routes/comment.js router.post(/:postId, auth, async (req, res) { const postId Number(req.params.postId); const { content } req.body; if (!content || content.length 1000) { return res.status(400).json({ message: 评论内容不能为空且不能超过1000字 }); } const [result] await pool.query( INSERT INTO comment (post_id, user_id, content) VALUES (?, ?, ?), [postId, req.user.id, content] ); res.status(201).json({ id: result.insertId }); });帖子详情页除了要返回帖子本身还需要带上作者信息和评论列表。为了减少请求次数可以设计成两个接口GET /api/posts/:id返回帖子详情。GET /api/comments/:postId返回评论列表。如果流量不大也可以一次返回帖子加评论减少前端加载次数。这里建议分开原因是要对评论单独做分页。5. 前端页面联调跑通帖子列表和登录页面5.1 Vue Router 路由设计前端至少需要这几个页面路由配置放在client/src/router/index.jsimport { createRouter, createWebHistory } from vue-router; const routes [ { path: /, name: Home, component: () import(../views/Home.vue) }, { path: /login, name: Login, component: () import(../views/Login.vue) }, { path: /register, name: Register, component: () import(../views/Register.vue) }, { path: /post/new, name: NewPost, component: () import(../views/NewPost.vue) }, { path: /post/:id, name: PostDetail, component: () import(../views/PostDetail.vue) }, ]; const router createRouter({ history: createWebHistory(), routes, }); export default router;这里使用了 Vite 的import()动态导入页面按需加载首屏速度会更快。如果项目不需要 SEO这种方式足够。5.2 统一请求封装和登录态管理创建一个client/src/api/http.jsimport axios from axios; const http axios.create({ baseURL: http://localhost:3000/api, timeout: 10000, }); http.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); export default http;所有需要登录的接口都会自动携带 JWT。前端路由守卫中也要判断登录状态router.beforeEach((to) { const token localStorage.getItem(token); if (to.path /post/new !token) { return /login; } });5.3 帖子列表页如何分页和渲染帖子列表接口返回分页信息前端用一个循环渲染即可。示例Home.vue核心逻辑script setup import { ref, onMounted } from vue; import http from ../api/http; const posts ref([]); const page ref(1); const total ref(0); async function loadPosts() { const { data } await http.get(/posts, { params: { page: page.value, pageSize: 10 }, }); posts.value data.list; total.value data.total; } onMounted(loadPosts); /script template div classpost-list article v-forpost in posts :keypost.id h2 RouterLink :to/post/${post.id}{{ post.title }}/RouterLink /h2 p{{ post.content.slice(0, 120) }}.../p span作者{{ post.username }} 分类{{ post.category }}/span /article /div /template这里用slice截取内容已经展示了列表摘要但更专业的做法是后端再返回一个summary字段避免把全文都传给前端。在真实项目中列表接口不应该返回完整的content正文否则数据量会非常大。后端列表接口的 SQL 如下使用LEFT JOIN联查用户名SELECT p.id, p.title, p.content, p.category, p.cover, p.view_count, p.created_at, u.username FROM post p LEFT JOIN user u ON p.user_id u.id ORDER BY p.created_at DESC LIMIT ? OFFSET ?分页参数在 Express 中需要转换为数字否则可能被拼成字符串const page Number(req.query.page) || 1; const pageSize Number(req.query.pageSize) || 10; const offset (page - 1) * pageSize;如果用户传入负数或非数字Number()会得到NaN造成 SQL 报错。建议在查询前校验if (!Number.isInteger(page) || page 1) { return res.status(400).json({ message: 分页参数不合法 }); }5.4 登录页和 Token 保存登录页表单提交后把返回的 token 存入localStorage并跳到首页async function handleLogin() { const { data } await http.post(/auth/login, { username: username.value, password: password.value, }); localStorage.setItem(token, data.token); localStorage.setItem(user, JSON.stringify(data.user)); router.push(/); }把 token 保存在localStorage有 XSS 风险。如果对安全要求较高可以选择把 token 放在 HttpOnly Cookie 中由后端设置 Cookie前端不再读取 token。小项目为了方便通常先用localStorage但上线前要评估风险。6. 用 Docker Compose 一键部署6.1 编写后端和前端 Dockerfile后端server/DockerfileFROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY src ./src COPY uploads ./uploads EXPOSE 3000 CMD [node, src/index.js]前端client/DockerfileFROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf前端构建完后只需要把静态文件复制到 Nginx 镜像中不需要在容器里运行 Node 服务。6.2 配置 Nginx 反向代理client/nginx.confserver { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://backend:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { proxy_pass http://backend:3000; } location / { try_files $uri $uri/ /index.html; } }这里用 Docker 内网服务名backend访问后端容器。前端 Vue Router 使用history模式时刷新页面会请求一个具体路径Nginx 需要把所有未命中的请求都回退到index.html否则会出现 404。6.3 docker-compose.yml 完整配置在项目根目录创建docker-compose.ymlversion: 3.8 services: mysql: image: mysql:8.0 container_name: ark-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ark-password MYSQL_DATABASE: arknights_community ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql - ./server/sql/init.sql:/docker-entrypoint-initdb.d/init.sql backend: build: ./server container_name: ark-backend restart: unless-stopped environment: DB_HOST: mysql DB_USER: root DB_PASSWORD: ark-password DB_NAME: arknights_community JWT_SECRET: replace-with-strong-secret ports: - 3000:3000 volumes: - ./server/uploads:/app/uploads depends_on: - mysql frontend: build: ./client container_name: ark-frontend restart: unless-stopped ports: - 8080:80 depends_on: - backend volumes: mysql-data:这里把server/sql/init.sql挂载到 MySQL 容器的初始化目录首次启动就会自动建表并插入测试数据。注意如果mysql-data卷已经存在MySQL 不会再次执行初始化脚本。想要重跑初始化脚本需要删除卷后重启。启动命令docker compose up -d --build docker compose ps查看容器日志docker compose logs -f backend启动后访问http://localhost:8080就能看到社区首页。如果图片上传目录没有做卷映射Docker 重建镜像后上传的图片会丢失所以uploads目录一定要挂载到宿主机。注意Docker Compose 适合个人项目和中小流量站点。生产环境还需要额外考虑进程守护、日志采集、数据库备份和横向扩容不能只停留在“容器能跑”这一步。7. 常见问题排查从开发到上线最容易踩的坑7.1 前端请求后端报 CORS 错误现象浏览器打开前端页面调用http://localhost:3000/api/auth/login时提示跨域。常见原因前端运行在http://localhost:5173后端运行在http://localhost:3000两个端口不同属于跨域请求。排查方式确认后端是否安装了cors并执行了app.use(cors())。确认浏览器控制台报错信息是CORS还是404。生产环境中确认 Nginx 反向代理是否配置正确。解决方式有两种后端开放跨域使用cors中间件简单直接适合开发环境。前端 Vite 代理在vite.config.js中配置server.proxy让/api请求转发到后端。// vite.config.js export default { server: { proxy: { /api: http://localhost:3000, }, }, };生产环境推荐用 Nginx 做反向代理这样前端请求的地址和后端接口地址一直同源几乎不会有跨域问题。7.2 端口被占用导致服务启动失败现象执行npm run dev后报错EADDRINUSE。排查方式Linux/macOS 使用lsof -i :3000。Windows 使用netstat -ano | findstr :3000。找到占用进程后确认是否可以结束不可以就修改后端监听端口。解决方案# 找到 PID 后结束进程 kill -9 12345 # 或者改用其他端口 PORT3001 node src/index.js注意前端和 Nginx 中也要同步修改端口否则会出现“后端明明启动了但前端请求 404”的情况。7.3 图片上传后访问返回 404现象multer提示上传成功数据库也存了/static/xxx.jpg但浏览器打开图片地址却 404。常见原因Express 没有挂载静态目录或 Docker 容器里/app/uploads目录不存在。排查方式先确认项目根目录是否存在uploads目录不存在就mkdir uploads。确认后端代码是否执行了app.use(/static, express.static(uploads))。如果在 Docker 中用docker exec -it ark-backend ls /app/uploads查看容器内是否有文件。解决方案mkdir -p server/uploads同时在docker-compose.yml中挂载 uploads 目录避免容器重建后文件丢失。7.4 MySQL 连接被拒绝ECONNREFUSED现象后端启动时报错connect ECONNREFUSED 127.0.0.1:3306。常见原因本地没启动 MySQL或者 Docker 容器中使用了localhost。排查方式本地开发先执行mysql -uroot -p确认可以连接。容器内后端访问 MySQL 时DB_HOST要写成mysql而不是localhost。确认 MySQL 容器端口是否映射到宿主机 3306。解决方案docker start ark-mysql或者在server/.env中重新指定数据库主机地址。7.5 帖子列表分页参数不合法导致接口 500现象前端传pageabc或page-1时后端查询报错。原因没有对分页参数做类型校验。解决方式const page Number(req.query.page); if (!Number.isInteger(page) || page 1) { return res.status(400).json({ message: page 参数不合法 }); }这个坑在接口联调阶段非常常见建议在所有接收查询参数或路径参数的地方都做基础校验。8. 上线前检查清单和扩展方向8.1 上线前三十分钟检查清单检查项检查方式说明敏感信息是否外泄检查.env和docker-compose.yml是否提交到 GitJWT 密钥、数据库密码绝不能公开上传文件限制是否生效上传一个超过 5MB 的图片multer的limits和后端错误处理是否返回 JSON数据库备份策略确认是否配置定时备份使用mysqldump或云数据库自带备份HTTPS 是否生效访问https://域名并检查证书社区网站会收集用户密码必须使用 HTTPS内容审核能力管理账号是否进入后台能删帖至少要能屏蔽违规帖子日志目录是否持久化确认容器日志不会被重启清空使用 Docker volume 或日志采集工具静态资源是否走 CDN检查图片加载速度如果用户分布广建议为uploads配置 CDN依赖是否存在漏洞运行npm audit上线前处理高危依赖漏洞8.2 社区网站还能怎么扩展这个项目跑通之后可以从多个方向继续深化而不是急着从头开发新项目。实时聊天可以接入 WebSocket 或使用类似 Socket.IO 的方案让玩家在帖子之外建立更直接的沟通。内容推荐可以根据浏览历史和点赞记录推荐同类干员攻略。图片资源多的时候可以把uploads目录迁移到对象存储 OSS。搜索功能如果对中文分词有要求可以引入 Elasticsearch 或使用 MySQL 的全文本索引但维护成本会高不少。还有一个容易忽略的点是 SEO。Vue 的 SPA 页面搜索引擎默认只能看到空壳 HTML如果希望网站被百度、Google 收录需要做服务端渲染SSR或者预渲染方案。明日方舟攻略类内容天然适合搜索流量所以 SEO 对这类社区网站影响很大。8.3 对独立开发者的实践建议这套项目用 100 小时可以完成到什么程度取决于你对前端、后端、部署的熟悉程度。第一次做时间主要消耗在环境配置和排错上这很正常。比较务实的做法是先写死数据把前端页面搭好再逐个替换成真实接口。不要把第一次目标定成“上线一个完美产品”而是“把一个最小闭环完整走通”。等整个流程经历过一遍再回头优化架构、加测试、补监控就不会有那么大的心理压力了。最后想强调一点社区类网站的核心不是代码写得有多优雅而是如何持续运营和维护。代码层面做好权限控制、数据备份和审核机制内容运营层面鼓励玩家产出高质量攻略网站才有长期价值。开发完成只是开始后面每一次版本迭代和内容治理才是真正考验耐心和判断力的地方。