本部分补齐 PROJECT_DOC.md 和 DOC_APPENDIX.md 中缺失的所有内容: 前端全部页面代码、所有工具类/配置类、所有 Mapper XML、完整部署流程、Dubbo 验证方法。
26. 前端全部页面代码
26.1 LoginView.vue(用户登录)
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import userApi from '@/api/userApi.js'
import { userTokenStore } from '@/stores/token.js'
import { userToken } from '@/stores/user.js'
const router = useRouter()
const tokenStore = userTokenStore()
const userStore = userToken()
const loginForm = ref({ username: '', password: '', captchaInput: '' })
const captchaData = ref({ key: '', imageBase64Data: '' })
// 加载验证码
function loadCaptcha() {
userApi.captcha().then(resp => {
if (resp.code === 200) captchaData.value = resp.data
})
}
// 登录
function login() {
if (!loginForm.value.username || !loginForm.value.password) {
ElMessage.warning('请输入用户名和密码')
return
}
userApi.login({
username: loginForm.value.username,
password: loginForm.value.password,
key: captchaData.value.key,
captchaInput: loginForm.value.captchaInput
}).then(resp => {
if (resp.code === 200) {
tokenStore.updateToken(resp.data) // 存 JWT
ElMessage.success('登录成功')
// 获取用户数据
userApi.getInfo().then(r => {
if (r.code === 200) userStore.updateUser(r.data)
})
router.push('/user/index')
} else {
ElMessage.error(resp.msg)
loadCaptcha()
}
})
}
loadCaptcha()
</script>
<template>
<div class="login-page">
<el-card class="login-card" shadow="always">
<h2>用户登录</h2>
<el-form :model="loginForm" label-width="0">
<el-form-item>
<el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="User" />
</el-form-item>
<el-form-item>
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码"
prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item>
<div class="captcha-row">
<el-input v-model="loginForm.captchaInput" placeholder="验证码" style="width: 60%" />
<img :src="captchaData.imageBase64Data" @click="loadCaptcha"
class="captcha-img" title="点击刷新" />
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%" @click="login">登 录</el-button>
</el-form-item>
<el-form-item>
<el-link type="primary" @click="router.push('/user/reg')">还没有账号?立即注册</el-link>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped>
.login-page {
display: flex; justify-content: center; align-items: center;
min-height: 500px; background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
}
.login-card {
width: 420px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); padding: 40px;
}
.login-card h2 { text-align: center; margin-bottom: 30px; color: #333; }
.captcha-row { display: flex; gap: 12px; align-items: center; }
.captcha-img { height: 38px; cursor: pointer; border: 1px solid #ddd; border-radius: 4px; }
</style>
26.2 RegView.vue(用户注册)
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import userApi from '@/api/userApi.js'
const router = useRouter()
const form = ref({ username: '', password: '', password1: '', captchaInput: '' })
const captchaData = ref({ key: '', imageBase64Data: '' })
function loadCaptcha() {
userApi.captcha().then(resp => {
if (resp.code === 200) captchaData.value = resp.data
})
}
function reg() {
if (!form.value.username || !form.value.password) {
ElMessage.warning('用户名和密码不能为空'); return
}
if (form.value.password !== form.value.password1) {
ElMessage.warning('两次密码不一致'); return
}
userApi.reg({
username: form.value.username,
password: form.value.password,
key: captchaData.value.key,
captchaInput: form.value.captchaInput
}).then(resp => {
if (resp.code === 200) {
ElMessage.success('注册成功,请登录')
router.push('/user/login')
} else {
ElMessage.error(resp.msg)
loadCaptcha()
}
})
}
loadCaptcha()
</script>
<template>
<div class="reg-page">
<el-card class="reg-card" shadow="always">
<h2>用户注册</h2>
<el-form :model="form" label-width="0">
<el-form-item>
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item>
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item>
<el-input v-model="form.password1" type="password" placeholder="确认密码" show-password />
</el-form-item>
<el-form-item>
<div class="captcha-row">
<el-input v-model="form.captchaInput" placeholder="验证码" style="width: 60%" />
<img :src="captchaData.imageBase64Data" @click="loadCaptcha" class="captcha-img" title="点击刷新" />
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%" @click="reg">注 册</el-button>
</el-form-item>
<el-form-item>
<el-link type="primary" @click="router.push('/user/login')">已有账号?立即登录</el-link>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped>
.reg-page { display: flex; justify-content: center; align-items: center; min-height: 500px; background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%); }
.reg-card { width: 420px; border-radius: 12px; padding: 40px; }
.reg-card h2 { text-align: center; margin-bottom: 30px; color: #333; }
.captcha-row { display: flex; gap: 12px; align-items: center; }
.captcha-img { height: 38px; cursor: pointer; border: 1px solid #ddd; border-radius: 4px; }
</style>
26.3 SearchView.vue(商品搜索/分类浏览)
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import goodsApi from '@/api/GoodsApi.js'
import categoryApi from '@/api/CategoryApi.js'
const route = useRoute()
const router = useRouter()
const SERVER_ADDR = ref(import.meta.env.VITE_SERVER_ADDR)
const goodsList = ref([])
const total = ref(0)
const pageNum = ref(1)
const pageSize = ref(20)
const categories = ref([])
const activeCategoryId = ref(null)
function loadCategories() {
categoryApi.selectAllParent().then(resp => {
if (resp.code === 200) categories.value = resp.data || []
})
}
function loadGoods() {
const cid = route.params.categoryId || activeCategoryId.value
goodsApi.selectByPage1({ categoryId: cid || undefined }, pageNum.value, pageSize.value)
.then(resp => {
if (resp.data) {
goodsList.value = resp.data.list || []
total.value = resp.data.total || 0
}
})
}
function onPageChange(p) { pageNum.value = p; loadGoods() }
function selectCategory(cid) {
activeCategoryId.value = cid
pageNum.value = 1
loadGoods()
}
watch(() => route.params.categoryId, () => {
activeCategoryId.value = Number(route.params.categoryId) || null
pageNum.value = 1
loadGoods()
})
onMounted(() => { loadCategories(); loadGoods() })
</script>
<template>
<div class="search-page">
<!-- 分类标签 -->
<div class="category-bar">
<span :class="{ tag: true, active: !activeCategoryId }" @click="selectCategory(null)">全部</span>
<span v-for="c in categories" :key="c.id"
:class="{ tag: true, active: activeCategoryId === c.id }"
@click="selectCategory(c.id)">{{ c.name }}</span>
</div>
<!-- 商品网格 -->
<div v-if="goodsList.length > 0" class="goods-grid">
<div v-for="g in goodsList" :key="g.id" class="goods-card"
@click="router.push({ path: '/user/goods', query: { id: g.id } })">
<el-image v-if="g.picList && g.picList.length"
:src="SERVER_ADDR + '/goods/pic/' + g.picList[0].url"
fit="cover" style="width:224px;height:224px" />
<div v-else class="no-pic">暂无图片</div>
<div class="card-info">
<p class="gd-name">{{ g.name }}</p>
<p class="gd-price">¥{{ g.price }}</p>
</div>
</div>
</div>
<el-empty v-else description="没有找到商品" />
<el-pagination v-if="total > pageSize" style="margin-top:20px;text-align:center"
background layout="prev, pager, next"
:total="total" :page-size="pageSize"
v-model:current-page="pageNum" @current-change="onPageChange" />
</div>
</template>
<style scoped>
.search-page { margin: 20px 15px; }
.category-bar { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }
.tag {
padding: 6px 18px; border-radius: 20px; background: #f5f5f5; cursor: pointer;
font-size: 14px; transition: all 0.2s;
}
.tag:hover { background: #ffe6d0; }
.tag.active { background: var(--theme-color, #FF6800); color: #fff; }
.goods-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
.goods-card {
background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;
}
.goods-card:hover { transform: translateY(-4px); box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
.no-pic { width:224px; height:224px; background:#f5f5f5; display:flex; align-items:center; justify-content:center; color:#999; }
.card-info { padding: 10px 12px; }
.gd-name { font-size:14px; color:#333; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-bottom:6px; }
.gd-price { font-size:16px; color:var(--theme-color,#FF6800); font-weight:bold; }
</style>
26.4 IndexView.vue — 用户首页
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import goodsApi from '@/api/GoodsApi.js'
import categoryApi from '@/api/CategoryApi.js'
const router = useRouter()
const SERVER_ADDR = ref(import.meta.env.VITE_SERVER_ADDR)
const recomGroups = ref([]) // [{category, goodsList}, ...]
// 按父分类分组加载推荐商品
function loadRecom() {
categoryApi.selectAllParent().then(resp => {
const parents = resp.data || []
const promises = parents.map(parent =>
goodsApi.selectByPage1({ categoryId: parent.id, recom: 1 }, 1, 5)
.then(r => ({ category: parent, goodsList: r.data ? r.data.list : [] }))
)
Promise.all(promises).then(results => {
recomGroups.value = results.filter(g => g.goodsList.length > 0)
})
})
}
onMounted(loadRecom)
</script>
<template>
<div class="index-page">
<!-- 轮播图 -->
<el-carousel height="650px" motion-blur>
<el-carousel-item v-for="i in 3" :key="i">
<div :style="{ background: ['#303133','#FF6800','#409EFF'][i-1],
height:'100%', display:'flex', alignItems:'center', justifyContent:'center',
color:'#fff', fontSize:'48px', fontWeight:'bold' }">
{{ ['新品首发','限时特惠','品牌直供'][i-1] }}
</div>
</el-carousel-item>
</el-carousel>
<!-- 广告位 -->
<el-row :gutter="20" class="ad-row">
<el-col :span="6" v-for="(ad,i) in ['手机数码','电脑办公','家用电器','智能家居']" :key="i">
<div class="ad-card" @click="router.push('/user/search')">
<div class="ad-icon">{{ ['📱','💻','📺','🏠'][i] }}</div>
<p>{{ ad }}</p>
</div>
</el-col>
</el-row>
<!-- 按父分类分组推荐商品 -->
<div v-for="group in recomGroups" :key="group.category.id" class="recom-section">
<div class="section-title">
<span class="title-bar"></span>
<span class="title-text" @click="router.push('/user/search/'+group.category.id)">
{{ group.category.name }} <small>查看更多 ></small>
</span>
</div>
<div class="goods-row">
<div v-for="g in group.goodsList" :key="g.id" class="goods-card"
@click="router.push({path:'/user/goods',query:{id:g.id}})">
<el-image v-if="g.picList && g.picList.length"
:src="SERVER_ADDR + '/goods/pic/' + g.picList[0].url"
fit="cover" style="width:224px;height:224px" />
<div v-else class="no-pic">暂无图片</div>
<div class="card-info">
<p class="gd-name">{{ g.name }}</p>
<p class="gd-price">¥{{ g.price }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.index-page { margin: 0 15px; }
.ad-row { margin: 20px 0; }
.ad-card { text-align: center; padding: 20px; background: #fff; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
.ad-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
.ad-icon { font-size: 40px; margin-bottom: 8px; }
.ad-card p { color: #333; font-size: 14px; margin: 0; }
.recom-section { margin: 30px 0; }
.section-title { display: flex; align-items: center; margin-bottom: 16px; }
.title-bar { width: 4px; height: 20px; background: var(--theme-color, #FF6800); margin-right: 10px; border-radius: 2px; }
.title-text { font-size: 18px; font-weight: bold; color: #333; cursor: pointer; }
.title-text small { font-size: 13px; color: #999; font-weight: normal; }
.goods-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
.goods-card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); overflow: hidden; cursor: pointer; transition: all 0.2s; }
.goods-card:hover { transform: translateY(-4px); box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
.no-pic { width:224px; height:224px; background:#f5f5f5; display:flex; align-items:center; justify-content:center; color:#999; }
.card-info { padding: 10px 12px; }
.gd-name { font-size:14px; color:#333; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-bottom:6px; }
.gd-price { font-size:16px; color:var(--theme-color,#FF6800); font-weight:bold; }
</style>
26.5 HomeView.vue(用户端父布局)
<script setup>
import UserHeader from '@/components/user/home/UserHeader.vue'
import UserFooter from '@/components/user/home/UserFooter.vue'
</script>
<template>
<el-container>
<el-header style="padding:0;height:auto">
<UserHeader />
</el-header>
<el-main style="background:#eee;min-height:500px;padding:0">
<div class="center">
<RouterView />
</div>
</el-main>
<el-footer style="padding:0;height:auto">
<UserFooter />
</el-footer>
</el-container>
</template>
26.6 UserHeader.vue(用户导航栏)
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { userToken } from '@/stores/user.js'
import { userTokenStore } from '@/stores/token.js'
const router = useRouter()
const userStore = userToken()
const tokenStore = userTokenStore()
const keyword = ref('')
function search() {
if (keyword.value.trim()) {
router.push({ path: '/user/search', query: { keyword: keyword.value.trim() } })
}
}
function logout() {
tokenStore.$reset()
userStore.$reset()
router.push('/user/index')
}
</script>
<template>
<!-- 顶部导航条 -->
<div class="nav_bg">
<div class="center top-nav">
<div>
<router-link to="/user/index">首页</router-link>
<router-link to="/user/search">全部商品</router-link>
<a href="#">新品上市</a>
<a href="#">限时特惠</a>
<a href="#">品牌专区</a>
<a href="#">关于我们</a>
</div>
<div>
<template v-if="tokenStore.tokenStr && userStore.userInfo">
<span>{{ userStore.userInfo.username }}</span>
<a @click="logout">退出</a>
<router-link to="/user/cart">购物车</router-link>
</template>
<template v-else>
<router-link to="/user/login">登录</router-link>
<router-link to="/user/reg">注册</router-link>
</template>
</div>
</div>
</div>
<!-- Logo+搜索区 -->
<div class="center search-area">
<div class="logo">在线商城</div>
<div class="search-box">
<el-input v-model="keyword" placeholder="搜索商品" @keyup.enter="search" size="large" />
<el-button type="primary" @click="search" size="large">搜索</el-button>
</div>
</div>
<!-- 分类菜单 -->
<div class="cat-bar">
<div class="center cat-list">
<span v-for="c in ['智能手机','电脑办公','家用电器','智能家居','运动户外']" :key="c"
@click="router.push('/user/search')">{{ c }}</span>
</div>
</div>
</template>
<style scoped>
.nav_bg { background: var(--theme-bg-color, #303133); height: 50px; line-height: 50px; }
.top-nav { display: flex; justify-content: space-between; color: #ccc; font-size: 14px; }
.top-nav a, .top-nav span { color: #ccc; margin: 0 12px; cursor: pointer; text-decoration: none; }
.top-nav a:hover { color: var(--theme-color, #FF6800); }
.search-area { display: flex; align-items: center; height: 80px; }
.logo { font-size: 28px; font-weight: bold; color: var(--theme-color, #FF6800); margin-right: 40px; }
.search-box { display: flex; width: 500px; }
.cat-bar { background: var(--theme-color, #FF6800); height: 40px; line-height: 40px; }
.cat-list { display: flex; gap: 30px; color: #fff; font-size: 14px; cursor: pointer; }
.cat-list span { cursor: pointer; }
.cat-list span:hover { color: wheat; }
</style>
26.7 CartView.vue(购物车 — 完整版)
<script setup>
import { ref } from 'vue'
import cartApi from '@/api/cartApi.js'
import { Delete, ShoppingCart } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const SERVER_ADDR = ref(import.meta.env.VITE_SERVER_ADDR)
const cartList = ref([])
const checkAllState = ref(false)
const halfState = ref(false)
const checkCount = ref(0)
const checkPrice = ref(0)
function selectBycondition() {
cartApi.selectBycodition().then(resp => {
cartList.value = resp.data || []
changeState()
})
}
function checkAllOrderNone() {
cartList.value.forEach(c => c.checkState = checkAllState.value)
changeState()
}
function changeState() {
let count = 0
cartList.value.forEach(c => { if (c.checkState) count++ })
checkAllState.value = count === cartList.value.length && count > 0
halfState.value = count > 0 && count < cartList.value.length
checkedPriceAndCount()
}
function checkedPriceAndCount() {
checkCount.value = 0; checkPrice.value = 0
cartList.value.forEach(c => {
if (c.checkState) {
checkCount.value += c.count
if (c.goods) checkPrice.value += c.goods.price * c.count
}
})
}
function updatecount(cart) {
cartApi.update({ id: cart.id, count: cart.count }).then(resp => {
if (resp.code === 200) {
cartApi.selectById(cart.id).then(r => {
cartList.value.forEach(c => { if (c.id === cart.id) c.count = r.data.count })
checkedPriceAndCount()
})
} else ElMessage.error(resp.msg)
})
}
function deleteCart(id) {
cartApi.delete(id).then(resp => {
if (resp.code === 200) {
ElMessage.success(resp.msg)
cartList.value = cartList.value.filter(c => c.id !== id)
changeState()
}
})
}
function deleteChecked() {
const ids = cartList.value.filter(c => c.checkState).map(c => c.id)
if (ids.length === 0) { return ElMessage.warning('没有选择商品') }
cartApi.deleteByIds(ids).then(resp => {
if (resp.code === 200) { ElMessage.success(resp.msg); selectBycondition() }
})
}
function deleteAll() {
const ids = cartList.value.map(c => c.id)
if (ids.length === 0) return
cartApi.deleteByIds(ids).then(resp => {
if (resp.code === 200) { ElMessage.success(resp.msg); selectBycondition() }
})
}
function toCreateOrder() {
const ids = cartList.value.filter(c => c.checkState).map(c => c.id)
if (ids.length === 0) { ElMessage.warning('请选择要结算的商品'); return }
router.push({ path: '/user/createOrder', query: { cartIds: ids } })
}
selectBycondition()
</script>
<template>
<div v-if="cartList.length > 0">
<el-row class="title"><el-col :span="2"><el-checkbox v-model="checkAllState" :indeterminate="halfState" @change="checkAllOrderNone">全选</el-checkbox></el-col><el-col :span="11">商品</el-col><el-col :span="2">单价</el-col><el-col :span="6">数量</el-col><el-col :span="2">小计</el-col><el-col :span="1">操作</el-col></el-row>
<el-row v-for="cart in cartList" :key="cart.id" class="cartItem">
<el-col :span="2"><el-checkbox v-model="cart.checkState" @change="changeState"/></el-col>
<el-col :span="2"><el-image v-if="cart.goods?.picList?.length" :src="SERVER_ADDR+'/goods/pic/'+cart.goods.picList[0].url" style="width:50px;height:50px" fit="contain"/><div v-else class="no-pic-sm">无图</div></el-col>
<el-col :span="9">{{ cart.goods?.name || '商品已下架' }}</el-col>
<el-col :span="2">{{ cart.goods?.price || '-' }}</el-col>
<el-col :span="6"><el-input-number v-model="cart.count" :min="1" @change="updatecount(cart)"/></el-col>
<el-col :span="2">{{ cart.goods ? (cart.goods.price * cart.count).toFixed(2) : '-' }}</el-col>
<el-col :span="1"><el-button type="danger" @click="deleteCart(cart.id)"><el-icon><delete/></el-icon></el-button></el-col>
</el-row>
<el-row class="title"><el-col :span="2"><el-checkbox v-model="checkAllState" :indeterminate="halfState" @change="checkAllOrderNone">全选</el-checkbox></el-col><el-col :span="3"><el-link :underline="false" @click="deleteChecked">删除选中</el-link></el-col><el-col :span="11"><el-link :underline="false" @click="deleteAll">清理购物车</el-link></el-col><el-col :span="3">已选{{checkCount}}件</el-col><el-col :span="3">总价: ¥{{checkPrice}}</el-col><el-col :span="2"><el-button type="danger" @click="toCreateOrder">结算</el-button></el-col></el-row>
</div>
<div v-else class="none">
<div class="empty-cart"><el-icon class="empty-icon"><ShoppingCart/></el-icon><p class="empty-text">购物车是空的</p><p class="empty-hint">快去挑选心仪的商品吧</p><el-button type="primary" @click="router.push('/user/index')">去逛逛</el-button></div>
</div>
</template>
<style scoped>
.title { margin:20px 0; padding-left:10px; font-size:14px; background:#fff; height:40px; line-height:40px; }
.cartItem { border-bottom:1px solid #eee; padding-left:10px; font-size:14px; background:#fff; height:60px; line-height:60px; transition:background 0.2s; }
.cartItem:hover { background:#fafafa; }
.no-pic-sm { width:50px; height:50px; background:#f5f5f5; display:flex; align-items:center; justify-content:center; color:#999; font-size:12px; }
.none { display:flex; align-items:center; justify-content:center; min-height:400px; background:#fff; }
.empty-cart { text-align:center; }
.empty-icon { font-size:64px; color:#ccc; margin-bottom:16px; }
.empty-text { font-size:18px; color:#666; margin-bottom:8px; }
.empty-hint { font-size:14px; color:#999; margin-bottom:20px; }
</style>
26.8 admin LoginView.vue(管理员登录)
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import adminApi from '@/api/AdminApi.js'
import { userTokenStore } from '@/stores/token.js'
const router = useRouter()
const tokenStore = userTokenStore()
const form = ref({ username: '', password: '', captchaInput: '' })
const captchaData = ref({ key: '', imageBase64Data: '' })
function loadCaptcha() {
adminApi.captcha().then(resp => { if (resp.code === 200) captchaData.value = resp.data })
}
function login() {
adminApi.login({ username: form.value.username, password: form.value.password, key: captchaData.value.key, captchaInput: form.value.captchaInput }).then(resp => {
if (resp.code === 200) { tokenStore.updateToken(resp.data); router.push('/admin/index') }
else { ElMessage.error(resp.msg); loadCaptcha() }
})
}
loadCaptcha()
</script>
<template>
<div class="login-page">
<el-card class="login-card">
<h2>管理员登录</h2>
<el-form :model="form" label-width="0">
<el-form-item><el-input v-model="form.username" placeholder="用户名" /></el-form-item>
<el-form-item><el-input v-model="form.password" type="password" placeholder="密码" show-password /></el-form-item>
<el-form-item><div class="captcha-row"><el-input v-model="form.captchaInput" placeholder="验证码" style="width:60%"/><img :src="captchaData.imageBase64Data" @click="loadCaptcha" class="captcha-img" title="刷新"/></div></el-form-item>
<el-form-item><el-button type="primary" style="width:100%" @click="login">登 录</el-button></el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped>
.login-page { display:flex; justify-content:center; align-items:center; min-height:100vh; background:#f5f7fa; }
.login-card { width:400px; border-radius:12px; padding:40px; }
.login-card h2 { text-align:center; margin-bottom:30px; }
.captcha-row { display:flex; gap:12px; align-items:center; }
.captcha-img { height:38px; cursor:pointer; border:1px solid #ddd; border-radius:4px; }
</style>
27. 全部工具类和配置类代码
27.1 RedisConfig
// common/src/main/java/org/example/mall/common/config/RedisConfig.java
package org.example.mall.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 序列化用 String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 序列化用 JSON
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
27.2 各微服务 WebConfig 完整代码
goods_service/WebConfig.java:
package org.example.mall.goods_service.config;
import org.example.mall.common.intercepter.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${com.picdir}")
private String picdir;
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 图片静态文件映射:/category/pic/xxx.jpg → 文件系统 picdir/xxx.jpg
registry.addResourceHandler("/category/pic/**", "/goods/pic/**")
.addResourceLocations("file:" + picdir);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 只拦截写操作(增删改),读操作放行
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/category/**", "/goods/**")
.excludePathPatterns(
"/category/search", "/category/allParent",
"/category/{id}", "/category/**/pic/**",
"/goods", "/goods/search", "/goods/{id}",
"/goods/**/pic/**", "/goods/upload"
);
}
}
user_service/WebConfig.java:
package org.example.mall.user_service.config;
import org.example.mall.common.intercepter.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// /user/** 和 /collect/** 需要认证
radio.addInterceptor(jwtInterceptor)
.addPathPatterns("/user/**", "/collect/**");
// /addr/** 不在拦截范围——AddrController每个方法手动校验token
}
}
admin_service/WebConfig.java:
package org.example.mall.admin_service.config;
import org.example.mall.common.intercepter.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/captcha", "/admin/login");
}
}
order_service/WebConfig.java:
package org.example.mall.order_service.config;
import org.example.mall.common.intercepter.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${com.picdir}")
private String picdir;
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/category/pic/**", "/goods/pic/**")
.addResourceLocations("file:" + picdir);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 购物车接口需要认证
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/cart/**");
// /order/** 不拦截——Controller按端点区分:
// create/pay/cancel/myOrders → 手动 @RequestHeader("token")
// notify → 不需要token(支付宝回调)
// query/search/selectById → 开放或精简返回
}
}
28. 全部 Mapper XML 完整 SQL 清单
28.1 goods_service Mapper SQL
GoodsMapper.xml(完整版,关键SQL):
<mapper namespace="org.example.mall.goods_service.mapper.GoodsMapper">
<resultMap id="goodsMap" type="goods">
<id column="id" property="id"/>
<result column="category_id" property="categoryId"/>
<association column="category_id" property="category" javaType="category"
select="org.example.mall.goods_service.mapper.CategoryMapper.selectById"/>
<collection column="id" property="picList" ofType="goodsPic"
select="org.example.mall.goods_service.mapper.GoodsPicMapper.selectByGoodsId"/>
</resultMap>
<insert id="insert" parameterType="goods" useGeneratedKeys="true" keyProperty="id">
INSERT INTO goods(name,decp,detail,price,market_price,purchase_price,
color,version,count,recom,category_id,score,status)
VALUES (#{name},#{decp},#{detail},#{price},#{marketPrice},#{purchasePrice},
#{color},#{version},#{count},#{recom},#{categoryId},#{score},#{status})
</insert>
<update id="update" parameterType="goods">
UPDATE goods
<set>
<if test="name!=null">name=#{name},</if>
<if test="decp!=null">decp=#{decp},</if>
<if test="detail!=null">detail=#{detail},</if>
<if test="price!=null">price=#{price},</if>
<if test="marketPrice!=null">market_price=#{marketPrice},</if>
<if test="purchasePrice!=null">purchase_price=#{purchasePrice},</if>
<if test="color!=null">color=#{color},</if>
<if test="version!=null">version=#{version},</if>
<if test="count!=null">count=#{count},</if>
<if test="recom!=null">recom=#{recom},</if>
<if test="categoryId!=null">category_id=#{categoryId},</if>
<if test="status!=null">status=#{status},</if>
</set>
WHERE id=#{id}
</update>
<delete id="delete">DELETE FROM goods WHERE id=#{id}</delete>
<select id="selectByid" resultMap="goodsMap">
SELECT * FROM goods WHERE id=#{id}
</select>
<select id="selectByCondition" resultMap="goodsMap">
SELECT * FROM goods
<where>
<if test="name!=null">AND name LIKE CONCAT('%',#{name},'%')</if>
<if test="categoryId!=null">AND category_id=#{categoryId}</if>
<if test="status!=null">AND status=#{status}</if>
</where>
</select>
<select id="selectByCondition1" resultMap="goodsMap">
SELECT g.* FROM goods g, category c
<where>
g.category_id=c.id
<if test="name!=null">AND g.name LIKE CONCAT('%',#{name},'%')</if>
<if test="categoryId!=null">AND (c.parent_id=#{categoryId} OR c.id=#{categoryId})</if>
<if test="status!=null">AND g.status=#{status}</if>
<if test="recom!=null">AND g.recom=#{recom}</if>
</where>
</select>
<select id="selectByCategoryId" resultMap="goodsMap">
SELECT g.* FROM goods g, category c
WHERE g.category_id=c.id AND g.status=1 AND (c.parent_id=#{categoryId} OR c.id=#{categoryId})
</select>
<!-- 扣减库存:WHERE count>=#{count} 防超卖 -->
<update id="reduceStock">
UPDATE goods SET count = count - #{count} WHERE id = #{id} AND count >= #{count}
</update>
</mapper>
GoodsPicMapper.xml:
<mapper namespace="org.example.mall.goods_service.mapper.GoodsPicMapper">
<insert id="insert">
INSERT INTO goods_pic(url, goods_id) VALUES
<foreach collection="picList" item="pic" separator=",">
(#{pic.url}, #{pic.goodsId})
</foreach>
</insert>
<delete id="delete">DELETE FROM goods_pic WHERE goods_id = #{goodsId}</delete>
<select id="selectByGoodsId" resultType="org.example.mall.common.bean.GoodsPic">
SELECT * FROM goods_pic WHERE goods_id = #{goodsId}
</select>
</mapper>
CategoryMapper.xml:
<mapper namespace="org.example.mall.goods_service.mapper.CategoryMapper">
<insert id="insert" parameterType="category" useGeneratedKeys="true" keyProperty="id">
INSERT INTO category(name, dscp, pic, parent_id, recom, status)
VALUES (#{name}, #{dscp}, #{pic}, #{parentId}, #{recom}, #{status})
</insert>
<update id="update" parameterType="category">
UPDATE category
<set>
<if test="name!=null">name=#{name},</if>
<if test="dscp!=null">dscp=#{dscp},</if>
<if test="pic!=null">pic=#{pic},</if>
<if test="parentId!=null">parent_id=#{parentId},</if>
<if test="recom!=null">recom=#{recom},</if>
<if test="status!=null">status=#{status},</if>
</set>
WHERE id=#{id}
</update>
<delete id="delete">DELETE FROM category WHERE id=#{id}</delete>
<select id="selectById" resultType="category">SELECT * FROM category WHERE id=#{id}</select>
<select id="selectAllParent" resultType="category">
SELECT * FROM category WHERE parent_id=0 AND status=1
</select>
<select id="selectByCondition" resultType="category">
SELECT * FROM category
<where>
<if test="name!=null">AND name LIKE CONCAT('%',#{name},'%')</if>
<if test="parentId!=null">AND parent_id=#{parentId}</if>
<if test="status!=null">AND status=#{status}</if>
</where>
ORDER BY parent_id, id
</select>
</mapper>
28.2 user_service Mapper SQL
UserMapper.xml:
<mapper namespace="org.example.mall.user_service.mapper.UserMapper">
<insert id="insert" parameterType="user">
INSERT INTO user(username,password,salt,realname,gender,phone,email,money,status)
VALUES (#{username},#{password},#{salt},#{realname},#{gender},#{phone},#{email},#{money},#{status})
</insert>
<update id="updateById" parameterType="user">
UPDATE user
<set>
<if test="password!=null">password=#{password},</if>
<if test="salt!=null">salt=#{salt},</if>
<if test="realname!=null">realname=#{realname},</if>
<if test="gender!=null">gender=#{gender},</if>
<if test="idCard!=null">id_card=#{idCard},</if>
<if test="phone!=null">phone=#{phone},</if>
<if test="email!=null">email=#{email},</if>
<if test="money!=null">money=#{money},</if>
<if test="status!=null">status=#{status},</if>
</set>
WHERE id=#{id}
</update>
<delete id="delete">DELETE FROM user WHERE id=#{id}</delete>
<select id="selectById" resultType="user">SELECT * FROM user WHERE id=#{id}</select>
<select id="selectByUsername" resultType="user">SELECT * FROM user WHERE username=#{username}</select>
<select id="selectByCondition" resultType="user">
SELECT * FROM user
<where>
<if test="username!=null">AND username LIKE CONCAT('%',#{username},'%')</if>
<if test="phone!=null">AND phone LIKE CONCAT('%',#{phone},'%')</if>
<if test="email!=null">AND email LIKE CONCAT('%',#{email},'%')</if>
<if test="status!=null">AND status=#{status}</if>
</where>
</select>
</mapper>
CollectMapper.xml(完整版):
<mapper namespace="org.example.mall.user_service.mapper.CollectMapper">
<resultMap id="collectWithGoods" type="collect">
<id column="c_id" property="id"/>
<result column="goods_id" property="goodsId"/>
<result column="user_id" property="userId"/>
<result column="c_create_time" property="createTime"/>
<association property="goods" javaType="org.example.mall.common.bean.Goods">
<id column="g_id" property="id"/>
<result column="g_name" property="name"/>
<result column="g_price" property="price"/>
<result column="g_market_price" property="marketPrice"/>
<result column="g_status" property="status"/>
<result column="g_color" property="color"/>
<result column="g_version" property="version"/>
<result column="g_count" property="count"/>
<collection property="picList" ofType="org.example.mall.common.bean.GoodsPic">
<id column="gp_id" property="id"/>
<result column="gp_url" property="url"/>
<result column="gp_goods_id" property="goodsId"/>
</collection>
</association>
</resultMap>
<insert id="insert" parameterType="collect">
INSERT INTO collect(goods_id, user_id) VALUES (#{goodsId}, #{userId})
</insert>
<delete id="delect">DELETE FROM collect WHERE id=#{id}</delete>
<select id="selectById" resultType="collect">SELECT * FROM collect WHERE id=#{id}</select>
<select id="selectByGoodsIdAndUserId" resultType="collect">
SELECT * FROM collect WHERE goods_id=#{goodsId} AND user_id=#{userId}
</select>
<!-- LEFT JOIN干三表,一次SQL拿回收藏+商品+图片 -->
<select id="selectByUserId" resultMap="collectWithGoods">
SELECT c.id AS c_id, c.goods_id, c.user_id, c.create_time AS c_create_time,
g.id AS g_id, g.name AS g_name, g.price AS g_price,
g.market_price AS g_market_price, g.status AS g_status,
g.color AS g_color, g.version AS g_version, g.count AS g_count,
gp.id AS gp_id, gp.url AS gp_url, gp.goods_id AS gp_goods_id
FROM collect c
LEFT JOIN goods g ON c.goods_id = g.id
LEFT JOIN goods_pic gp ON g.id = gp.goods_id
WHERE c.user_id = #{userId}
ORDER BY c.create_time DESC
</select>
</mapper>
AddrMapper.xml:
<mapper namespace="org.example.mall.user_service.mapper.AddrMapper">
<insert id="insert" parameterType="addr">
INSERT INTO addr(contact,phone,province,city,district,street,address,user_id,status)
VALUES (#{contact},#{phone},#{province},#{city},#{district},#{street},#{address},#{userId},#{status})
</insert>
<update id="update" parameterType="addr">
UPDATE addr
<set>
<if test="contact!=null and contact!=''">contact=#{contact},</if>
<if test="phone!=null and phone!=''">phone=#{phone},</if>
<if test="province!=null">province=#{province},</if>
<if test="city!=null">city=#{city},</if>
<if test="district!=null">district=#{district},</if>
<if test="street!=null">street=#{street},</if>
<if test="address!=null and address!=''">address=#{address},</if>
<if test="status!=null">status=#{status},</if>
</set>
WHERE id=#{id}
</update>
<delete id="delete">DELETE FROM addr WHERE id=#{id}</delete>
<select id="selectById" resultType="addr">SELECT * FROM addr WHERE id=#{id}</select>
<select id="selectByUserId" resultType="addr">
SELECT * FROM addr WHERE user_id=#{userId} ORDER BY status DESC, id DESC
</select>
<update id="clearDefault">UPDATE addr SET status=0 WHERE user_id=#{userId}</update>
<update id="setDefault">UPDATE addr SET status=1 WHERE id=#{id}</update>
</mapper>
28.3 admin_service Mapper SQL
AdminMapper.xml:
<mapper namespace="org.example.mall.admin_service.mapper.AdminMapper">
<insert id="insert" parameterType="admin">
INSERT INTO admin(username,password,salt,phone,email,realname,status)
VALUES (#{username},#{password},#{salt},#{phone},#{email},#{realname},#{status})
</insert>
<update id="update" parameterType="admin">
UPDATE admin
<set>
<if test="password!=null">password=#{password},</if>
<if test="salt!=null">salt=#{salt},</if>
<if test="phone!=null">phone=#{phone},</if>
<if test="email!=null">email=#{email},</if>
<if test="realname!=null">realname=#{realname},</if>
<if test="status!=null">status=#{status},</if>
</set>
WHERE id=#{id}
</update>
<delete id="delete">DELETE FROM admin WHERE id=#{id}</delete>
<select id="selectById" resultType="admin">SELECT * FROM admin WHERE id=#{id}</select>
<select id="selectByUsername" resultType="admin">SELECT * FROM admin WHERE username=#{username}</select>
<select id="selectByCondition" resultType="admin">
SELECT * FROM admin
<where>
<if test="username!=null">AND username LIKE CONCAT('%',#{username},'%')</if>
<if test="phone!=null">AND phone LIKE CONCAT('%',#{phone},'%')</if>
<if test="status!=null">AND status=#{status}</if>
</where>
</select>
</mapper>
28.4 order_service Mapper SQL
CartMapper.xml:
<mapper namespace="org.example.mall.order_service.mapper.CartMapper">
<insert id="insert" parameterType="cart">
INSERT INTO cart(count, user_id, goods_id) VALUES (#{count}, #{userId}, #{goodsId})
</insert>
<update id="update" parameterType="cart">
UPDATE cart
<set>
<if test="count!=null">count=#{count},</if>
<if test="userId!=null">user_id=#{userId},</if>
<if test="goodsId!=null">goods_id=#{goodsId},</if>
</set>
WHERE id=#{id}
</update>
<delete id="delete">DELETE FROM cart WHERE id=#{id}</delete>
<delete id="deleteByIds">
DELETE FROM cart WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<select id="selectById" resultType="Cart">SELECT * FROM cart WHERE id=#{id}</select>
<select id="selectByGoodsIdAndUserId" resultType="Cart">
SELECT * FROM cart WHERE user_id=#{userId} AND goods_id=#{goodsId}
</select>
<select id="selectByCondition" resultType="org.example.mall.common.bean.Cart">
SELECT * FROM cart
<where>
<if test="id!=null">AND id=#{id}</if>
<if test="goodsId!=null">AND goods_id=#{goodsId}</if>
<if test="userId!=null">AND user_id=#{userId}</if>
</where>
</select>
</mapper>
OrderMapper.xml(完整版):
<mapper namespace="org.example.mall.order_service.mapper.OrderMapper">
<resultMap id="orderMap" type="org.example.mall.common.bean.Order">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="pay_type" property="payType"/>
<result column="express" property="express"/>
<result column="addr_id" property="addrId"/>
<result column="addr_detail" property="addrDetail"/>
<result column="status" property="status"/>
<result column="items" property="items"/>
<result column="create_time" property="createTime"/>
<result column="username" property="username"/>
</resultMap>
<insert id="insert" parameterType="org.example.mall.common.bean.Order">
INSERT INTO `order`(id, user_id, pay_type, express, addr_id, addr_detail, status, items, create_time)
VALUES (#{id}, #{userId}, #{payType}, #{express}, #{addrId}, #{addrDetail}, #{status}, #{items}, NOW())
</insert>
<select id="selectByCondition" resultMap="orderMap">
SELECT o.*, u.username FROM `order` o LEFT JOIN `user` u ON o.user_id = u.id
<where>
<if test="condition.userId!=null">AND o.user_id = #{condition.userId}</if>
<if test="condition.status!=null">AND o.status = #{condition.status}</if>
</where>
ORDER BY o.create_time DESC
</select>
<select id="selectById" resultMap="orderMap">
SELECT o.*, u.username FROM `order` o LEFT JOIN `user` u ON o.user_id = u.id WHERE o.id = #{id}
</select>
<select id="selectByUserId" resultMap="orderMap">
SELECT o.*, u.username FROM `order` o LEFT JOIN `user` u ON o.user_id = u.id
WHERE o.user_id = #{userId} ORDER BY o.create_time DESC
</select>
<select id="selectAddrById" resultType="org.example.mall.common.bean.Addr">
SELECT * FROM addr WHERE id = #{id}
</select>
<update id="updateById">
UPDATE `order`
<set>
<if test="status!=null">status=#{status},</if>
<if test="express!=null and express!=''">express=#{express},</if>
<if test="addrDetail!=null and addrDetail!=''">addr_detail=#{addrDetail},</if>
<if test="items!=null">items=#{items},</if>
</set>
WHERE id = #{id}
</update>
<delete id="delete">DELETE FROM `order` WHERE id = #{id}</delete>
<update id="cancelExpiredOrders">
UPDATE `order` SET status = 4 WHERE status = 0 AND create_time < #{before}
</update>
</mapper>
29. 部署详细步骤
29.1 环境变量注入方式
方式一:export(当前终端有效)
export ALIPAY_APP_ID=9021000165698953
export ALIPAY_PRIVATE_KEY="MIIEvAIBADAN..."
export ALIPAY_PUBLIC_KEY="MIIBIjAN..."
export ALIPAY_RETURN_URL=http://你的IP/#/user/payResult
export ALIPAY_NOTIFY_URL=http://你的IP/order/notify
java -jar order_service/target/order_service-0.0.1-SNAPSHOT.jar
方式二:systemd服务(Linux长期运行推荐)
[Unit]
Description=Mall Order Service
[Service]
User=mall
WorkingDirectory=/opt/mall
ExecStart=/usr/bin/java -jar /opt/mall/order_service.jar
Environment="ALIPAY_APP_ID=9021000165698953"
Environment="ALIPAY_PRIVATE_KEY=MIIEvAIBADANBgkqh..."
Environment="ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEF..."
Environment="ALIPAY_RETURN_URL=http://你的域名/#/user/payResult"
Environment="ALIPAY_NOTIFY_URL=http://你的域名/order/notify"
Restart=always
[Install]
WantedBy=multi-user.target
方式三:Windows批处理
set ALIPAY_APP_ID=9021000165698953
set ALIPAY_PRIVATE_KEY=MIIEvAIBADANBgkqhkiG...
set ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEF...
set ALIPAY_RETURN_URL=http://你的IP/#/user/payResult
set ALIPAY_NOTIFY_URL=http://你的IP/order/notify
java -jar order_service-0.0.1-SNAPSHOT.jar
29.2 JVM启动参数优化
java -Xms256m -Xmx512m -XX:+UseG1GC -jar xxx.jar
| 参数 | 值 | 说明 |
|---|---|---|
-Xms |
256m | 初始堆内存 |
-Xmx |
512m | 最大堆内存 |
-XX:+UseG1GC |
– | 使用G1垃圾回收器 |
29.3 nginx前端部署
# 1. 复制前端产物的静态文件
cp -r front/dist/* /usr/share/nginx/html/
# 2. 复制nginx配置文件
cp nginx/conf/nginx.conf /etc/nginx/nginx.conf
# 3. 测试配置+重载
nginx -t
nginx -s reload
29.4 打包脚本(一键打包前后端)
#!/bin/bash
# build.sh
set -e
echo "=== 打包后端 ==="
mvn clean package -DskipTests -q
echo "后端打包完成"
echo "=== 打包前端 ==="
cd front
npm run build
cd ..
echo "前端打包完成"
echo "产物:"
echo " 前端: front/dist/"
echo " 后端:"
ls -lh */target/*.jar
30. Dubbo 启动验证
30.1 确认 ZooKeeper 注册成功
启动 goods_service 后,用 ZooKeeper 客户端检查:
# 连接 ZooKeeper
cd zookeeper-x.x.x
bin/zkCli.cmd -server 127.0.0.1:2181
# 查看注册的 Dubbo 服务
ls /dubbo/org.example.mall.common.service.GoodsService/providers
# 应该看到一个 URL 编码的地址,如:
# [dubbo://192.168.1.5:38090/org.example.mall.common.service.GoodsService?...]
如果能看到 provider 地址,说明 goods_service 已成功注册到 ZooKeeper,可以被 order_service 调用了。
30.2 验证 Dubbo 调用
启动 order_service 后,看启动日志:
[Dubbo] Registering consumer for service org.example.mall.common.service.GoodsService
表示 order_service 作为消费者已连上 goods_service。
实际调用测试:进入前台→加入购物车→点击结算→如果看到商品清单,说明 Dubbo 调用成功(order_service 通过 Dubbo 从 goods_service 查到了商品信息)。
30.3 常见 Dubbo 启动问题
| 现象 | 原因 | 解决 |
|---|---|---|
Failed to connect to /127.0.0.1:2181 |
ZooKeeper还没启动 | 先启动zkServer.cmd |
No provider available |
goods_service没注册成功 | 检查 goods_service 日志、@EnableDubbo是否正确、application.yml dubbo配置 |
Connection refused |
Dubbo端口被防火墙拦 | 开放38090等端口或关闭防火墙测试 |
31. 完整 API 响应示例
31.1 下单成功响应
{
"code": 200,
"msg": "下单成功",
"data": {
"id": "1876543210987654321",
"userId": 1,
"payType": 1,
"addrId": 1,
"addrDetail": "山东省青岛市城阳区城阳街道 农大西苑A座 - 汤姆 - 13322332233",
"status": 0,
"items": "[{"goodsId":45,"goodsName":"索尼 WH-1000XM5","goodsPic":"goods_045_main.jpg","price":2399.00,"count":2,"subtotal":4798.00}]",
"createTime": "2026-07-28 15:30:00",
"detailList": [
{
"goodsId": 45,
"goodsName": "索尼 WH-1000XM5",
"goodsPic": "goods_045_main.jpg",
"price": 2399.00,
"count": 2,
"subtotal": 4798.00
}
],
"totalPrice": 4798.00
}
}
31.2 支付表单响应
{
"code": 200,
"msg": "",
"data": "<form name="punchout_form" method="post" action="https://openapi-sandbox.dl.alipaydev.com/gateway.do?charset=utf-8&method=alipay.trade.page.pay&sign=..."><input type="hidden" name="biz_content" value="{...}"/><input type="submit" value="submit" style="display:none"/></form><script>document.forms[0].submit();</script>"
}
31.3 仪表盘响应
{
"code": 200,
"msg": "",
"data": {
"totalOrders": 100,
"totalUsers": 50,
"totalGoods": 30,
"todayOrders": 5,
"todaySales": 15997.00,
"statusCnt": {
"pending": 10,
"paid": 80,
"shipped": 3,
"completed": 5,
"cancelled": 2
},
"recentOrders": [
{"id": "1876543210987654321", "userId": 1, "status": 1, "createTime": "2026-07-28 15:30:00", "username": "tom"}
],
"recentUsers": [
{"id": 5, "username": "cindy", "phone": "15588889999", "email": "cindy@mall-demo.com", "regTime": "2025-01-05 12:00:00", "status": 0}
]
}
}
至此,三份文档完整覆盖了项目的:
- 全部功能原理 +代码逐行注释(PROJECT_DOC.md 第1-18章)
- 全部环境搭建 + 配置 + 部署步骤(DOC_APPENDIX.md 第19-25章)
- 全部缺失代码 + Mapper XML + 前端页面 + 工具类 + API示例 + Dubbo验证(本文档第26-31章)
三份文档加起来可以:理解每一行代码 → 从零搭建环境 → 写出全部文件 → 部署运行 → 验证功能