06 / WORK / CASE STUDY
NKUST Equipment Borrowing System
Barcode lending, inventory, compensation and a meal-fund ledger for a university department — solo-built and deployed since 2025.
- TYPE
- FULL STACK
- SCOPE
- Deployed · solo build
- STACK
- DJANGO · DRF · MYSQL · REACT · PLAYWRIGHT
- LINKS
- LIVE
01 / PROBLEM
The department's equipment lending lived inside a legacy platform module with messy data, and the shared meal fund was a hand-maintained spreadsheet. This system took over both: formalizing lending (overdue, compensation, stocktakes, kits) and turning the fund into real ledger accounting with balances, settlement periods and cash verification.
02 / CONSTRAINTS
- Had to absorb the legacy database — old borrowing records were riddled with empty fields.
- Front-desk lending is high-frequency and run by non-technical staff — each transaction must finish in seconds.
- One VPS, no containers or Redis: caching is in-process LocMemCache, with rate limits on a DB cache to share across workers.
03 / ARCHITECTURE
A Django 5 + DRF modular monolith split into 13 domain apps (borrowing, finance, audit, inventory, kits…) — 27 models, 23 resource routes plus 86 custom actions — with a React 19 + Ant Design frontend of 40 pages. JWT lives in HttpOnly cookies with refresh rotation and blacklisting; RBAC is modeled as Roles and Capabilities.
Auditing is its own subsystem: middleware records every operation (sensitive-action flags, latency, response status), plus field-level model change logs. The finance subsystem runs the meal fund: transactions maintain account balances on write, with settlement periods and cash verification — and the whole ledger can be rebuilt from the original spreadsheet in one import.
04 / RESPONSIBILITIES
Independent full-stack development (single author across the entire git history, 2025/09–2026/07): data models, APIs, permissions, the React frontend, test strategy and deployment (nginx + gunicorn under systemd).
05 / CHALLENGES → SOLUTIONS
CHALLENGE
Legacy borrowing records had to move into the new schema, but the data was too dirty to import directly.
SOLUTION
Built a migration toolkit (one mapper per entity, a standalone verifier, dry-run and reports), keeping the legacy DB behind its own connection. One real run took 180 seconds and moved 9,138 borrow tickets, 3,336 students and 370 pieces of equipment — while 2,271 legacy rows failed on empty fields. Those errors live in the migration report as tracked data-quality debt, not swallowed.
CHALLENGE
The front desk needs speed — manual entry was too slow, and desktop and mobile scan with entirely different hardware.
SOLUTION
Made the whole flow barcode-driven: the backend generates Code128 labels; the frontend auto-switches by device — scanner-gun input on desktop, camera (@zxing) on mobile — with batch scanning and short-window dedup. One scan pulls up the equipment and its ticket.
CHALLENGE
Years of old and new records were tangled in the meal-fund spreadsheet — formalizing it couldn't lose a single row.
SOLUTION
Wrote a two-pass import command: pass one creates accounts and categories, pass two handles opening balances, settlement periods and every transaction — all inside a DB transaction, with dry-run and re-runs. Balances maintain themselves on write, and month-end cash verification closes the loop.
06 / SYSTEM FACTS
13
domain apps
86
custom API actions
1,112
backend test functions
36
Playwright E2E specs
07 / LESSONS
Migration guards belong in the mapper: the 2,271 failures were nearly all one class of empty-field problem — clean first, import second, and that error report never exists.
Silencing errors into 'everything is fine' is worse than breaking. The duplicate pages and fake-data displays a late UX audit surfaced are debt from the fast-stacking phase — now numbered, listed, and being paid down.
08 / SCREENS
All screens below show demo (mock) data — no real user data.



NEXT
Large-scale Django Microservices Commerce Platform