去年帮朋友改造传统书店的线上系统时,我深刻体会到现代图书商城需要同时满足两类用户需求:读者需要流畅的浏览购书体验,管理员则需要高效的商品和订单管理能力。这个基于SpringBoot+Vue的双角色图书商城方案,正是针对这种业务场景的完整解决方案。
采用前后端分离架构,后端用SpringBoot提供RESTful API接口,前端用Vue实现动态交互界面。这种组合既能保证后台服务的稳定性,又能提供媲美原生应用的前端体验。特别值得一提的是权限控制系统,通过JWT实现的双角色鉴权机制,让同一套系统可以智能识别读者和管理员身份,展示完全不同的功能界面。
SpringBoot 2.7作为核心框架,配合以下关键组件:
数据库采用MySQL 8.0,主要表结构包括:
sql复制CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`author` varchar(50) NOT NULL,
`price` decimal(10,2) NOT NULL,
`stock` int NOT NULL DEFAULT '0',
`cover_url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Vue 3组合式API开发,主要技术栈:
项目采用模块化结构:
code复制src/
├── api/ # 接口定义
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── stores/ # 状态管理
├── styles/ # 全局样式
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── admin/ # 管理端页面
│ └── client/ # 用户端页面
采用RBAC模型设计权限体系,关键实现步骤:
java复制public enum Role {
CUSTOMER(0, "普通用户"),
ADMIN(1, "管理员");
// 构造方法和getter
}
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
return http.build();
}
}
javascript复制router.beforeEach((to, from, next) => {
if (to.meta.requiresAdmin && !store.getters.isAdmin) {
next('/forbidden')
} else {
next()
}
})
结合Elasticsearch的全文检索方案:
json复制PUT /books
{
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word" },
"author": { "type": "keyword" },
"price": { "type": "double" }
}
}
}
java复制public interface BookRepository extends ElasticsearchRepository<Book, Long> {
List<Book> findByTitleOrAuthor(String title, String author);
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title\", \"author\"]}}")
List<Book> fullTextSearch(String keyword);
}
javascript复制const fetchSuggestions = debounce(async (query) => {
if (query) {
const res = await api.searchSuggest(query)
suggestions.value = res.data
}
}, 300)
java复制public class CartItem {
private Long bookId;
private String title;
private Integer quantity;
private BigDecimal price;
private String coverUrl;
}
java复制@Transactional
public Order createOrder(Long userId, List<CartItem> items) {
// 1. 检查库存
items.forEach(item -> {
int affected = bookMapper.reduceStock(item.getBookId(), item.getQuantity());
if (affected == 0) {
throw new BusinessException("库存不足");
}
});
// 2. 计算总价
BigDecimal total = items.stream()
.map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 3. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setTotalAmount(total);
orderMapper.insert(order);
// 4. 添加订单项
List<OrderItem> orderItems = items.stream()
.map(item -> convertToOrderItem(order.getId(), item))
.collect(Collectors.toList());
orderItemMapper.batchInsert(orderItems);
return order;
}
支付宝沙箱环境集成步骤:
yaml复制alipay:
app-id: 2021000123456789
gateway: https://openapi.alipaydev.com/gateway.do
merchant-private-key: MIICXQIBAAKBgQD...
alipay-public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCn...
notify-url: /api/pay/notify
return-url: /order/pay/success
java复制public String createPayOrder(Order order) {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(alipayProperties.getReturnUrl());
request.setNotifyUrl(alipayProperties.getNotifyUrl());
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(order.getOrderNo());
model.setTotalAmount(order.getTotalAmount().toString());
model.setSubject("图书订单-" + order.getOrderNo());
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
return alipayClient.pageExecute(request).getBody();
}
采用多级缓存方案:
缓存更新策略示例:
java复制@Cacheable(value = "book", key = "#id")
public Book getBookById(Long id) {
return bookMapper.selectById(id);
}
@CacheEvict(value = "book", key = "#book.id")
public void updateBook(Book book) {
bookMapper.updateById(book);
}
javascript复制const BookDetail = () => import('./views/BookDetail.vue')
vue复制<img v-lazy="book.coverUrl" alt="图书封面">
javascript复制// 使用Promise.all合并多个请求
const [bookRes, recommendRes] = await Promise.all([
api.getBookDetail(id),
api.getRecommendBooks()
])
使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./mysql/data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
elasticsearch:
image: elasticsearch:7.17.0
environment:
- discovery.type=single-node
ulimits:
memlock:
soft: -1
hard: -1
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
- elasticsearch
Nginx配置示例:
nginx复制server {
listen 80;
server_name bookshop.example.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
}
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
yaml复制spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
javascript复制// 错误方式 - 不会触发视图更新
state.items[0] = newItem
// 正确方式
state.items.splice(0, 1, newItem)
这个项目最让我惊喜的是Vue 3的组合式API与SpringBoot的协同开发体验。通过清晰的接口契约定义,前后端开发可以完全并行进行。在权限控制方面,建议采用白名单机制,对于管理端路由要特别注意防护。图书搜索功能可以进一步优化为拼音搜索和错别字容错,这在电商场景中非常实用。