188 lines
4.8 KiB
JavaScript
188 lines
4.8 KiB
JavaScript
const { query, transaction } = require('../config/database')
|
|
const { paginate } = require('../utils/pagination')
|
|
|
|
async function ensureStockLogTable() {
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS stock_logs (
|
|
id INT NOT NULL AUTO_INCREMENT,
|
|
goods_id INT NOT NULL,
|
|
change_type VARCHAR(20) NOT NULL COMMENT 'inbound/adjust/purchase/sale',
|
|
delta INT NOT NULL,
|
|
quantity_after INT NOT NULL,
|
|
operator_id INT,
|
|
remark VARCHAR(255),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
KEY goods_id (goods_id),
|
|
KEY created_at (created_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表'
|
|
`)
|
|
}
|
|
|
|
// 获取库存列表
|
|
async function getStockList(ctx) {
|
|
const keyword = ctx.query.keyword
|
|
const threshold = parseInt(ctx.query.threshold) || 0
|
|
|
|
let sql = `
|
|
SELECT
|
|
s.id,
|
|
s.goods_id,
|
|
g.name as goods_name,
|
|
g.images as goods_image,
|
|
g.unit as goods_unit,
|
|
s.quantity,
|
|
s.warehouse
|
|
FROM stock s
|
|
LEFT JOIN goods g ON s.goods_id = g.id
|
|
WHERE 1=1
|
|
`
|
|
const params = []
|
|
|
|
if (keyword) {
|
|
sql += ' AND (g.name LIKE ? OR g.barcode LIKE ?)'
|
|
params.push(`%${keyword}%`, `%${keyword}%`)
|
|
}
|
|
|
|
if (threshold > 0) {
|
|
sql += ' AND s.quantity <= ?'
|
|
params.push(threshold)
|
|
}
|
|
|
|
sql += ' ORDER BY s.quantity ASC'
|
|
|
|
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
|
|
|
|
ctx.body = {
|
|
code: 200,
|
|
...result
|
|
}
|
|
}
|
|
|
|
// 调整库存
|
|
async function adjustStock(ctx) {
|
|
const operator = ctx.state.user
|
|
if (!operator) {
|
|
ctx.status = 401
|
|
ctx.body = { code: 401, message: '未登录' }
|
|
return
|
|
}
|
|
const goodsId = parseInt(ctx.params.id)
|
|
const { quantity, type, remark } = ctx.request.body || {}
|
|
|
|
const qty = parseInt(quantity)
|
|
if (!qty || qty <= 0 || qty > 100000) {
|
|
ctx.body = { code: 400, message: '请输入 1-100000 之间的整数' }
|
|
return
|
|
}
|
|
if (type !== 'add' && type !== 'sub') {
|
|
ctx.body = { code: 400, message: 'type 必须是 add 或 sub' }
|
|
return
|
|
}
|
|
|
|
await ensureStockLogTable()
|
|
|
|
try {
|
|
const newQuantity = await transaction(async (conn) => {
|
|
const [rows] = await conn.execute('SELECT * FROM stock WHERE goods_id = ? FOR UPDATE', [goodsId])
|
|
let current = rows[0]
|
|
if (!current) {
|
|
if (type === 'sub') {
|
|
throw new Error('库存不足,无法减少')
|
|
}
|
|
await conn.execute(
|
|
'INSERT INTO stock (goods_id, quantity, warehouse) VALUES (?, ?, ?)',
|
|
[goodsId, qty, '默认仓库']
|
|
)
|
|
return qty
|
|
}
|
|
const delta = type === 'add' ? qty : -qty
|
|
const next = current.quantity + delta
|
|
if (next < 0) throw new Error('库存不足,无法减少')
|
|
|
|
await conn.execute('UPDATE stock SET quantity = ? WHERE goods_id = ?', [next, goodsId])
|
|
await conn.execute(
|
|
'UPDATE goods SET stock = ? WHERE id = ?',
|
|
[Math.max(0, next), goodsId]
|
|
)
|
|
await conn.execute(
|
|
'INSERT INTO stock_logs (goods_id, change_type, delta, quantity_after, operator_id, remark) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[goodsId, 'adjust', delta, next, operator.id, remark || '库存调整']
|
|
)
|
|
return next
|
|
})
|
|
|
|
const [updated] = await query('SELECT * FROM stock WHERE goods_id = ?', [goodsId])
|
|
ctx.body = { code: 200, data: updated, message: '库存调整成功', newQuantity }
|
|
} catch (error) {
|
|
ctx.status = 400
|
|
ctx.body = { code: 400, message: error.message || '调整失败' }
|
|
}
|
|
}
|
|
|
|
// 获取单个商品的库存
|
|
async function getStockByGoodsId(ctx) {
|
|
const goodsId = ctx.params.id
|
|
|
|
const stockRecords = await query('SELECT * FROM stock WHERE goods_id = ?', [goodsId])
|
|
|
|
if (stockRecords.length > 0) {
|
|
ctx.body = {
|
|
code: 200,
|
|
data: stockRecords[0]
|
|
}
|
|
} else {
|
|
ctx.body = {
|
|
code: 200,
|
|
data: { goods_id: goodsId, quantity: 0, warehouse: '默认仓库' }
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取库存变动日志
|
|
async function getStockLogs(ctx) {
|
|
const goodsId = ctx.query.goods_id
|
|
const page = parseInt(ctx.query.page) || 1
|
|
const pageSize = parseInt(ctx.query.pageSize) || 20
|
|
|
|
await ensureStockLogTable()
|
|
|
|
let sql = `
|
|
SELECT
|
|
sl.id,
|
|
sl.goods_id,
|
|
g.name as goods_name,
|
|
sl.change_type,
|
|
sl.delta,
|
|
sl.quantity_after,
|
|
sl.operator_id,
|
|
sl.remark,
|
|
sl.created_at
|
|
FROM stock_logs sl
|
|
LEFT JOIN goods g ON sl.goods_id = g.id
|
|
WHERE 1=1
|
|
`
|
|
const params = []
|
|
|
|
if (goodsId) {
|
|
sql += ' AND sl.goods_id = ?'
|
|
params.push(goodsId)
|
|
}
|
|
|
|
sql += ' ORDER BY sl.created_at DESC'
|
|
|
|
const result = await paginate(query, sql, params, page, pageSize)
|
|
|
|
ctx.body = {
|
|
code: 200,
|
|
...result
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getStockList,
|
|
adjustStock,
|
|
getStockByGoodsId,
|
|
getStockLogs
|
|
}
|