当前位置: 首页 > news >正文

使用node Express 框架开发一个前后端分离的二手交易平台项目

今天跟大家分享node学习,利用Express框架来开发项目。
最近我用Express框架开发了一个二手交易平台。一共有两个端,一个端是管理后台端,一个是用户端。实现的功能有:
1、系统功能
(1)平台管理端
· 用户管理
· 商品分类管理
· 二手商品管理
· 订单管理
· 管理员管理
· 资讯管理
(2)用户端
· 二手商品展示
· 分类筛选
· 商品详情
· 订单凭证与我的订单
· 联系卖家
· 资讯查看
· 我的发布
node环境:16.20以上。
用户可以在手机端可以发布二手商品,在用户端页可以下单,因为是练习项目,就没法线上支付,只能上传凭证。然后可以通过管理后台进行二手交易平台管理。
部分代码:

const AdminModel = require('../models/admin.model')
const JwtUtil = require('../utils/jwt')
const Response = require('../utils/response')
const AppError = require('../utils/appError')
const asyncHandler = require('../utils/asyncHandler')class AdminController {/*** 管理员登录*/login = asyncHandler(async (req, res) => {const { username, password } = req.bodyconst admin = await AdminModel.login(username, password)if (!admin) {return res.json(Response.error('用户名或密码错误'))}// 默认设置为超级管理员角色const token = JwtUtil.generateToken({id: admin.id,username: admin.username,role: 'admin', // 默认设置为admin角色is_super: true // 默认设置为超级管理员})// 返回时过滤敏感信息const { password: _, ...safeAdmin } = adminres.json(Response.success({ token, admin: safeAdmin }, '登录成功'))})/*** 获取所有管理员*/getAll = asyncHandler(async (req, res) => {const admins = await AdminModel.getAll()res.json(Response.success(admins))})/*** 获取单个管理员*/getOne = asyncHandler(async (req, res) => {const admin = await AdminModel.findById(req.params.id)if (!admin) {return res.json(Response.error('管理员不存在'))}res.json(Response.success(admin))})/*** 创建管理员*/create = asyncHandler(async (req, res) => {try {const adminId = await AdminModel.create(req.body)const admin = await AdminModel.findById(adminId)res.json(Response.success(admin, '创建成功'))} catch (error) {res.json(Response.error(error.message))}})/*** 更新管理员*/update = asyncHandler(async (req, res) => {try {await AdminModel.update(req.params.id, req.body)const admin = await AdminModel.findById(req.params.id)res.json(Response.success(admin, '更新成功'))} catch (error) {res.json(Response.error(error.message))}})/*** 删除管理员*/delete = asyncHandler(async (req, res) => {try {await AdminModel.delete(req.params.id)res.json(Response.success(null, '删除成功'))} catch (error) {res.json(Response.error(error.message))}})
}module.exports = new AdminController()
const db = require('../utils/database')
const { AppError } = require('../utils/error-handler')
const crypto = require('crypto')
const DateFormat = require('../utils/dateFormat')class AdminModel {static hashPassword(password) {return crypto.createHash('md5').update(password).digest('hex')}static async login(username, password) {const hashedPassword = this.hashPassword(password)const sql = 'SELECT id, username, nickname, is_super, created_time, updated_time FROM admin WHERE username = ? AND password = ?'const result = await db.query(sql, [username, hashedPassword])return result[0] ? DateFormat.formatDBData(result[0]) : null}static async findById(id) {const sql = 'SELECT id, username, nickname, is_super, created_time, updated_time FROM admin WHERE id = ?'const result = await db.query(sql, [id])return result[0] ? DateFormat.formatDBData(result[0]) : null}static async findByUsername(username) {const sql = 'SELECT id, username, nickname, is_super, created_time, updated_time FROM admin WHERE username = ?'const result = await db.query(sql, [username])return result[0] ? DateFormat.formatDBData(result[0]) : null}static async getAll() {const sql = 'SELECT id, username, nickname, is_super, created_time, updated_time FROM admin'const result = await db.query(sql)return DateFormat.formatDBData(result)}static async create(adminData) {const { username, password, nickname } = adminData// 检查用户名是否已存在const existingAdmin = await this.findByUsername(username)if (existingAdmin) {throw new AppError('用户名已存在', 400)}const hashedPassword = this.hashPassword(password)const currentTime = new Date()const sql = 'INSERT INTO admin (username, password, nickname, created_time) VALUES (?, ?, ?, ?)'const result = await db.query(sql, [username, hashedPassword, nickname, currentTime])return result.insertId}static async update(id, adminData) {const { nickname, password } = adminData// 检查管理员是否存在const admin = await this.findById(id)if (!admin) {throw new AppError('管理员不存在', 404)}// 不允许修改超级管理员if (admin.is_super) {throw new AppError('不能修改超级管理员信息', 403)}let sql = 'UPDATE admin SET 'const params = []const updates = []if (nickname) {updates.push('nickname = ?')params.push(nickname)}if (password) {updates.push('password = ?')params.push(this.hashPassword(password))}if (updates.length === 0) {throw new AppError('没有要更新的数据', 400)}sql += updates.join(', ')sql += ' WHERE id = ?'params.push(id)await db.query(sql, params)return await this.findById(id)}static async delete(id) {// 检查管理员是否存在const admin = await this.findById(id)if (!admin) {throw new AppError('管理员不存在', 404)}// 不允许删除超级管理员if (admin.is_super) {throw new AppError('不能删除超级管理员', 403)}const sql = 'DELETE FROM admin WHERE id = ?'await db.query(sql, [id])return true}
}module.exports = AdminModel

在这里插入图片描述

<template><div class="news-management"><!-- 搜索区域 --><div class="search-container"><el-form :inline="true" :model="searchForm" class="search-form"><el-form-item label="标题"><el-input v-model="searchForm.title" placeholder="请输入标题关键词" clearable></el-input></el-form-item><el-form-item label="状态"><el-select v-model="searchForm.status" placeholder="请选择状态" clearable><el-option label="正常" :value="1"></el-option><el-option label="禁用" :value="0"></el-option></el-select></el-form-item><el-form-item><el-button type="primary" @click="handleSearch">搜索</el-button><el-button @click="resetSearch">重置</el-button></el-form-item></el-form></div><!-- 操作区域 --><div class="operation-container"><el-button type="primary" @click="handleAdd">新增资讯</el-button></div><!-- 表格区域 --><el-tablev-loading="tableLoading":data="newsList"borderstyle="width: 100%"><el-table-column prop="id" label="ID" align="center"></el-table-column><el-table-column prop="titles" label="标题" show-overflow-tooltip></el-table-column><el-table-column label="发布时间" align="center"><template slot-scope="scope">{{ formatDateTime(scope.row.publish_time) }}</template></el-table-column><el-table-column label="封面图" align="center"><template slot-scope="scope"><el-image v-if="scope.row.cover_image":src="scope.row.imageUrl || ''"style="width: 80px; height: 60px"fit="cover"><div slot="error" class="image-error"><i class="el-icon-picture-outline"></i></div></el-image><span v-else>无封面图</span></template></el-table-column><el-table-column label="状态" align="center"><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-tag v-for="(tag, index) in getTagsArray(scope.row.tags)" :key="index"size="small"style="margin-right: 5px; margin-bottom: 5px">{{ tag }}</el-tag><span v-if="!getTagsArray(scope.row.tags).length">无标签</span></template></el-table-column><el-table-column label="创建时间" align="center"><template slot-scope="scope">{{ scope.row.created_time }}</template></el-table-column><el-table-column label="操作" align="center"><template slot-scope="scope"><el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button><el-button type="text" size="small" @click="handleDelete(scope.row)">删除</el-button></template></el-table-column></el-table><!-- 分页区域 --><div class="pagination-container"><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange":current-page="pagination.page":page-sizes="[10, 20, 50, 100]":page-size="pagination.pageSize"layout="total, sizes, prev, pager, next, jumper":total="pagination.total"></el-pagination></div><!-- 新增/编辑资讯对话框 --><el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="70%"><el-form :model="newsForm" :rules="rules" ref="newsForm" label-width="100px"><el-form-item label="标题" prop="titles"><el-input v-model="newsForm.titles" placeholder="请输入资讯标题"></el-input></el-form-item><el-form-item label="发布时间" prop="publish_time"><el-date-pickerv-model="newsForm.publish_time"type="datetime"placeholder="选择发布时间"value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker></el-form-item><el-form-item label="状态" prop="status"><el-radio-group v-model="newsForm.status"><el-radio :label="1">正常</el-radio><el-radio :label="0">禁用</el-radio></el-radio-group></el-form-item><el-form-item label="封面图" prop="cover_image"><file-upload v-model="newsForm.cover_image" list-type="picture-card" :accept="'.jpg,.jpeg,.png,.gif'":limit="1"tip="只能上传jpg/png/gif文件,且不超过10MB"><i class="el-icon-plus"></i></file-upload></el-form-item><el-form-item label="正文图" prop="content_image"><file-upload v-model="newsForm.content_image" list-type="picture-card" :accept="'.jpg,.jpeg,.png,.gif'":limit="1"tip="只能上传jpg/png/gif文件,且不超过10MB"><i class="el-icon-plus"></i></file-upload></el-form-item><el-form-item label="标签" prop="tags"><el-tag:key="tag"v-for="tag in dynamicTags"closable:disable-transitions="false"@close="handleTagClose(tag)">{{tag}}</el-tag><el-inputclass="input-new-tag"v-if="inputVisible"v-model="inputValue"ref="saveTagInput"size="small"@keyup.enter.native="handleInputConfirm"@blur="handleInputConfirm"></el-input><el-button v-else class="button-new-tag" size="small" @click="showInput">+ 添加标签</el-button></el-form-item><el-form-item label="内容" prop="contents"><el-inputtype="textarea":rows="6"placeholder="请输入资讯内容"v-model="newsForm.contents"></el-input></el-form-item><el-form-item label="备注" prop="remarks"><el-inputtype="textarea":rows="3"placeholder="请输入备注信息"v-model="newsForm.remarks"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button></div></el-dialog></div>
</template><script>
import { getNewsList, getNewsDetail, createNews, updateNews, deleteNews } from '@/api/news'
import { getFileById } from '@/api/file'
import FileUpload from '@/components/FileUpload'export default {name: 'NewsManagement',components: {FileUpload},data() {return {// 搜索表单searchForm: {title: '',status: ''},// 表格加载状态tableLoading: false,// 资讯列表数据newsList: [],// 分页信息pagination: {page: 1,pageSize: 10,total: 0},// 对话框标题dialogTitle: '新增资讯',// 对话框可见性dialogVisible: false,// 提交按钮加载状态submitLoading: false,// 资讯表单newsForm: {titles: '',publish_time: '',contents: '',status: 1,remarks: '',cover_image: '',content_image: '',tags: ''},// 表单验证规则rules: {titles: [{ required: true, message: '请输入资讯标题', trigger: 'blur' },{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }],publish_time: [{ required: true, message: '请选择发布时间', trigger: 'change' }],status: [{ required: true, message: '请选择状态', trigger: 'change' }],contents: [{ required: true, message: '请输入资讯内容', trigger: 'blur' }]},// 标签相关dynamicTags: [],inputVisible: false,inputValue: ''}},created() {this.fetchNewsList()},methods: {// 获取资讯列表async fetchNewsList() {this.tableLoading = truetry {const res = await getNewsList({page: this.pagination.page,pageSize: this.pagination.pageSize,title: this.searchForm.title,status: this.searchForm.status})if (res.code === 1000) {// 处理图片URLfor (const item of  res.data.list) {if (item.cover_image) {try {const fileId = JSON.parse(item.cover_image)[0];const fileRes = await getFileById(fileId);if (fileRes.code === 1000 && fileRes.data) {item.imageUrl = fileRes.data.full_url || fileRes.data.url;}} catch (err) {console.error('处理图片URL失败:', err);}}}this.newsList = res.data.listthis.pagination.total = res.data.total} else {this.$message.error(res.msg || '获取资讯列表失败')}} catch (error) {console.error('获取资讯列表失败:', error)this.$message.error('获取资讯列表失败')} finally {this.tableLoading = false}},// 搜索handleSearch() {this.pagination.page = 1this.fetchNewsList()},// 重置搜索resetSearch() {this.searchForm = {title: '',status: ''}this.handleSearch()},// 每页条数变化handleSizeChange(val) {this.pagination.pageSize = valthis.fetchNewsList()},// 当前页变化handleCurrentChange(val) {this.pagination.page = valthis.fetchNewsList()},// 新增资讯handleAdd() {this.dialogTitle = '新增资讯'this.newsForm = {titles: '',publish_time: new Date().toISOString().slice(0, 19).replace('T', ' '),contents: '',status: 1,remarks: '',cover_image: '',content_image: '',tags: ''}this.dynamicTags = []this.dialogVisible = true// 重置表单验证this.$nextTick(() => {this.$refs.newsForm && this.$refs.newsForm.clearValidate()})},// 编辑资讯async handleEdit(row) {this.dialogTitle = '编辑资讯'this.dialogVisible = truetry {const res = await getNewsDetail(row.id)if (res.code === 1000) {if(res.data.cover_image){res.data.cover_image = JSON.parse(res.data.cover_image);}if(res.data.content_image){res.data.content_image = JSON.parse(res.data.content_image);}this.newsForm = { ...res.data }// 处理标签this.dynamicTags = this.getTagsArray(res.data.tags)} else {this.$message.error(res.msg || '获取资讯详情失败')}} catch (error) {console.error('获取资讯详情失败:', error)this.$message.error('获取资讯详情失败')}// 重置表单验证this.$nextTick(() => {this.$refs.newsForm && this.$refs.newsForm.clearValidate()})},// 删除资讯handleDelete(row) {this.$confirm('确认删除该资讯吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(async () => {try {const res = await deleteNews(row.id)if (res.code === 1000) {this.$message.success('删除成功')this.fetchNewsList()} else {this.$message.error(res.msg || '删除失败')}} catch (error) {console.error('删除资讯失败:', error)this.$message.error('删除失败')}}).catch(() => {})},// 提交表单submitForm() {this.$refs.newsForm.validate(async valid => {if (!valid) returnthis.submitLoading = true// 处理标签this.newsForm.tags = JSON.stringify(this.dynamicTags)try {let resif (this.newsForm.id) {// 编辑res = await updateNews(this.newsForm.id, this.newsForm)} else {// 新增res = await createNews(this.newsForm)}if (res.code === 1000) {this.$message.success(res.msg || '操作成功')this.dialogVisible = falsethis.fetchNewsList()} else {this.$message.error(res.msg || '操作失败')}} catch (error) {console.error('提交资讯表单失败:', error)this.$message.error('操作失败')} finally {this.submitLoading = false}})},// 获取图片URLasync getImageUrl(fileId) {if (!fileId) return ''const fileIdArray = JSON.parse(fileId);try {const res = await getFileById(fileIdArray[0])if (res.code === 1000 && res.data) {return res.data.full_url || res.data.url}return ''} catch (error) {console.error('获取图片URL失败:', error)return ''}},// 格式化日期时间formatDateTime(dateTime) {if (!dateTime) return '';try {const date = new Date(dateTime);if (isNaN(date.getTime())) return dateTime;const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0');const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;} catch (error) {console.error('格式化日期时间失败:', error);return dateTime;}},// 解析标签JSONgetTagsArray(tags) {if (!tags) return []try {if (typeof tags === 'string') {return JSON.parse(tags)}return Array.isArray(tags) ? tags : []} catch (error) {console.error('解析标签失败:', error)return []}},// 标签相关方法handleTagClose(tag) {this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1)},showInput() {this.inputVisible = truethis.$nextTick(_ => {this.$refs.saveTagInput.$refs.input.focus()})},handleInputConfirm() {const inputValue = this.inputValueif (inputValue && this.dynamicTags.indexOf(inputValue) === -1) {this.dynamicTags.push(inputValue)}this.inputVisible = falsethis.inputValue = ''}}
}
</script><style scoped>
.news-management {background: white;border-radius: 8px;padding: 20px;
}.page-header {margin-bottom: 20px;
}.page-header h2 {margin: 0 0 10px 0;color: #333;
}.page-header p {margin: 0;color: #666;
}.search-container {margin-bottom: 20px;
}.operation-container {margin-bottom: 20px;display: flex;justify-content: flex-end;
}.pagination-container {margin-top: 20px;text-align: right;
}.image-error {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;background: #f5f7fa;color: #909399;font-size: 20px;
}.el-tag + .el-tag {margin-left: 10px;
}.button-new-tag {margin-left: 10px;height: 32px;line-height: 30px;padding-top: 0;padding-bottom: 0;
}.input-new-tag {width: 90px;margin-left: 10px;vertical-align: bottom;
}
</style>

代码慢慢写的多了,然后对语法就更熟悉了。
管理端截图:
在这里插入图片描述
用户端:
在这里插入图片描述

我也搭建了一个预览地址:https://test.wwwoop.com/?s=/er-shou-ping-tai-web&no=Second-hand%20trading%20platform001&rand=0.029277694490667527

http://www.dtcms.com/a/505094.html

相关文章:

  • 建设网站一般多少钱wordpress分页导航代码
  • 东莞知名网站如何上传自己做的网站
  • 注册网站除了域名网站制作沈阳
  • h5网站显示的图标怎么做的做旅游在网上用什么网站推广
  • 河南郑州app建设网站长丰县住房和城乡建设局网站
  • 肃州区建设局网站react用于做PC网站
  • 获取文件MD5
  • 网站如何留住客户公司设计网站推荐
  • 网站开发demo是什么牡丹江免费信息网
  • 百度云wordpress怎么搭建网站台州企业网站搭建特点
  • 外贸网站建设外wordpress纯静态
  • 用ps做网站方法大学生创新创业大赛ppt
  • Java基础——面向对象进阶复习知识点8
  • 【点云】point Transformer V1文章梳理
  • 做ppt的图片网站如何做网站线上监控
  • 盐田做网站中文wordpress模版
  • c语言做项目网站网络营销是什么
  • 织梦网站分页问题h5制作开发地点
  • Dependencies下载和使用教程(附安装包)
  • 北京网站推广排名h5编辑工具
  • 网站怎么修改好之后再上线ps做网页怎么在网站上打开
  • 企业网站设计要求大港建站公司
  • 做游戏网站用什么系统做服务器上的wordpress
  • 黄村网站建设怎么看出网站有没有做404页面
  • 网站是怎么挣钱的云南系统开发
  • 淄博网站网站建设网站备案找谁
  • 网站建设设计作业学网站建设哪里好
  • 企业建站方案公司网站制作范文
  • 做弩的网站一分钟用自己的电脑做网站
  • 微能力者恶魔网站谁做的成都网站设计费用