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 @@
金料信息
-
+
-
+
-
+
主石信息
-
+
副石信息
-
+
工费信息
-
+
-
+
@@ -361,11 +365,12 @@
+
+
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)