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

今天跟大家分享node学习,利用Express框架来开发项目。

最近我用Express框架开发了一个二手交易平台。一共有两个端,一个端是管理后台端,一个是用户端。实现的功能有:

1、系统功能

(1)平台管理端

· 用户管理

· 商品分类管理

· 二手商品管理

· 订单管理

· 管理员管理

· 资讯管理

(2)用户端

· 二手商品展示

· 分类筛选

· 商品详情

· 订单凭证与我的订单

· 联系卖家

· 资讯查看

· 我的发布

node环境:16.20以上。

用户可以在手机端可以发布二手商品,在用户端页可以下单,因为是练习项目,就没法线上支付,只能上传凭证。然后可以通过管理后台进行二手交易平台管理。

部分代码:

bash 复制代码
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.body
    const 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 } = admin
    res.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()
bash 复制代码
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
bash 复制代码
<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-table
      v-loading="tableLoading"
      :data="newsList"
      border
      style="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-picker
            v-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-input
            class="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-input
            type="textarea"
            :rows="6"
            placeholder="请输入资讯内容"
            v-model="newsForm.contents">
          </el-input>
        </el-form-item>
        <el-form-item label="备注" prop="remarks">
          <el-input
            type="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 = true
      try {
        const res = await getNewsList({
          page: this.pagination.page,
          pageSize: this.pagination.pageSize,
          title: this.searchForm.title,
          status: this.searchForm.status
        })
        
        if (res.code === 1000) {
       
      
          // 处理图片URL
          for (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.list
          this.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 = 1
      this.fetchNewsList()
    },
    
    // 重置搜索
    resetSearch() {
      this.searchForm = {
        title: '',
        status: ''
      }
      this.handleSearch()
    },
    
    // 每页条数变化
    handleSizeChange(val) {
      this.pagination.pageSize = val
      this.fetchNewsList()
    },
    
    // 当前页变化
    handleCurrentChange(val) {
      this.pagination.page = val
      this.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 = true
      
      try {
        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) return
        
        this.submitLoading = true
        
        // 处理标签
        this.newsForm.tags = JSON.stringify(this.dynamicTags)
        
        try {
          let res
          if (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 = false
            this.fetchNewsList()
          } else {
            this.$message.error(res.msg || '操作失败')
          }
        } catch (error) {
          console.error('提交资讯表单失败:', error)
          this.$message.error('操作失败')
        } finally {
          this.submitLoading = false
        }
      })
    },
    
    // 获取图片URL
    async 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;
      }
    },
    
    // 解析标签JSON
    getTagsArray(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 = true
      this.$nextTick(_ => {
        this.$refs.saveTagInput.$refs.input.focus()
      })
    },
    
    handleInputConfirm() {
      const inputValue = this.inputValue
      if (inputValue && this.dynamicTags.indexOf(inputValue) === -1) {
        this.dynamicTags.push(inputValue)
      }
      this.inputVisible = false
      this.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

相关推荐
来旺3 小时前
互联网大厂Java面试全解析及三轮问答专项
java·数据库·spring boot·安全·缓存·微服务·面试
since �3 小时前
前端转Java,从0到1学习教程
java·前端·学习
詩句☾⋆᭄南笙3 小时前
Mybatis一对一、一对多
java·mybatis·resulttype·resultmap·一对多·一对一
Andya_net3 小时前
Java | 基于redis实现分布式批量设置各个数据中心的服务器配置方案设计和代码实践
java·服务器·分布式
lang201509283 小时前
Spring Boot 外部化配置最佳实践指南
java·spring boot
小奋斗3 小时前
面试官:[1] == '1'和[1] == 1结果是什么?
前端·面试
萌萌哒草头将军3 小时前
尤雨溪宣布 oxfmt 即将发布!比 Prettier 快45倍 🚀🚀🚀
前端·webpack·vite
weixin_405023373 小时前
webpack 学习
前端·学习·webpack
云中雾丽3 小时前
flutter中 Future 详细介绍
前端