Constructing a University Second-hand Marketplace Using Vue and Spring Boot
Architecture Overview
Excessive commodity production has led to significant resource surplus, creating a demand for efficient redistribution channels. This university-oriented resale platform enables students to liquidate unused possessions efficiently, minimizing resource wastage.
The application is engineered utilizing IntelliJ IDEA. The client-side leverages Vue.js integrated with the View UI component framework for visual rendering, while the server-side operates on the Spring Boot framework. The full stack comprises a Windows environment, Java runtime, Tomcat servlet container, MySQL data storage, and MyBatis persistence mapping. Adopting a Browser/Server paradigm with decoupled front-end and back-end endpoints, the solution bifurcates into a public-facing storefront and an administrative dashboard, facilitating product listings, acquisitions, interactive messaging, and real-time news feeds.
- Client Layer: Vue 2.7.10
- Server Layer: Spring Boot 3.1.10
- Data Layer: MySQL 8.0.31
Functional Architecture
The marketplace operational requirements encompass six core domains: Administration Hub, Item Registry, Physical Inspection Scheduler, Purchase Request Manager, Dispute Resolution Board, and Bulletin Publisher. These domains function within a web-driven backend interface.
Administration Hub
This domain encapsulates foundational system configurations: credential governance, organizational hierarchy mapping, access control matrices, activity tracking, and centralized asset storage.
Credential governance allows administrators to perform CRUD operations on registered accounts.
Organizational hierarchy mapping defines the internal departmental structure and staff affiliations of the operating entity.
Access control matrices dictate menu-level visibility assigned to distinct user roles.
Activity tracking records authentication events to audit and trace user behavior patterns.
Centralized asset storage manages visual assets like item photographs, warranty certificates, and legal agreements.
Item Registry Module
The item registry curates all listed pre-owned commodities. Administrators possess full CRUD capabilities over item metadata entries.
Physical Inspection Scheduler
When a prospective purchaser shows preliminary intreest, they may request an in-person examination. Registered members initiate viewing requests through the item registry; vendors subsequently receive these requests to coordinate offline meetups.
Purchase Request Manager
A purchase request signifies a buyer's intent to acquire a specific commodity. Purchasers select desired listings, submit a proposed monetary offer alongside optional remarks, and transmit the transaction proposal. Vendors hold the prerogative to endorse or reject the proposal. Endorsement triggers the generation of a formal transaction contract, solidifying the exchange.
Dispute Resolution Board
Transaction processes inevitably generate inquiries and conflicts. The dispute resolution board offers a dedicated space for buyers, vendors, and platform operators to articulate concerns. Participants may post original messages or compose threaded replies to exsiting threads.
Bulletin Publisher
This module disseminates marketplace updates and anti-fraud advisory content, ensuring registered members remain informed about relevant news.
Data Model Specification
Account Entity
The central account entity captures profile attributes: telecommunication number, residential location, electronic mailbox, government identifier, and biological sex classification.
Item Entity
The item entity tracks listing attributes: unique identifier, manufacturer trademark, third-party verification status, visual representation, monetary valuation, vendor identity, vendor telecommunication number, and supplementary annotations.
Inspection Request Entity
The inspection request entity records scheduling attributes: item identifier, manufacturer trademark, visual representation, vendor identity, vendor telecommunication number, scheduled timestamp, designated location, and supplementary annotations.
Purchase Proposal Entity
The purchase proposal entity archives transaction initiation attributes: item identifier, manufacturer trademark, visual representation, vendor identity, vendor telecommunication number, submission timestamp, proposed monetary valuation, and supplementary annotations.
Dispute Thread Entity
The dispute thread entity preserves communication attributes: textual content, author identity, submission timestamp, response status indicator, respondent identity, response timestamp, response textual content, and supplementary annotations.
Bulletin Entity
The bulletin entity stores broadcast attributes: publisher identifier, publisher identity, broadcast content, visual assets, attached documents, publication timestamp, display priority value, and publication status flag.
Core Implementation
Client Credential Verification
@GetMapping("/authenticateClient")
public ServiceResponse<String> authenticateClient(@RequestParam String accountName, @RequestParam String secretKey) {
LambdaQueryWrapper<Account> filter = new LambdaQueryWrapper<>();
filter.eq(Account::getUsername, accountName);
List<Account> matchedAccounts = accountService.list(filter);
if (matchedAccounts.isEmpty()) {
return ServiceResponse.failure("Account not registered");
}
Account targetAccount = matchedAccounts.get(0);
boolean secretValid = new BCryptPasswordEncoder().matches(secretKey, targetAccount.getEncodedPassword());
if (!secretValid) {
return ServiceResponse.failure("Invalid credentials");
}
String jwtToken = tokenGenerator.generateToken(targetAccount.getUsername(), true);
var authContext = new UsernamePasswordAuthenticationToken(
new AuthenticatedPrincipal(targetAccount), null, null
);
SecurityContextHolder.getContext().setAuthentication(authContext);
return ServiceResponse.success(jwtToken);
}
Client Enrollment
@GetMapping("/enrollClient")
public ServiceResponse<String> enrollClient(@RequestParam String accountName, @RequestParam String phoneNum, @RequestParam String secretKey) {
LambdaQueryWrapper<Account> duplicateFilter = new LambdaQueryWrapper<>();
duplicateFilter.and(wrapper -> wrapper.eq(Account::getUsername, accountName).or().eq(Account::getPhone, phoneNum));
long existingCount = accountService.count(duplicateFilter);
if (existingCount > 0) {
return ServiceResponse.failure("Account or phone already exists");
}
Account freshAccount = new Account();
freshAccount.setUsername(accountName);
freshAccount.setDisplayName(accountName);
freshAccount.setPhone(phoneNum);
freshAccount.setMailbox(phoneNum + "@campus.edu");
String hashedSecret = new BCryptPasswordEncoder().encode(secretKey);
freshAccount.setEncodedPassword(hashedSecret);
freshAccount.setRoleCategory(0);
accountService.persistOrUpdate(freshAccount);
LambdaQueryWrapper<SystemRole> roleFilter = new LambdaQueryWrapper<>();
roleFilter.eq(SystemRole::isDefaultAssignment, true);
List<SystemRole> defaultRoles = systemRoleService.list(roleFilter);
for (SystemRole role : defaultRoles) {
AccountRoleMapping mapping = new AccountRoleMapping();
mapping.setAccountId(freshAccount.getId());
mapping.setRoleId(role.getId());
accountRoleMappingService.persistOrUpdate(mapping);
}
String jwtToken = tokenGenerator.generateToken(freshAccount.getUsername(), true);
var authContext = new UsernamePasswordAuthenticationToken(
new AuthenticatedPrincipal(freshAccount), null, null
);
SecurityContextHolder.getContext().setAuthentication(authContext);
return ServiceResponse.success(jwtToken);
}
Inspection Request Creation
@PostMapping("/scheduleInspection")
public ServiceResponse<Object> scheduleInspection(@RequestParam String listingId) {
ResaleItem targetItem = resaleItemService.fetchById(listingId);
if (targetItem == null) {
return ServiceResponse.failure("Listing unavailable");
}
Account activeUser = authUtility.getActiveUser();
LambdaQueryWrapper<InspectionRequest> overlapFilter = new LambdaQueryWrapper<>();
overlapFilter.eq(InspectionRequest::getItemRef, listingId);
overlapFilter.eq(InspectionRequest::getRequesterRef, activeUser.getId());
long overlapCount = inspectionRequestService.count(overlapFilter);
if (overlapCount > 0L) {
return ServiceResponse.failure("Duplicate inspection request detected");
}
InspectionRequest newRequest = new InspectionRequest();
newRequest.setItemRef(listingId);
newRequest.setItemTrademark(targetItem.getTrademark());
newRequest.setValuation(targetItem.getValuation());
newRequest.setVisualRef(targetItem.getVisualRef());
newRequest.setVendorRef(targetItem.getVendorId());
newRequest.setVendorIdentity(targetItem.getVendorIdentity());
newRequest.setVendorTelecom(targetItem.getVendorTelecom());
newRequest.setRequesterRef(activeUser.getId());
newRequest.setRequesterIdentity(activeUser.getDisplayName());
newRequest.setCreationTimestamp(LocalDateTime.now().toString());
newRequest.setAgreementStatus(false);
newRequest.setAgreementTimestamp("");
inspectionRequestService.persistOrUpdate(newRequest);
return ServiceResponse.success();
}
Transaction Endorsement
@PostMapping("/endorseTransaction")
public ServiceResponse<Object> endorseTransaction(@RequestParam String transactionRef) {
InspectionRequest storedRequest = inspectionRequestService.fetchById(transactionRef);
if (storedRequest == null) {
return ServiceResponse.failure("Transaction record missing");
}
storedRequest.setEndorsementFlag(true);
inspectionRequestService.persistOrUpdate(storedRequest);
return ServiceResponse.success();
}
Dispute Thread Retrieval
@GetMapping("/fetchDisputeThreads")
public ServiceResponse<IPage<DisputeThread>> fetchDisputeThreads(@ModelAttribute DisputeThread threadFilter, @ModelAttribute PaginationSpec pageSpec) {
LambdaQueryWrapper<DisputeThread> queryFilter = new LambdaQueryWrapper<>();
if (StringUtils.hasText(threadFilter.getRecordDate())) {
queryFilter.eq(DisputeThread::getRecordDate, threadFilter.getRecordDate());
}
if (StringUtils.hasText(threadFilter.getParentThreadRef())) {
queryFilter.eq(DisputeThread::getParentThreadRef, threadFilter.getParentThreadRef());
} else {
queryFilter.eq(DisputeThread::getParentThreadRef, "");
}
IPage<DisputeThread> resultPage = disputeThreadService.page(
PageUtility.convertToPage(pageSpec), queryFilter
);
return ServiceResponse.success(resultPage);
}