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) { sql += ' LIMIT ?' params.push(parseInt(ctx.query.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 }