You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Node.js的迷你图书馆网站借还功能故障排查求助

问题:图书馆借还功能无法更新书籍状态

我基于Node.js开发了一个带借还POST接口的迷你图书馆网站,首页需求如下:

  • 左侧用缩进式复选框表单展示可借书籍标题,搭配Borrow按钮
  • 右侧用缩进式复选框表单展示已借书籍标题,搭配Return按钮
  • 勾选书籍点击按钮后,书籍状态更新:借走的书籍从左侧消失并出现在右侧,归还的书籍反之

目前已完成对应的.hbs模板文件、/home页面的GET方法,以及/borrow、/return功能的POST方法,但借还功能未正常工作:点击按钮后页面仅刷新,无法更新书籍的available属性。


相关代码

1. Handlebars模板(Home.hbs)

{{> Heading}}

<div class="container">
  <div class="left-container">
    <h2>Available Books</h2>
    <form action="/borrow" method="POST">
      {{#each availableBooks}}
        {{#if available}}
          <div class="book">
            <input type="checkbox" name="books[]" value="{{title}}">
            <span>{{title}}</span>
          </div>
        {{/if}}
      {{/each}}
      <button type="submit" name="action" value="borrow" id="borrowButton" disabled>Borrow</button>
    </form>
  </div>

  <div class="right-container">
    <h2>Borrowed Books</h2>
    <form action="/return" method="POST">
      {{#each borrowedBooks}}
        {{#unless available}}
          <div class="book">
            <input type="checkbox" name="books[]" value="{{title}}">
            <span>{{title}}</span>
          </div>
        {{/unless}}
      {{/each}}
      <button type="submit" name="action" value="return" id="returnButton" disabled>Return</button>
    </form>
  </div>
</div>

<script>
  // Enable borrow button when at least one checkbox is selected in the left container
  const borrowButton = document.getElementById('borrowButton');
  const leftContainerCheckboxes = document.querySelectorAll('.left-container input[type="checkbox"]');

  leftContainerCheckboxes.forEach((checkbox) => {
    checkbox.addEventListener('change', () => {
      const anyChecked = Array.from(leftContainerCheckboxes).some((checkbox) => checkbox.checked);
      borrowButton.disabled = !anyChecked;
    });
  });

  // Enable return button when at least one checkbox is selected in the right container
  const returnButton = document.getElementById('returnButton');
  const rightContainerCheckboxes = document.querySelectorAll('.right-container input[type="checkbox"]');

  rightContainerCheckboxes.forEach((checkbox) => {
    checkbox.addEventListener('change', () => {
      returnButton.disabled = !Array.from(rightContainerCheckboxes).some((checkbox) => checkbox.checked);
    });
  });
</script>

2. Node.js后端代码

const HTTP_PORT = process.env.PORT || 3000;

const express = require("express");
const exphbs = require("express-handlebars");
const path = require('path');
const fs = require('fs');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();

app.use(express.static(path.join(__dirname, '/public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false
}));

function onHttpStart() {
  console.log("Express http server listening on: " + HTTP_PORT);
}

let booksData = [];

fs.readFile('books.json', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }

  booksData = JSON.parse(data);
  
});

app.engine(".hbs", exphbs.engine({
  extname: ".hbs",
  defaultLayout: false,
  layoutsDir: path.join(__dirname, "/views")
}));

app.set("view engine", ".hbs");

// Middleware to check session authentication
function authenticateSession(req, res, next) {
  if (req.session.username) {
    next(); // User is authenticated, continue to the next middleware
  } else {
    res.redirect('/LandingPage'); // User is not authenticated, redirect to LandingPage
  }
}

app.get('/', (req, res) => {
  res.render('Landing Page', { layout: false });
});

app.get("/LandingPage", function(req, res) {
  res.render('Landing Page', { layout: false });
});

app.get("/SignIn", function(req, res) {
  res.render('Sign In', { heading: 'uLibrary', layout: false });
});

app.post('/signin', express.urlencoded({ extended: false }), (req, res) => {
  const { username, password } = req.body;

  // Read the users.json file
  fs.readFile('./users.json', 'utf8', (err, data) => {
    if (err) {
      console.error(err);
      return res.status(500).send('Internal Server Error');
    }

    const users = JSON.parse(data);

    // Check if the entered username exists in the users.json file
    if (!users.hasOwnProperty(username)) {
      return res.redirect('/LandingPage');
    }

    // Check if the entered password matches the stored password for the username
    if (users[username] !== password) {
      return res.redirect('/LandingPage');
    }

    // Successful sign in, set username in session
    req.session.username = username;

    // Redirect to the Home Webpage
    res.redirect('/home');
  });
});

app.get('/home', authenticateSession, (req, res) => {
  const username = req.session.username;
  const availableBooks = booksData.filter((book) => book.available);
  const borrowedBooks = booksData.filter((book) => !book.available);
  res.render('Home', { 
    heading: 'Home',
    availableBooks: availableBooks,
    borrowedBooks: borrowedBooks,
    layout: false 
  });
  console.log(booksData);
});

app.post('/borrow', (req, res) => {
  const selectedBooks = req.body.books || []; // Array of selected book titles
  console.log(selectedBooks);
  
  // Update the availability property for selected books
  selectedBooks.forEach((selectedBook) => {
    const book = booksData.find((item) => item.title === selectedBook);
    console.log("book selected:")
    console.log(book);
    if (book) {
      console.log(book.available);
      book.available = false;
    }
  });
  
  fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => {
    if (err) {
      console.error(err);
    } else {
      console.log('books.json updated successfully.');
    }
  });

  res.redirect('/home');
});

// Handle Return button click
app.post('/return', (req, res) => {
  const selectedBooks = req.body.books || []; // Array of selected book titles
  console.log(selectedBooks);
  // Update the availability property for selected books
  selectedBooks.forEach((selectedBook) => {
    const book = booksData.find((item) => item.title === selectedBook);
    console.log(book);
    console.log(book.available);
    if (book) {
      book.available = true;
    }
  });

  fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => {
    if (err) {
      console.error(err);
    } else {
      console.log('books.json updated successfully.');
    }
  });

  res.redirect('/home');
});

app.listen(HTTP_PORT, onHttpStart);

3. books.json数据

[
    {
        "title": "The Shining",
        "author": "Stephen King",
        "available": true
    },
    {
        "title": "Rainbow Six",
        "author": "Tom Clancy",
        "available": false
    },
    {
        "title": "Steve Jobs",
        "author": "Walter Isaacson",
        "available": true
    },
    {
        "title": "Elon Musk",
        "author": "Ashley Vance",
        "available": false
    }
]

问题分析与修复方案

1. 核心问题:表单数组参数无法解析

你的Express配置中使用了express.urlencoded({ extended: false }),extended: false会调用Node.js内置的querystring模块解析表单数据,但该模块不支持解析books[]这种数组格式的参数,导致req.body.books始终为undefined,无法获取选中的书籍列表,自然无法更新书籍状态。

修复:将extended改为true,使用qs模块解析表单数据,支持数组参数:

app.use(express.urlencoded({ extended: true }));

2. 模板冗余判断导致潜在展示问题

后端已经通过filter得到了availableBooks(仅包含available: true的书籍)和borrowedBooks(仅包含available: false的书籍),但模板中又额外添加了{{#if available}}和{{#unless available}}的判断,完全冗余,甚至可能在数据异常时导致展示错误。

修复:删除模板中的冗余判断:

{{!-- 左侧可借书籍表单 --}}
{{#each availableBooks}}
  <div class="book">
    <input type="checkbox" name="books[]" value="{{title}}">
    <span>{{title}}</span>
  </div>
{{/each}}

{{!-- 右侧已借书籍表单 --}}
{{#each borrowedBooks}}
  <div class="book">
    <input type="checkbox" name="books[]" value="{{title}}">
    <span>{{title}}</span>
  </div>
{{/each}}

3. 异步文件写入未完成即跳转

当前代码中fs.writeFile是异步操作,但你在调用它之后立刻执行了res.redirect('/home'),可能出现文件还未完成写入就跳转的情况(虽然内存中的booksData已经更新,但如果服务器重启,未写入的数据会丢失)。

修复:将res.redirect放在fs.writeFile的回调函数中,确保文件写入完成后再跳转:

// /borrow接口修改
fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => {
  if (err) {
    console.error(err);
  } else {
    console.log('books.json updated successfully.');
  }
  res.redirect('/home'); // 写入完成后再跳转
});

// /return接口同理修改
fs.writeFile('books.json', JSON.stringify(booksData), 'utf8', (err) => {
  if (err) {
    console.error(err);
  } else {
    console.log('books.json updated successfully.');
  }
  res.redirect('/home');
});

4. 借还接口缺乏登录验证

当前/borrow和/return接口没有添加authenticateSession中间件,未登录用户也能调用这些接口,存在安全隐患。

修复:给这两个接口添加登录验证:

app.post('/borrow', authenticateSession, (req, res) => {
  // 原有代码
});

app.post('/return', authenticateSession, (req, res) => {
  // 原有代码
});

内容的提问来源于stack exchange,提问作者bubs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 05:25:54