更新完善页面
This commit is contained in:
+66
-15
@@ -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])
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user