管理后台
This commit is contained in:
parent
ea7994e017
commit
3fb5192b4b
13
admin/src/api/goldPrice.ts
Normal file
13
admin/src/api/goldPrice.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import http from './request'
|
||||
|
||||
export function getGoldPriceLogs() {
|
||||
return http.get('/admin/gold-price')
|
||||
}
|
||||
|
||||
export function getLatestGoldPrice() {
|
||||
return http.get('/admin/gold-price/latest')
|
||||
}
|
||||
|
||||
export function setGoldPrice(price: number) {
|
||||
return http.post('/admin/gold-price', { price })
|
||||
}
|
||||
|
|
@ -39,6 +39,10 @@
|
|||
<el-icon><User /></el-icon>
|
||||
<template #title>用户管理</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/gold-price">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<template #title>金价配置</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<template #title>系统设置</template>
|
||||
|
|
@ -101,6 +105,7 @@ const titleMap: Record<string, string> = {
|
|||
'/orders': '订单管理',
|
||||
'/molds': '版房管理',
|
||||
'/users': '用户管理',
|
||||
'/gold-price': '金价配置',
|
||||
'/settings': '系统设置',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ const router = createRouter({
|
|||
name: 'UserList',
|
||||
component: () => import('../views/user/UserList.vue'),
|
||||
},
|
||||
{
|
||||
path: 'gold-price',
|
||||
name: 'GoldPrice',
|
||||
component: () => import('../views/settings/GoldPrice.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -240,7 +240,8 @@
|
|||
:header-cell-style="{ background: '#f0f0ff', color: '#1d1e3a', fontWeight: 600, fontSize: '13px', padding: '10px 0' }"
|
||||
:cell-style="{ padding: '8px 0', fontSize: '13px' }">
|
||||
<el-table-column type="index" label="#" width="50" fixed align="center" />
|
||||
<el-table-column prop="modelName" label="型号名称" min-width="120" fixed show-overflow-tooltip />
|
||||
<el-table-column prop="modelName" label="规格名称" min-width="120" fixed show-overflow-tooltip />
|
||||
<el-table-column prop="barcode" label="条型码" width="130" fixed show-overflow-tooltip />
|
||||
<el-table-column label="基本参数" align="center">
|
||||
<el-table-column prop="fineness" label="成色" width="70" align="center" />
|
||||
<el-table-column prop="mainStone" label="主石" width="70" align="center" />
|
||||
|
|
@ -296,7 +297,10 @@
|
|||
<el-form :model="specForm" label-width="90px" size="small">
|
||||
<div class="dialog-section">基本信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="型号名称"><el-input v-model="specForm.modelName" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="规格名称"><el-input v-model="specForm.modelName" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="条型码"><el-input v-model="specForm.barcode" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="成色"><el-input v-model="specForm.fineness" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
|
|
@ -309,37 +313,37 @@
|
|||
<div class="dialog-section">金料信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="金料总重"><el-input-number v-model="specForm.goldTotalWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金料净重"><el-input-number v-model="specForm.goldNetWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金料净重"><el-input-number v-model="specForm.goldNetWeight" :precision="4" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="损耗"><el-input-number v-model="specForm.loss" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="金耗"><el-input-number v-model="specForm.goldLoss" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金耗"><el-input-number v-model="specForm.goldLoss" :precision="4" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金价"><el-input-number v-model="specForm.goldPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金值"><el-input-number v-model="specForm.goldValue" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="金值"><el-input-number v-model="specForm.goldValue" :precision="2" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<div class="dialog-section">主石信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="数量"><el-input-number v-model="specForm.mainStoneCount" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="石重"><el-input-number v-model="specForm.mainStoneWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="单价"><el-input-number v-model="specForm.mainStoneUnitPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="金额"><el-input-number v-model="specForm.mainStoneAmount" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="金额"><el-input-number v-model="specForm.mainStoneAmount" :precision="2" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<div class="dialog-section">副石信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="数量"><el-input-number v-model="specForm.sideStoneCount" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="石重"><el-input-number v-model="specForm.sideStoneWeight" :precision="4" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="单价"><el-input-number v-model="specForm.sideStoneUnitPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="金额"><el-input-number v-model="specForm.sideStoneAmount" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="金额"><el-input-number v-model="specForm.sideStoneAmount" :precision="2" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<div class="dialog-section">工费信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="配件金额"><el-input-number v-model="specForm.accessoryAmount" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="加工工费"><el-input-number v-model="specForm.processingFee" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="镶石工费"><el-input-number v-model="specForm.settingFee" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="总工费"><el-input-number v-model="specForm.totalLaborCost" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="总工费"><el-input-number v-model="specForm.totalLaborCost" :precision="2" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="总价"><el-input-number v-model="specForm.totalPrice" :precision="2" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="总价"><el-input-number v-model="specForm.totalPrice" :precision="2" disabled controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -361,11 +365,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type UploadFile } from 'element-plus'
|
||||
import { Back, Plus, Upload, Download, Delete, Check, VideoCamera, Edit } from '@element-plus/icons-vue'
|
||||
import { getProductDetail, createProduct, updateProduct, getCategories, exportSpecData, getSpecDataList, createSpecData, deleteSpecData, updateSpecData } from '../../api/product'
|
||||
import { getLatestGoldPrice } from '../../api/goldPrice'
|
||||
import http, { getUploadUrl } from '../../api/request'
|
||||
|
||||
const route = useRoute()
|
||||
|
|
@ -438,8 +443,9 @@ const specDataRows = ref<any[]>([])
|
|||
const specDialogVisible = ref(false)
|
||||
const specSaving = ref(false)
|
||||
const editingSpecId = ref<number | null>(null)
|
||||
const globalGoldPrice = ref(0)
|
||||
const specForm = reactive({
|
||||
modelName: '', fineness: '', mainStone: '', subStone: '', ringSize: '',
|
||||
modelName: '', barcode: '', fineness: '', mainStone: '', subStone: '', ringSize: '',
|
||||
goldTotalWeight: 0, goldNetWeight: 0, loss: 0, goldLoss: 0,
|
||||
goldPrice: 0, goldValue: 0,
|
||||
mainStoneCount: 0, mainStoneWeight: 0, mainStoneUnitPrice: 0, mainStoneAmount: 0,
|
||||
|
|
@ -450,15 +456,39 @@ const specForm = reactive({
|
|||
function resetSpecForm() {
|
||||
editingSpecId.value = null
|
||||
Object.assign(specForm, {
|
||||
modelName: '', fineness: '', mainStone: '', subStone: '', ringSize: '',
|
||||
modelName: '', barcode: '', fineness: '', mainStone: '', subStone: '', ringSize: '',
|
||||
goldTotalWeight: 0, goldNetWeight: 0, loss: 0, goldLoss: 0,
|
||||
goldPrice: 0, goldValue: 0,
|
||||
goldPrice: globalGoldPrice.value, goldValue: 0,
|
||||
mainStoneCount: 0, mainStoneWeight: 0, mainStoneUnitPrice: 0, mainStoneAmount: 0,
|
||||
sideStoneCount: 0, sideStoneWeight: 0, sideStoneUnitPrice: 0, sideStoneAmount: 0,
|
||||
accessoryAmount: 0, processingFee: 0, settingFee: 0, totalLaborCost: 0, totalPrice: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// 自动计算规格数据
|
||||
function recalcSpec() {
|
||||
const f = specForm
|
||||
const n = (v: any) => Number(v) || 0
|
||||
f.goldNetWeight = +(n(f.goldTotalWeight) - n(f.mainStoneWeight) * 0.2 - n(f.sideStoneWeight) * 0.2).toFixed(4)
|
||||
if (f.goldNetWeight < 0) f.goldNetWeight = 0
|
||||
f.goldLoss = +(f.goldNetWeight * n(f.loss)).toFixed(4)
|
||||
f.goldValue = +(f.goldLoss * n(f.goldPrice)).toFixed(2)
|
||||
f.mainStoneAmount = +(n(f.mainStoneWeight) * n(f.mainStoneUnitPrice)).toFixed(2)
|
||||
f.sideStoneAmount = +(n(f.sideStoneWeight) * n(f.sideStoneUnitPrice)).toFixed(2)
|
||||
f.totalLaborCost = +(n(f.accessoryAmount) + n(f.processingFee) + n(f.settingFee)).toFixed(2)
|
||||
f.totalPrice = +(f.goldValue + f.mainStoneAmount + f.sideStoneAmount + f.totalLaborCost).toFixed(2)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
specForm.goldTotalWeight, specForm.loss, specForm.goldPrice,
|
||||
specForm.mainStoneWeight, specForm.mainStoneUnitPrice,
|
||||
specForm.sideStoneWeight, specForm.sideStoneUnitPrice,
|
||||
specForm.accessoryAmount, specForm.processingFee, specForm.settingFee,
|
||||
],
|
||||
recalcSpec,
|
||||
)
|
||||
|
||||
async function loadSpecData() {
|
||||
if (!isEdit.value) return
|
||||
try {
|
||||
|
|
@ -470,7 +500,7 @@ async function loadSpecData() {
|
|||
function handleEditSpec(row: any) {
|
||||
editingSpecId.value = row.id || row._tempId
|
||||
Object.assign(specForm, {
|
||||
modelName: row.modelName, fineness: row.fineness, mainStone: row.mainStone, subStone: row.subStone || '', ringSize: row.ringSize,
|
||||
modelName: row.modelName, barcode: row.barcode || '', fineness: row.fineness, mainStone: row.mainStone, subStone: row.subStone || '', ringSize: row.ringSize,
|
||||
goldTotalWeight: row.goldTotalWeight, goldNetWeight: row.goldNetWeight, loss: row.loss, goldLoss: row.goldLoss,
|
||||
goldPrice: row.goldPrice, goldValue: row.goldValue,
|
||||
mainStoneCount: row.mainStoneCount, mainStoneWeight: row.mainStoneWeight, mainStoneUnitPrice: row.mainStoneUnitPrice, mainStoneAmount: row.mainStoneAmount,
|
||||
|
|
@ -504,7 +534,10 @@ async function handleCreateSpec() {
|
|||
}
|
||||
specDialogVisible.value = false
|
||||
resetSpecForm()
|
||||
} catch { ElMessage.error('新增失败') }
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || '操作失败'
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
finally { specSaving.value = false }
|
||||
}
|
||||
|
||||
|
|
@ -688,6 +721,11 @@ onMounted(async () => {
|
|||
const res: any = await getCategories()
|
||||
categories.value = res.data
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
const gRes: any = await getLatestGoldPrice()
|
||||
globalGoldPrice.value = gRes.data?.price || 0
|
||||
specForm.goldPrice = globalGoldPrice.value
|
||||
} catch { /* ignore */ }
|
||||
loadProduct()
|
||||
loadSpecData()
|
||||
})
|
||||
|
|
|
|||
122
admin/src/views/settings/GoldPrice.vue
Normal file
122
admin/src/views/settings/GoldPrice.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-card shadow="never" class="page-card">
|
||||
<template #header>
|
||||
<div class="page-card__header">
|
||||
<span class="page-card__title">金价配置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="gold-price-form">
|
||||
<div class="current-price">
|
||||
<span class="current-price__label">当前金价:</span>
|
||||
<span class="current-price__value" v-if="currentPrice > 0">¥{{ currentPrice.toFixed(2) }} / 克</span>
|
||||
<span class="current-price__empty" v-else>暂未配置</span>
|
||||
</div>
|
||||
<div class="price-input">
|
||||
<el-input-number v-model="newPrice" :precision="2" :min="0.01" :step="10" controls-position="right" placeholder="输入新金价" style="width: 240px" />
|
||||
<el-button type="primary" :loading="saving" @click="handleSave" style="margin-left: 12px">
|
||||
更新金价
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="price-tip">更新金价后,系统将自动重算所有商品规格的金值和总价</div>
|
||||
</div>
|
||||
|
||||
<el-divider>修改记录</el-divider>
|
||||
|
||||
<el-table :data="logs" v-loading="loading" stripe size="small"
|
||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }">
|
||||
<el-table-column type="index" label="#" width="60" align="center" />
|
||||
<el-table-column prop="price" label="金价(元/克)" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #e4393c; font-weight: 600; font-size: 15px">¥{{ Number(row.price).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="修改时间" min-width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getGoldPriceLogs, getLatestGoldPrice, setGoldPrice } from '../../api/goldPrice'
|
||||
|
||||
const currentPrice = ref(0)
|
||||
const newPrice = ref(0)
|
||||
const logs = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
if (!iso) return '-'
|
||||
const d = new Date(iso)
|
||||
const Y = d.getFullYear()
|
||||
const M = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const D = String(d.getDate()).padStart(2, '0')
|
||||
const h = String(d.getHours()).padStart(2, '0')
|
||||
const m = String(d.getMinutes()).padStart(2, '0')
|
||||
const s = String(d.getSeconds()).padStart(2, '0')
|
||||
return `${Y}-${M}-${D} ${h}:${m}:${s}`
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [latestRes, logsRes]: any[] = await Promise.all([getLatestGoldPrice(), getGoldPriceLogs()])
|
||||
currentPrice.value = latestRes.data?.price || 0
|
||||
newPrice.value = currentPrice.value || 0
|
||||
logs.value = logsRes.data || []
|
||||
} catch {
|
||||
ElMessage.error('获取金价数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!newPrice.value || newPrice.value <= 0) {
|
||||
ElMessage.warning('请输入有效的金价')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将金价更新为 ¥${newPrice.value.toFixed(2)}/克?\n系统将自动重算所有商品规格价格。`,
|
||||
'确认更新金价',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const res: any = await setGoldPrice(newPrice.value)
|
||||
ElMessage.success(res.message || '金价更新成功')
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('更新金价失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-card { border-radius: 12px; border: none; }
|
||||
.page-card :deep(.el-card__header) { border-bottom: 1px solid #f0f0f0; padding: 16px 20px; }
|
||||
.page-card__header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.page-card__title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.gold-price-form { margin-bottom: 20px; }
|
||||
.current-price { margin-bottom: 16px; }
|
||||
.current-price__label { font-size: 14px; color: #666; }
|
||||
.current-price__value { font-size: 24px; font-weight: 700; color: #e4393c; }
|
||||
.current-price__empty { font-size: 14px; color: #999; }
|
||||
.price-input { display: flex; align-items: center; margin-bottom: 8px; }
|
||||
.price-tip { font-size: 12px; color: #999; }
|
||||
</style>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// 手动切换后端地址,部署时改成线上域名即可
|
||||
// const BASE_URL = 'http://localhost:3000'
|
||||
const BASE_URL = 'http://115.190.188.216:2850'
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
// const BASE_URL = 'http://115.190.188.216:2850'
|
||||
|
||||
export { BASE_URL }
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ CREATE TABLE IF NOT EXISTS spec_data (
|
|||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
product_id INT NOT NULL,
|
||||
model_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
barcode VARCHAR(64) DEFAULT NULL,
|
||||
fineness VARCHAR(64) NOT NULL DEFAULT '',
|
||||
main_stone VARCHAR(64) NOT NULL DEFAULT '',
|
||||
sub_stone VARCHAR(100) DEFAULT '',
|
||||
|
|
@ -79,7 +80,8 @@ CREATE TABLE IF NOT EXISTS spec_data (
|
|||
setting_fee DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
total_labor_cost DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
total_price DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||
UNIQUE INDEX idx_barcode (barcode)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 购物车项表
|
||||
|
|
@ -172,3 +174,10 @@ CREATE TABLE IF NOT EXISTS system_configs (
|
|||
config_value TEXT DEFAULT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 金价记录表
|
||||
CREATE TABLE IF NOT EXISTS gold_price_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
price DECIMAL(12,2) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,17 @@ export async function adminCreateProduct(req: Request, res: Response): Promise<v
|
|||
return
|
||||
}
|
||||
|
||||
// 校验款号唯一
|
||||
if (styleNo) {
|
||||
const [dup] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id FROM products WHERE style_no = ?', [styleNo]
|
||||
)
|
||||
if (dup.length > 0) {
|
||||
res.status(400).json({ code: 400, message: `款号 "${styleNo}" 已存在,请使用其他款号` })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await conn.beginTransaction()
|
||||
|
||||
const categoryIdStr = Array.isArray(categoryId) ? JSON.stringify(categoryId) : (categoryId ? JSON.stringify([categoryId]) : null)
|
||||
|
|
@ -135,6 +146,17 @@ export async function adminUpdateProduct(req: Request, res: Response): Promise<v
|
|||
const { id } = req.params
|
||||
const { name, basePrice, styleNo, stock, totalStock, loss, laborCost, categoryId, bannerImages, bannerVideo, detailImages, thumb, sideStone, style, setting, status, detailParams } = req.body
|
||||
|
||||
// 校验款号唯一(排除自身)
|
||||
if (styleNo) {
|
||||
const [dup] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id FROM products WHERE style_no = ? AND id != ?', [styleNo, id]
|
||||
)
|
||||
if (dup.length > 0) {
|
||||
res.status(400).json({ code: 400, message: `款号 "${styleNo}" 已存在,请使用其他款号` })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await conn.beginTransaction()
|
||||
|
||||
const categoryIdStr = Array.isArray(categoryId) ? JSON.stringify(categoryId) : (categoryId ? JSON.stringify([categoryId]) : null)
|
||||
|
|
|
|||
87
server/src/controllers/goldPrice.ts
Normal file
87
server/src/controllers/goldPrice.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Request, Response } from 'express'
|
||||
import pool from '../utils/db'
|
||||
import { RowDataPacket, ResultSetHeader } from 'mysql2'
|
||||
|
||||
// GET /api/admin/gold-price - 获取金价历史记录
|
||||
export async function getGoldPriceLogs(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id, price, created_at FROM gold_price_logs ORDER BY id DESC LIMIT 50'
|
||||
)
|
||||
res.json({ code: 0, data: rows })
|
||||
} catch (err) {
|
||||
console.error('getGoldPriceLogs error:', err)
|
||||
res.status(500).json({ code: 500, message: '获取金价记录失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/admin/gold-price/latest - 获取最新金价
|
||||
export async function getLatestGoldPrice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT price FROM gold_price_logs ORDER BY id DESC LIMIT 1'
|
||||
)
|
||||
const price = rows.length > 0 ? Number(rows[0].price) : 0
|
||||
res.json({ code: 0, data: { price } })
|
||||
} catch (err) {
|
||||
console.error('getLatestGoldPrice error:', err)
|
||||
res.status(500).json({ code: 500, message: '获取最新金价失败' })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/gold-price - 设置新金价并重算所有规格
|
||||
export async function setGoldPrice(req: Request, res: Response): Promise<void> {
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
const { price } = req.body
|
||||
if (!price || Number(price) <= 0) {
|
||||
res.status(400).json({ code: 400, message: '请输入有效的金价' })
|
||||
return
|
||||
}
|
||||
const newPrice = Number(price)
|
||||
|
||||
await conn.beginTransaction()
|
||||
|
||||
// 记录金价
|
||||
await conn.execute<ResultSetHeader>(
|
||||
'INSERT INTO gold_price_logs (price) VALUES (?)', [newPrice]
|
||||
)
|
||||
|
||||
// 重算所有规格数据
|
||||
// 公式: 净重 = 总重 - 主石重*0.2 - 副石重*0.2
|
||||
// 金耗 = 净重 * 损耗
|
||||
// 金值 = 金耗 * 金价
|
||||
// 主石金额 = 主石重 * 主石单价
|
||||
// 副石金额 = 副石重 * 副石单价
|
||||
// 总工费 = 配件 + 加工 + 镶石
|
||||
// 总价 = 金值 + 主石金额 + 副石金额 + 总工费
|
||||
await conn.execute(
|
||||
`UPDATE spec_data SET
|
||||
gold_price = ?,
|
||||
gold_net_weight = GREATEST(gold_total_weight - main_stone_weight * 0.2 - side_stone_weight * 0.2, 0),
|
||||
gold_loss = GREATEST(gold_total_weight - main_stone_weight * 0.2 - side_stone_weight * 0.2, 0) * loss,
|
||||
gold_value = GREATEST(gold_total_weight - main_stone_weight * 0.2 - side_stone_weight * 0.2, 0) * loss * ?,
|
||||
main_stone_amount = main_stone_weight * main_stone_unit_price,
|
||||
side_stone_amount = side_stone_weight * side_stone_unit_price,
|
||||
total_labor_cost = accessory_amount + processing_fee + setting_fee,
|
||||
total_price = GREATEST(gold_total_weight - main_stone_weight * 0.2 - side_stone_weight * 0.2, 0) * loss * ?
|
||||
+ main_stone_weight * main_stone_unit_price
|
||||
+ side_stone_weight * side_stone_unit_price
|
||||
+ accessory_amount + processing_fee + setting_fee`,
|
||||
[newPrice, newPrice, newPrice]
|
||||
)
|
||||
|
||||
// 统计更新了多少条
|
||||
const [countRows] = await conn.execute<RowDataPacket[]>('SELECT COUNT(*) as cnt FROM spec_data')
|
||||
const updated = countRows[0].cnt
|
||||
|
||||
await conn.commit()
|
||||
res.json({ code: 0, message: `金价已更新为 ${newPrice},已重算 ${updated} 条规格数据` })
|
||||
} catch (err) {
|
||||
await conn.rollback()
|
||||
console.error('setGoldPrice error:', err)
|
||||
res.status(500).json({ code: 500, message: '设置金价失败' })
|
||||
} finally {
|
||||
conn.release()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { RowDataPacket, ResultSetHeader } from 'mysql2'
|
|||
|
||||
// CSV column headers matching spec_data table fields
|
||||
const CSV_HEADERS = [
|
||||
'model_name', 'fineness', 'main_stone', 'sub_stone', 'ring_size',
|
||||
'model_name', 'barcode', 'fineness', 'main_stone', 'sub_stone', 'ring_size',
|
||||
'gold_total_weight', 'gold_net_weight', 'loss', 'gold_loss',
|
||||
'gold_price', 'gold_value',
|
||||
'main_stone_count', 'main_stone_weight', 'main_stone_unit_price', 'main_stone_amount',
|
||||
|
|
@ -14,7 +14,7 @@ const CSV_HEADERS = [
|
|||
|
||||
// 中文表头映射(导出用)
|
||||
const CSV_HEADERS_CN: Record<string, string> = {
|
||||
model_name: '款号', fineness: '成色', main_stone: '主石', sub_stone: '副石', ring_size: '手寸',
|
||||
model_name: '规格名称', barcode: '条型码', fineness: '成色', main_stone: '主石', sub_stone: '副石', ring_size: '手寸',
|
||||
gold_total_weight: '金料总重', gold_net_weight: '金料净重', loss: '损耗', gold_loss: '金损',
|
||||
gold_price: '金价', gold_value: '金料价值',
|
||||
main_stone_count: '主石数', main_stone_weight: '主石重', main_stone_unit_price: '主石单价', main_stone_amount: '主石金额',
|
||||
|
|
@ -102,7 +102,7 @@ export async function adminGetSpecData(req: Request, res: Response): Promise<voi
|
|||
try {
|
||||
const { id } = req.params
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, product_id AS productId, model_name AS modelName, fineness, main_stone AS mainStone,
|
||||
`SELECT id, product_id AS productId, model_name AS modelName, barcode, fineness, main_stone AS mainStone,
|
||||
sub_stone AS subStone, ring_size AS ringSize, gold_total_weight AS goldTotalWeight, gold_net_weight AS goldNetWeight,
|
||||
loss, gold_loss AS goldLoss, gold_price AS goldPrice, gold_value AS goldValue,
|
||||
main_stone_count AS mainStoneCount, main_stone_weight AS mainStoneWeight,
|
||||
|
|
@ -126,14 +126,18 @@ export async function adminCreateSpecData(req: Request, res: Response): Promise<
|
|||
try {
|
||||
const { id } = req.params
|
||||
const d = req.body
|
||||
if (d.barcode) {
|
||||
const [dup] = await pool.execute<RowDataPacket[]>('SELECT id FROM spec_data WHERE barcode = ?', [d.barcode])
|
||||
if (dup.length > 0) { res.status(400).json({ code: 400, message: `条型码 "${d.barcode}" 已存在` }); return }
|
||||
}
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`INSERT INTO spec_data (product_id, model_name, fineness, main_stone, sub_stone, ring_size,
|
||||
`INSERT INTO spec_data (product_id, model_name, barcode, fineness, main_stone, sub_stone, ring_size,
|
||||
gold_total_weight, gold_net_weight, loss, gold_loss, gold_price, gold_value,
|
||||
main_stone_count, main_stone_weight, main_stone_unit_price, main_stone_amount,
|
||||
side_stone_count, side_stone_weight, side_stone_unit_price, side_stone_amount,
|
||||
accessory_amount, processing_fee, setting_fee, total_labor_cost, total_price)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id, d.modelName||'', d.fineness||'', d.mainStone||'', d.subStone||'', d.ringSize||'',
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id, d.modelName||'', d.barcode||null, d.fineness||'', d.mainStone||'', d.subStone||'', d.ringSize||'',
|
||||
d.goldTotalWeight||0, d.goldNetWeight||0, d.loss||0, d.goldLoss||0,
|
||||
d.goldPrice||0, d.goldValue||0,
|
||||
d.mainStoneCount||0, d.mainStoneWeight||0, d.mainStoneUnitPrice||0, d.mainStoneAmount||0,
|
||||
|
|
@ -164,14 +168,18 @@ export async function adminUpdateSpecData(req: Request, res: Response): Promise<
|
|||
try {
|
||||
const { specId } = req.params
|
||||
const d = req.body
|
||||
if (d.barcode) {
|
||||
const [dup] = await pool.execute<RowDataPacket[]>('SELECT id FROM spec_data WHERE barcode = ? AND id != ?', [d.barcode, specId])
|
||||
if (dup.length > 0) { res.status(400).json({ code: 400, message: `条型码 "${d.barcode}" 已存在` }); return }
|
||||
}
|
||||
await pool.execute(
|
||||
`UPDATE spec_data SET model_name=?, fineness=?, main_stone=?, sub_stone=?, ring_size=?,
|
||||
`UPDATE spec_data SET model_name=?, barcode=?, fineness=?, main_stone=?, sub_stone=?, ring_size=?,
|
||||
gold_total_weight=?, gold_net_weight=?, loss=?, gold_loss=?, gold_price=?, gold_value=?,
|
||||
main_stone_count=?, main_stone_weight=?, main_stone_unit_price=?, main_stone_amount=?,
|
||||
side_stone_count=?, side_stone_weight=?, side_stone_unit_price=?, side_stone_amount=?,
|
||||
accessory_amount=?, processing_fee=?, setting_fee=?, total_labor_cost=?, total_price=?
|
||||
WHERE id=?`,
|
||||
[d.modelName||'', d.fineness||'', d.mainStone||'', d.subStone||'', d.ringSize||'',
|
||||
[d.modelName||'', d.barcode||null, d.fineness||'', d.mainStone||'', d.subStone||'', d.ringSize||'',
|
||||
d.goldTotalWeight||0, d.goldNetWeight||0, d.loss||0, d.goldLoss||0,
|
||||
d.goldPrice||0, d.goldValue||0,
|
||||
d.mainStoneCount||0, d.mainStoneWeight||0, d.mainStoneUnitPrice||0, d.mainStoneAmount||0,
|
||||
|
|
@ -254,7 +262,7 @@ export async function importSpecData(req: Request, res: Response): Promise<void>
|
|||
// Clear existing spec data for this product
|
||||
await conn.execute('DELETE FROM spec_data WHERE product_id = ?', [id])
|
||||
|
||||
const numericFields = CSV_HEADERS.filter((h) => h !== 'model_name' && h !== 'fineness' && h !== 'main_stone' && h !== 'sub_stone' && h !== 'ring_size')
|
||||
const numericFields = CSV_HEADERS.filter((h) => h !== 'model_name' && h !== 'barcode' && h !== 'fineness' && h !== 'main_stone' && h !== 'sub_stone' && h !== 'ring_size')
|
||||
const errors: string[] = []
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
} from '../controllers/adminCategory'
|
||||
import { adminGetConfigs, adminUpdateConfig } from '../controllers/config'
|
||||
import { adminGetUsers } from '../controllers/adminUser'
|
||||
import { getGoldPriceLogs, getLatestGoldPrice, setGoldPrice } from '../controllers/goldPrice'
|
||||
|
||||
const csvUpload = multer({ storage: multer.memoryStorage() })
|
||||
|
||||
|
|
@ -91,3 +92,8 @@ adminRoutes.put('/configs/:key', adminUpdateConfig)
|
|||
|
||||
// User management
|
||||
adminRoutes.get('/users', adminGetUsers)
|
||||
|
||||
// Gold price management
|
||||
adminRoutes.get('/gold-price', getGoldPriceLogs)
|
||||
adminRoutes.get('/gold-price/latest', getLatestGoldPrice)
|
||||
adminRoutes.post('/gold-price', setGoldPrice)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user