diff --git a/admin/src/api/goldPrice.ts b/admin/src/api/goldPrice.ts new file mode 100644 index 00000000..84282049 --- /dev/null +++ b/admin/src/api/goldPrice.ts @@ -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 }) +} diff --git a/admin/src/layout/AdminLayout.vue b/admin/src/layout/AdminLayout.vue index 06e616e0..e87587fd 100644 --- a/admin/src/layout/AdminLayout.vue +++ b/admin/src/layout/AdminLayout.vue @@ -39,6 +39,10 @@ + + + + @@ -101,6 +105,7 @@ const titleMap: Record = { '/orders': '订单管理', '/molds': '版房管理', '/users': '用户管理', + '/gold-price': '金价配置', '/settings': '系统设置', } diff --git a/admin/src/router/index.ts b/admin/src/router/index.ts index 8a9f217d..fc59088d 100644 --- a/admin/src/router/index.ts +++ b/admin/src/router/index.ts @@ -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'), + }, ], }, ], diff --git a/admin/src/views/product/ProductForm.vue b/admin/src/views/product/ProductForm.vue index 2a2283f2..db60229b 100644 --- a/admin/src/views/product/ProductForm.vue +++ b/admin/src/views/product/ProductForm.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' }"> - + + @@ -296,7 +297,10 @@
基本信息
- + + + + @@ -309,37 +313,37 @@
金料信息
- + - + - +
主石信息
- +
副石信息
- +
工费信息
- + - +
+ + diff --git a/miniprogram/utils/request.ts b/miniprogram/utils/request.ts index e3dcde00..4f50c27a 100644 --- a/miniprogram/utils/request.ts +++ b/miniprogram/utils/request.ts @@ -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 } diff --git a/server/migrations/001_init.sql b/server/migrations/001_init.sql index 94f4c470..ea0791ab 100644 --- a/server/migrations/001_init.sql +++ b/server/migrations/001_init.sql @@ -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; diff --git a/server/src/controllers/adminProduct.ts b/server/src/controllers/adminProduct.ts index 9add14a6..80c5dba5 100644 --- a/server/src/controllers/adminProduct.ts +++ b/server/src/controllers/adminProduct.ts @@ -78,6 +78,17 @@ export async function adminCreateProduct(req: Request, res: Response): Promise( + '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( + '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) diff --git a/server/src/controllers/goldPrice.ts b/server/src/controllers/goldPrice.ts new file mode 100644 index 00000000..b802c341 --- /dev/null +++ b/server/src/controllers/goldPrice.ts @@ -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 { + try { + const [rows] = await pool.execute( + '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 { + try { + const [rows] = await pool.execute( + '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 { + 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( + '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('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() + } +} diff --git a/server/src/controllers/specDataIO.ts b/server/src/controllers/specDataIO.ts index 398841e8..900005d6 100644 --- a/server/src/controllers/specDataIO.ts +++ b/server/src/controllers/specDataIO.ts @@ -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 = { - 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( - `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('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( - `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('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 // 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++) { diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 98bead75..05a655c4 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -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)