更新完善页面

This commit is contained in:
董海洋
2026-06-03 14:15:55 +08:00
parent 4b7ae9c933
commit 1675662537
57 changed files with 7625 additions and 883 deletions
+66 -15
View File
@@ -1,46 +1,70 @@
const { query } = require('../config/database')
function currentUserId(ctx) {
return ctx.state.user ? ctx.state.user.id : null
}
function ensureOwner(ctx, row, action) {
if (!row) return true
if (ctx.state.user.role === 2) return true
return row.user_id === currentUserId(ctx)
}
async function getAddresses(ctx) {
const userId = ctx.query.user_id
const userId = currentUserId(ctx)
if (!userId) {
ctx.body = { code: 400, message: '缺少 user_id 参数' }
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const rows = await query(
'SELECT * FROM addresses WHERE user_id = ? ORDER BY is_default DESC, created_at DESC',
[userId]
)
ctx.body = { code: 200, data: rows }
}
async function getAddressById(ctx) {
const id = ctx.params.id
const rows = await query('SELECT * FROM addresses WHERE id = ?', [id])
if (rows.length > 0) {
ctx.body = { code: 200, data: rows[0] }
} else {
if (rows.length === 0) {
ctx.status = 404
ctx.body = { code: 404, message: '地址不存在' }
return
}
if (!ensureOwner(ctx, rows[0])) {
ctx.status = 403
ctx.body = { code: 403, message: '无权查看该地址' }
return
}
ctx.body = { code: 200, data: rows[0] }
}
async function createAddress(ctx) {
const { user_id, name, phone, region, detail, is_default = 0 } = ctx.request.body
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { name, phone, region, detail, is_default = 0 } = ctx.request.body || {}
if (!user_id || !name || !phone || !detail) {
if (!name || !phone || !detail) {
ctx.body = { code: 400, message: '缺少必填参数' }
return
}
if (!/^1\d{10}$/.test(phone)) {
ctx.body = { code: 400, message: '手机号格式错误' }
return
}
if (is_default) {
await query('UPDATE addresses SET is_default = 0 WHERE user_id = ?', [user_id])
await query('UPDATE addresses SET is_default = 0 WHERE user_id = ?', [userId])
}
const result = await query(
'INSERT INTO addresses (user_id, name, phone, region, detail, is_default) VALUES (?, ?, ?, ?, ?, ?)',
[user_id, name, phone, region || '', detail, is_default ? 1 : 0]
[userId, name, phone, region || '', detail, is_default ? 1 : 0]
)
ctx.body = { code: 200, data: { id: result.insertId } }
@@ -48,13 +72,19 @@ async function createAddress(ctx) {
async function updateAddress(ctx) {
const id = ctx.params.id
const updates = ctx.request.body
const updates = ctx.request.body || {}
const current = await query('SELECT * FROM addresses WHERE id = ?', [id])
if (!current.length) {
if (current.length === 0) {
ctx.status = 404
ctx.body = { code: 404, message: '地址不存在' }
return
}
if (!ensureOwner(ctx, current[0])) {
ctx.status = 403
ctx.body = { code: 403, message: '无权修改该地址' }
return
}
if (updates.is_default) {
await query('UPDATE addresses SET is_default = 0 WHERE user_id = ?', [current[0].user_id])
@@ -64,6 +94,10 @@ async function updateAddress(ctx) {
const params = []
for (const key of ['name', 'phone', 'region', 'detail', 'is_default']) {
if (updates[key] !== undefined) {
if (key === 'phone' && !/^1\d{10}$/.test(updates[key])) {
ctx.body = { code: 400, message: '手机号格式错误' }
return
}
fields.push(`${key} = ?`)
params.push(key === 'is_default' ? (updates[key] ? 1 : 0) : updates[key])
}
@@ -80,6 +114,17 @@ async function updateAddress(ctx) {
async function deleteAddress(ctx) {
const id = ctx.params.id
const current = await query('SELECT user_id FROM addresses WHERE id = ?', [id])
if (current.length === 0) {
ctx.status = 404
ctx.body = { code: 404, message: '地址不存在' }
return
}
if (!ensureOwner(ctx, current[0])) {
ctx.status = 403
ctx.body = { code: 403, message: '无权删除该地址' }
return
}
await query('DELETE FROM addresses WHERE id = ?', [id])
ctx.body = { code: 200, message: '删除成功' }
}
@@ -88,10 +133,16 @@ async function setDefault(ctx) {
const id = ctx.params.id
const rows = await query('SELECT * FROM addresses WHERE id = ?', [id])
if (!rows.length) {
if (rows.length === 0) {
ctx.status = 404
ctx.body = { code: 404, message: '地址不存在' }
return
}
if (!ensureOwner(ctx, rows[0])) {
ctx.status = 403
ctx.body = { code: 403, message: '无权操作该地址' }
return
}
await query('UPDATE addresses SET is_default = 0 WHERE user_id = ?', [rows[0].user_id])
await query('UPDATE addresses SET is_default = 1 WHERE id = ?', [id])
+233
View File
@@ -0,0 +1,233 @@
const { query, transaction } = require('../config/database')
const { sanitizeInt } = require('../utils/validators')
function currentUserId(ctx) {
return ctx.state.user ? ctx.state.user.id : null
}
async function getCart(ctx) {
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const sql = `
SELECT
c.id,
c.goods_id,
c.quantity,
c.weight,
c.selected,
g.name as goods_name,
g.price,
g.unit,
g.stock,
g.images,
g.pricing_type
FROM carts c
LEFT JOIN goods g ON c.goods_id = g.id
WHERE c.user_id = ? AND g.status != 0
`
const items = await query(sql, [userId])
const cartItems = items.map(item => {
let images = []
try {
images = item.images ? JSON.parse(item.images) : []
} catch {}
return {
id: item.goods_id,
name: item.goods_name,
price: parseFloat(item.price),
unit: item.unit,
stock: item.stock,
images: images,
pricingType: item.pricing_type,
quantity: item.quantity,
weight: item.weight,
selected: item.selected === 1
}
})
ctx.body = { code: 200, data: cartItems }
}
async function addToCart(ctx) {
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { goodsId, quantity, weight } = ctx.request.body || {}
if (!goodsId) {
ctx.body = { code: 400, message: '缺少商品ID' }
return
}
const qty = sanitizeInt(quantity, 1, 1, 9999)
if (qty === null) {
ctx.body = { code: 400, message: '数量必须是 1-9999 之间的整数' }
return
}
const wgt = weight !== undefined && weight !== null ? parseFloat(weight) : null
if (wgt !== null && (isNaN(wgt) || wgt < 0)) {
ctx.body = { code: 400, message: '重量必须为非负数' }
return
}
await transaction(async (conn) => {
const [rows] = await conn.execute('SELECT * FROM carts WHERE user_id = ? AND goods_id = ? FOR UPDATE', [userId, goodsId])
if (rows.length > 0) {
await conn.execute('UPDATE carts SET quantity = quantity + ?, weight = ?, updated_at = NOW() WHERE user_id = ? AND goods_id = ?', [qty, wgt, userId, goodsId])
} else {
await conn.execute('INSERT INTO carts (user_id, goods_id, quantity, weight) VALUES (?, ?, ?, ?)', [userId, goodsId, qty, wgt])
}
})
ctx.body = { code: 200, message: '添加成功' }
}
async function updateCartItem(ctx) {
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { goodsId, quantity, weight, selected } = ctx.request.body || {}
if (!goodsId) {
ctx.body = { code: 400, message: '缺少商品ID' }
return
}
const updates = []
const params = []
if (quantity !== undefined) {
const qty = sanitizeInt(quantity, 1, 0, 9999)
if (qty === null) {
ctx.body = { code: 400, message: '数量必须是 0-9999 之间的整数' }
return
}
updates.push('quantity = ?')
params.push(qty)
}
if (weight !== undefined) {
const wgt = weight === null ? null : parseFloat(weight)
if (wgt !== null && (isNaN(wgt) || wgt < 0)) {
ctx.body = { code: 400, message: '重量必须为非负数' }
return
}
updates.push('weight = ?')
params.push(wgt)
}
if (selected !== undefined) {
updates.push('selected = ?')
params.push(selected ? 1 : 0)
}
if (updates.length === 0) {
ctx.body = { code: 400, message: '没有需要更新的字段' }
return
}
params.push(userId, goodsId)
const result = await query(
`UPDATE carts SET ${updates.join(', ')}, updated_at = NOW() WHERE user_id = ? AND goods_id = ?`,
params
)
if (result.affectedRows === 0) {
ctx.body = { code: 404, message: '购物车中不存在该商品' }
return
}
ctx.body = { code: 200, message: '更新成功' }
}
async function removeFromCart(ctx) {
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { goodsId } = ctx.request.body || {}
if (!goodsId) {
ctx.body = { code: 400, message: '缺少商品ID' }
return
}
await query('DELETE FROM carts WHERE user_id = ? AND goods_id = ?', [userId, goodsId])
ctx.body = { code: 200, message: '删除成功' }
}
async function clearCart(ctx) {
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
await query('DELETE FROM carts WHERE user_id = ?', [userId])
ctx.body = { code: 200, message: '清空成功' }
}
async function syncCart(ctx) {
const userId = currentUserId(ctx)
if (!userId) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { cart } = ctx.request.body || {}
if (!Array.isArray(cart)) {
ctx.body = { code: 400, message: '购物车数据格式错误' }
return
}
if (cart.length > 100) {
ctx.body = { code: 400, message: '购物车商品数不能超过 100' }
return
}
await transaction(async (conn) => {
await conn.execute('DELETE FROM carts WHERE user_id = ?', [userId])
if (cart.length > 0) {
const values = cart.map(item => [
userId,
item.id || item.goods_id,
sanitizeInt(item.quantity, 1, 1, 9999) || 1,
item.weight || null,
1
])
const placeholders = values.map(() => '(?, ?, ?, ?, ?)').join(', ')
const flatParams = values.flat()
await conn.execute(
`INSERT INTO carts (user_id, goods_id, quantity, weight, selected) VALUES ${placeholders}`,
flatParams
)
}
})
ctx.body = { code: 200, message: '同步成功' }
}
module.exports = {
getCart,
addToCart,
updateCartItem,
removeFromCart,
clearCart,
syncCart
}
+126 -50
View File
@@ -37,8 +37,12 @@ async function getGoods(ctx) {
}
if (ctx.query.keyword) {
sql += ' AND name LIKE ?'
params.push(`%${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') {
@@ -59,7 +63,7 @@ async function getGoods(ctx) {
sql += ' LIMIT ?'
params.push(parseInt(ctx.query.limit))
const goods = await query(sql, params)
ctx.body = { code: 200, data: processGoodsImages(goods) }
ctx.body = { code: 0, data: processGoodsImages(goods) }
return
}
@@ -67,7 +71,7 @@ async function getGoods(ctx) {
if (result.data) result.data = processGoodsImages(result.data)
ctx.body = {
code: 200,
code: 0,
...result
}
}
@@ -91,7 +95,7 @@ async function getGoodsById(ctx) {
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,
@@ -99,6 +103,14 @@ async function createGoods(ctx) {
}
return
}
if (stock !== undefined && stock !== 0) {
ctx.body = {
code: 400,
message: '新建商品请保持库存为 0,通过「入库/采购」或「库存调整」接口补充'
}
return
}
// 将图片URL转换为相对路径存储
const relativeImages = (images || []).map(img => toRelativeUrl(img))
@@ -139,10 +151,29 @@ async function createGoods(ctx) {
}
}
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 { name, price, unit, categoryId, images, stock, pricingType, isHot, isNew, description } = ctx.request.body
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,
@@ -150,54 +181,57 @@ async function updateGoods(ctx) {
}
return
}
const relativeImages = (images || []).map(img => toRelativeUrl(img))
const sql = `UPDATE goods SET
name = ?, price = ?, original_price = ?, unit = ?, category_id = ?, images = ?,
stock = ?, pricing_type = ?, is_hot = ?, is_new = ?, description = ?
WHERE id = ?`
const params = [
name,
parseFloat(price),
parseFloat(ctx.request.body.originalPrice || 0),
unit,
categoryId || null,
JSON.stringify(relativeImages),
parseInt(stock) || 0,
parseInt(pricingType) || 1,
parseInt(isHot) || 0,
parseInt(isNew) || 0,
description || '',
goodsId
]
try {
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])
const result = await query(sql, params)
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) {
if (existing.length > 0 && images) {
const oldImages = parseImages(existing[0].images)
const oldFiles = oldImages.filter(u => u.startsWith('/uploads/') && !relativeImages.includes(u))
deleteImageFiles(oldFiles)
}
ctx.body = {
code: 200,
message: '更新成功'
}
ctx.body = { code: 200, message: '更新成功' }
} else {
ctx.body = {
code: 404,
message: '商品不存在'
}
ctx.body = { code: 404, message: '商品不存在' }
}
} catch (error) {
console.error('更新商品失败:', error)
ctx.body = {
code: 500,
message: '更新失败'
}
ctx.body = { code: 500, message: '更新失败' }
}
}
@@ -231,10 +265,52 @@ async function deleteGoods(ctx) {
}
}
// 批量更新商品
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
deleteGoods,
batchUpdate
}
+92
View File
@@ -0,0 +1,92 @@
const { query } = require('../config/database')
async function getHomeCategories(ctx) {
const sql = `
SELECT
hc.id,
hc.sort_order,
c.id as category_id,
c.name as category_name,
c.icon as category_icon,
c.image as category_image
FROM home_categories hc
LEFT JOIN categories c ON hc.category_id = c.id
WHERE hc.is_enabled = 1 AND c.status = 1
ORDER BY hc.sort_order ASC
`
const categories = await query(sql)
const data = categories.map(item => ({
id: item.category_id,
name: item.category_name,
icon: item.category_icon,
image: item.category_image,
sortOrder: item.sort_order
}))
ctx.body = {
code: 200,
data
}
}
async function updateHomeCategories(ctx) {
const { categories } = ctx.request.body
if (!Array.isArray(categories)) {
ctx.body = {
code: 400,
message: '分类数据格式错误'
}
return
}
await query('UPDATE home_categories SET is_enabled = 0')
for (const item of categories) {
if (item.categoryId && item.isEnabled) {
await query(
'UPDATE home_categories SET is_enabled = 1, sort_order = ? WHERE category_id = ?',
[item.sortOrder, item.categoryId]
)
}
}
ctx.body = {
code: 200,
message: '更新成功'
}
}
async function getAllCategoriesForConfig(ctx) {
const categories = await query('SELECT id, name, icon, image FROM categories WHERE status = 1 ORDER BY sort_order ASC')
const homeCategories = await query('SELECT category_id, sort_order, is_enabled FROM home_categories')
const homeMap = {}
for (const hc of homeCategories) {
homeMap[hc.category_id] = {
sortOrder: hc.sort_order,
isEnabled: hc.is_enabled === 1
}
}
const data = categories.map(cat => ({
id: cat.id,
name: cat.name,
icon: cat.icon,
image: cat.image,
...(homeMap[cat.id] || { sortOrder: 999, isEnabled: false })
}))
ctx.body = {
code: 200,
data
}
}
module.exports = {
getHomeCategories,
updateHomeCategories,
getAllCategoriesForConfig
}
+162 -23
View File
@@ -1,20 +1,76 @@
const { query, transaction } = require('../config/database')
const { paginate } = require('../utils/pagination')
const orderService = require('../services/orderService')
const { requireAuth } = require('./users')
const ORDER_UPDATEABLE_FIELDS = ['status', 'total_price', 'totalPrice', 'cart']
function allowedUpdateFields(body) {
const result = {}
for (const key of ORDER_UPDATEABLE_FIELDS) {
if (key in body) {
result[key] = body[key]
}
}
return result
}
async function getOrders(ctx) {
const operator = await requireAuth(ctx)
if (!operator) return
const { page, pageSize, status } = ctx.query
let sql = 'SELECT * FROM orders WHERE 1=1'
let sql = `
SELECT
o.*,
JSON_ARRAYAGG(
JSON_OBJECT(
'id', oi.id,
'order_id', oi.order_id,
'goods_id', oi.goods_id,
'goods_name', oi.goods_name,
'price', oi.price,
'quantity', oi.quantity,
'weight', oi.weight,
'subtotal', oi.subtotal,
'unit', oi.unit
)
) as items_json
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE 1=1
`
const params = []
if (operator.role !== 1) {
sql += ' AND o.user_id = ?'
params.push(operator.id)
}
if (status) {
sql += ' AND status = ?'
sql += ' AND o.status = ?'
params.push(status)
}
sql += ' ORDER BY created_at DESC'
sql += ' GROUP BY o.id ORDER BY o.created_at DESC'
const result = await paginate(query, sql, params, page, pageSize)
const rows = result.data || []
await orderService.attachOrderItems(rows)
const rows = (result.data || []).map(row => {
let items = []
try {
const itemsJson = row.items_json
if (itemsJson) {
items = typeof itemsJson === 'string' ? JSON.parse(itemsJson) : itemsJson
if (items.length === 1 && items[0].id === null) {
items = []
}
}
} catch {}
const { items_json, ...order } = row
return { ...order, items }
})
result.data = rows
ctx.body = {
code: 200,
@@ -23,11 +79,20 @@ async function getOrders(ctx) {
}
async function getOrderById(ctx) {
const operator = await requireAuth(ctx)
if (!operator) return
const orderId = ctx.params.id
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length > 0) {
const order = orders[0]
if (operator.role !== 1 && order.user_id !== operator.id) {
ctx.body = { code: 403, message: '无权查看此订单' }
return
}
order.items = await orderService.getOrderItems(orderId)
ctx.body = {
code: 200,
@@ -42,9 +107,26 @@ async function getOrderById(ctx) {
}
async function createOrder(ctx) {
const { id, userId, totalPrice, cart, remark, customerName, customerPhone, orderType, status } = ctx.request.body
const operator = await requireAuth(ctx)
if (!operator) return
const orderId = id || `order_${Date.now()}_${Math.floor(Math.random() * 1000)}`
const { totalPrice, cart, remark, customerName, customerPhone, orderType, status } = ctx.request.body
const userId = ctx.request.body.userId || operator.id
if (operator.role !== 1 && userId !== operator.id) {
ctx.body = { code: 403, message: '无权为他人创建订单' }
return
}
if (!cart || (Array.isArray(cart) && cart.length === 0)) {
ctx.body = { code: 400, message: '购物车不能为空' }
return
}
const items = typeof cart === 'string' ? JSON.parse(cart) : cart
const calculatedTotalPrice = await orderService.recalculateTotalPrice(items)
const orderId = `ORD${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`
const orderStatus = status || 'pending'
const userInfo = JSON.stringify({
remark: remark || '',
@@ -54,9 +136,28 @@ async function createOrder(ctx) {
})
await transaction(async (conn) => {
for (const item of items) {
const goodsId = item.id || item.goods_id || item.goodsId
if (!goodsId) continue
const qty = item.pricingType === 2 ? 1 : (item.quantity || 1)
const [rows] = await conn.execute('SELECT id, name, stock FROM goods WHERE id = ? FOR UPDATE', [goodsId])
if (rows.length === 0) {
throw new Error('商品不存在')
}
if (rows[0].stock < qty) {
throw new Error(`${rows[0].name} 库存不足(当前库存: ${rows[0].stock},需要: ${qty}`)
}
if (orderStatus === 'completed') {
await conn.execute('UPDATE goods SET stock = stock - ?, sales = COALESCE(sales, 0) + ? WHERE id = ?', [qty, qty, goodsId])
await conn.execute('UPDATE stock SET quantity = quantity - ? WHERE goods_id = ?', [qty, goodsId])
}
}
await conn.execute(
'INSERT INTO orders (id, user_id, status, total_price, cart, user_info) VALUES (?, ?, ?, ?, ?, ?)',
[orderId, userId || null, status || 'pending', totalPrice, typeof cart === 'string' ? cart : JSON.stringify(cart), userInfo]
[orderId, userId || null, orderStatus, calculatedTotalPrice, typeof cart === 'string' ? cart : JSON.stringify(cart), userInfo]
)
await orderService.insertOrderItems(conn, orderId, cart)
})
@@ -76,42 +177,75 @@ async function createOrder(ctx) {
}
async function updateOrder(ctx) {
const operator = await requireAuth(ctx)
if (!operator) return
const orderId = ctx.params.id
const updates = ctx.request.body
const body = allowedUpdateFields(ctx.request.body)
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length > 0) {
const prevStatus = orders[0].status
const order = orders[0]
if (operator.role !== 1 && order.user_id !== operator.id) {
ctx.body = { code: 403, message: '无权修改此订单' }
return
}
const prevStatus = order.status
const updateFields = []
const updateParams = []
if (updates.status !== undefined) {
if (body.status !== undefined) {
updateFields.push('status = ?')
updateParams.push(updates.status)
updateParams.push(body.status)
}
const totalPrice = updates.total_price !== undefined ? updates.total_price : updates.totalPrice
const totalPrice = body.total_price !== undefined ? body.total_price : body.totalPrice
if (totalPrice !== undefined) {
updateFields.push('total_price = ?')
updateParams.push(totalPrice)
}
if (updates.cart !== undefined) {
const cartStr = typeof updates.cart === 'string' ? updates.cart : JSON.stringify(updates.cart)
if (body.cart !== undefined) {
const cartStr = typeof body.cart === 'string' ? body.cart : JSON.stringify(body.cart)
updateFields.push('cart = ?')
updateParams.push(cartStr)
}
if (updateFields.length > 0) {
updateParams.push(orderId)
await query(`UPDATE orders SET ${updateFields.join(', ')} WHERE id = ?`, updateParams)
if (updateFields.length === 0) {
ctx.body = { code: 400, message: '没有需要更新的字段' }
return
}
if (updates.cart !== undefined) {
await transaction(async (conn) => {
const newStatus = body.status !== undefined ? body.status : prevStatus
await transaction(async (conn) => {
if (newStatus === 'completed' && prevStatus !== 'completed') {
const items = body.cart !== undefined
? (typeof body.cart === 'string' ? JSON.parse(body.cart) : body.cart)
: JSON.parse(order.cart || '[]')
for (const item of items) {
const goodsId = item.id || item.goods_id || item.goodsId
if (!goodsId) continue
const qty = item.pricingType === 2 ? 1 : (item.quantity || 1)
const [rows] = await conn.execute('SELECT stock FROM goods WHERE id = ? FOR UPDATE', [goodsId])
if (rows.length > 0 && rows[0].stock >= qty) {
await conn.execute('UPDATE goods SET stock = stock - ?, sales = COALESCE(sales, 0) + ? WHERE id = ?', [qty, qty, goodsId])
await conn.execute('UPDATE stock SET quantity = quantity - ? WHERE goods_id = ?', [qty, goodsId])
}
}
}
updateParams.push(orderId)
await conn.execute(`UPDATE orders SET ${updateFields.join(', ')} WHERE id = ?`, updateParams)
if (body.cart !== undefined) {
await conn.execute('DELETE FROM order_items WHERE order_id = ?', [orderId])
await orderService.insertOrderItems(conn, orderId, updates.cart)
})
}
await orderService.insertOrderItems(conn, orderId, body.cart)
}
})
const updatedOrders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
const completed = updatedOrders[0]
@@ -121,6 +255,11 @@ async function updateOrder(ctx) {
setImmediate(() => orderService.processOrderComplete(completed))
}
// 订单状态变更时发送微信订阅消息通知
if (newStatus !== prevStatus && completed.user_id) {
setImmediate(() => orderService.sendWechatNotification(completed.user_id, completed.id, newStatus, completed.total_price))
}
ctx.body = {
code: 200,
data: completed
+368
View File
@@ -0,0 +1,368 @@
const crypto = require('crypto')
const fetch = require('node-fetch')
const { query } = require('../config/database')
// 微信支付配置
const APPID = process.env.WECHAT_APPID
const MCH_ID = process.env.WECHAT_MCH_ID
const API_KEY = process.env.WECHAT_API_KEY
const NOTIFY_URL = process.env.WECHAT_NOTIFY_URL
const CERT_PATH = process.env.WECHAT_CERT_PATH
const KEY_PATH = process.env.WECHAT_KEY_PATH
// 微信支付API地址
const UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
const REFUND_URL = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
const ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery'
/**
* 生成随机字符串
* @param {number} [length=32] - 字符串长度
* @returns {string} 随机字符串
*/
function generateNonceStr(length = 32) {
return crypto.randomBytes(length).toString('hex').slice(0, length)
}
/**
* 生成微信支付签名
* @param {Object} params - 参与签名的参数对象
* @returns {string} 签名字符串
*/
function generateSign(params) {
// 按字典序排序参数
const sortedKeys = Object.keys(params).sort()
// 拼接键值对(跳过空值和sign字段)
const stringA = sortedKeys
.filter(key => params[key] !== '' && params[key] !== undefined && params[key] !== null && key !== 'sign')
.map(key => `${key}=${params[key]}`)
.join('&')
// 拼接API密钥
const stringSignTemp = `${stringA}&key=${API_KEY}`
// MD5签名并转大写
return crypto.createHash('md5').update(stringSignTemp, 'utf8').digest('hex').toUpperCase()
}
/**
* 对象转XML
* @param {Object} obj - 需要转换的对象
* @returns {string} XML字符串
*/
function buildXML(obj) {
let xml = '<xml>'
for (const key of Object.keys(obj)) {
const val = obj[key]
if (typeof val === 'number') {
xml += `<${key}>${val}</${key}>`
} else {
xml += `<${key}><![CDATA[${val}]]></${key}>`
}
}
xml += '</xml>'
return xml
}
/**
* XML转对象
* @param {string} xml - XML字符串
* @returns {Promise<Object>} 解析后的对象
*/
function parseXML(xml) {
return new Promise((resolve, reject) => {
const obj = {}
const regex = /<(\w+)>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/\1>/g
let match
while ((match = regex.exec(xml)) !== null) {
if (match[1] !== 'xml') {
obj[match[1]] = match[2]
}
}
resolve(obj)
})
}
/**
* 创建支付订单
* 调用微信支付统一下单API(JSAPI),返回支付参数
*/
async function createPayment(ctx) {
const { orderId } = ctx.request.body
if (!orderId) {
ctx.body = { code: 400, message: '订单ID不能为空' }
return
}
// 查询订单信息
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length === 0) {
ctx.body = { code: 404, message: '订单不存在' }
return
}
const order = orders[0]
// 检查订单状态
if (order.status === 'paid') {
ctx.body = { code: 400, message: '订单已支付' }
return
}
// 检查支付配置
if (!APPID || !MCH_ID || !API_KEY || !NOTIFY_URL) {
ctx.body = { code: 500, message: '微信支付配置不完整' }
return
}
// 获取用户openid
const user = ctx.state.user
if (!user || !user.openid) {
ctx.body = { code: 400, message: '无法获取用户openid,请先绑定微信' }
return
}
// 构造统一下单参数
const nonceStr = generateNonceStr()
const outTradeNo = orderId
const totalFee = Math.round(parseFloat(order.total_price) * 100) // 金额转换为分
const body = `订单-${orderId}`
const params = {
appid: APPID,
mch_id: MCH_ID,
nonce_str: nonceStr,
body: body,
out_trade_no: outTradeNo,
total_fee: totalFee,
spbill_create_ip: ctx.ip || '127.0.0.1',
notify_url: NOTIFY_URL,
trade_type: 'JSAPI',
openid: user.openid
}
// 生成签名
params.sign = generateSign(params)
try {
// 调用微信统一下单API
const xmlData = buildXML(params)
const response = await fetch(UNIFIED_ORDER_URL, {
method: 'POST',
body: xmlData,
headers: { 'Content-Type': 'application/xml' }
})
const resultXml = await response.text()
const result = await parseXML(resultXml)
if (result.return_code !== 'SUCCESS') {
console.error('微信统一下单通信失败:', result.return_msg)
ctx.body = { code: 500, message: `微信支付通信失败: ${result.return_msg}` }
return
}
if (result.result_code !== 'SUCCESS') {
console.error('微信统一下单业务失败:', result.err_code_des)
ctx.body = { code: 500, message: `微信支付下单失败: ${result.err_code_des}` }
return
}
// 生成小程序支付参数
const prepayId = result.prepay_id
const payParams = {
appId: APPID,
timeStamp: Math.floor(Date.now() / 1000).toString(),
nonceStr: generateNonceStr(),
package: `prepay_id=${prepayId}`,
signType: 'MD5'
}
payParams.paySign = generateSign(payParams)
ctx.body = {
code: 200,
data: payParams
}
} catch (error) {
console.error('创建支付失败:', error)
ctx.body = { code: 500, message: '创建支付失败' }
}
}
/**
* 微信支付回调通知
* 验证签名,更新订单状态为paid
*/
async function paymentNotify(ctx) {
try {
const xml = ctx.request.body
const result = await parseXML(xml)
// 验证签名
const sign = result.sign
const calculatedSign = generateSign(result)
if (sign !== calculatedSign) {
console.error('微信支付回调签名验证失败')
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>'
return
}
// 检查支付结果
if (result.result_code !== 'SUCCESS') {
console.error('微信支付回调支付失败:', result.err_code_des)
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[支付失败]]></return_msg></xml>'
return
}
const outTradeNo = result.out_trade_no
const transactionId = result.transaction_id
// 查询订单
const orders = await query('SELECT * FROM orders WHERE id = ?', [outTradeNo])
if (orders.length === 0) {
console.error('微信支付回调订单不存在:', outTradeNo)
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>'
return
}
const order = orders[0]
// 防止重复通知
if (order.status === 'paid') {
ctx.body = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'
return
}
// 验证金额(微信返回的金额单位为分)
const totalFee = parseInt(result.total_fee)
const orderTotalFee = Math.round(parseFloat(order.total_price) * 100)
if (totalFee !== orderTotalFee) {
console.error('微信支付回调金额不一致:', { totalFee, orderTotalFee })
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[金额不一致]]></return_msg></xml>'
return
}
// 更新订单状态为paid
await query(
'UPDATE orders SET status = ?, transaction_id = ? WHERE id = ? AND status != ?',
['paid', transactionId, outTradeNo, 'paid']
)
console.info('微信支付成功,订单已更新:', outTradeNo)
ctx.body = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'
} catch (error) {
console.error('处理微信支付回调异常:', error)
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[内部错误]]></return_msg></xml>'
}
}
/**
* 申请退款
* 调用微信退款API
*/
async function refundPayment(ctx) {
const { orderId, reason } = ctx.request.body
if (!orderId) {
ctx.body = { code: 400, message: '订单ID不能为空' }
return
}
// 查询订单信息
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length === 0) {
ctx.body = { code: 404, message: '订单不存在' }
return
}
const order = orders[0]
// 检查订单状态,只有已支付的订单才能退款
if (order.status !== 'paid') {
ctx.body = { code: 400, message: '订单未支付,无法退款' }
return
}
// 检查支付配置
if (!APPID || !MCH_ID || !API_KEY) {
ctx.body = { code: 500, message: '微信支付配置不完整' }
return
}
// 构造退款参数
const nonceStr = generateNonceStr()
const outTradeNo = orderId
const outRefundNo = `RF${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`
const totalFee = Math.round(parseFloat(order.total_price) * 100)
const refundFee = totalFee // 默认全额退款
const params = {
appid: APPID,
mch_id: MCH_ID,
nonce_str: nonceStr,
out_trade_no: outTradeNo,
out_refund_no: outRefundNo,
total_fee: totalFee,
refund_fee: refundFee,
refund_desc: reason || '用户申请退款'
}
// 生成签名
params.sign = generateSign(params)
try {
// 读取证书(微信退款API需要双向证书)
let fetchOptions = {
method: 'POST',
body: buildXML(params),
headers: { 'Content-Type': 'application/xml' }
}
if (CERT_PATH && KEY_PATH) {
const fs = require('fs')
fetchOptions.agent = new (require('https').Agent)({
cert: fs.readFileSync(CERT_PATH),
key: fs.readFileSync(KEY_PATH)
})
}
const response = await fetch(REFUND_URL, fetchOptions)
const resultXml = await response.text()
const result = await parseXML(resultXml)
if (result.return_code !== 'SUCCESS') {
console.error('微信退款通信失败:', result.return_msg)
ctx.body = { code: 500, message: `微信退款通信失败: ${result.return_msg}` }
return
}
if (result.result_code !== 'SUCCESS') {
console.error('微信退款业务失败:', result.err_code_des)
ctx.body = { code: 500, message: `微信退款失败: ${result.err_code_des}` }
return
}
// 更新订单状态为refunded
await query(
'UPDATE orders SET status = ?, refund_no = ? WHERE id = ?',
['refunded', outRefundNo, orderId]
)
ctx.body = {
code: 200,
data: {
outRefundNo,
refundFee: refundFee / 100
}
}
} catch (error) {
console.error('申请退款失败:', error)
ctx.body = { code: 500, message: '申请退款失败' }
}
}
module.exports = {
createPayment,
paymentNotify,
refundPayment
}
+15 -9
View File
@@ -89,21 +89,28 @@ async function deletePointsGoods(ctx) {
}
async function exchangePointsGoods(ctx) {
const { userId, goodsId, quantity } = ctx.request.body
const user = ctx.state.user
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { goodsId, quantity } = ctx.request.body || {}
if (!userId || !goodsId) {
if (!goodsId) {
ctx.body = { code: 400, message: '参数不完整' }
return
}
const qty = quantity || 1
const userId = user.id
const qty = Math.max(1, Math.min(parseInt(quantity) || 1, 99))
const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [userId])
if (users.length === 0) {
ctx.body = { code: 404, message: '用户不存在' }
return
}
const user = users[0]
const currentUserRow = users[0]
const goods = await query('SELECT * FROM points_goods WHERE id = ? AND is_show = 1', [goodsId])
if (goods.length === 0) {
@@ -118,13 +125,14 @@ async function exchangePointsGoods(ctx) {
}
const totalPoints = goodsItem.points * qty
if (user.points < totalPoints) {
if (currentUserRow.points < totalPoints) {
ctx.body = { code: 400, message: '积分不足' }
return
}
let newPoints
await transaction(async (conn) => {
const newPoints = user.points - totalPoints
newPoints = currentUserRow.points - totalPoints
await conn.execute('UPDATE users SET points = ? WHERE id = ?', [newPoints, userId])
await conn.execute('UPDATE points_goods SET stock = stock - ? WHERE id = ?', [qty, goodsId])
await conn.execute(
@@ -136,9 +144,7 @@ async function exchangePointsGoods(ctx) {
ctx.body = {
code: 200,
message: '兑换成功',
data: {
remainingPoints: newPoints
}
data: { remainingPoints: newPoints }
}
}
+37 -9
View File
@@ -51,9 +51,9 @@ async function getPurchaseById(ctx) {
}
async function createPurchase(ctx) {
const { supplier_id, items, remarks } = ctx.request.body
const { supplier_id, items, remarks } = ctx.request.body || {}
if (!supplier_id || !items || items.length === 0) {
if (!supplier_id || !Array.isArray(items) || items.length === 0) {
ctx.body = { code: 400, message: '请选择供应商和采购商品' }
return
}
@@ -67,19 +67,25 @@ async function createPurchase(ctx) {
let total = 0
for (const item of items) {
total += (item.purchase_price || 0) * (item.quantity || 0)
const qty = parseInt(item.quantity) || 0
const price = parseFloat(item.purchase_price) || 0
if (qty <= 0 || price < 0) {
ctx.body = { code: 400, message: '数量/单价不合法' }
return
}
total += price * qty
}
const result = await transaction(async (conn) => {
const purchaseResult = await conn.execute(
const [purchaseResult] = await conn.execute(
'INSERT INTO purchases (supplier_id, supplier_name, total, remarks) VALUES (?, ?, ?, ?)',
[supplier_id, supplier.name, total, remarks || '']
)
const purchaseId = purchaseResult[0].insertId
const purchaseId = purchaseResult.insertId
for (const item of items) {
const goods = await conn.execute('SELECT name FROM goods WHERE id = ?', [item.goods_id])
const goodsName = goods[0].length > 0 ? goods[0][0].name : ''
const [goods] = await conn.execute('SELECT name FROM goods WHERE id = ?', [item.goods_id])
const goodsName = goods.length > 0 ? goods[0].name : ''
await conn.execute(
'INSERT INTO purchase_items (purchase_id, goods_id, goods_name, quantity, purchase_price) VALUES (?, ?, ?, ?, ?)',
[purchaseId, item.goods_id, goodsName, item.quantity || 0, item.purchase_price || 0]
@@ -96,6 +102,12 @@ async function createPurchase(ctx) {
}
async function inboundPurchase(ctx) {
const operator = ctx.state.user
if (!operator) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const id = parseInt(ctx.params.id)
const purchases = await query('SELECT * FROM purchases WHERE id = ?', [id])
@@ -111,11 +123,20 @@ async function inboundPurchase(ctx) {
}
const items = await query('SELECT * FROM purchase_items WHERE purchase_id = ?', [id])
if (items.length === 0) {
ctx.body = { code: 400, message: '采购单无明细' }
return
}
await transaction(async (conn) => {
for (const item of items) {
const existing = await conn.execute('SELECT * FROM stock WHERE goods_id = ?', [item.goods_id])
if (existing[0].length > 0) {
const [goods] = await conn.execute('SELECT id, stock FROM goods WHERE id = ? FOR UPDATE', [item.goods_id])
if (goods.length === 0) {
throw new Error(`商品 ${item.goods_id} 不存在,无法入库`)
}
const [stockRows] = await conn.execute('SELECT quantity FROM stock WHERE goods_id = ? FOR UPDATE', [item.goods_id])
if (stockRows.length > 0) {
await conn.execute(
'UPDATE stock SET quantity = quantity + ? WHERE goods_id = ?',
[item.quantity, item.goods_id]
@@ -130,6 +151,13 @@ async function inboundPurchase(ctx) {
'UPDATE goods SET stock = stock + ? WHERE id = ?',
[item.quantity, item.goods_id]
)
try {
await conn.execute(
'INSERT INTO stock_logs (goods_id, change_type, delta, quantity_after, operator_id, remark) VALUES (?, ?, ?, ?, ?, ?)',
[item.goods_id, 'purchase', item.quantity, (stockRows[0]?.quantity || 0) + item.quantity, operator.id, `采购入库 #${id}`]
)
} catch {}
}
await conn.execute('UPDATE purchases SET status = 1 WHERE id = ?', [id])
})
+277
View File
@@ -0,0 +1,277 @@
const { query, transaction } = require('../config/database')
const { paginate } = require('../utils/pagination')
const REFUNDABLE_STATUSES = ['paid', 'completed']
function currentUserId(ctx) {
return ctx.state.user ? ctx.state.user.id : null
}
function currentUser(ctx) {
return ctx.state.user
}
async function getRefunds(ctx) {
const { page, pageSize, status } = ctx.query
let sql = `
SELECT
r.*,
o.status as order_status,
o.total_price as order_amount,
u.phone as user_phone,
u.name as user_name
FROM refunds r
LEFT JOIN orders o ON r.order_id = o.id
LEFT JOIN users u ON r.user_id = u.id
WHERE 1=1
`
const params = []
if (status !== undefined && status !== '') {
sql += ' AND r.status = ?'
params.push(parseInt(status))
}
sql += ' ORDER BY r.created_at DESC'
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
const rows = (result.data || []).map(item => ({
id: item.id,
orderId: item.order_id,
userId: item.user_id,
userPhone: item.user_phone,
userName: item.user_name,
type: item.type,
reason: item.reason,
amount: parseFloat(item.amount),
status: item.status,
adminRemark: item.admin_remark,
orderStatus: item.order_status,
orderAmount: parseFloat(item.order_amount),
processedAt: item.processed_at,
createdAt: item.created_at
}))
result.data = rows
ctx.body = {
code: 200,
...result
}
}
async function getRefundById(ctx) {
const refundId = parseInt(ctx.params.id)
const refunds = await query(`
SELECT
r.*,
o.status as order_status,
o.total_price as order_amount,
u.phone as user_phone,
u.name as user_name
FROM refunds r
LEFT JOIN orders o ON r.order_id = o.id
LEFT JOIN users u ON r.user_id = u.id
WHERE r.id = ?
`, [refundId])
if (refunds.length === 0) {
ctx.status = 404
ctx.body = { code: 404, message: '退款申请不存在' }
return
}
const item = refunds[0]
const user = currentUser(ctx)
if (user.role !== 2 && user.role !== 1 && item.user_id !== user.id) {
ctx.status = 403
ctx.body = { code: 403, message: '无权查看该退款' }
return
}
ctx.body = {
code: 200,
data: {
id: item.id,
orderId: item.order_id,
userId: item.user_id,
userPhone: item.user_phone,
userName: item.user_name,
type: item.type,
reason: item.reason,
amount: parseFloat(item.amount),
status: item.status,
adminRemark: item.admin_remark,
orderStatus: item.order_status,
orderAmount: parseFloat(item.order_amount),
processedAt: item.processed_at,
createdAt: item.created_at
}
}
}
async function createRefund(ctx) {
const user = currentUser(ctx)
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const { orderId, type, reason, amount } = ctx.request.body || {}
if (!orderId || !reason) {
ctx.body = { code: 400, message: '缺少必要参数' }
return
}
const userId = user.id
const orders = await query('SELECT * FROM orders WHERE id = ? AND user_id = ?', [orderId, userId])
if (orders.length === 0) {
ctx.body = { code: 404, message: '订单不存在' }
return
}
const order = orders[0]
if (!REFUNDABLE_STATUSES.includes(order.status)) {
ctx.body = { code: 400, message: `订单当前状态(${order.status})不可申请退款` }
return
}
const existingRefund = await query('SELECT * FROM refunds WHERE order_id = ? AND status = 0', [orderId])
if (existingRefund.length > 0) {
ctx.body = { code: 400, message: '该订单已有待处理的退款申请' }
return
}
const orderTotal = parseFloat(order.total_price)
let refundAmount = orderTotal
if (amount !== undefined && amount !== null) {
const parsed = parseFloat(amount)
if (isNaN(parsed) || parsed <= 0) {
ctx.body = { code: 400, message: '退款金额无效' }
return
}
if (parsed > orderTotal) {
ctx.body = { code: 400, message: `退款金额不能超过订单金额 ¥${orderTotal.toFixed(2)}` }
return
}
refundAmount = parsed
}
const result = await transaction(async (conn) => {
const [refundResult] = await conn.execute(
'INSERT INTO refunds (order_id, user_id, type, reason, amount) VALUES (?, ?, ?, ?, ?)',
[orderId, userId, type || 1, reason, refundAmount]
)
await conn.execute("UPDATE orders SET status = 'refunding' WHERE id = ?", [orderId])
return refundResult.insertId
})
ctx.body = {
code: 200,
message: '退款申请已提交',
data: { id: result, orderId, amount: refundAmount }
}
}
async function processRefund(ctx) {
const refundId = parseInt(ctx.params.id)
const { status, adminRemark } = ctx.request.body || {}
if (status !== 1 && status !== 2) {
ctx.body = { code: 400, message: '请选择正确的处理结果' }
return
}
const refunds = await query('SELECT * FROM refunds WHERE id = ?', [refundId])
if (refunds.length === 0) {
ctx.body = { code: 404, message: '退款申请不存在' }
return
}
const refund = refunds[0]
if (refund.status !== 0) {
ctx.body = { code: 400, message: '该退款申请已处理' }
return
}
await transaction(async (conn) => {
await conn.execute(
'UPDATE refunds SET status = ?, admin_remark = ?, processed_at = NOW() WHERE id = ?',
[status, adminRemark || '', refundId]
)
if (status === 1) {
await conn.execute("UPDATE orders SET status = 'refunded' WHERE id = ?", [refund.order_id])
const [userRows] = await conn.execute('SELECT points FROM users WHERE id = ?', [refund.user_id])
if (userRows.length > 0) {
const deductPoints = Math.min(Math.floor(refund.amount), userRows[0].points)
if (deductPoints > 0) {
const newPoints = userRows[0].points - deductPoints
await conn.execute('UPDATE users SET points = ? WHERE id = ?', [newPoints, refund.user_id])
await conn.execute(
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
[refund.user_id, 'spend', deductPoints, `订单退款扣除积分: ${refund.order_id}`]
)
}
}
} else {
await conn.execute("UPDATE orders SET status = 'completed' WHERE id = ?", [refund.order_id])
}
})
ctx.body = {
code: 200,
message: status === 1 ? '已同意退款' : '已拒绝退款'
}
}
async function getUserRefunds(ctx) {
const user = currentUser(ctx)
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录' }
return
}
const requestedId = parseInt(ctx.query.userId)
const userId = (user.role === 2 || user.role === 1) && requestedId ? requestedId : user.id
const refunds = await query(`
SELECT
r.*,
o.status as order_status
FROM refunds r
LEFT JOIN orders o ON r.order_id = o.id
WHERE r.user_id = ?
ORDER BY r.created_at DESC
`, [userId])
const rows = refunds.map(item => ({
id: item.id,
orderId: item.order_id,
type: item.type,
reason: item.reason,
amount: parseFloat(item.amount),
status: item.status,
adminRemark: item.admin_remark,
orderStatus: item.order_status,
processedAt: item.processed_at,
createdAt: item.created_at
}))
ctx.body = {
code: 200,
data: rows
}
}
module.exports = {
getRefunds,
getRefundById,
createRefund,
processRefund,
getUserRefunds
}
+8 -1
View File
@@ -15,12 +15,19 @@ async function getTodayStats(ctx) {
[todayStart, todayEnd]
)
const stockThreshold = parseInt(process.env.STOCK_WARN_THRESHOLD) || 10
const lowStockResult = await query(
'SELECT COUNT(*) as lowStockCount FROM goods WHERE stock < ? AND status != 0',
[stockThreshold]
)
ctx.body = {
code: 200,
data: {
sales: orderResult[0].totalSales,
orders: orderResult[0].orderCount,
customers: customerResult[0].customerCount
customers: customerResult[0].customerCount,
lowStockCount: lowStockResult[0].lowStockCount
}
}
}
+67 -51
View File
@@ -1,13 +1,31 @@
const { query } = require('../config/database')
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
SELECT
s.id,
s.goods_id,
g.name as goods_name,
@@ -43,64 +61,62 @@ async function getStockList(ctx) {
// 调整库存
async function adjustStock(ctx) {
const goodsId = ctx.params.id
const { quantity, type } = ctx.request.body
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 || {}
if (!quantity || quantity <= 0) {
ctx.body = {
code: 400,
message: '请输入有效的调整数量'
}
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
}
// 先检查库存记录是否存在
let stockRecords = await query('SELECT * FROM stock WHERE goods_id = ?', [goodsId])
await ensureStockLogTable()
if (stockRecords.length === 0) {
// 如果库存记录不存在,先创建
if (type === 'add') {
await query(
'INSERT INTO stock (goods_id, quantity, warehouse) VALUES (?, ?, ?)',
[goodsId, quantity, '默认仓库']
)
} else {
ctx.body = {
code: 400,
message: '库存不足,无法减少'
}
return
}
} else {
const currentStock = stockRecords[0]
let newQuantity
if (type === 'add') {
newQuantity = currentStock.quantity + quantity
} else {
newQuantity = currentStock.quantity - quantity
if (newQuantity < 0) {
ctx.body = {
code: 400,
message: '库存不足,无法减少'
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('库存不足,无法减少')
}
return
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 query(
'UPDATE stock SET quantity = ? WHERE goods_id = ?',
[newQuantity, goodsId]
)
}
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 updatedStock = await query('SELECT * FROM stock WHERE goods_id = ?', [goodsId])
ctx.body = {
code: 200,
data: updatedStock[0],
message: '库存调整成功'
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 || '调整失败' }
}
}
+312 -132
View File
@@ -1,82 +1,90 @@
const { query } = require('../config/database')
const { query, transaction } = require('../config/database')
const crypto = require('crypto')
const { paginate } = require('../utils/pagination')
const { DEFAULT_PASSWORD } = require('../config/constants')
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex')
}
const { hashPassword, verifyPassword, needsRehash } = require('../utils/password')
const { signAccess, signRefresh, verifyToken, ACCESS_TTL, REFRESH_TTL } = require('../utils/jwt')
const { toLegacyToken } = require('../utils/legacy-token')
const { extractToken, getUserByToken } = require('../middleware/auth')
function generateToken() {
return crypto.randomBytes(32).toString('hex')
}
// 用户登录(支持双重身份)
async function login(ctx) {
const { phone, password, loginType } = ctx.request.body
if (!phone || !password) {
ctx.body = {
code: 400,
message: '请输入手机号和密码'
}
return
}
const users = await query('SELECT * FROM users WHERE phone = ?', [phone])
if (users.length === 0) {
ctx.body = {
code: 401,
message: '用户不存在'
}
return
}
const user = users[0]
if (user.status === 0) {
ctx.body = {
code: 401,
message: '账号已被禁用'
}
return
}
if (user.password !== md5(password)) {
ctx.body = {
code: 401,
message: '密码错误'
}
return
}
// 店员登录需要验证角色
if (loginType === 'staff' && user.role !== 1) {
ctx.body = {
code: 401,
message: '该账号不是店员账号'
}
return
}
const token = generateToken()
await query('UPDATE users SET token = ? WHERE id = ?', [token, user.id])
const userInfo = {
async function issueTokenPair(user) {
const access = signAccess(user)
const refresh = signRefresh(user)
const dbToken = generateToken()
await query('UPDATE users SET token = ? WHERE id = ?', [dbToken, user.id])
return { access, refresh, legacy: toLegacyToken(dbToken), accessTtl: ACCESS_TTL, refreshTtl: REFRESH_TTL }
}
function publicUser(user, tokenObj) {
const base = {
id: user.id,
phone: user.phone,
name: user.name,
avatar: user.avatar,
points: user.points,
role: user.role,
token
role: user.role
}
ctx.body = {
code: 200,
data: userInfo
if (tokenObj) {
return { ...base, token: tokenObj.access, accessToken: tokenObj.access, refreshToken: tokenObj.refresh, legacyToken: tokenObj.legacy, accessTtl: tokenObj.accessTtl, refreshTtl: tokenObj.refreshTtl }
}
return base
}
// 用户登录(支持双重身份)
async function login(ctx) {
const { phone, password, loginType } = ctx.request.body
if (!phone || !password) {
ctx.body = { code: 400, message: '请输入手机号和密码' }
return
}
const users = await query('SELECT * FROM users WHERE phone = ?', [phone])
if (users.length === 0) {
ctx.body = { code: 401, message: '用户不存在' }
return
}
const user = users[0]
if (user.status === 0) {
ctx.body = { code: 401, message: '账号已被禁用' }
return
}
if (!verifyPassword(password, user.password)) {
ctx.body = { code: 401, message: '密码错误' }
return
}
if (loginType === 'admin' && user.role !== 2) {
ctx.body = { code: 401, message: '该账号不是管理员账号' }
return
}
if (loginType === 'staff' && user.role !== 1) {
ctx.body = { code: 401, message: '该账号不是店员账号' }
return
}
if (loginType === 'customer' && user.role !== 0) {
ctx.body = { code: 401, message: '该账号不是普通用户账号' }
return
}
let tokenObj
if (needsRehash(user.password)) {
const upgraded = hashPassword(password)
await query('UPDATE users SET password = ? WHERE id = ?', [upgraded, user.id])
}
tokenObj = await issueTokenPair(user)
ctx.body = { code: 200, data: publicUser(user, tokenObj) }
}
// 用户注册(普通用户)
@@ -87,6 +95,15 @@ async function register(ctx) {
return
}
if (password.length < 8) {
ctx.body = { code: 400, message: '密码至少8位' }
return
}
if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
ctx.body = { code: 400, message: '密码需包含字母和数字' }
return
}
const existing = await query('SELECT * FROM users WHERE phone = ?', [phone])
if (existing.length > 0) {
ctx.body = { code: 400, message: '该手机号已注册' }
@@ -95,7 +112,7 @@ async function register(ctx) {
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(password), name, '', 0, 0]
[phone, hashPassword(password), name, '', 0, 0]
)
ctx.body = {
@@ -106,19 +123,44 @@ async function register(ctx) {
}
async function requireAuth(ctx) {
const user = await getUserByToken(extractToken(ctx))
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录或登录已失效' }
return null
}
return user
}
async function requireStaffAuth(ctx) {
const authHeader = ctx.headers.authorization || ''
const token = authHeader.replace('Bearer ', '')
if (!token) {
ctx.body = { code: 401, message: '未登录,请先登录店员账号' }
const user = await getUserByToken(extractToken(ctx))
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录或登录已失效' }
return null
}
const operators = await query('SELECT * FROM users WHERE token = ? AND role = 1 AND status = 1', [token])
if (operators.length === 0) {
ctx.body = { code: 401, message: '权限不足,仅店员可操作' }
if (user.role !== 1 && user.role !== 2) {
ctx.status = 403
ctx.body = { code: 403, message: '权限不足,仅店员或管理员可操作' }
return null
}
return operators[0]
return user
}
async function requireAdminAuth(ctx) {
const user = await getUserByToken(extractToken(ctx))
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录或登录已失效' }
return null
}
if (user.role !== 2) {
ctx.status = 403
ctx.body = { code: 403, message: '权限不足,仅管理员可操作' }
return null
}
return user
}
async function createUser(phone, name, role) {
@@ -127,7 +169,7 @@ async function createUser(phone, name, role) {
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(DEFAULT_PASSWORD), name, '', 0, role]
[phone, hashPassword(DEFAULT_PASSWORD), name, '', 0, role]
)
return {
@@ -143,7 +185,7 @@ async function createUser(phone, name, role) {
}
}
// 店员注册(需要员权限)
// 店员注册(需要管理员权限)
async function registerStaff(ctx) {
const { phone, name } = ctx.request.body
if (!phone || !name) {
@@ -151,7 +193,7 @@ async function registerStaff(ctx) {
return
}
const operator = await requireStaffAuth(ctx)
const operator = await requireAdminAuth(ctx)
if (!operator) return
const result = await createUser(phone, name, 1)
@@ -223,39 +265,65 @@ async function getUserInfo(ctx) {
}
}
// 获取用户列表
// 获取用户列表(管理员操作)
async function getUsers(ctx) {
const operator = await requireAdminAuth(ctx)
if (!operator) return
let sql = 'SELECT id, phone, name, points, role, status, created_at FROM users WHERE status = 1'
const params = []
if (ctx.query.role !== undefined) {
sql += ' AND role = ?'
params.push(parseInt(ctx.query.role))
}
if (ctx.query.keyword) {
sql += ' AND (phone LIKE ? OR name LIKE ?)'
params.push(`%${ctx.query.keyword}%`, `%${ctx.query.keyword}%`)
}
sql += ' ORDER BY created_at DESC'
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
ctx.body = {
code: 200,
...result
}
}
// 更新用户信息
const USER_UPDATEABLE_FIELDS = ['name', 'avatar', 'points', 'status']
// 更新用户信息(管理员可改任意人;本人仅可改 name/avatar
async function updateUser(ctx) {
const operator = await requireAuth(ctx)
if (!operator) return
const userId = parseInt(ctx.params.id)
const { name, avatar, points, status } = ctx.request.body
const body = ctx.request.body
let allowedFields = USER_UPDATEABLE_FIELDS
if (operator.role !== 2) {
if (userId !== operator.id) {
ctx.body = { code: 403, message: '无权修改他人信息' }
return
}
allowedFields = ['name', 'avatar']
}
const filtered = {}
for (const key of allowedFields) {
if (key in body) {
filtered[key] = body[key]
}
}
const { name, avatar, points, status } = filtered
const updateFields = []
const updateParams = []
if (name !== undefined) {
updateFields.push('name = ?')
updateParams.push(name)
@@ -298,10 +366,18 @@ async function updateUser(ctx) {
}
}
// 删除用户
// 删除/禁用用户(管理员操作)
async function deleteUser(ctx) {
const operator = await requireAdminAuth(ctx)
if (!operator) return
const userId = parseInt(ctx.params.id)
if (userId === operator.id) {
ctx.body = { code: 400, message: '不能禁用自己' }
return
}
const result = await query('UPDATE users SET status = 0 WHERE id = ?', [userId])
if (result.affectedRows > 0) {
@@ -341,15 +417,16 @@ async function changePassword(ctx) {
const user = users[0]
if (user.password !== md5(oldPassword)) {
if (!verifyPassword(oldPassword, user.password)) {
ctx.body = {
code: 400,
message: '原密码错误'
}
return
}
await query('UPDATE users SET password = ? WHERE id = ?', [md5(newPassword), user.id])
// 如果是旧版MD5哈希,登录时已自动升级;此处确保修改密码后使用scrypt
await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(newPassword), user.id])
ctx.body = {
code: 200,
@@ -357,10 +434,13 @@ async function changePassword(ctx) {
}
}
// 重置密码(员操作)
// 重置密码(管理员操作)
async function resetPassword(ctx) {
const operator = await requireAdminAuth(ctx)
if (!operator) return
const { userId } = ctx.request.body
if (!userId) {
ctx.body = {
code: 400,
@@ -368,10 +448,10 @@ async function resetPassword(ctx) {
}
return
}
const defaultPassword = DEFAULT_PASSWORD
const result = await query('UPDATE users SET password = ? WHERE id = ?', [md5(defaultPassword), userId])
const result = await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(defaultPassword), userId])
if (result.affectedRows > 0) {
ctx.body = {
@@ -386,61 +466,98 @@ async function resetPassword(ctx) {
}
}
// 调整积分
// 调整积分(店员或管理员可操作)
async function addPoints(ctx) {
const operator = await requireStaffAuth(ctx)
if (!operator) return
const { userId, points, description } = ctx.request.body
if (!userId || points === undefined) {
ctx.body = {
code: 400,
message: '请指定用户ID和积分变动值'
}
ctx.body = { code: 400, message: '请指定用户ID和积分变动值' }
return
}
const pointsInt = parseInt(points)
if (isNaN(pointsInt)) {
ctx.body = {
code: 400,
message: '积分值无效'
}
ctx.body = { code: 400, message: '积分值无效' }
return
}
// 检查用户是否存在
const MAX_DELTA = 100000
if (Math.abs(pointsInt) > MAX_DELTA) {
ctx.body = { code: 400, message: `单次积分变动不能超过 ${MAX_DELTA}` }
return
}
if (pointsInt === 0) {
ctx.body = { code: 400, message: '积分变动值不能为0' }
return
}
if (pointsInt < 0) {
ctx.body = { code: 400, message: '不支持直接扣除积分,请使用专门的积分扣除接口' }
return
}
const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [userId])
if (users.length === 0) {
ctx.body = {
code: 404,
message: '用户不存在'
}
ctx.body = { code: 404, message: '用户不存在' }
return
}
const user = users[0]
const newPoints = Math.max(0, user.points + pointsInt)
// 更新用户积分
await query('UPDATE users SET points = ? WHERE id = ?', [newPoints, userId])
// 记录积分变动日志
await query(
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
[userId, pointsInt >= 0 ? 'earn' : 'spend', Math.abs(pointsInt), description || '管理员调整积分']
)
const newPoints = user.points + pointsInt
await transaction(async (conn) => {
await conn.execute('UPDATE users SET points = ? WHERE id = ?', [newPoints, userId])
await conn.execute(
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
[userId, 'earn', Math.abs(pointsInt), description || '店员/管理员增加积分']
)
})
ctx.body = {
code: 200,
message: '积分调整成功',
data: {
userId,
oldPoints: user.points,
newPoints,
change: pointsInt
}
message: '积分增加成功',
data: { userId, oldPoints: user.points, newPoints, change: pointsInt }
}
}
// 退出登录(使服务端 token 失效)
async function logout(ctx) {
const user = await getUserByToken(extractToken(ctx))
if (user) {
await query('UPDATE users SET token = NULL WHERE id = ?', [user.id])
}
ctx.body = { code: 200, message: '已退出登录' }
}
// 刷新 access token
async function refreshToken(ctx) {
const { refreshToken: token } = ctx.request.body
if (!token) {
ctx.status = 400
ctx.body = { code: 400, message: '缺少 refreshToken' }
return
}
const payload = verifyToken(token)
if (!payload || payload.type !== 'refresh') {
ctx.status = 401
ctx.body = { code: 401, message: 'refreshToken 无效或已过期' }
return
}
const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [payload.sub])
if (users.length === 0) {
ctx.status = 401
ctx.body = { code: 401, message: '账号不存在或已禁用' }
return
}
const user = users[0]
const tokenObj = await issueTokenPair(user)
ctx.body = { code: 200, data: publicUser(user, tokenObj) }
}
// 获取积分记录
async function getPointsLogs(ctx) {
const userId = parseInt(ctx.query.userId)
@@ -464,8 +581,69 @@ async function getPointsLogs(ctx) {
}
}
// 微信一键登录
async function wechatLogin(ctx) {
const { code } = ctx.request.body
if (!code) {
ctx.body = { code: 400, message: '缺少微信登录code' }
return
}
const fetch = require('node-fetch')
const APPID = process.env.WECHAT_APPID
const SECRET = process.env.WECHAT_SECRET
if (!APPID || !SECRET) {
ctx.body = { code: 500, message: '微信登录未配置' }
return
}
try {
// 调用微信接口获取 openid 和 session_key
const res = await fetch(
`https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
)
const data = await res.json()
if (data.errcode) {
ctx.body = { code: 400, message: `微信登录失败: ${data.errmsg}` }
return
}
const { openid } = data
// 查找是否已有绑定该 openid 的用户
const users = await query('SELECT * FROM users WHERE openid = ? AND status = 1', [openid])
if (users.length > 0) {
// 已有用户,直接登录
const user = users[0]
const tokenObj = await issueTokenPair(user)
ctx.body = { code: 200, data: publicUser(user, tokenObj) }
} else {
// 新用户,自动注册
const phone = `wx_${openid.slice(0, 10)}`
const name = `微信用户`
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role, openid) VALUES (?, ?, ?, ?, ?, ?, ?)',
[phone, hashPassword(Math.random().toString(36)), name, '', 0, 0, openid]
)
const newUser = { id: result.insertId, phone, name, avatar: '', points: 0, role: 0, openid }
const tokenObj = await issueTokenPair(newUser)
ctx.body = { code: 200, data: publicUser(newUser, tokenObj), isNewUser: true }
}
} catch (err) {
console.error('微信登录异常:', err)
ctx.body = { code: 500, message: '微信登录失败' }
}
}
module.exports = {
requireAuth,
requireStaffAuth,
requireAdminAuth,
login,
wechatLogin,
register,
registerStaff,
registerByStaff,
@@ -476,5 +654,7 @@ module.exports = {
changePassword,
resetPassword,
addPoints,
getPointsLogs
getPointsLogs,
logout,
refreshToken
}