管理后台

This commit is contained in:
18631081161 2026-03-18 22:23:54 +08:00
parent ea7994e017
commit 3fb5192b4b
11 changed files with 342 additions and 27 deletions

View 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 })
}

View File

@ -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': '系统设置',
}

View File

@ -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'),
},
],
},
],

View File

@ -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()
})

View 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>

View File

@ -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 }

View File

@ -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;

View File

@ -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)

View 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()
}
}

View File

@ -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++) {

View File

@ -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)