Vue3+SpringBoot二手商城实战:前后端分离工程落地指南
2026/9/18 8:34:04 网站建设 项目流程

简介:本资源是一份面向计算机专业本科生的毕业设计论文文档,聚焦大学生二手电子产品交易平台的系统化设计与实现,适用于Java Web开发、前后端分离实践及毕业论文写作参考。全文基于Vue+SpringBoot技术栈展开,涵盖平台需求分析、系统架构设计、前后端功能模块实现、数据挖掘在交易优化中的应用,以及信息管理系统在交易监控与报表生成中的实际作用,内容兼具理论深度与工程落地性。资源为单个3.35MB的Word文档(.docx),完整包含摘要、中英文关键词、目录、绪论、相关技术介绍、系统设计与实现、总结与展望等标准论文结构,附有详细技术选型依据与对比分析。目前已有193人学习下载,可直接用于毕业答辩材料准备、课程设计复盘或Vue+SpringBoot全栈开发学习参考。

1. 这不是又一个“毕设Demo”:Vue+SpringBoot二手电子商城的真实工程切口

很多同学拿到“大学生二手电子产品商城”这个题目,第一反应是套个若依、RuoYi-Vue或SpringBoot脚手架,填几个CRUD接口、改几处页面样式,交差了事。但真正拆过这份毕业设计文档的人会发现:它在技术选型上埋了一个关键伏笔——前端用 Vue(非Vue2旧模板),后端用 SpringBoot(非SSM老架构),数据库用 MySQL(非H2内存库),且明确要求“B/S架构”“MVC三层分离”“管理员/用户双角色权限控制”。这不是在堆砌技术名词,而是在模拟一个真实轻量级电商系统的最小可行闭环:商品发布→浏览搜索→下单支付→订单管理→后台审核。尤其值得注意的是,文档中反复强调“实用性”“易用性”“结构清晰”,说明它刻意规避了微服务、分布式事务、高并发秒杀等超纲内容,把焦点收束在单体应用内可落地的前后端分离实践上。对刚走出课堂的开发者而言,它是一份极佳的“过渡型项目”:既不会因过度简化失去工程感,又不会因过度复杂陷入理论空转。你不需要部署K8s集群,但必须搞懂@RequestBody怎么接收Vue发来的JSON、vue-router如何与SpringBoot静态资源路径协同、MySQL的datetime字段如何被MyBatis-Plus自动映射为JavaLocalDateTime——这些才是校招面试官真正在意的“能跑通的细节”。

2. Vue前端工程搭建:从环境配置到路由守卫的实战闭环

2.1 Vue 3 + Vite 环境初始化与依赖安装

毕业论文虽未指定Vue版本,但结合“Vue-SpringBoot解决方案”的表述及当前主流实践,应采用 Vue 3(Composition API)配合 Vite 构建工具。Vite 的冷启动速度和热更新效率远超传统 Webpack,对开发体验提升显著。执行以下命令完成初始化:

# 创建项目(使用npm) npm create vue@latest # 按提示选择:✔ Add TypeScript? ... Yes # ✔ Add JSX Support? ... No # ✔ Add Vue Router for Single Page Application development? ... Yes # ✔ Add Pinia for state management? ... Yes # ✔ Add Vitest for Unit testing? ... No(毕设阶段可暂略) # ✔ Add Cypress for both Unit and End-to-End testing? ... No # ✔ Add ESLint for code quality? ... Yes # ✔ Add Prettier for code formatting? ... Yes

提示create vue@latest会自动拉取 Vue 官方推荐的最新脚手架,避免手动配置vue-cli的兼容性问题。若网络受限,可先npm config set registry https://registry.npmmirror.com切换国内镜像源。

安装完成后,进入项目目录并安装核心业务依赖:

cd vue-springboot-campus-market npm install axios@1.6.7 element-plus@2.7.6 @vueuse/core@10.9.0
  • axios@1.6.7:稳定版HTTP客户端,支持拦截器统一处理Token;
  • element-plus@2.7.6:成熟UI组件库,提供el-tableel-form等电商后台必需组件;
  • @vueuse/core:提供useStorage(本地缓存用户Token)、useDebounceFn(防抖搜索)等实用组合式函数。

2.2 基于角色的路由守卫与权限控制实现

系统存在“管理员”与“普通用户”两类角色,需在前端层面拦截非法访问。Vue Router 4 提供router.beforeEach全局前置守卫,结合Pinia状态管理实现动态权限校验:

// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue') }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } // 显式标记无需登录 }, { path: '/admin', name: 'AdminDashboard', component: () => import('@/views/admin/Dashboard.vue'), meta: { requiresAuth: true, role: 'admin' } }, { path: '/user/profile', name: 'UserProfile', component: () => import('@/views/user/Profile.vue'), meta: { requiresAuth: true, role: 'user' } } ] }) // 全局路由守卫 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() // 若目标路由需要认证 if (to.meta.requiresAuth) { // 尝试从localStorage恢复用户状态 if (!userStore.token) { try { await userStore.fetchUserInfo() // 调用API获取用户信息并设置token } catch (error) { next('/login') // 获取失败,跳转登录页 return } } // 角色校验:管理员只能访问admin路由,用户只能访问user路由 if (to.meta.role && userStore.role !== to.meta.role) { next(userStore.role === 'admin' ? '/admin' : '/user/profile') return } } next() // 放行 }) export default router

参数说明meta.requiresAuth控制是否需要登录;meta.role指定该路由允许的角色类型。userStore.fetchUserInfo()内部调用/api/user/info接口,返回{ id, username, role, token },并将token存入localStorage供后续请求携带。此设计避免了每次刷新页面都需重新登录,符合“实用性”要求。

2.3 商品列表页的响应式布局与搜索过滤逻辑

二手商品列表页是核心交互场景,需支持关键词搜索、分类筛选、价格区间过滤。Vue 3 的refcomputed可高效实现数据驱动:

<!-- src/views/user/MarketList.vue --> <template> <div class="market-list"> <!-- 搜索栏 --> <el-input v-model="searchKeyword" placeholder="输入商品名称、品牌搜索..." @input="debouncedSearch" clearable /> <!-- 分类筛选下拉框 --> <el-select v-model="selectedCategory" placeholder="全部分类" @change="filterByCategory" > <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" /> </el-select> <!-- 价格区间滑块 --> <el-slider v-model="priceRange" range :min="0" :max="5000" @change="filterByPrice" /> <!-- 商品卡片列表 --> <div class="goods-grid"> <el-card v-for="item in filteredGoods" :key="item.id" class="goods-card" > <img :src="item.picture" alt="商品图片" class="goods-img" /> <div class="goods-info"> <h3>{{ item.name }}</h3> <p class="price">¥{{ item.price }}</p> <p class="brand">{{ item.brand }} | {{ item.condition }}成新</p> <el-button type="primary" size="small" @click="goToDetail(item.id)"> 查看详情 </el-button> </div> </el-card> </div> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue' import { useDebounceFn } from '@vueuse/core' import { getGoodsList } from '@/api/goods' // 响应式数据 const searchKeyword = ref('') const selectedCategory = ref<number | null>(null) const priceRange = ref<[number, number]>([0, 5000]) const allGoods = ref<any[]>([]) const categories = ref<{id: number, name: string}[]>([]) // 计算属性:过滤后的商品列表 const filteredGoods = computed(() => { return allGoods.value.filter(item => { const keywordMatch = item.name.includes(searchKeyword.value) || item.brand.includes(searchKeyword.value) const categoryMatch = !selectedCategory.value || item.categoryId === selectedCategory.value const priceMatch = item.price >= priceRange.value[0] && item.price <= priceRange.value[1] return keywordMatch && categoryMatch && priceMatch }) }) // 防抖搜索(避免频繁请求) const debouncedSearch = useDebounceFn(() => { // 实际项目中此处应调用API,毕设可先用本地数据模拟 }, 300) // 初始化加载商品数据 onMounted(async () => { try { const res = await getGoodsList() allGoods.value = res.data // categories 数据可从 /api/category/list 接口获取 } catch (error) { console.error('加载商品失败:', error) } }) </script>

逻辑说明filteredGoods是一个计算属性,实时响应searchKeywordselectedCategorypriceRange的变化,无需手动触发filter方法。useDebounceFn将搜索输入延迟300ms执行,防止用户连续敲击时触发多次无效请求。getGoodsList()应封装为 Axios 请求,URL 指向 SpringBoot 后端/api/goods/list接口,返回 JSON 格式商品数组。

3. SpringBoot后端开发:从RESTful接口设计到MyBatis-Plus分页查询

3.1 RESTful风格接口规范与Controller层实现

毕业论文强调“B/S架构”与“MVC三层分离”,后端需严格遵循 RESTful 设计原则:资源路径用名词(/api/goods)、动作用HTTP方法(GET/POST/PUT/DELETE)、状态码语义化(200成功、401未授权、404不存在)。以商品管理为例,Controller 层代码如下:

// src/main/java/com/example/market/controller/GoodsController.java @RestController @RequestMapping("/api/goods") @RequiredArgsConstructor public class GoodsController { private final GoodsService goodsService; /** * GET /api/goods?page=1&size=10&keyword=手机&categoryId=2 * 分页查询商品列表(支持关键词、分类ID过滤) */ @GetMapping public Result<Page<Goods>> list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { Page<Goods> result = goodsService.listWithFilter(page, size, keyword, categoryId); return Result.success(result); } /** * POST /api/goods * 用户发布二手商品(需JWT Token校验) */ @PostMapping @PreAuthorize("hasRole('USER')") public Result<String> publish(@RequestBody @Valid Goods goods, Authentication authentication) { Long userId = ((JwtAuthenticationToken) authentication).getTokenAttributes() .get("userId", Long.class); goods.setUserId(userId); goods.setStatus(GoodsStatus.PENDING); // 待审核状态 goodsService.save(goods); return Result.success("发布成功,等待管理员审核"); } /** * GET /api/goods/{id} * 查询单个商品详情(含关联的评论) */ @GetMapping("/{id}") public Result<GoodsDetailVO> detail(@PathVariable Long id) { GoodsDetailVO vo = goodsService.getDetailById(id); if (vo == null) { return Result.fail("商品不存在"); } return Result.success(vo); } }

参数说明@RequestParam绑定查询参数,@PathVariable绑定路径变量,@RequestBody接收JSON请求体。@PreAuthorize("hasRole('USER')")是Spring Security注解,确保只有角色为USER的用户才能调用发布接口。Result<T>是自定义统一封装类,包含codemsgdata字段,避免前端重复解析状态。

3.2 MyBatis-Plus分页插件配置与多条件动态查询

MyBatis-Plus 的Page对象与QueryWrapper是实现分页与动态查询的核心。需在 SpringBoot 配置类中启用分页插件:

// src/main/java/com/example/market/config/MybatisPlusConfig.java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

GoodsServicelistWithFilter方法使用QueryWrapper构建动态SQL:

// src/main/java/com/example/market/service/impl/GoodsServiceImpl.java @Service @RequiredArgsConstructor public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService { private final GoodsMapper goodsMapper; @Override public Page<Goods> listWithFilter(Integer page, Integer size, String keyword, Long categoryId) { Page<Goods> pageObj = new Page<>(page, size); QueryWrapper<Goods> wrapper = new QueryWrapper<>(); // 动态添加WHERE条件 if (StringUtils.isNotBlank(keyword)) { wrapper.like("name", keyword).or().like("brand", keyword); } if (categoryId != null && categoryId > 0) { wrapper.eq("category_id", categoryId); } // 状态为上架(非删除、非下架) wrapper.eq("status", GoodsStatus.ONLINE.getCode()); return goodsMapper.selectPage(pageObj, wrapper); } }

逻辑说明QueryWrapper会根据keywordcategoryId是否为空,智能拼接WHERE子句。例如当keyword="iPhone"categoryId=2时,生成的SQL为SELECT * FROM goods WHERE (name LIKE '%iPhone%' OR brand LIKE '%iPhone%') AND category_id = 2 AND status = 1 LIMIT 0,10selectPage方法自动注入LIMITCOUNT(*),无需手动写分页SQL。

3.3 JWT Token认证与用户权限拦截器

毕业论文要求“管理员/用户双角色”,需通过 JWT 实现无状态认证。Spring Security 配置如下:

// src/main/java/com/example/market/config/SecurityConfig.java @Configuration @EnableWebSecurity @EnableMethodSecurity // 启用@PreAuthorize注解 public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 毕设可关闭CSRF(前后端分离场景) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/api/login", "/api/register", "/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } }

JwtAuthenticationFilterAuthorizationHeader 中提取Token,解析出用户ID和角色,并存入SecurityContext

// src/main/java/com/example/market/filter/JwtAuthenticationFilter.java public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && tokenProvider.validateToken(token)) { Authentication auth = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }

关键点JwtTokenProvider需实现generateToken()(登录成功时生成)、validateToken()(校验签名与过期时间)、getAuthentication()(解析Token载荷,创建UsernamePasswordAuthenticationToken)。载荷中必须包含userIdrole字段,供@PreAuthorize注解读取。

4. 数据库设计与MyBatis-Plus实体映射:从E-R图到Java对象

4.1 核心数据表结构解析与字段设计依据

毕业论文附录中的 E-R 图与数据表定义是数据库设计的直接依据。以goods(二手商品)表为例,其字段设计需兼顾业务需求与查询效率:

字段名类型长度说明设计依据
idBIGINT-主键,自增所有表通用主键
nameVARCHAR200商品名称用户搜索核心字段,需索引
priceDECIMAL(10,2)售价金融类数据,用DECIMAL避免浮点误差
category_idBIGINT-外键,关联category支持分类筛选,需建立索引
user_idBIGINT-发布者ID,外键关联用户表,记录归属关系
brandVARCHAR100品牌搜索高频字段,如“苹果”、“华为”
conditionTINYINT-新旧程度(1-5)枚举值,节省存储空间
pictureLONGTEXT-图片URL(JSON数组)毕设阶段存URL而非二进制,降低DB压力
statusTINYINT-状态(0-待审核,1-上架,2-下架,3-已售)支持后台审核流,避免硬删除

注意picture字段存储多个图片URL,格式为["https://xxx/1.jpg","https://xxx/2.jpg"],前端解析为数组渲染轮播图。status字段用TINYINT而非VARCHAR,便于SQL条件判断(WHERE status = 1WHERE status = 'ONLINE'效率更高)。

4.2 MyBatis-Plus实体类与@TableField注解详解

MyBatis-Plus 通过@TableName@TableId注解将Java类与数据库表映射。针对goods表,实体类定义如下:

// src/main/java/com/example/market/entity/Goods.java import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("goods") public class Goods { @TableId(type = IdType.ASSIGN_ID) // 使用雪花算法生成分布式ID private Long id; private String name; private BigDecimal price; @TableField("category_id") private Long categoryId; @TableField("user_id") private Long userId; private String brand; @TableField("condition") private Integer condition; // 1-5成新 @TableField("picture") private String picture; // JSON字符串 @TableField("status") private Integer status; // 对应GoodsStatus枚举 @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; }

参数说明@TableId(type = IdType.ASSIGN_ID)指定主键策略为雪花算法,生成全局唯一Long型ID,避免数据库自增ID在分库分表时的冲突风险;@TableField("category_id")明确指定数据库字段名,解决Java驼峰命名与数据库下划线命名的映射问题;fill = FieldFill.INSERT表示createTime字段在插入时由MyBatis-Plus自动设置为当前时间,无需在Controller中手动赋值。

4.3 多表关联查询:商品详情与评论的VO封装

商品详情页需同时展示商品信息与用户评论,需进行goodsgoods_comment表的关联查询。MyBatis-Plus 不推荐在@Select中写复杂JOIN,而是采用@TableField(exist = false)+ 服务层组装的方式:

// src/main/java/com/example/market/vo/GoodsDetailVO.java import lombok.Data; import java.util.List; @Data public class GoodsDetailVO { private Long id; private String name; private BigDecimal price; private String brand; private Integer condition; private String picture; private String description; // 商品描述 // 关联的评论列表 private List<CommentVO> comments; } // src/main/java/com/example/market/vo/CommentVO.java @Data public class CommentVO { private Long id; private String nickname; // 评论者昵称 private String avatarUrl; // 头像URL private String content; // 评论内容 private LocalDateTime createTime; }

GoodsService.getDetailById()方法通过两次查询组装VO:

@Override public GoodsDetailVO getDetailById(Long id) { // 1. 查询商品基本信息 Goods goods = this.getById(id); if (goods == null) return null; GoodsDetailVO vo = BeanUtil.copyProperties(goods, GoodsDetailVO.class); // 2. 查询关联评论(按时间倒序) QueryWrapper<GoodsComment> commentWrapper = new QueryWrapper<>(); commentWrapper.eq("goods_id", id).orderByDesc("create_time"); List<GoodsComment> comments = goodsCommentService.list(commentWrapper); // 3. 转换为VO列表 vo.setComments(comments.stream() .map(c -> { CommentVO cv = new CommentVO(); cv.setId(c.getId()); cv.setNickname(c.getNickname()); cv.setAvatarUrl(c.getAvatarUrl()); cv.setContent(c.getContent()); cv.setCreateTime(c.getCreateTime()); return cv; }) .collect(Collectors.toList())); return vo; }

优势:相比单次JOIN查询,此方式更易维护、调试和扩展。若未来需增加“点赞数”统计,只需在CommentVO中添加likeCount字段,并在Stream中调用commentLikeService.countByCommentId(c.getId())即可,无需修改SQL。

5. 前后端联调与常见问题排错:从CORS到跨域Cookie的实战方案

5.1 开发环境跨域问题的三种解决路径

Vue 开发服务器(http://localhost:5173)与 SpringBoot 后端(http://localhost:8080)端口不同,必然触发浏览器CORS(跨域资源共享)限制。毕业设计中需选择一种可靠方案:

方案一:Vue代理(开发阶段首选)

vite.config.ts中配置代理,将/api前缀请求转发至后端:

// vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, // 修改请求头中的host为target rewrite: (path) => path.replace(/^\/api/, '') // 去掉/api前缀 } } } })

此时前端请求axios.get('/api/goods')会被代理到http://localhost:8080/goods,浏览器认为是同源请求,彻底规避CORS。

方案二:SpringBoot全局CORS配置(测试/演示阶段)

若需独立运行前端,可在SpringBoot中开启CORS:

// src/main/java/com/example/market/config/WebMvcConfig.java @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173") // 允许的前端地址 .allowCredentials(true) // 允许携带Cookie/Token .maxAge(3600) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"); } }

注意allowCredentials(true)必须配合allowedOrigins指定具体域名(不能为*),否则浏览器会拒绝响应。

方案三:Nginx反向代理(生产部署标准做法)

将Vue打包产物(dist目录)与SpringBoot JAR包部署在同一台服务器,用Nginx统一入口:

# nginx.conf server { listen 80; server_name campus-market.example.com; location / { root /var/www/vue-dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; # 转发到SpringBoot proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

此时所有请求均走http://campus-market.example.com,彻底消除跨域。

5.2 登录状态丢失的典型原因与修复步骤

学生在联调时常遇到“登录成功,但刷新页面后变回未登录状态”,根本原因在于Token未正确持久化或请求未携带。排查步骤如下:

  1. 检查前端Token存储位置
    登录成功后,确认localStorage.setItem('token', response.data.token)是否执行。打开浏览器开发者工具 → Application → Local Storage,查看token键值是否存在。

  2. 验证Axios请求拦截器是否注入Token
    src/utils/request.ts中检查拦截器:

    // 请求拦截器:添加Authorization头 service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` // 注意Bearer空格 } return config })
  3. 后端JWT解析是否匹配Header格式
    JwtAuthenticationFilter.resolveToken()方法中,request.getHeader("Authorization")返回值应为"Bearer eyJhbGciOi...。若前端传的是Authorization: eyJhbGciOi...(缺少Bearer前缀),则解析失败。可通过Postman测试:GET http://localhost:8080/api/user/info,Headers中添加Authorization: Bearer <your-token>

  4. 检查Spring Security是否放行OPTIONS预检请求
    若控制台出现403 Forbidden且请求Method为OPTIONS,说明CORS预检被拦截。在SecurityConfigauthorizeHttpRequests中,确保/api/**路径被permitAll()authenticated()正确覆盖,且addFilterBefore的顺序无误。

5.3 MySQL中文乱码与日期格式化问题速查表

问题现象根本原因解决方案
插入中文显示为???MySQL服务端字符集非utf8mb4修改my.cnf
[client]
default-character-set = utf8mb4
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
LocalDateTime返回JSON为{}空对象Jackson未配置JavaTimeModuleapplication.yml中添加:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
MyBatis-Plus插入时间字段为0000-00-00 00:00:00MySQL SQL模式包含NO_ZERO_DATE执行SQL:
SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));

关键操作:修改MySQL配置后,必须重启MySQL服务(sudo systemctl restart mysqld)并重新创建数据库(CREATE DATABASE campus_market CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;),旧数据库需执行ALTER DATABASE campus_market CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询