管理后台
This commit is contained in:
parent
3fb5192b4b
commit
53ce69a1f9
|
|
@ -15,6 +15,7 @@ server {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
client_max_body_size 50m;
|
||||||
}
|
}
|
||||||
|
|
||||||
# 上传文件代理
|
# 上传文件代理
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,9 @@
|
||||||
<el-table-column prop="id" label="ID" width="70" />
|
<el-table-column prop="id" label="ID" width="70" />
|
||||||
<el-table-column prop="name" label="商品名称" min-width="150" />
|
<el-table-column prop="name" label="商品名称" min-width="150" />
|
||||||
<el-table-column prop="style_no" label="款号" width="120" />
|
<el-table-column prop="style_no" label="款号" width="120" />
|
||||||
<el-table-column prop="stock" label="当前库存" width="100" />
|
<el-table-column prop="stock" label="库存" width="100">
|
||||||
<el-table-column prop="total_stock" label="总库存" width="100" />
|
|
||||||
<el-table-column label="库存比例" width="140">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-progress
|
<el-tag :type="row.stock <= 5 ? 'danger' : 'warning'" size="small">{{ row.stock }}</el-tag>
|
||||||
:percentage="row.total_stock > 0 ? Math.round((row.stock / row.total_stock) * 100) : 0"
|
|
||||||
:color="row.stock / row.total_stock < 0.2 ? '#f56c6c' : '#e6a23c'"
|
|
||||||
:stroke-width="16"
|
|
||||||
:text-inside="true"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100">
|
<el-table-column label="操作" width="100">
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,7 @@
|
||||||
<el-input-number v-model="form.stock" :min="0" controls-position="right" style="width:100%" />
|
<el-input-number v-model="form.stock" :min="0" controls-position="right" style="width:100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<!-- 总库存已隐藏 -->
|
||||||
<el-form-item label="总库存" prop="totalStock">
|
|
||||||
<el-input-number v-model="form.totalStock" :min="0" controls-position="right" style="width:100%" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="32">
|
<el-row :gutter="32">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
|
|
@ -318,7 +314,7 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<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.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.goldPrice" :precision="2" disabled controls-position="right" style="width:100%" /><div style="font-size:11px;color:#999;margin-top:2px">读取自「金价配置」</div></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-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>
|
</el-row>
|
||||||
<div class="dialog-section">主石信息</div>
|
<div class="dialog-section">主石信息</div>
|
||||||
|
|
@ -431,7 +427,7 @@ const rules = {
|
||||||
basePrice: [{ required: true, message: '请输入基础价格', trigger: 'blur' }],
|
basePrice: [{ required: true, message: '请输入基础价格', trigger: 'blur' }],
|
||||||
styleNo: [{ required: true, message: '请输入款号', trigger: 'blur' }],
|
styleNo: [{ required: true, message: '请输入款号', trigger: 'blur' }],
|
||||||
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
|
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
|
||||||
totalStock: [{ required: true, message: '请输入总库存', trigger: 'blur' }],
|
|
||||||
loss: [{ required: true, message: '请输入损耗', trigger: 'blur' }],
|
loss: [{ required: true, message: '请输入损耗', trigger: 'blur' }],
|
||||||
laborCost: [{ required: true, message: '请输入工费', trigger: 'blur' }],
|
laborCost: [{ required: true, message: '请输入工费', trigger: 'blur' }],
|
||||||
categoryId: [{ required: true, validator: (_r: any, _v: any, cb: any) => form.categoryId.length > 0 ? cb() : cb(new Error('请选择分类')), trigger: 'change' }],
|
categoryId: [{ required: true, validator: (_r: any, _v: any, cb: any) => form.categoryId.length > 0 ? cb() : cb(new Error('请选择分类')), trigger: 'change' }],
|
||||||
|
|
@ -681,8 +677,9 @@ async function handleSubmit() {
|
||||||
router.replace(`/products/${res.data.id}/edit`)
|
router.replace(`/products/${res.data.id}/edit`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (e: any) {
|
||||||
ElMessage.error('保存失败')
|
const msg = e?.response?.data?.message || '保存失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import pool from '../utils/db'
|
||||||
import { RowDataPacket } from 'mysql2'
|
import { RowDataPacket } from 'mysql2'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter products where stock/totalStock < 0.1, sorted by stock ascending.
|
* Filter products where stock < 20, sorted by stock ascending.
|
||||||
* Pure logic extracted for testability.
|
* Pure logic extracted for testability.
|
||||||
*/
|
*/
|
||||||
export function filterStockAlerts(
|
export function filterStockAlerts(
|
||||||
products: { id: number; name: string; style_no: string; stock: number; total_stock: number }[]
|
products: { id: number; name: string; style_no: string; stock: number }[]
|
||||||
): typeof products {
|
): typeof products {
|
||||||
return products
|
return products
|
||||||
.filter((p) => p.total_stock > 0 && p.stock / p.total_stock < 0.1)
|
.filter((p) => p.stock < 20)
|
||||||
.sort((a, b) => a.stock - b.stock)
|
.sort((a, b) => a.stock - b.stock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,9 +18,9 @@ export function filterStockAlerts(
|
||||||
export async function getStockAlerts(_req: Request, res: Response): Promise<void> {
|
export async function getStockAlerts(_req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||||
`SELECT id, name, style_no, stock, total_stock
|
`SELECT id, name, style_no, stock
|
||||||
FROM products
|
FROM products
|
||||||
WHERE total_stock > 0 AND stock / total_stock < 0.1
|
WHERE stock < 20
|
||||||
ORDER BY stock ASC`
|
ORDER BY stock ASC`
|
||||||
)
|
)
|
||||||
res.json({ code: 0, data: rows })
|
res.json({ code: 0, data: rows })
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,26 @@ const productArb = fc.record({
|
||||||
name: fc.string({ minLength: 1, maxLength: 20 }),
|
name: fc.string({ minLength: 1, maxLength: 20 }),
|
||||||
style_no: fc.string({ minLength: 0, maxLength: 10 }),
|
style_no: fc.string({ minLength: 0, maxLength: 10 }),
|
||||||
stock: fc.integer({ min: 0, max: 10000 }),
|
stock: fc.integer({ min: 0, max: 10000 }),
|
||||||
total_stock: fc.integer({ min: 0, max: 10000 }),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Feature: jewelry-mall, Property 7: 库存预警阈值判断
|
// Feature: jewelry-mall, Property 7: 库存预警阈值判断
|
||||||
// **Validates: Requirements 8.4**
|
// **Validates: Requirements 8.4**
|
||||||
describe('Property 7: 库存预警阈值判断', () => {
|
describe('Property 7: 库存预警阈值判断', () => {
|
||||||
it('预警列表应恰好包含所有低库存商品且按库存从少到多排序', () => {
|
it('预警列表应恰好包含所有库存<20的商品且按库存从少到多排序', () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(
|
fc.property(
|
||||||
fc.array(productArb, { minLength: 0, maxLength: 20 }),
|
fc.array(productArb, { minLength: 0, maxLength: 20 }),
|
||||||
(products) => {
|
(products) => {
|
||||||
const alerts = filterStockAlerts(products)
|
const alerts = filterStockAlerts(products)
|
||||||
|
|
||||||
// Every alert should satisfy the threshold condition
|
// Every alert should satisfy the threshold condition (stock < 20)
|
||||||
for (const a of alerts) {
|
for (const a of alerts) {
|
||||||
expect(a.total_stock).toBeGreaterThan(0)
|
expect(a.stock).toBeLessThan(20)
|
||||||
expect(a.stock / a.total_stock).toBeLessThan(0.1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every product meeting the condition should be in alerts
|
// Every product meeting the condition should be in alerts
|
||||||
const expectedIds = products
|
const expectedIds = products
|
||||||
.filter((p) => p.total_stock > 0 && p.stock / p.total_stock < 0.1)
|
.filter((p) => p.stock < 20)
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
const alertIds = alerts.map((a) => a.id)
|
const alertIds = alerts.map((a) => a.id)
|
||||||
expect(alertIds.sort()).toEqual(expectedIds.sort())
|
expect(alertIds.sort()).toEqual(expectedIds.sort())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user