第三部分:完整代码补全

本部分补齐 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>查看更多 &gt;</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 &lt; #{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章)

三份文档加起来可以:理解每一行代码 → 从零搭建环境 → 写出全部文件 → 部署运行 → 验证功能

暂无评论

发送评论 编辑评论


				
上一篇