资源分享应用全栈实践:从后端到多端客户端的完整实现
可以扫码查看应用功能
本文详细介绍了一个完整的资源分享系统的设计与实现,涵盖后端 API、管理后台、以及支持 H5/小程序/App 三端的客户端应用。适合准备做类似项目的开发者参考。
📋 目录
一、项目背景
1.1 业务需求
在内容管理和资源分享场景中,需要一个统一的资源管理系统,支持:
- 资源分类管理:支持多级分类,灵活排序
- 资源详情展示:富文本内容、图片展示、链接提取
- 多端访问:H5、微信小程序、App 三端统一体验
- 后台管理:便捷的资源增删改查、分类管理
- 访问统计:自动记录资源访问量
1.2 技术选型
| 端 | 技术栈 | 说明 |
|---|---|---|
| 后端 | Node.js + Koa + Sequelize + MySQL | RESTful API,ORM 操作数据库 |
| 管理后台 | Vue 2 + Element UI | 后台资源管理界面 |
| 客户端 | uni-app + uView UI | 一套代码,多端打包 |
二、整体架构设计
2.1 系统架构图
┌─────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ H5 │ │ 小程序 │ │ App │ │
│ │ (uni-app)│ │ (uni-app) │ │ (uni-app)│ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ HTTP/HTTPS
▼
┌─────────────────────────────────────────────────────────┐
│ API 层 │
│ ┌──────────────────────────────────────────────┐ │
│ │ /api/v1/client/resource/* (客户端接口) │ │
│ │ /api/v1/admin/resource/* (管理端接口) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ Sequelize ORM
▼
┌─────────────────────────────────────────────────────────┐
│ 数据层 │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ resource │ │resource_detail│ │resource_ │ │
│ │ (资源主表) │ │ (资源详情表) │ │category │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ ┌──────────────┐ │
│ │resource_config│ (资源配置表) │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 数据库设计
核心表结构
1. 资源分类表(resource_category)
CREATE TABLE `resource_category` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类主键ID',
`name` varchar(100) NOT NULL COMMENT '分类名称',
`sort` int DEFAULT 0 COMMENT '排序(数字越小越靠前)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 资源主表(resource)
CREATE TABLE `resource` (
`id` varchar(12) NOT NULL COMMENT '资源ID(随机12位字符)',
`title` varchar(255) NOT NULL COMMENT '资源标题',
`summary` text COMMENT '资源简介',
`cover_img` varchar(255) COMMENT '封面图URL',
`content_img` varchar(255) COMMENT '内容展示图URL',
`size` varchar(50) COMMENT '资源大小',
`price` varchar(50) COMMENT '资源价格',
`is_top` tinyint(1) DEFAULT 0 COMMENT '是否置顶',
`category_id` bigint COMMENT '分类ID',
`status` tinyint(1) DEFAULT 1 COMMENT '状态 0-下架 1-上架',
`view_count` int DEFAULT 0 COMMENT '访问量',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 资源详情表(resource_detail)
CREATE TABLE `resource_detail` (
`id` bigint NOT NULL AUTO_INCREMENT,
`resource_id` varchar(12) NOT NULL COMMENT '关联资源ID',
`content` text COMMENT '详情内容(Markdown格式)',
`password` varchar(500) COMMENT '提取码/链接(支持多个,用||分隔)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_resource_id` (`resource_id`),
CONSTRAINT `fk_detail_resource` FOREIGN KEY (`resource_id`)
REFERENCES `resource` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. 资源配置表(resource_config)
CREATE TABLE `resource_config` (
`id` int NOT NULL AUTO_INCREMENT,
`enable_list` tinyint(1) DEFAULT 1 COMMENT '是否开启资源列表',
`description` text COMMENT '资源说明',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计亮点
- 随机 ID 设计:资源 ID 使用 12 位随机字符串,而非自增数字,避免暴露资源数量,提升安全性
- 表分离设计:主表存储基本信息,详情表存储富文本内容,提升查询性能
- 外键约束:使用外键保证数据一致性,支持级联删除
- 索引优化:为
category_id、create_time建立索引,提升查询效率
三、后端服务(Server)
3.1 技术栈
- 框架:Koa.js(轻量级 Node.js Web 框架)
- ORM:Sequelize(MySQL ORM)
- 认证:JWT(jsonwebtoken)
- 跨域:koa2-cors
3.2 目录结构
server/
├── app/
│ ├── api/
│ │ └── v1/
│ │ ├── index.js # 客户端路由
│ │ └── admin.js # 管理端路由
│ ├── controllers/
│ │ └── resource.js # 客户端资源控制器
│ ├── controllers_admin/
│ │ └── resource.js # 管理端资源控制器
│ ├── models/
│ │ ├── resource.js # 资源模型
│ │ ├── resource_category.js # 分类模型
│ │ ├── resource_detail.js # 详情模型
│ │ └── resource_config.js # 配置模型
│ └── core/
│ └── DB.js # 数据库连接
└── bin/
└── www # 启动入口
3.3 核心接口实现
客户端接口(无需认证)
1. 资源列表接口
// GET /api/v1/client/resource/list
async list(ctx) {
let { page = 1, size = 10, key = '', categoryId, isTop } = ctx.query
const where = { status: 1 }
// 关键字搜索:支持标题和简介
if (key) {
where[Op.or] = [
{ title: { [Op.like]: `%${key}%` } },
{ summary: { [Op.like]: `%${key}%` } }
]
}
// 分类筛选
if (categoryId) {
where.category_id = Number(categoryId)
}
// 置顶筛选
if (isTop !== undefined) {
where.is_top = Number(isTop)
}
const { count, rows } = await DB.resource.findAndCountAll({
where,
order: [['is_top', 'DESC'], ['create_time', 'DESC']],
offset: (page - 1) * size,
limit: size,
include: [
{ model: DB.resource_category, as: 'category', attributes: ['id', 'name', 'sort'] }
]
})
global.Response.success(ctx, undefined, {
rows,
meta: { page, size, count, totalPages: Math.ceil(count / size) }
})
}
2. 资源详情接口(自动增加访问量)
// GET /api/v1/client/resource/detail?id=xxx
async detail(ctx) {
const { id } = ctx.query
if (!id) return global.Response.error(ctx, '缺少资源id')
const data = await DB.resource.findOne({
where: { id, status: 1 },
include: [
{ model: DB.resource_category, as: 'category' },
{ model: DB.resource_detail, as: 'detail' }
]
})
if (!data) return global.Response.error(ctx, '资源不存在或已下架')
// 访问量 +1(异步执行,不阻塞返回)
DB.resource.increment({ view_count: 1 }, { where: { id } }).catch(() => {})
global.Response.success(ctx, undefined, data)
}
3. 资源配置接口
// GET /api/v1/client/resource/config
async getConfig(ctx) {
let config = await DB.resource_config.findOne({ where: { id: 1 } })
if (!config) {
config = { id: 1, enable_list: 1, description: '' }
}
global.Response.success(ctx, undefined, config)
}
管理端接口(需要 JWT 认证)
1. 创建资源
// POST /api/v1/admin/resource/create
async createResource(ctx) {
const { title, category_id, summary, cover_img, content_img,
size, price, is_top, create_time, content, password } = ctx.request.body
// 生成随机 12 位 ID
const id = genResourceId()
// 创建主表记录
const resource = await DB.resource.create({
id, title, category_id, summary, cover_img, content_img,
size, price, is_top: is_top ? 1 : 0, create_time, status: 1
})
// 创建详情表记录
if (content || password) {
await DB.resource_detail.create({
resource_id: id, content, password
})
}
global.Response.success(ctx, '创建成功', resource)
}
2. 更新资源配置
// PUT /api/v1/admin/resource/config/update
async updateConfig(ctx) {
const { enable_list, description } = ctx.request.body
let config = await DB.resource_config.findOne({ where: { id: 1 } })
if (config) {
await DB.resource_config.update(
{ enable_list, description, update_time: new Date() },
{ where: { id: 1 } }
)
} else {
await DB.resource_config.create({
id: 1, enable_list, description, update_time: new Date()
})
}
global.Response.success(ctx, '更新成功')
}
3.4 技术亮点
- 随机 ID 生成:避免暴露资源数量,提升安全性
- 异步访问量更新:使用
increment+catch,不阻塞接口返回 - 参数校验:严格的参数边界检查,防止 SQL 注入和异常查询
- 关联查询优化:使用 Sequelize
include一次性获取关联数据,减少数据库查询次数
四、管理后台(Admin)
4.1 技术栈
- 框架:Vue 2.x
- UI 组件库:Element UI
- Markdown 编辑器:mavon-editor
- HTTP 请求:axios
4.2 核心页面
1. 资源列表页(Resource.vue)
功能特性:
- 资源列表展示(标题、分类、状态、创建时间)
- 搜索功能(按标题、分类筛选)
- 分页展示
- 编辑/删除操作
关键代码:
<template>
<div class="resource">
<SearchCard
@search="searchResources"
@add="$router.push('/resource/create')"
:options="categoryList"
tip="资源"
select="分类"
needAdd="true"
/>
<el-card class="app-container">
<el-table :data="list" border fit highlight-current-row>
<el-table-column label="标题" prop="title" />
<el-table-column label="分类" prop="category.name" />
<el-table-column label="状态" prop="status">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.status === 1 ? "上架" : "下架" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleEdit(scope.row.id)"
>
编辑
</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
:current-page.sync="page"
layout="total, prev, pager, next"
:total="count"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
2. 资源创建/编辑页(ResourceCreate.vue / ResourceEdit.vue)
功能特性:
- 表单验证
- 富文本编辑器(Markdown)
- 图片 URL 输入(支持文件云路径)
- 分类选择
- 置顶/状态设置
关键代码:
<template>
<el-card class="resource-form">
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="分类" prop="category_id">
<el-select v-model="form.category_id" placeholder="请选择分类">
<el-option
v-for="item in categoryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="提取信息">
<mavon-editor
ref="md"
v-model="form.content"
code-style="atom-one-dark"
:toolbarsFlag="true"
:subfield="true"
style="min-height: 360px;"
/>
</el-form-item>
<el-form-item label="提取码">
<el-input v-model="form.password" placeholder="多个链接用 || 分隔" />
</el-form-item>
<el-form-item>
<el-button @click="reset">重置</el-button>
<el-button type="primary" @click="submit">立即创建</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
4.3 API 封装
// admin/src/api/resource.js
import http from "@/utils/http";
export default {
// 资源列表
httpResourceList(params) {
return http.get("/resource/list", { params });
},
// 资源详情
httpResourceDetail(id) {
return http.get("/resource/detail", { params: { id } });
},
// 新增资源
httpResourceCreate(data) {
return http.post("/resource/create", data);
},
// 更新资源
httpResourceUpdate(data) {
return http.put("/resource/update", data);
},
// 删除资源
httpResourceDelete(id) {
return http.delete("/resource/delete", { params: { id } });
},
// 分类列表
httpResourceCategoryList() {
return http.get("/resource/category/list");
},
// 资源配置
httpResourceConfig() {
return http.get("/resource/config");
},
httpResourceConfigUpdate(data) {
return http.put("/resource/config/update", data);
},
};
五、多端客户端(Resource Mini)
5.1 技术栈
- 框架:uni-app(Vue 2)
- UI 组件库:uView UI 1.8.4
- Markdown 渲染:markdown-it
- HTTP 请求:uni.request(封装)
5.2 项目结构
resource_mini/
├── src/
│ ├── api/
│ │ └── common.js # API 接口封装
│ ├── pages/
│ │ ├── home/
│ │ │ └── index.vue # 首页(资源列表)
│ │ └── detail/
│ │ └── index.vue # 详情页
│ ├── utils/
│ │ ├── request.js # 请求封装
│ │ └── define.js # 全局配置
│ └── uview-ui/ # uView UI 组件库
├── pages.json # 页面配置
└── vue.config.js # Vue CLI 配置
5.3 核心页面实现
1. 首页(资源列表)
功能特性:
- 顶部导航(支持安全区域适配)
- 搜索栏(关键字搜索)
- 分类侧边栏(抽屉式)
- 资源列表(卡片式展示)
- 分页组件
- 空状态展示
- 资源配置控制(可关闭列表)
关键代码:
<template>
<view class="page" :class="{ 'no-scroll': showCategory }">
<!-- 安全区域占位 -->
<view
class="safe-area-top"
:style="{ height: statusBarHeight + 'px' }"
></view>
<!-- 顶部导航 -->
<view class="nav" v-if="!loading && enableList">
<view class="logo">
<text class="gradient-text-multi">资源宝库</text>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar" v-if="!loading && enableList">
<u-search
v-model="keyword"
@search="handleSearch"
@custom="handleSearch"
@clear="handleSearch"
/>
<view class="nav-actions">
<view class="menu-icon" @click="showCategoryMenu">
<u-icon name="list-dot" size="40" color="#666"></u-icon>
</view>
</view>
</view>
<!-- 资源列表 -->
<view class="list" v-if="enableList && !loading && list.length > 0">
<view
class="item"
v-for="(item, index) in list"
:key="item.id || index"
@click="goDetail(item.id)"
>
<view class="info">
<view class="title">{{ item.title }}</view>
<view class="meta">
<text v-if="item.is_top" class="badge top">置顶</text>
<text class="badge cate">
{{ (item.category && item.category.name) || "资源" }}
</text>
<text class="time">{{ item.create_time }}</text>
</view>
</view>
<image
class="cover"
:src="coverUrl(item.cover_img)"
mode="aspectFill"
/>
</view>
</view>
<!-- 空状态 -->
<view
class="empty-state"
v-if="enableList && !loading && list.length === 0"
>
<u-icon name="file-text" size="120" color="#d0d0d0"></u-icon>
<text class="empty-text">暂无资源数据</text>
</view>
<!-- 列表关闭提示 -->
<view class="empty-state" v-if="!loading && !enableList">
<u-icon name="lock" size="120" color="#d0d0d0"></u-icon>
<text class="empty-text">资源列表已关闭</text>
<text class="empty-tip" v-if="config && config.description">
{{ config.description }}
</text>
</view>
</view>
</template>
<script>
import {
getResourceList,
getResourceCategoryList,
getResourceConfig,
} from "../../api/common.js";
export default {
data() {
return {
list: [],
categoryList: [],
showCategory: false,
currentCategoryId: null,
keyword: "",
pagination: { page: 1, size: 10, count: 0, totalPages: 0 },
loading: false,
statusBarHeight: 0,
config: null,
enableList: true,
};
},
created() {
// 获取系统信息,设置状态栏高度
try {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
} catch (e) {
this.statusBarHeight = 0;
}
// 先加载配置,再根据配置决定是否加载列表
this.loadConfig();
this.loadCategoryList();
},
methods: {
// 加载资源配置
async loadConfig() {
try {
const res = await getResourceConfig();
if (res.code === 200 && res.data) {
this.config = res.data;
this.enableList = res.data.enable_list === 1;
if (this.enableList) {
this.loadList();
}
} else {
this.enableList = true;
this.loadList();
}
} catch (e) {
console.error("加载资源配置失败", e);
this.enableList = true;
this.loadList();
}
},
// 加载资源列表
async loadList() {
if (!this.enableList) return;
if (this.loading) return;
this.loading = true;
try {
const params = {
page: this.pagination.page,
size: this.pagination.size,
};
if (this.currentCategoryId) {
params.categoryId = this.currentCategoryId;
}
if (this.keyword) {
params.key = this.keyword;
}
const res = await getResourceList(params);
if (res.code === 200) {
this.list = res.data?.rows || [];
this.pagination.count = res.data?.meta?.count || 0;
this.pagination.totalPages = res.data?.meta?.count
? Math.ceil(res.data.meta.count / res.data.meta.size)
: 0;
}
} catch (e) {
console.error("加载资源列表失败", e);
uni.showToast({ title: "加载失败,请重试", icon: "none" });
} finally {
this.loading = false;
}
},
},
};
</script>
2. 详情页
功能特性:
- 资源详情展示(标题、分类、元信息)
- Markdown 内容渲染(H5 使用 v-html,小程序使用 rich-text)
- 资源链接展示(支持多个链接,用 || 分隔)
- 一键复制链接
- 自动增加访问量
关键代码:
<template>
<view class="page">
<!-- 安全区域占位 -->
<view
class="safe-area-top"
:style="{ height: statusBarHeight + 'px' }"
></view>
<!-- 详情内容 -->
<view class="content" v-if="detail">
<view class="header">
<view class="title">{{ detail.title }}</view>
<view class="meta">
<text class="badge cate">
{{ detail.category ? detail.category.name : "资源" }}
</text>
<text class="time">{{ detail.create_time }}</text>
</view>
</view>
<!-- 封面图 -->
<image
v-if="detail.cover_img"
class="cover-img"
:src="coverUrl(detail.cover_img)"
mode="aspectFill"
/>
<!-- 详情内容(富文本) -->
<view
class="detail-content"
v-if="detail.detail && detail.detail.content"
>
<view class="section-title">资源详情</view>
<!-- H5 平台使用 v-html -->
<!-- #ifdef H5 -->
<div
class="markdown-body"
v-html="formatContent(detail.detail.content)"
></div>
<!-- #endif -->
<!-- 小程序平台使用 rich-text -->
<!-- #ifndef H5 -->
<rich-text
class="markdown-body"
:nodes="formatContent(detail.detail.content)"
></rich-text>
<!-- #endif -->
</view>
<!-- 资源链接 -->
<view class="password-section" v-if="resourceLinks.length > 0">
<view class="section-title">资源链接</view>
<view
class="password-box"
v-for="(link, index) in resourceLinks"
:key="index"
>
<text class="password-text">{{ link }}</text>
<view class="copy-btn" @click="copyLink(link)">复制</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getResourceDetail } from "../../api/common.js";
import MarkdownIt from "markdown-it";
export default {
data() {
return {
id: "",
detail: null,
loading: false,
md: null,
statusBarHeight: 0,
};
},
created() {
// 初始化 markdown-it
this.md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
// 获取系统信息
try {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
} catch (e) {
this.statusBarHeight = 0;
}
},
onLoad(options) {
this.id = options.id;
if (!this.id) {
uni.showToast({ title: "缺少资源ID", icon: "none" });
setTimeout(() => this.goBack(), 1500);
return;
}
this.loadDetail();
},
computed: {
// 将资源链接按 || 分割成数组
resourceLinks() {
if (!this.detail || !this.detail.detail || !this.detail.detail.password) {
return [];
}
const links = this.detail.detail.password.split("||");
return links.map((link) => link.trim()).filter((link) => link.length > 0);
},
},
methods: {
async loadDetail() {
this.loading = true;
try {
const res = await getResourceDetail(this.id);
if (res.code === 200) {
this.detail = res.data;
}
} catch (e) {
console.error("加载资源详情失败", e);
uni.showToast({ title: "加载失败,请重试", icon: "none" });
} finally {
this.loading = false;
}
},
formatContent(content) {
if (!content) return "";
try {
let html = this.md.render(content);
// 处理图片路径
html = html.replace(/<img\s+src="([^"]+)"/g, (match, src) => {
if (src && !src.startsWith("http")) {
return `<img src="https://zhouqm.oss-cn-beijing.aliyuncs.com/resource/${src}" style="max-width: 100%; width: 100%; height: auto; display: block; box-sizing: border-box;"`;
}
return match;
});
// H5 平台直接返回 HTML
// #ifdef H5
return html;
// #endif
// 小程序平台返回 HTML 字符串(rich-text 会自动解析)
return html;
} catch (e) {
console.error("Markdown 解析失败", e);
return content.replace(/\n/g, "<br>");
}
},
copyLink(link) {
if (!link) return;
uni.setClipboardData({
data: link,
success: () => {
uni.showToast({ title: "已复制到剪贴板", icon: "success" });
},
});
},
},
};
</script>
5.4 多端适配
1. 条件编译
// #ifdef H5
// H5 平台特有代码
window.open(url, "_blank");
// #endif
// #ifdef MP-WEIXIN
// 微信小程序特有代码
uni.navigateTo({ url: `/pages/webview/webview?url=${url}` });
// #endif
// #ifdef APP-PLUS
// App 平台特有代码
plus.runtime.openURL(url);
// #endif
2. 安全区域适配
// 获取系统信息,设置状态栏高度
try {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
} catch (e) {
this.statusBarHeight = 0;
}
<!-- 安全区域占位 -->
<view class="safe-area-top" :style="{ height: statusBarHeight + 'px' }"></view>
3. 部署配置
H5 部署到子路径:
// vue.config.js
module.exports = {
publicPath: process.env.NODE_ENV === "production" ? "/resource_mini/" : "/",
devServer: {
proxy: {
"/blogapi": {
target: "http://xx.xx.xx.xxx:xxxx",
changeOrigin: true,
pathRewrite: { "^/blogapi": "" },
},
},
},
};
// pages.json
{
"h5": {
"router": {
"mode": "history",
"base": "/resource_mini/"
}
}
}
六、核心功能实现
6.1 资源 ID 随机生成
设计目的:避免使用自增 ID,防止暴露资源数量,提升安全性。
实现方式:
// server/app/controllers_admin/resource.js
function genResourceId() {
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
6.2 访问量自动统计
实现方式:在详情接口中异步更新访问量,不阻塞接口返回。
// 访问量 +1(异步执行,忽略错误)
DB.resource.increment({ view_count: 1 }, { where: { id } }).catch(() => {});
6.3 资源配置控制
功能:支持动态开启/关闭资源列表,显示自定义说明文字。
实现:
- 后端:
resource_config表存储配置 - 前端:首页加载时先获取配置,根据
enable_list决定是否显示列表
6.4 Markdown 渲染多端适配
H5 平台:使用 v-html 直接渲染 HTML
小程序平台:使用 rich-text 组件渲染 HTML 字符串
<!-- H5 平台 -->
<!-- #ifdef H5 -->
<div class="markdown-body" v-html="formatContent(content)"></div>
<!-- #endif -->
<!-- 小程序平台 -->
<!-- #ifndef H5 -->
<rich-text class="markdown-body" :nodes="formatContent(content)"></rich-text>
<!-- #endif -->
6.5 分类侧边栏(抽屉式)
实现要点:
- 使用
position: fixed+transform: translateX()实现侧滑 - 遮罩层阻止背景滚动
- 记录滚动位置,关闭时恢复
<view class="page" :class="{ 'no-scroll': showCategory }">
<!-- 遮罩 -->
<view
class="category-mask"
v-if="showCategory"
@click="closeCategoryMenu"
@touchmove.prevent
></view>
<!-- 侧边栏 -->
<view class="category-sidebar" :class="{ active: showCategory }">
<!-- 分类列表 -->
</view>
</view>
.page.no-scroll {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.category-sidebar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 60%;
max-width: 500rpx;
background: #fff;
z-index: 999;
transform: translateX(100%);
transition: transform 0.3s ease;
&.active {
transform: translateX(0);
}
}
七、技术亮点与难点
7.1 技术亮点
- 一套代码多端运行:使用 uni-app 实现 H5/小程序/App 三端统一,大幅降低开发成本
- 随机 ID 设计:提升安全性,避免暴露资源数量
- 表分离设计:主表 + 详情表分离,提升查询性能
- 异步访问量更新:不阻塞接口返回,提升用户体验
- 配置化控制:支持动态开启/关闭资源列表,灵活管理
- 安全区域适配:完美适配不同设备的顶部安全区域
- Markdown 多端渲染:H5 和小程序使用不同渲染方式,保证兼容性
7.2 技术难点与解决方案
难点 1:Node.js v24 兼容性问题
问题:buffer-equal-constant-time 包在 Node.js v24 中访问 SlowBuffer 报错。
解决方案:
// server/slowbuffer-polyfill.js
(() => {
const buf = require("buffer");
global.SlowBuffer = global.SlowBuffer || buf.SlowBuffer || Buffer;
})();
// package.json
{
"scripts": {
"start": "node -r ./slowbuffer-polyfill.js bin/www",
"dev": "nodemon --require ./slowbuffer-polyfill.js bin/www"
}
}
难点 2:数据库外键约束兼容性
问题:将资源 ID 从自增数字改为随机字符串后,外键约束类型不匹配。
解决方案:
-- 1. 删除旧外键
ALTER TABLE `resource_detail` DROP FOREIGN KEY `fk_detail_resource`;
-- 2. 修改字段类型
ALTER TABLE `resource_detail`
MODIFY `resource_id` VARCHAR(12) NOT NULL;
-- 3. 重新添加外键
ALTER TABLE `resource_detail`
ADD CONSTRAINT `fk_detail_resource`
FOREIGN KEY (`resource_id`)
REFERENCES `resource` (`id`)
ON DELETE CASCADE;
八、总结与展望
8.1 项目总结
本项目实现了一个完整的资源管理系统,涵盖:
- ✅ 后端 API:RESTful 接口,支持资源 CRUD、分类管理、配置管理
- ✅ 管理后台:Vue + Element UI,提供便捷的资源管理界面
- ✅ 多端客户端:uni-app 实现 H5/小程序/App 三端统一
- ✅ 数据库设计:合理的表结构设计,支持外键约束和索引优化
- ✅ 用户体验:搜索、分类筛选、分页、空状态、配置控制等完善功能
8.2 技术栈总结
| 层级 | 技术栈 | 说明 |
|---|---|---|
| 后端 | Node.js + Koa + Sequelize + MySQL | 轻量级、高效、ORM 操作 |
| 管理后台 | Vue 3 + Element UI + mavon-editor | 成熟稳定、组件丰富 |
| 客户端 | uni-app + uView UI + markdown-it | 一套代码多端运行 |
8.3 未来优化方向
-
性能优化
- 添加 Redis 缓存,减少数据库查询
- 图片 CDN 加速
- 列表虚拟滚动(大数据量场景)
-
功能扩展
- 资源收藏功能
- 用户评论系统
- 资源推荐算法
- 搜索历史记录
-
体验优化
- 骨架屏加载
- 下拉刷新优化
- 图片懒加载
- PWA 支持(H5)
-
安全加固
- API 限流
- 防爬虫机制
- 内容审核
附录:相关资源
API 接口文档
客户端接口(无需认证):
GET /api/v1/client/resource/list- 资源列表GET /api/v1/client/resource/detail?id=xxx- 资源详情GET /api/v1/client/resource/category/list- 分类列表GET /api/v1/client/resource/config- 获取配置
管理端接口(需要 JWT 认证):
GET /api/v1/admin/resource/list- 资源列表POST /api/v1/admin/resource/create- 创建资源PUT /api/v1/admin/resource/update- 更新资源DELETE /api/v1/admin/resource/delete- 删除资源GET /api/v1/admin/resource/config- 获取配置PUT /api/v1/admin/resource/config/update- 更新配置
项目地址
- 后端服务:
server/目录 - 管理后台:
admin/目录 - 多端客户端:
resource_mini/目录
💡 提示:本文档基于实际项目经验总结,如有问题欢迎交流讨论。如果对你有帮助,欢迎点赞、收藏、转发!