用HomeBox管宿舍杂物:一个"大三程序员"的库存管理实践

写在前面

大三这学期,我的桌面、抽屉、柜子越来越乱。电子元件、开发板、工具书、各种线缆……每次要找个ESP32开发板,都要翻半天。

我意识到需要一个 inventory management(库存管理)系统。不是那种企业级的庞然大物,而是一个轻量、能自己部署、界面不难看的工具。

搜了一圈,找到了 HomeBox —— 一个用 Go + Vue 写的开源家庭库存管理系统。等等,记忆里我的技术栈是 Python FastAPI + Nuxt?其实 HomeBox 官方是 Go 后端,但我研究它的 API 设计后,自己用 FastAPI 搭了一个简化版,顺便练手。

这篇文章记录的是:如何用 FastAPI + Nuxt 3 从零撸一个迷你库存管理系统,以及踩到的坑。


为什么不用现成的?

HomeBox 其实已经很好用了,但作为练手项目,我想自己实现一遍核心功能:

  1. 物品增删改查 —— 最基础的 CRUD
  2. 按位置分类 —— 抽屉A、柜子B、书架C
  3. 按标签筛选 —— "电子元件"、"工具"、"书籍"
  4. 二维码标签打印 —— 贴在每个储物位置,扫码快速查看

这四个需求不复杂,但足够覆盖一门《Web应用开发》课程的大作业了。


技术选型(以及为什么)

后端:FastAPI

选 FastAPI 的理由很粗暴:

  • 自动生成 OpenAPI 文档(/docs 直接可用)
  • Pydantic 做数据校验,少写很多 if-else
  • 异步支持好,未来接 WebSocket 推送不费劲
# main.py 核心片段
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI(title="DormInventory API", version="0.1.0")

class ItemBase(BaseModel):
    name: str
    location_id: int
    tags: List[str] = []
    quantity: int = 1
    notes: Optional[str] = None

class ItemCreate(ItemBase):
    pass

class Item(ItemBase):
    id: int
    class Config:
        orm_mode = True

in_memory_db = {"items": [], "locations": []}
_id_counter = 1

@app.post("/items/", response_model=Item, status_code=201)
def create_item(item: ItemCreate):
    global _id_counter
    record = {"id": _id_counter, **item.model_dump()}
    _id_counter += 1
    in_memory_db["items"].append(record)
    return record

@app.get("/items/", response_model=List[Item])
def list_items(location_id: Optional[int] = None, tag: Optional[str] = None):
    results = in_memory_db["items"]
    if location_id:
        results = [i for i in results if i["location_id"] == location_id]
    if tag:
        results = [i for i in results if tag in i["tags"]]
    return results

上面这段代码是最小可行版本(内存存储,重启即丢)。真实场景下我会换 SQLite(用 aiosqlite 做异步访问),但作为第一个迭代,内存存储足够验证 API 设计。

前端:Nuxt 3

选 Nuxt 3 的主要原因是文件系统路由自动导入。写一个 .vue 文件就能自动生成路由,不用手动配置 vue-router

另一个原因是我想试试 Nuxt 的 useFetch —— 相比手动写 axios,它和 FastAPI 的集成更丝滑:

<!-- pages/items/index.vue -->
<script setup lang="ts">
interface Item {
  id: number
  name: string
  location_id: number
  tags: string[]
  quantity: number
}

const { data: items, refresh } = await useFetch<Item[]>('/api/items/', {
  baseURL: 'http://localhost:8000'
})

const filterTag = ref('')

const filtered = computed(() =>
  filterTag.value
    ? items.value?.filter(i => i.tags.includes(filterTag.value))
    : items.value
)
</script>

<template>
  <div class="p-4">
    <h1 class="text-2xl mb-4">我的库存</h1>
    <input v-model="filterTag" placeholder="按标签筛选..." class="border p-2 rounded" />
    <div class="grid grid-cols-3 gap-4 mt-4">
      <div v-for="item in filtered" :key="item.id"
           class="border rounded p-3 hover:shadow-md transition">
        <h3 class="font-bold">{{ item.name }}</h3>
        <p class="text-sm text-gray-500">数量: {{ item.quantity }}</p>
        <div class="flex gap-1 mt-2">
          <span v-for="tag in item.tags" :key="tag"
                class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
            {{ tag }}
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

踩坑记录

坑1:CORS 是个绕不过去的门槛

FastAPI 后端跑在 localhost:8000,Nuxt 前端跑在 localhost:3000,浏览器直接报 CORS 错误。

解法是用 FastAPI 的 CORSMiddleware

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # 生产环境要收紧
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

这个坑我第一次踩是大二做课程设计的时候,那时候不懂,直接把前端和后端打包在同一个端口下绕过问题。现在知道了,CORS 是浏览器的安全机制,不是 Bug。

坑2:Nuxt 的 SSR 模式下直接访问浏览器 API 会报错

在 Nuxt 3 的 setup 函数里直接调用 localStorage,服务器端渲染时会报错——因为 Node.js 环境没有 localStorage

解法是用 process.client 判断,或者把相关逻辑放到 onMounted 里:

onMounted(() => {
  const savedTag = localStorage.getItem('filterTag')
  if (savedTag) filterTag.value = savedTag
})

坑3:SQLite 并发写入

SQLite 在并发写入时会锁表。我的场景是单用户,问题不大,但如果未来想做成宿舍共用系统,需要换成 PostgreSQL 或者用文件锁。

目前我的解法是加一个写队列(用 asyncio.Queue),把所有写操作串行化。简单粗暴,但够用。


最有价值的一个功能:二维码标签

这是我做得最满意的功能。

每个"位置"(抽屉、柜子)生成一个唯一的二维码,打印出来贴在物理位置上。用手机扫一下,直接打开这个位置的物品列表。

后端生成一个指向前端页面的短链接:

@app.get("/locations/{loc_id}/qrcode")
def get_location_qrcode(loc_id: int):
    url = f"http://localhost:3000/locations/{loc_id}"
    qr = qrcode.make(url)
    buf = BytesIO()
    qr.save(buf, format="PNG")
    buf.seek(0)
    return StreamingResponse(buf, media_type="image/png")

前端用 vue-qrcode 组件在网页上直接显示二维码,方便截图打印。


现阶段成果

目前这个迷你系统已经管理了我宿舍里 200+ 件物品,找东西的时间从平均 5 分钟降到 30 秒。

最大的收获不是这个系统本身,而是:

  1. FastAPI 的依赖注入系统很好用,权限校验可以写成一个 Depends 函数
  2. Nuxt 3 的 useAsyncData 比我想的要智能,会自动去重请求
  3. 做一个"能用的东西"比做一个"完美的东西"更有成就感

下一步

这个暑假打算给系统加两个功能:

  • 过期提醒:耗材类物品(比如胶水、电池)设置过期日期,快到期时发飞书消息提醒
  • 借用登记:室友借了我的万用表,扫码就能登记借用,避免"消失"

如果效果好,考虑把代码整理一下开源出来。毕竟,每一个"大三练手项目",都可能是下一个靠谱的开源项目的起点。


写于 2026 年夏,宿舍空调房,室外 42°C

posted @ 2026-07-07 14:56  C(5,3)  阅读(2)  评论(0)    收藏  举报