317 lines
8.3 KiB
JavaScript
317 lines
8.3 KiB
JavaScript
const { query } = require('../config/database')
|
|
const { toRelativeUrl, processGoodsImages } = require('../utils/image-url')
|
|
const { paginate } = require('../utils/pagination')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
|
|
function parseImages(images) {
|
|
if (!images) return []
|
|
try {
|
|
const parsed = typeof images === 'string' ? JSON.parse(images) : images
|
|
return Array.isArray(parsed) ? parsed : []
|
|
} catch { return [] }
|
|
}
|
|
|
|
function deleteImageFiles(urls) {
|
|
for (const url of urls) {
|
|
const filePath = path.join(__dirname, '..', 'public', url.replace(/^\//, ''))
|
|
fs.unlink(filePath, () => {})
|
|
}
|
|
}
|
|
|
|
async function getGoods(ctx) {
|
|
let sql = 'SELECT * FROM goods WHERE 1=1'
|
|
const params = []
|
|
|
|
if (ctx.query.hot === '1') {
|
|
sql += ' AND is_hot = 1'
|
|
}
|
|
|
|
if (ctx.query.isNew === '1') {
|
|
sql += ' AND is_new = 1'
|
|
}
|
|
|
|
if (ctx.query.category_id) {
|
|
sql += ' AND category_id = ?'
|
|
params.push(parseInt(ctx.query.category_id))
|
|
}
|
|
|
|
if (ctx.query.keyword) {
|
|
const kw = String(ctx.query.keyword).trim().slice(0, 50)
|
|
if (kw) {
|
|
const escaped = kw.replace(/[\\%_]/g, c => '\\' + c)
|
|
sql += ' AND name LIKE ?'
|
|
params.push(`%${escaped}%`)
|
|
}
|
|
}
|
|
|
|
if (ctx.query.inStock === '1') {
|
|
sql += ' AND stock > 0'
|
|
}
|
|
|
|
const sortField = ctx.query.sortBy || 'id'
|
|
const sortOrder = ctx.query.sortOrder === 'asc' ? 'ASC' : 'DESC'
|
|
const validSortFields = ['id', 'price', 'sales', 'stock', 'created_at']
|
|
|
|
if (validSortFields.includes(sortField)) {
|
|
sql += ` ORDER BY ${sortField} ${sortOrder}`
|
|
} else {
|
|
sql += ' ORDER BY id DESC'
|
|
}
|
|
|
|
if (ctx.query.limit && !ctx.query.page) {
|
|
const limit = Math.min(10000, Math.max(1, parseInt(ctx.query.limit) || 20))
|
|
sql += ' LIMIT ?'
|
|
params.push(limit)
|
|
const goods = await query(sql, params)
|
|
ctx.body = { code: 200, data: processGoodsImages(goods) }
|
|
return
|
|
}
|
|
|
|
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
|
|
if (result.data) result.data = processGoodsImages(result.data)
|
|
|
|
ctx.body = {
|
|
code: 200,
|
|
...result
|
|
}
|
|
}
|
|
|
|
async function getGoodsById(ctx) {
|
|
const goodsId = parseInt(ctx.params.id)
|
|
const goods = await query('SELECT * FROM goods WHERE id = ?', [goodsId])
|
|
|
|
if (goods.length > 0) {
|
|
ctx.body = {
|
|
code: 200,
|
|
data: processGoodsImages(goods)[0]
|
|
}
|
|
} else {
|
|
ctx.body = {
|
|
code: 404,
|
|
message: '商品不存在'
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createGoods(ctx) {
|
|
const { name, price, unit, categoryId, images, stock, pricingType, isHot, isNew, remark, goodsNo, barcode } = ctx.request.body
|
|
|
|
if (!name || !price || !unit) {
|
|
ctx.body = {
|
|
code: 400,
|
|
message: '缺少必填字段'
|
|
}
|
|
return
|
|
}
|
|
|
|
if (stock !== undefined && stock !== 0) {
|
|
ctx.body = {
|
|
code: 400,
|
|
message: '新建商品请保持库存为 0,通过「入库/采购」或「库存调整」接口补充'
|
|
}
|
|
return
|
|
}
|
|
|
|
// 将图片URL转换为相对路径存储
|
|
const relativeImages = (images || []).map(img => toRelativeUrl(img))
|
|
|
|
const sql = `INSERT INTO goods
|
|
(name, price, cost_price, unit, category_id, images, stock, pricing_type, is_hot, is_new, remark, goods_no, barcode)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
const params = [
|
|
name,
|
|
parseFloat(price),
|
|
parseFloat(ctx.request.body.costPrice || 0),
|
|
unit,
|
|
categoryId || null,
|
|
JSON.stringify(relativeImages),
|
|
parseInt(stock) || 0,
|
|
parseInt(pricingType) || 1,
|
|
parseInt(isHot) || 0,
|
|
parseInt(isNew) || 0,
|
|
remark || '',
|
|
goodsNo || '',
|
|
barcode || ''
|
|
]
|
|
|
|
try {
|
|
const result = await query(sql, params)
|
|
ctx.body = {
|
|
code: 200,
|
|
message: '添加成功',
|
|
data: { id: result.insertId }
|
|
}
|
|
} catch (error) {
|
|
console.error('添加商品失败:', error)
|
|
ctx.body = {
|
|
code: 500,
|
|
message: '添加失败'
|
|
}
|
|
}
|
|
}
|
|
|
|
const GOODS_UPDATEABLE_FIELDS = [
|
|
'name', 'price', 'originalPrice', 'unit', 'categoryId',
|
|
'images', 'pricingType', 'isHot', 'isNew', 'description', 'goodsNo', 'barcode', 'remark'
|
|
]
|
|
|
|
async function updateGoods(ctx) {
|
|
const goodsId = parseInt(ctx.params.id)
|
|
const body = ctx.request.body
|
|
|
|
if ('stock' in body) {
|
|
ctx.body = { code: 400, message: '请通过「库存调整」接口修改库存,不能直接编辑' }
|
|
return
|
|
}
|
|
|
|
const filtered = {}
|
|
for (const key of GOODS_UPDATEABLE_FIELDS) {
|
|
if (key in body) {
|
|
filtered[key] = body[key]
|
|
}
|
|
}
|
|
|
|
const { name, price, unit, pricingType, isHot, isNew, description } = filtered
|
|
|
|
if (!name || !price || !unit) {
|
|
ctx.body = {
|
|
code: 400,
|
|
message: '缺少必填字段'
|
|
}
|
|
return
|
|
}
|
|
|
|
const setFields = []
|
|
const params = []
|
|
|
|
setFields.push('name = ?', 'price = ?', 'original_price = ?', 'unit = ?')
|
|
params.push(name, parseFloat(price), parseFloat(filtered.originalPrice || 0), unit)
|
|
|
|
setFields.push('category_id = ?')
|
|
params.push(filtered.categoryId || null)
|
|
|
|
if (filtered.images !== undefined) {
|
|
const relativeImages = (filtered.images || []).map(img => toRelativeUrl(img))
|
|
setFields.push('images = ?')
|
|
params.push(JSON.stringify(relativeImages))
|
|
|
|
const existing = await query('SELECT images FROM goods WHERE id = ?', [goodsId])
|
|
if (existing.length > 0) {
|
|
const oldImages = parseImages(existing[0].images)
|
|
const oldFiles = oldImages.filter(u => u.startsWith('/uploads/') && !relativeImages.includes(u))
|
|
deleteImageFiles(oldFiles)
|
|
}
|
|
}
|
|
|
|
if (filtered.goodsNo !== undefined) {
|
|
setFields.push('goods_no = ?')
|
|
params.push(filtered.goodsNo || '')
|
|
}
|
|
if (filtered.barcode !== undefined) {
|
|
setFields.push('barcode = ?')
|
|
params.push(filtered.barcode || '')
|
|
}
|
|
if (filtered.remark !== undefined) {
|
|
setFields.push('remark = ?')
|
|
params.push(filtered.remark || '')
|
|
}
|
|
|
|
setFields.push('pricing_type = ?', 'is_hot = ?', 'is_new = ?', 'description = ?')
|
|
params.push(parseInt(pricingType) || 1, parseInt(isHot) || 0, parseInt(isNew) || 0, description || '')
|
|
|
|
params.push(goodsId)
|
|
|
|
try {
|
|
const result = await query(`UPDATE goods SET ${setFields.join(', ')} WHERE id = ?`, params)
|
|
if (result.affectedRows > 0) {
|
|
ctx.body = { code: 200, message: '更新成功' }
|
|
} else {
|
|
ctx.body = { code: 404, message: '商品不存在' }
|
|
}
|
|
} catch (error) {
|
|
console.error('更新商品失败:', error)
|
|
ctx.body = { code: 500, message: '更新失败' }
|
|
}
|
|
}
|
|
|
|
async function deleteGoods(ctx) {
|
|
const goodsId = parseInt(ctx.params.id)
|
|
|
|
try {
|
|
const existing = await query('SELECT images FROM goods WHERE id = ?', [goodsId])
|
|
const result = await query('DELETE FROM goods WHERE id = ?', [goodsId])
|
|
if (result.affectedRows > 0) {
|
|
if (existing.length > 0) {
|
|
const oldImages = parseImages(existing[0].images)
|
|
deleteImageFiles(oldImages.filter(u => u.startsWith('/uploads/')))
|
|
}
|
|
ctx.body = {
|
|
code: 200,
|
|
message: '删除成功'
|
|
}
|
|
} else {
|
|
ctx.body = {
|
|
code: 404,
|
|
message: '商品不存在'
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('删除商品失败:', error)
|
|
ctx.body = {
|
|
code: 500,
|
|
message: '删除失败'
|
|
}
|
|
}
|
|
}
|
|
|
|
// 批量更新商品
|
|
async function batchUpdate(ctx) {
|
|
const { ids, isOnSale } = ctx.request.body
|
|
|
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
ctx.body = { code: 400, message: '请选择要操作的商品' }
|
|
return
|
|
}
|
|
|
|
// 允许批量更新的字段
|
|
const setFields = []
|
|
const params = []
|
|
|
|
if (isOnSale !== undefined) {
|
|
setFields.push('is_on_sale = ?')
|
|
params.push(parseInt(isOnSale))
|
|
}
|
|
|
|
if (setFields.length === 0) {
|
|
ctx.body = { code: 400, message: '缺少更新字段' }
|
|
return
|
|
}
|
|
|
|
// 构建 IN 占位符
|
|
const placeholders = ids.map(() => '?').join(', ')
|
|
params.push(...ids)
|
|
|
|
try {
|
|
const sql = `UPDATE goods SET ${setFields.join(', ')} WHERE id IN (${placeholders})`
|
|
const result = await query(sql, params)
|
|
ctx.body = {
|
|
code: 200,
|
|
message: `成功更新${result.affectedRows}个商品`,
|
|
data: { affectedRows: result.affectedRows }
|
|
}
|
|
} catch (error) {
|
|
console.error('批量更新商品失败:', error)
|
|
ctx.body = { code: 500, message: '批量更新失败' }
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getGoods,
|
|
getGoodsById,
|
|
createGoods,
|
|
updateGoods,
|
|
deleteGoods,
|
|
batchUpdate
|
|
} |