refactor: 重构项目结构,迁移后端至 mengyastore-backend-go,新增 Java 后端、前端功能更新及部署文档

This commit is contained in:
2026-03-27 15:10:53 +08:00
parent 84874707f5
commit 63781358b2
71 changed files with 2123 additions and 250 deletions

View File

@@ -2106,9 +2106,6 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2126,9 +2123,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2146,9 +2140,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2166,9 +2157,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2186,9 +2174,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2206,9 +2191,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2226,9 +2208,6 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2252,9 +2231,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2278,9 +2254,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2304,9 +2277,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2330,9 +2300,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2356,9 +2323,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [

View File

@@ -120,7 +120,7 @@
<script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { fetchAdminToken, fetchStats, recordSiteVisit } from './modules/shared/api'
import { verifyAdminToken, fetchStats, recordSiteVisit } from './modules/shared/api'
import { authState, isLoggedIn, clearAuth, getLoginUrl } from './modules/shared/auth'
import { wishlistCount, loadWishlist } from './modules/shared/useWishlist'
import ChatWidget from './modules/chat/ChatWidget.vue'
@@ -171,10 +171,10 @@ const submitAdminToken = async () => {
if (!input) return
tokenError.value = ''
try {
const correctToken = await fetchAdminToken()
if (input === correctToken) {
const valid = await verifyAdminToken(input)
if (valid) {
showAdminModal.value = false
router.push(`/admin?token=${encodeURIComponent(correctToken)}`)
router.push(`/admin?token=${encodeURIComponent(input)}`)
} else {
tokenError.value = '令牌错误,请重试'
}

View File

@@ -42,7 +42,6 @@
v-model:token="token"
:message="!token || message ? message : ''"
:inline-message="token && message ? message : ''"
@auto-get="autoGetToken"
/>
<!-- Section: Products -->
@@ -96,7 +95,6 @@ import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
fetchAdminProducts,
fetchAdminToken,
createProduct,
updateProduct,
toggleProduct,
@@ -183,12 +181,6 @@ const refresh = async () => {
}
}
const autoGetToken = async () => {
const fetched = await fetchAdminToken()
token.value = fetched
syncQuery()
await refresh()
}
const loadMaintenance = async () => {
try {

View File

@@ -135,7 +135,7 @@ const load = async () => {
loading.value = true
try {
conversations.value = await fetchAdminAllConversations(props.adminToken)
// Refresh current conversation messages if one is selected
// 若当前已选中某个会话,则刷新其消息列表
if (selectedAccount.value) {
currentMessages.value = conversations.value[selectedAccount.value] || []
await scrollThreadBottom()

View File

@@ -98,7 +98,7 @@ const pagedOrders = computed(() => {
return props.orders.slice(start, start + PAGE_SIZE)
})
// Reset to page 1 when orders list changes
// 订单列表变化时重置到第一页
watch(() => props.orders.length, () => { currentPage.value = 1 })
const remove = (id) => {

View File

@@ -5,26 +5,35 @@
<span class="smtp-desc tag">下单/发货时自动给用户发送通知邮件支持 QQ / 163 / Gmail / 自定义域名邮箱</span>
<span v-if="message" class="msg-tag" :class="{ error: message.includes('失败') }">{{ message }}</span>
</div>
<div class="smtp-fields">
<div class="smtp-enable-row">
<label class="smtp-toggle">
<input type="checkbox" v-model="form.enabled" />
<span>启用邮件通知</span>
</label>
<span class="smtp-status-tag" :class="form.enabled ? 'tag-on' : 'tag-off'">
{{ form.enabled ? '已启用' : '已关闭' }}
</span>
</div>
<div class="smtp-fields" :class="{ 'smtp-fields-disabled': !form.enabled }">
<label class="smtp-field">
<span>发件邮箱</span>
<input v-model="form.email" type="email" placeholder="noreply@yourdomain.com" />
<input v-model="form.email" type="email" placeholder="noreply@yourdomain.com" :disabled="!form.enabled" />
</label>
<label class="smtp-field">
<span>SMTP 密码 / 授权码</span>
<input v-model="form.password" type="password" placeholder="QQ/163 填授权码;其他填密码" autocomplete="new-password" />
<input v-model="form.password" type="password" placeholder="QQ/163 填授权码;其他填密码" autocomplete="new-password" :disabled="!form.enabled" />
</label>
<label class="smtp-field">
<span>发件人名称</span>
<input v-model="form.fromName" type="text" placeholder="萌芽小店" />
<input v-model="form.fromName" type="text" placeholder="萌芽小店" :disabled="!form.enabled" />
</label>
<label class="smtp-field">
<span>SMTP 主机</span>
<input v-model="form.host" type="text" placeholder="smtp.qq.com" />
<input v-model="form.host" type="text" placeholder="smtp.qq.com" :disabled="!form.enabled" />
</label>
<label class="smtp-field smtp-field-port">
<span>端口</span>
<input v-model="form.port" type="text" placeholder="465" />
<input v-model="form.port" type="text" placeholder="465" :disabled="!form.enabled" />
</label>
<button class="primary smtp-save-btn" type="button" :disabled="saving" @click="save">
{{ saving ? '保存中...' : '保存配置' }}
@@ -46,6 +55,7 @@ const emit = defineEmits(['save'])
const saving = ref(false)
const form = reactive({
enabled: true,
email: '',
password: '',
fromName: '',
@@ -55,6 +65,7 @@ const form = reactive({
watch(() => props.config, (cfg) => {
if (!cfg) return
form.enabled = cfg.enabled !== false
form.email = cfg.email || ''
form.password = cfg.password || ''
form.fromName = cfg.fromName || ''
@@ -101,6 +112,47 @@ const save = async () => {
color: var(--muted);
}
.smtp-enable-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.smtp-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 14px;
color: var(--text);
font-weight: 500;
}
.smtp-toggle input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent);
cursor: pointer;
}
.smtp-status-tag {
font-size: 12px;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
}
.tag-on {
background: rgba(74, 222, 128, 0.15);
color: #2d8a4e;
}
.tag-off {
background: rgba(0,0,0,0.06);
color: #888;
}
.smtp-fields {
display: flex;
gap: 10px;
@@ -108,6 +160,11 @@ const save = async () => {
align-items: flex-end;
}
.smtp-fields-disabled {
opacity: 0.45;
pointer-events: none;
}
.smtp-field {
display: flex;
flex-direction: column;

View File

@@ -3,8 +3,7 @@
<div class="form-field token-field">
<label>管理 Token</label>
<div class="token-input-wrap">
<input :value="token" @input="$emit('update:token', $event.target.value)" placeholder="粘贴 token 后自动加载…" />
<button class="ghost small" type="button" @click="$emit('auto-get')">自动获取</button>
<input :value="token" @input="$emit('update:token', $event.target.value)" placeholder="粘贴管理员令牌后自动加载…" />
</div>
</div>
<p
@@ -28,7 +27,7 @@ defineProps({
inlineMessage: { type: String, default: '' }
})
defineEmits(['update:token', 'auto-get'])
defineEmits(['update:token'])
</script>
<style scoped>

View File

@@ -52,7 +52,7 @@ onMounted(async () => {
}
try {
// Verify token and get up-to-date user info from SproutGate
// 验证 token 并从 SproutGate 获取最新用户信息
const verifyData = await verifySproutGateToken(token)
if (!verifyData.valid) {
status.value = 'error'
@@ -71,7 +71,7 @@ onMounted(async () => {
status.value = 'success'
setTimeout(() => router.push('/'), 1000)
} catch {
// If verify fails (network issue), fall back to fragment data
// 验证失败(如网络异常)时,回退使用 URL fragment 中的数据
setAuth({ token, account, username, avatarUrl: fragmentAvatar })
displayName.value = username || account
status.value = 'success'

View File

@@ -121,7 +121,7 @@ const loadMessages = async () => {
messages.value = await fetchMyChatMessages(props.userToken)
await scrollBottom()
} catch {
// silently ignore polling errors
// 静默忽略轮询错误,避免频繁弹出错误提示
}
}

View File

@@ -1,11 +1,18 @@
import axios from 'axios'
const apiBaseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
const apiBaseURL =
import.meta.env.VITE_API_BASE_URL ||
import.meta.env.VITE_API_BASE ||
(typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8080')
const api = axios.create({
baseURL: apiBaseURL
})
// 管理员请求头构建辅助函数,使用 X-Admin-Token 请求头。
// 后端保留 ?token= 查询参数作为旧版兼容,新请求统一使用请求头以兼容 Spring Security。
const adminHeaders = (token) => ({ 'X-Admin-Token': token })
const authApi = axios.create({
baseURL: 'https://auth.api.shumengya.top'
})
@@ -60,40 +67,40 @@ export const fetchMyOrders = async (authToken) => {
return data.data || []
}
export const fetchAdminToken = async () => {
const { data } = await api.get('/api/admin/token')
return data.token || ''
export const verifyAdminToken = async (token) => {
const { data } = await api.post('/api/admin/verify', { token })
return data.valid === true
}
export const fetchAdminProducts = async (token) => {
const { data } = await api.get('/api/admin/products', { params: { token } })
const { data } = await api.get('/api/admin/products', { headers: adminHeaders(token) })
return data.data || []
}
export const createProduct = async (token, payload) => {
const { data } = await api.post('/api/admin/products', payload, {
params: { token }
headers: adminHeaders(token)
})
return data.data
}
export const updateProduct = async (token, id, payload) => {
const { data } = await api.put(`/api/admin/products/${id}`, payload, {
params: { token }
headers: adminHeaders(token)
})
return data.data
}
export const toggleProduct = async (token, id, active) => {
const { data } = await api.patch(`/api/admin/products/${id}/status`, { active }, {
params: { token }
headers: adminHeaders(token)
})
return data.data
}
export const deleteProduct = async (token, id) => {
const { data } = await api.delete(`/api/admin/products/${id}`, {
params: { token }
headers: adminHeaders(token)
})
return data
}
@@ -120,12 +127,12 @@ export const removeFromWishlist = async (token, productId) => {
}
export const fetchAdminOrders = async (token) => {
const { data } = await api.get('/api/admin/orders', { params: { token } })
const { data } = await api.get('/api/admin/orders', { headers: adminHeaders(token) })
return data.data || []
}
export const deleteAdminOrder = async (token, orderId) => {
await api.delete(`/api/admin/orders/${orderId}`, { params: { token } })
await api.delete(`/api/admin/orders/${orderId}`, { headers: adminHeaders(token) })
}
export const fetchSiteMaintenance = async () => {
@@ -135,19 +142,19 @@ export const fetchSiteMaintenance = async () => {
export const setSiteMaintenance = async (token, maintenance, reason) => {
const { data } = await api.post('/api/admin/site/maintenance', { maintenance, reason }, {
params: { token }
headers: adminHeaders(token)
})
return data.data || {}
}
// ---- SMTP Config ----
export const fetchSMTPConfig = async (token) => {
const { data } = await api.get('/api/admin/site/smtp', { params: { token } })
const { data } = await api.get('/api/admin/site/smtp', { headers: adminHeaders(token) })
return data.data || {}
}
export const setSMTPConfig = async (token, cfg) => {
const { data } = await api.post('/api/admin/site/smtp', cfg, { params: { token } })
const { data } = await api.post('/api/admin/site/smtp', cfg, { headers: adminHeaders(token) })
return data.data
}
@@ -168,13 +175,13 @@ export const sendChatMessage = async (userToken, content) => {
// ---- Chat (admin) ----
export const fetchAdminAllConversations = async (adminToken) => {
const { data } = await api.get('/api/admin/chat', { params: { token: adminToken } })
const { data } = await api.get('/api/admin/chat', { headers: adminHeaders(adminToken) })
return data.data?.conversations || {}
}
export const fetchAdminConversation = async (adminToken, account) => {
const { data } = await api.get(`/api/admin/chat/${encodeURIComponent(account)}`, {
params: { token: adminToken }
headers: adminHeaders(adminToken)
})
return data.data?.messages || []
}
@@ -183,13 +190,13 @@ export const adminSendChatReply = async (adminToken, account, content) => {
const { data } = await api.post(
`/api/admin/chat/${encodeURIComponent(account)}`,
{ content },
{ params: { token: adminToken } }
{ headers: adminHeaders(adminToken) }
)
return data.data?.message || null
}
export const adminClearConversation = async (adminToken, account) => {
await api.delete(`/api/admin/chat/${encodeURIComponent(account)}`, {
params: { token: adminToken }
headers: adminHeaders(adminToken)
})
}

View File

@@ -29,7 +29,7 @@ const addToWishlist = async (productId) => {
try {
wishlistIds.value = await apiAddToWishlist(authState.token, productId)
} catch {
// ignore
// 忽略错误
}
}
@@ -38,7 +38,7 @@ const removeFromWishlist = async (productId) => {
try {
wishlistIds.value = await apiRemoveFromWishlist(authState.token, productId)
} catch {
// ignore
// 忽略错误
}
}