Springboot + vue + uni-app小程序web端全套家具商场_springboot vue uniapp
Springboot + vue + uni-app小程序web端全套家具商场
文章目录
-
-
- Springboot + vue + uni-app小程序web端全套家具商场
-
- 1、项目概述
- 2、技术栈
- 3、系统功能模块
- 4、项目截图
-
- 4.1、web端
- 4.2、小程序端
- 5、核心代码
-
- 5.1、管理端
- 5.2、小程序端
- 5.3、后端
-
1、项目概述
这是一个基于SpringBoot + Vue3 + uni-app的全栈家具电商平台,包含Web后台管理系统和微信小程序端,专注于家具产品的在线销售与管理。平台实现了从商品管理、订单处理到用户交互的完整电商业务流程。
2、技术栈
后端技术栈
- 核心框架: Spring Boot 3
- 数据库: MySQL 8.0
- ORM框架: MyBatis-Plus
- 认证授权: Spring Security + JWT
- 缓存: Redis
- 文件存储: 阿里云OSS/七牛云
前端管理系统
- 前端框架: Vue 3 + Composition API
- UI组件库: Element Plus
- 状态管理: Pinia
- 路由: Vue Router
- HTTP客户端: Axios
- 可视化: ECharts
- 构建工具: Vite
微信小程序端
- 开发框架: uni-app (基于Vue.js)
- UI组件库: uView UI
- 状态管理: Vuex
- 网络请求: uni.request封装
- 推送通知: 微信模板消息
3、系统功能模块
后台管理系统
商品管理:家具分类管理、商品SPU/SKU管理、商品上下架、商品评价管理
订单管理:订单列表与状态跟踪、退款/退货处理、订单统计与分析
用户管理:用户维护、用户行为分析
内容管理:首页轮播图配置、家具搭配推荐
数据统计:销售数据可视化、用户增长分析、商品热度排行
小程序端
首页:个性化推荐、促销活动展示、分类快捷入口
商品模块:家具分类浏览、商品搜索与筛选、商品详情、收藏与分享
购物流程:购物车管理、地址选择、支付、订单状态追踪
用户中心:个人信息管理、订单历史、收藏夹
4、项目截图
4.1、web端
登录页
数据看板
主页推荐
订单管理
分类管理
商品管理
4.2、小程序端
登录页
首页
分类页
详情页
个人页面
5、核心代码
5.1、管理端
项目结构
核心代码
{{ dashboardData[item.key] }} {{ item.label }} 近七日订单趋势 近七日用户趋势 import { ref, onMounted, watch, onActivated } from \'vue\'import { useRoute } from \'vue-router\'import * as echarts from \'echarts\'import { DashBoardAPI } from \'@/api/admin/dashboard\'const route = useRoute()let orderChartInstance = nulllet userChartInstance = null// 新增加载状态const loading = ref(false)// 响应式数据const dashboardData = ref({ orderCount: 0, orderPreCount: 0, orderNowCount: 0, orderReceiptCount: 0, orderFinishCount: 0, orderCancelCount: 0, orderCountList: [], userCountList: []})const orderChart = ref(null)const userChart = ref(null)// 统计项配置const statItems = ref([ { label: \'订单总数\', key: \'orderCount\' }, { label: \'待付款\', key: \'orderPreCount\' }, { label: \'待发货\', key: \'orderNowCount\' }, { label: \'待收货\', key: \'orderReceiptCount\' }, { label: \'已完成\', key: \'orderFinishCount\' }, { label: \'已取消\', key: \'orderCancelCount\' }])// 防抖函数const debounce = (fn, delay = 300) => { let timer return (...args) => { clearTimeout(timer) timer = setTimeout(() => fn.apply(this, args), delay) }}const fetchData = async () => { try { loading.value = true const res = await DashBoardAPI() dashboardData.value = res.data.data initCharts() } finally { loading.value = false }}// 生成最近7天日期const generateDates = () => { const dates = [] for (let i = 6; i >= 0; i--) { const date = new Date() date.setDate(date.getDate() - i) dates.push(`${date.getMonth() + 1}/${date.getDate()}`) } return dates}// 初始化图表const initCharts = () => { const dates = generateDates() // 订单图表 const orderChartInstance = echarts.init(orderChart.value) orderChartInstance.setOption({ xAxis: { type: \'category\', data: dates, axisLabel: { color: \'#666\' } }, yAxis: { type: \'value\' }, series: [{ data: dashboardData.value.orderCountList, type: \'bar\', itemStyle: { color: \'#409EFF\' }, barWidth: \'30%\' }], tooltip: { trigger: \'axis\' }, grid: { left: \'3%\', right: \'3%\', bottom: \'3%\', containLabel: true } }) // 用户图表 const userChartInstance = echarts.init(userChart.value) userChartInstance.setOption({ xAxis: { type: \'category\', data: dates, axisLabel: { color: \'#666\' } }, yAxis: { type: \'value\' }, series: [{ data: dashboardData.value.userCountList, type: \'bar\', itemStyle: { color: \'#67C23A\' }, barWidth: \'30%\' }], tooltip: { trigger: \'axis\' }, grid: { left: \'3%\', right: \'3%\', bottom: \'3%\', containLabel: true } })}// 监听路由变化watch( () => route.path, (newVal, oldVal) => { if (newVal === \'/dashboard\') { // 根据实际路由路径调整 debounce(fetchData)() } })// 处理keep-alive缓存onActivated(() => { if (orderChartInstance) orderChartInstance.dispose() if (userChartInstance) userChartInstance.dispose() fetchData()})onMounted(() => { fetchData()})// 添加窗口resize监听const handleResize = debounce(() => { orderChartInstance?.resize() userChartInstance?.resize()}, 200)window.addEventListener(\'resize\', handleResize).dashboard-container { padding: 20px; max-width: 1600px; margin: 0 auto;}.stat-card { text-align: center; transition: transform 0.3s; margin-bottom: 16px;}.stat-card:hover { transform: translateY(-3px);}.chart-row { display: flex; flex-wrap: wrap;}/* 响应式调整 */@media (max-width: 768px) { .el-col-md-12 { max-width: 100%; flex: 0 0 100%; } .stat-card { margin-bottom: 12px; }}@media (min-width: 1200px) { .el-col-lg-8 { max-width: 33.3333%; flex: 0 0 33.3333%; }}.dashboard-container { padding: 20px;}.stat-card { text-align: center; transition: transform 0.3s;}.stat-card:hover { transform: translateY(-5px);}.stat-value { font-size: 24px; font-weight: bold; color: #409EFF; margin-bottom: 8px;}.stat-label { color: #666; font-size: 14px;}.chart-title { font-size: 16px; font-weight: bold; color: #333;}.mb-8 { margin-bottom: 32px;}
商品管理
添加商品 搜索 {{ (currentPage - 1) * pageSize + $index + 1 }} 商品 {{ row.name }} ¥{{ row.price.toFixed(2) }} 10 ? \'success\' : row.stock > 0 ? \'warning\' : \'danger\'\" effect=\"light\"> {{ row.stock }} {{ row.categoryName }} {{ formatDate(row.createTime) }} {{ spec.name }}: {{ spec.values.join(\'、\') }} {{ attr.name }} {{ attr.value }} <!-- 编辑 --> 删除 添加规格 添加属性 取消 确认提交 import { ref, computed, onMounted, reactive } from \'vue\'import { useRoute } from \'vue-router\' // 引入 useRouteimport { Search, Plus, Delete, Goods, Picture} from \'@element-plus/icons-vue\'import { getProductsAPI } from \'@/api/product/product\'import { ElMessage, ElMessageBox } from \'element-plus\'import { getCategoriesAPI } from \'@/api/category/category\'//上��图片import { uploadFile } from \'@/api/baseApi\'//新增接口import { addProductAPI, deleteProductAPI } from \'@/api/product/product\'const route = useRoute() // 获取路由实例const categoryId = computed(() => route.params.id) // 获取路由参数 idconst products = ref([])const searchText = ref(\'\')const currentPage = ref(1)const pageSize = ref(5)const loading = ref(false)const categoryOptions = ref([])// 在获取数据时处理规格分组const getProductsData = async () => { loading.value = true try { const res = await getProductsAPI() products.value = res.data.data.map(product => { // 规格分组处理 const groupedSpecs = product.specList.reduce((acc, spec) => { const existing = acc.find(item => item.name === spec.name) if (existing) { existing.values.push(spec.value) } else { acc.push({ name: spec.name, values: [spec.value] }) } return acc }, []) return { ...product, groupedSpecs // 添加分组后的规格数据 } }) ElMessage.success(\'商品数据加载成功\') } catch (error) { ElMessage.error(\'获取商品数据失败\') console.error(error) } finally { loading.value = false }}//获取新增分类const categoryData = async () => { try { const res = await getCategoriesAPI() categoryOptions.value = res.data.data } catch (error) { console.error(\'获取分类数据失败\', error) }}// 搜索处理const filteredData = computed(() => { let result = products.value // 根据路由参数筛选商品 result = result.filter(item => item.categoryParentId == categoryId.value) // 文本搜索 if (searchText.value) { const search = searchText.value.toLowerCase() result = result.filter(item => { return ( item.name.toLowerCase().includes(search) || item.description.toLowerCase().includes(search) ) }) } return result})// 当前页数据const currentPageData = computed(() => { return filteredData.value.slice( (currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value )})// 分页事件处理const handleSizeChange = (val) => { pageSize.value = val currentPage.value = 1}const handleCurrentChange = (val) => { currentPage.value = val}// 搜索事件const handleSearch = () => { currentPage.value = 1}// 格式化日期const formatDate = (dateString) => { const date = new Date(dateString) return date.toLocaleDateString(\'zh-CN\', { year: \'numeric\', month: \'2-digit\', day: \'2-digit\', hour: \'2-digit\', minute: \'2-digit\' })}// 对话框相关状态const dialogVisible = ref(false)const formRef = ref(null)// 表单数据const form = reactive({ name: \'\', description: \'\', price: 0, stock: 0, cascaderValue: [], // 级联选择值 isHot: 0, images: [], specs: [], attributes: []})// 文件列表const fileList = ref([])// 分类数据格式转换const cascaderOptions = computed(() => { return categoryOptions.value.map(cat => ({ value: cat.id, label: cat.categoryName, children: cat.children?.map(child => ({ value: child.id, label: child.name })) || [] }))})// 表单验证规则const rules = reactive({ name: [{ required: true, message: \'请输入商品名称\', trigger: \'blur\' }], price: [{ required: true, message: \'请输入价格\', trigger: \'blur\' }], stock: [{ required: true, message: \'请输入库存\', trigger: \'blur\' }], cascaderValue: [{ required: true, message: \'请选择商品分类\', trigger: \'change\' }]})// 图片上传处理const handleUploadChange = async (file) => { try { const formData = new FormData() formData.append(\'file\', file.raw) const res = await uploadFile(formData) form.images.push(res.data.data) ElMessage.success(\'图片上传成功\') } catch (error) { ElMessage.error(\'图片上传失败\') console.error(error) }}const handleRemove = (file) => { const index = form.images.indexOf(file.url) if (index > -1) { form.images.splice(index, 1) }}// 规格管理const addSpec = () => { form.specs.push({ name: \'\', values: \'\' })}const removeSpec = (index) => { form.specs.splice(index, 1)}// 属性管理const addAttr = () => { form.attributes.push({ name: \'\', value: \'\' })}const removeAttr = (index) => { form.attributes.splice(index, 1)}// 提交表单const submitForm = async () => { try { await formRef.value.validate() const payload = { name: form.name, description: form.description, price: form.price, stock: form.stock, parentCategoryId: form.cascaderValue[0], // 第一个元素是父分类 categoryId: form.cascaderValue[1], // 第二个元素是子分类 isHot: form.isHot ? 1 : 0, // 处理是否热门 images: form.images.join(\',\'), // 图片路径拼接 spec: form.specs.map(spec => `${spec.name},${spec.values}`), attribute: form.attributes.map(attr => `${attr.name},${attr.value}`) } await addProductAPI(payload) ElMessage.success(\'商品添加成功\') dialogVisible.value = false // 刷新商品列表 getProductsData() } catch (error) { console.error(\'提交失败:\', error) ElMessage.error(\'商品添加失败\') }}const handleDelete = async (productId) => { try { // 添加确认对话框 await ElMessageBox.confirm(\'确定要删除该商品吗?\', \'警告\', { confirmButtonText: \'确定\', cancelButtonText: \'取消\', type: \'warning\' }) // 调用删除API await deleteProductAPI(productId) ElMessage.success(\'删除成功\') // 重新获取商品列表数据 await getProductsData() } catch (error) { // 处理用户取消的情况 if (error !== \'cancel\') { ElMessage.error(\'删除失败\') console.error(error) } }}// 暴露打开对话框方法const openDialog = () => { dialogVisible.value = true // 重置表单 Object.assign(form, { name: \'\', description: \'\', price: 0, stock: 0, cascaderValue: [], images: [], specs: [], attributes: [] }) fileList.value = []}onMounted(() => { getProductsData() categoryData()}).product-container { padding: 20px; background-color: #f5f7fa; min-height: calc(100vh - 120px);}.header-card { margin-bottom: 20px;}.card-header { display: flex; justify-content: space-between; align-items: center;}.card-header h2 { margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px;}.search-area { display: flex; gap: 15px; flex-wrap: wrap; align-items: center;}.search-input { width: 350px;}.filter-select { width: 180px;}.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px;}.stat-card { text-align: center;}.stat-header { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 14px; color: #606266;}.stat-value { font-size: 24px; font-weight: bold; color: #303133; margin-top: 10px;}.table-card { margin-bottom: 20px;}.pagination-container { margin-top: 20px; display: flex; justify-content: center;}.product-name { display: flex; align-items: center; gap: 8px;}.product-tag { font-size: 12px;}.price-tag { color: #f56c6c; font-weight: bold;}.spec-item,.attr-item { display: flex; align-items: center; gap: 8px; margin-bottom: 5px;}.image-container { display: flex; flex-wrap: wrap; gap: 8px;}.product-image { width: 60px; height: 60px; border-radius: 4px; border: 1px solid #ebeef5; transition: transform 0.3s;}.product-image:hover { transform: scale(1.05); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);}.image-error { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: #f5f7fa; color: #909399;}/* 响应式调整 */@media (max-width: 768px) { .search-input { width: 100%; } .filter-select { width: 100%; } .stat-cards { grid-template-columns: repeat(2, 1fr); }}/* 规格样式 */.spec-container { display: flex; flex-direction: column; gap: 6px;}.spec-item { display: flex; align-items: center; line-height: 1.4;}.spec-tag { flex-shrink: 0;}.spec-values { margin-left: 4px; word-break: break-word;}/* 属性样式 */.attr-container { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;}.attr-item { display: flex; align-items: center; gap: 4px; min-width: 0;}.attr-tag { flex-shrink: 0;}.attr-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}/* 响应式调整 */@media (max-width: 768px) { .attr-container { grid-template-columns: 1fr; }}.image-container { display: flex; flex-wrap: wrap; gap: 8px;}.product-image { width: 60px; height: 60px; border-radius: 4px; border: 1px solid #ebeef5; transition: transform 0.3s; cursor: pointer;}.product-image:hover { transform: scale(1.05); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);}.image-error { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: #f5f7fa; color: #909399;}.spec-item,.attr-item { margin-bottom: 10px; display: flex; align-items: center;}
5.2、小程序端
项目结构
核心代码
import { onLoad } from \'@dcloudio/uni-app\';import { getProductDetailAPI, getProductByCategoryAPI, getProductSpecAPI, getProductSkuAPI} from \'@/services/product\'import { ref, computed } from \'vue\'import AddressPanel from \'./component/AddressPanel.vue\'import ServicePanel from \'./component/ServicePanel.vue\'import type { SkuPopupEvent, SkuPopupInstanceType } from \'@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup\';import { postMemberCartAPI } from \'@/services/cart\'import { useAddressStore } from \'@/stores/modules/address\'const { safeAreaInsets } = uni.getSystemInfoSync()const goods = ref(null)const categoryProductData = ref([])// 是否显示skuconst isShowSku = ref(false)const localdata = ref({})// 按钮模式enum SkuMode { Both = 1, Cart = 2, Buy = 3,}const mode = ref(SkuMode.Cart)// 打开SKU弹窗修改按钮模式const openSkuPopup = (val: SkuMode) => { // 显示SKU弹窗 isShowSku.value = true // 修改按钮模式 mode.value = val}const query = defineProps()const getProductDetailData = async () => { try { const res = await getProductDetailAPI(query.id) goods.value = res.data.data await getProductByCategoryData() // 获取规格数据 const spec = await getProductSpecAPI(query.id) // 记录规格顺序(重要!) const specOrder = spec.data.data.map(v => v.name) // 获取SKU数据 const skus = await getProductSkuAPI(query.id) // 构建 localdata localdata.value = { id: goods.value?.id, name: goods.value?.name, imageUrl: goods.value?.images[0], spec_list: spec.data.data.map(v => ({ name: v.name, list: [...new Set(v.values)].map(value => ({ name: value, id: value // 保持id与sku_id_arr对应 })) })), sku_list: skus.data.data.map(v => ({ _id: v.id, goods_id: v.goodsId, goods_name: v.goodsName, image: v.image, price: v.price * 100, stock: v.stock, sku_name_arr: specOrder.map(specName => { const specItem = v.specs.find(s => s.name === specName) return specItem ? specItem.value : \'\' }), sku_id_arr: specOrder.map(specName => { const specItem = v.specs.find(s => s.name === specName) return specItem ? specItem.value : \'\' // 保持与sku_name_arr一致 }) })) } // 调试输出 console.log(\'规格顺序:\', specOrder) console.log(\'处理后的SKU数据:\', JSON.stringify(localdata.value.sku_list)) } catch (error) { console.error(\'请求失败:\', error) uni.showToast({ title: \'数据加载失败\', icon: \'none\' }) }}// SKU组件实例const skuPopupRef = ref()// 计算被选中的值const selectArrText = computed(() => { return skuPopupRef.value?.selectArr?.join(\' \').trim() || \'请选择商品规格\'})const getProductByCategoryData = async () => { try { // 使用可选链操作符和.value访问 const categoryId = goods.value?.categoryId if (!categoryId) return // 确保categoryId存在 const res = await getProductByCategoryAPI(categoryId) categoryProductData.value = res.data.data } catch (error) { console.error(\'请求失败:\', error) uni.showToast({ title: \'数据加载失败\', icon: \'none\' }) }}// 轮播图变化事件const currentIndex = ref(0)const onChange = (e: any) => { currentIndex.value = e.detail.current}const onTapImage = (url: string) => { uni.previewImage({ urls: goods.value!.images, current: url, })}const popup = ref()// 弹出层条件渲染const popupName = ref()const openPopup = (name: typeof popupName.value) => { // 修改弹出层名称 popupName.value = name // 打开弹出层 popup.value?.open()}// 加入购物车事件const onAddCart = async (ev: SkuPopupEvent) => { let res = await postMemberCartAPI({ skuId: ev._id, number: ev.buy_num }) if (res.data.code === 200) { uni.showToast({ title: \'加入购物车成功\', icon: \'none\' }) } else { uni.showToast({ title: \'加入购物车失败\', icon: \'none\' }) } isShowSku.value = false}const onByNow = async (ev: SkuPopupEvent) => { uni.navigateTo({ url: \'/pagesOrder/create/create?skuId=\' + ev._id + \'&number=\' + ev.buy_num })}const addressStore = useAddressStore()// 收货地址const selectAddress = computed(() => { return addressStore.selectedAddress})onLoad(() => { getProductDetailData() // 只需要调用这一个,它内部会触发第二个请求}) {{ currentIndex + 1 }} / {{ goods?.images.length }} ¥ {{ goods?.price }} {{ goods?.name }} {{ goods?.description }} 选择 {{ selectArrText }} 送至 {{ selectAddress ? `${selectAddress.fullLocation} ${selectAddress.address}` : \'请选择收获地址\' }} 服务 无忧退 快速退款 免费包邮 详情 {{ item.name }} {{ item.value }} 同类推荐 {{ item.name }} ¥ {{ item.price }} 购物车 加入购物车 立即购买 page { height: 100%; overflow: hidden; display: flex; flex-direction: column;}.viewport { background-color: #f4f4f4;}.panel { margin-top: 20rpx; background-color: #fff; .title { display: flex; justify-content: space-between; align-items: center; height: 90rpx; line-height: 1; padding: 30rpx 60rpx 30rpx 6rpx; position: relative; text { padding-left: 10rpx; font-size: 28rpx; color: #333; font-weight: 600; border-left: 4rpx solid #27ba9b; } navigator { font-size: 24rpx; color: #666; } }}.arrow { &::after { position: absolute; top: 50%; right: 30rpx; content: \'\\e6c2\'; color: #ccc; font-family: \'erabbit\' !important; font-size: 32rpx; transform: translateY(-50%); }}/* 商品信息 */.goods { background-color: #fff; .preview { height: 750rpx; position: relative; .image { width: 750rpx; height: 750rpx; } .indicator { height: 40rpx; padding: 0 24rpx; line-height: 40rpx; border-radius: 30rpx; color: #fff; font-family: Arial, Helvetica, sans-serif; background-color: rgba(0, 0, 0, 0.3); position: absolute; bottom: 30rpx; right: 30rpx; .current { font-size: 26rpx; } .split { font-size: 24rpx; margin: 0 1rpx 0 2rpx; } .total { font-size: 24rpx; } } } .meta { position: relative; border-bottom: 1rpx solid #eaeaea; .price { height: 130rpx; padding: 25rpx 30rpx 0; color: #fff; font-size: 34rpx; box-sizing: border-box; background-color: #35c8a9; } .number { font-size: 56rpx; } .brand { width: 160rpx; height: 80rpx; overflow: hidden; position: absolute; top: 26rpx; right: 30rpx; } .name { max-height: 88rpx; line-height: 1.4; margin: 20rpx; font-size: 32rpx; color: #333; } .desc { line-height: 1; padding: 0 20rpx 30rpx; font-size: 24rpx; color: #cf4444; } } .action { padding-left: 20rpx; .item { height: 90rpx; padding-right: 60rpx; border-bottom: 1rpx solid #eaeaea; font-size: 26rpx; color: #333; position: relative; display: flex; align-items: center; &:last-child { border-bottom: 0 none; } } .label { width: 60rpx; color: #898b94; margin: 0 16rpx 0 10rpx; } .text { flex: 1; -webkit-line-clamp: 1; } }}/* 商品详情 */.detail { padding-left: 20rpx; .content { margin-left: -20rpx; .image { width: 100%; } } .properties { padding: 0 20rpx; margin-bottom: 30rpx; .item { display: flex; line-height: 2; padding: 10rpx; font-size: 26rpx; color: #333; border-bottom: 1rpx dashed #ccc; } .label { width: 200rpx; } .value { flex: 1; } }}/* 同类推荐 */.similar { .content { padding: 0 20rpx 200rpx; background-color: #f4f4f4; display: flex; flex-wrap: wrap; .goods { width: 340rpx; padding: 24rpx 20rpx 20rpx; margin: 20rpx 7rpx; border-radius: 10rpx; background-color: #fff; } .image { width: 300rpx; height: 260rpx; } .name { height: 80rpx; margin: 10rpx 0; font-size: 26rpx; color: #262626; } .price { line-height: 1; font-size: 20rpx; color: #cf4444; } .number { font-size: 26rpx; margin-left: 2rpx; } } navigator { &:nth-child(even) { margin-right: 0; } }}/* 底部工具栏 */.toolbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 1; background-color: #fff; height: 100rpx; padding: 0 20rpx var(--window-bottom); border-top: 1rpx solid #eaeaea; display: flex; justify-content: space-between; align-items: center; box-sizing: content-box; .buttons { display: flex; &>view { width: 220rpx; text-align: center; line-height: 72rpx; font-size: 26rpx; color: #fff; border-radius: 72rpx; } .addcart { background-color: #ffa868; } .buynow, .payment { background-color: #27ba9b; margin-left: 20rpx; } } .icons { padding-right: 10rpx; display: flex; align-items: center; flex: 1; .icons-button { flex: 1; text-align: center; line-height: 1.4; padding: 0; margin: 0; border-radius: 0; font-size: 20rpx; color: #333; background-color: #fff; &::after { border: none; } } text { display: block; font-size: 34rpx; } }}
5.3、后端
项目结构
核心代码
@Servicepublic class ProductSkuServiceImpl extends ServiceImpl implements ProductSkuService { @Autowired private IProductSpecService productSpecService; @Autowired private ProductMapper productManager; /** * 根据商品id获取商品sku信息 * * @param productId * @return */ @Override public List getProductById(Integer productId) { Product product = productManager.selectById(productId); List list = this.lambdaQuery() .eq(ProductSku::getProductId, productId) .list(); List vos = new ArrayList(); for (ProductSku productSku : list) { ProductSkuVO vo = new ProductSkuVO(); vo.setId(productSku.getId()); vo.setGoodsId(productSku.getProductId()); vo.setGoodsName(product.getName()); vo.setStock(productSku.getStock()); vo.setPrice(productSku.getPrice()); vo.setImage(productSku.getImage()); List specs = productSpecService.lambdaQuery() .eq(ProductSpec::getProductSkuId, productSku.getId()) .list(); vo.setSpecs(specs); vos.add(vo); } return vos; } @Override public List getSkuById(Integer skuId) { List list = this.lambdaQuery() .eq(ProductSku::getId, skuId) .list(); List vos = new ArrayList(); for (ProductSku productSku : list) { ProductSkuVO vo = new ProductSkuVO(); Product product = productManager.selectById(productSku.getProductId()); vo.setId(productSku.getId()); vo.setGoodsId(productSku.getProductId()); vo.setGoodsName(product.getName()); vo.setStock(productSku.getStock()); vo.setPrice(productSku.getPrice()); vo.setImage(productSku.getImage()); List specs = productSpecService.lambdaQuery() .eq(ProductSpec::getProductSkuId, productSku.getId()) .list(); vo.setSpecs(specs); vos.add(vo); } return vos; }}
@Servicepublic class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService { @Autowired private ProductMapper productMapper; @Autowired private IProductImageService productImageService; @Autowired private IProductAttributeService productAttributeService; /** * 商品列表分页查询 * * @param page * @return */ @Override public IPage<ProductVO> getList(IPage<ProductVO> page, Integer hotId) { return productMapper.getList(page, hotId); } /** * 根据分类id查询商品 * * @param categoryId * @return */ @Override public List<ProductVO> getByCategoryId(Integer categoryId) { List<Product> list = this.lambdaQuery() .eq(Product::getCategoryId, categoryId) .list(); List<ProductVO> vos = new ArrayList<>(); for (Product product : list) { ProductVO vo = new ProductVO(); BeanUtils.copyProperties(product, vo); List<ProductImage> images = productImageService.lambdaQuery() .eq(ProductImage::getProductId, product.getId()) .list(); if (images.size() > 0) { vo.setImageUrl(images.get(0).getImageUrl()); } vos.add(vo); } return vos; } /** * 根据id查询商品详情 * * @param id * @return */ @Override public ProductDetailVO getProductDetailById(Integer id) { Product product = this.getById(id); if (product != null) { ProductDetailVO vo = new ProductDetailVO(); BeanUtils.copyProperties(product, vo); //获取商品图片 List<ProductImage> productImages = productImageService.lambdaQuery() .eq(ProductImage::getProductId, product.getId()) .list(); if (!productImages.isEmpty()) { List<String> imageList = productImages.stream().map(ProductImage::getImageUrl).collect(Collectors.toList()); vo.setImages(imageList); } //获取属性值 List<ProductAttribute> productAttributeList = productAttributeService.lambdaQuery() .eq(ProductAttribute::getProductId, product.getId()) .list(); if (!productAttributeList.isEmpty()) { vo.setAttributes(productAttributeList); } return vo; } return null; }}
package com.funrniur.app.service.impl;import com.funrniur.app.dto.CartDTO;import com.funrniur.app.mapper.ProductMapper;import com.funrniur.app.service.IProductService;import com.funrniur.app.service.IProductSpecService;import com.funrniur.app.service.ProductSkuService;import com.funrniur.app.vo.CartVO;import com.funrniur.common.login.LoginUser;import com.funrniur.common.login.LoginUserHolder;import com.funrniur.model.entity.Cart;import com.funrniur.app.mapper.CartMapper;import com.funrniur.app.service.ICartService;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.funrniur.model.entity.Product;import com.funrniur.model.entity.ProductSku;import com.funrniur.model.entity.ProductSpec;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.time.LocalDateTime;import java.util.Collections;import java.util.List;import java.util.stream.Collectors;/** * * 购物车表 服务实现类 *
* * @author chen * @since 2025-03-09 */@Servicepublic class CartServiceImpl extends ServiceImpl implements ICartService { @Autowired private ProductSkuService productSkuService; @Autowired private ProductMapper productMapper; @Autowired private IProductSpecService productSpecService; /** * 添加购物车 * * @param cartDTO */ @Override public void addCart(CartDTO cartDTO) { LoginUser loginUser = LoginUserHolder.getLoginUser(); ProductSku sku = this.productSkuService.getById(cartDTO.getSkuId()); Cart dbCart = this.lambdaQuery() .eq(Cart::getUserId, loginUser.getUserId()) .eq(Cart::getProductSkuId, cartDTO.getSkuId()) .eq(Cart::getProductId, sku.getProductId()) .one(); if (dbCart != null){ dbCart.setQuantity(dbCart.getQuantity() + cartDTO.getNumber()); this.updateById(dbCart); return; } Cart cart = new Cart(); cart.setUserId(loginUser.getUserId().intValue()); cart.setProductId(sku.getProductId()); cart.setProductSkuId(cartDTO.getSkuId()); cart.setQuantity(cartDTO.getNumber()); cart.setAddTime(LocalDateTime.now()); cart.setSelected(0); this.save(cart); } /** * 获取购物车列表 * * @return */ @Override public List getList() { LoginUser loginUser = LoginUserHolder.getLoginUser(); List list = this.lambdaQuery() .eq(Cart::getUserId, loginUser.getUserId()) .list(); if (list == null || list.isEmpty()){ return Collections.emptyList(); } return list.stream().map(cart -> { CartVO cartVO = new CartVO(); cartVO.setId(cart.getId()); cartVO.setSkuId(cart.getProductSkuId()); cartVO.setNumber(cart.getQuantity()); cartVO.setProductId(cart.getProductId()); Product product = productMapper.selectById(cart.getProductId()); cartVO.setName(product.getName()); ProductSku productSku = productSkuService.getById(cart.getProductSkuId()); cartVO.setImage(productSku.getImage()); cartVO.setPrice(productSku.getPrice()); cartVO.setStock(productSku.getStock()); cartVO.setSelected(cart.getSelected()); List specList = productSpecService.lambdaQuery() .eq(ProductSpec::getProductSkuId, cart.getProductSkuId()) .list(); String attrsText = specList.stream() .map(ProductSpec::getValue) // 提取规格名称 .collect(Collectors.joining(\" \")); // 用空格连接 cartVO.setAttrsText(attrsText); return cartVO; }).collect(Collectors.toList()); }}