Ai config

This commit is contained in:
董海洋
2026-05-26 13:37:55 +08:00
parent 55452a2d21
commit 0c7ed3498d
42 changed files with 1264 additions and 767 deletions
+7
View File
@@ -9,6 +9,13 @@ DB_NAME=miniprogram
# AI 配置(阿里云 DashScope
DASHSCOPE_API_KEY=sk-your-api-key
# 微信小程序配置(订阅消息用)
WECHAT_APPID=your_appid
WECHAT_SECRET=your_secret
WECHAT_ORDER_TEMPLATE_ID=your_template_id
# 服务器配置
PORT=3006
NODE_ENV=development
CORS_ORIGIN=*
BASE_URL=http://your_ip_or_domain:3006
+16
View File
@@ -0,0 +1,16 @@
{
"env": {
"node": true,
"commonjs": true,
"es2022": true
},
"extends": ["eslint:recommended"],
"parserOptions": {
"ecmaVersion": 2022
},
"rules": {
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"no-console": "off",
"no-undef": "error"
}
}
+13 -3
View File
@@ -9,14 +9,14 @@ const app = new Koa()
const router = new Router()
app.use(cors({
origin: '*',
origin: process.env.CORS_ORIGIN || '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}))
app.use(bodyParser({
jsonLimit: '10mb',
formLimit: '10mb'
jsonLimit: '5mb',
formLimit: '5mb'
}))
app.use(require('koa-static')(path.join(__dirname, 'public')))
@@ -56,6 +56,11 @@ const statsRoutes = require('./routes/stats')
const priceListRoutes = require('./routes/price-list')
const pointsLogsRoutes = require('./routes/points-logs')
const recognizeRoutes = require('./routes/recognize')
const reportRoutes = require('./routes/reports')
const exportRoutes = require('./routes/export')
const subscribeRoutes = require('./routes/subscribe')
const addressRoutes = require('./routes/addresses')
const goodsSpecRoutes = require('./routes/goods-specs')
router.use('/api/orders', orderRoutes)
router.use('/api/categories', categoryRoutes)
@@ -71,6 +76,11 @@ router.use('/api/stats', statsRoutes)
router.use('/api/price-list', priceListRoutes)
router.use('/api/points/logs', pointsLogsRoutes)
router.use('/api/recognize', recognizeRoutes)
router.use('/api/reports', reportRoutes)
router.use('/api/export', exportRoutes)
router.use('/api/subscribe', subscribeRoutes)
router.use('/api/addresses', addressRoutes)
router.use('/api/goods-specs', goodsSpecRoutes)
app.use(router.routes())
app.use(router.allowedMethods())
+42
View File
@@ -0,0 +1,42 @@
module.exports = {
// 积分倍率 (每消费1元获得的积分)
POINTS_RATE: 1,
// 默认密码
DEFAULT_PASSWORD: process.env.DEFAULT_PASSWORD || '123456',
// 利润率估算
COST_RATIO: 0.6,
PROFIT_RATIO: 0.4,
// 分页默认值
DEFAULT_PAGE_SIZE: 20,
MAX_PAGE_SIZE: 100,
// 库存预警阈值
LOW_STOCK_THRESHOLD: 5,
// 上传限制
UPLOAD_MAX_SIZE: 5 * 1024 * 1024,
UPLOAD_ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
// AI 配置
AI_TEXT_MODEL: 'qwen3.5-flash',
AI_VL_MODEL: 'qwen-vl-max',
AI_TEMPERATURE: 0.7,
AI_VL_TEMPERATURE: 0.3,
AI_MAX_TOKENS: 500,
AI_TIMEOUT: 30000,
AI_VL_TIMEOUT: 60000,
AI_RECOGNIZE_LIMIT: 5,
// 微信通知
WECHAT_TOKEN_BUFFER: 300,
WECHAT_MINIPROGRAM_STATE: 'formal',
// 默认分类颜色
DEFAULT_CATEGORY_COLOR: '#1890ff',
// 订单号随机后缀范围
ORDER_ID_RANDOM_MAX: 1000,
}
+16
View File
@@ -62,9 +62,25 @@ async function initDatabase() {
}
}
async function transaction(callback) {
const connection = await pool.getConnection()
try {
await connection.beginTransaction()
const result = await callback(connection)
await connection.commit()
return result
} catch (error) {
await connection.rollback()
throw error
} finally {
connection.release()
}
}
module.exports = {
pool,
query,
transaction,
initDatabase,
config
}
+4 -10
View File
@@ -1,15 +1,9 @@
// 服务端域名配置常量 - 备案完成后只需修改这里
// 服务端域名配置 - 从环境变量读取,默认 IP
const BASE_URL = process.env.BASE_URL || 'http://110.42.255.239:3006'
const DOMAIN_CONFIG = {
// 临时域名(备案期间使用)
BASE_URL: 'http://110.42.255.239:3006',
// 正式域名(备案完成后启用,取消注释并注释上面一行)
// BASE_URL: 'https://donghy.top',
// API 路径前缀
BASE_URL: BASE_URL,
API_PATH: '/api',
// 图片路径前缀
IMG_PATH: '/img'
}
+72 -2
View File
@@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS `categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '分类名称',
`icon` varchar(50) DEFAULT NULL COMMENT '分类图标',
`color` varchar(20) DEFAULT '#1890ff' COMMENT '分类颜色',
`is_show` tinyint(1) DEFAULT 1 COMMENT '是否显示',
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
@@ -9,6 +10,8 @@ CREATE TABLE IF NOT EXISTS `categories` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品分类表';
ALTER TABLE `categories` ADD COLUMN IF NOT EXISTS `color` varchar(20) DEFAULT '#1890ff' COMMENT '分类颜色';
CREATE TABLE IF NOT EXISTS `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL COMMENT '商品名称',
@@ -39,10 +42,13 @@ CREATE TABLE IF NOT EXISTS `users` (
`points` int(11) DEFAULT 0 COMMENT '积分',
`role` tinyint(1) DEFAULT 0 COMMENT '角色 0-普通用户 1-店员',
`status` tinyint(1) DEFAULT 1 COMMENT '状态 1-正常 0-禁用',
`openid` varchar(100) DEFAULT NULL COMMENT '微信openid',
`token` varchar(100) DEFAULT NULL COMMENT '登录token',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `phone` (`phone`)
UNIQUE KEY `phone` (`phone`),
KEY `openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
CREATE TABLE IF NOT EXISTS `orders` (
@@ -59,6 +65,23 @@ CREATE TABLE IF NOT EXISTS `orders` (
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
CREATE TABLE IF NOT EXISTS `order_items` (
`id` int NOT NULL AUTO_INCREMENT,
`order_id` varchar(64) NOT NULL COMMENT '订单ID',
`goods_id` int NOT NULL COMMENT '商品ID',
`goods_name` varchar(255) NOT NULL COMMENT '商品名称',
`price` decimal(10,2) NOT NULL COMMENT '单价',
`quantity` int NOT NULL DEFAULT 1 COMMENT '数量',
`weight` decimal(10,2) DEFAULT NULL COMMENT '称重(kg)',
`subtotal` decimal(10,2) NOT NULL COMMENT '小计',
`unit` varchar(20) DEFAULT '' COMMENT '单位',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `order_id` (`order_id`),
KEY `goods_id` (`goods_id`),
CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单商品明细表';
CREATE TABLE IF NOT EXISTS `points_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
@@ -123,6 +146,34 @@ CREATE TABLE IF NOT EXISTS `purchase_items` (
CONSTRAINT `purchase_items_ibfk_2` FOREIGN KEY (`goods_id`) REFERENCES `goods` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采购单明细表';
CREATE TABLE IF NOT EXISTS `goods_specs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_id` int(11) NOT NULL COMMENT '商品ID',
`spec_name` varchar(100) NOT NULL COMMENT '规格名称(如 330ml/500ml/1L',
`price` decimal(10,2) NOT NULL COMMENT '规格售价',
`stock` int(11) DEFAULT 0 COMMENT '规格库存',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `goods_id` (`goods_id`),
CONSTRAINT `goods_specs_ibfk_1` FOREIGN KEY (`goods_id`) REFERENCES `goods` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品规格表';
CREATE TABLE IF NOT EXISTS `addresses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
`name` varchar(100) NOT NULL COMMENT '收货人姓名',
`phone` varchar(20) NOT NULL COMMENT '联系电话',
`region` varchar(200) DEFAULT '' COMMENT '地区',
`detail` varchar(500) NOT NULL COMMENT '详细地址',
`is_default` tinyint(1) DEFAULT 0 COMMENT '是否默认',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `addresses_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收货地址表';
CREATE TABLE IF NOT EXISTS `points_goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL COMMENT '商品名称',
@@ -134,4 +185,23 @@ CREATE TABLE IF NOT EXISTS `points_goods` (
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分商品表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分商品表';
-- 订单表索引:按状态筛选、按时间排序
ALTER TABLE `orders` ADD INDEX IF NOT EXISTS `idx_orders_status` (`status`);
ALTER TABLE `orders` ADD INDEX IF NOT EXISTS `idx_orders_created_at` (`created_at`);
-- 商品表索引:热销/新品筛选、名称搜索
ALTER TABLE `goods` ADD INDEX IF NOT EXISTS `idx_goods_is_hot` (`is_hot`);
ALTER TABLE `goods` ADD INDEX IF NOT EXISTS `idx_goods_is_new` (`is_new`);
ALTER TABLE `goods` ADD INDEX IF NOT EXISTS `idx_goods_name` (`name`(100));
-- 分类表索引:按排序字段排序
ALTER TABLE `categories` ADD INDEX IF NOT EXISTS `idx_categories_sort_order` (`sort_order`);
-- 积分记录表索引:按类型筛选
ALTER TABLE `points_logs` ADD INDEX IF NOT EXISTS `idx_points_logs_type` (`type`);
-- 采购单表索引:按状态筛选、按时间排序
ALTER TABLE `purchases` ADD INDEX IF NOT EXISTS `idx_purchases_status` (`status`);
ALTER TABLE `purchases` ADD INDEX IF NOT EXISTS `idx_purchases_created_at` (`created_at`);
+109
View File
@@ -0,0 +1,109 @@
const { query } = require('../config/database')
async function getAddresses(ctx) {
const userId = ctx.query.user_id
if (!userId) {
ctx.body = { code: 400, message: '缺少 user_id 参数' }
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 {
ctx.body = { code: 404, message: '地址不存在' }
}
}
async function createAddress(ctx) {
const { user_id, name, phone, region, detail, is_default = 0 } = ctx.request.body
if (!user_id || !name || !phone || !detail) {
ctx.body = { code: 400, message: '缺少必填参数' }
return
}
if (is_default) {
await query('UPDATE addresses SET is_default = 0 WHERE user_id = ?', [user_id])
}
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]
)
ctx.body = { code: 200, data: { id: result.insertId } }
}
async function updateAddress(ctx) {
const id = ctx.params.id
const updates = ctx.request.body
const current = await query('SELECT * FROM addresses WHERE id = ?', [id])
if (!current.length) {
ctx.body = { code: 404, message: '地址不存在' }
return
}
if (updates.is_default) {
await query('UPDATE addresses SET is_default = 0 WHERE user_id = ?', [current[0].user_id])
}
const fields = []
const params = []
for (const key of ['name', 'phone', 'region', 'detail', 'is_default']) {
if (updates[key] !== undefined) {
fields.push(`${key} = ?`)
params.push(key === 'is_default' ? (updates[key] ? 1 : 0) : updates[key])
}
}
if (fields.length > 0) {
params.push(id)
await query(`UPDATE addresses SET ${fields.join(', ')} WHERE id = ?`, params)
}
const updated = await query('SELECT * FROM addresses WHERE id = ?', [id])
ctx.body = { code: 200, data: updated[0] }
}
async function deleteAddress(ctx) {
const id = ctx.params.id
await query('DELETE FROM addresses WHERE id = ?', [id])
ctx.body = { code: 200, message: '删除成功' }
}
async function setDefault(ctx) {
const id = ctx.params.id
const rows = await query('SELECT * FROM addresses WHERE id = ?', [id])
if (!rows.length) {
ctx.body = { code: 404, 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])
ctx.body = { code: 200, message: '设置成功' }
}
module.exports = {
getAddresses,
getAddressById,
createAddress,
updateAddress,
deleteAddress,
setDefault
}
+7 -7
View File
@@ -26,7 +26,7 @@ async function getCategoryById(ctx) {
}
async function createCategory(ctx) {
const { name, icon, sortOrder = 0 } = ctx.request.body
const { name, icon, color, sortOrder = 0 } = ctx.request.body
if (!name) {
ctx.body = {
@@ -37,20 +37,20 @@ async function createCategory(ctx) {
}
const result = await query(
'INSERT INTO categories (name, icon, sort_order) VALUES (?, ?, ?)',
[name, icon || '', parseInt(sortOrder)]
'INSERT INTO categories (name, icon, color, sort_order) VALUES (?, ?, ?, ?)',
[name, icon || '', color || '#1890ff', parseInt(sortOrder)]
)
ctx.body = {
code: 200,
message: '添加成功',
data: { id: result.insertId, name, icon, sort_order: sortOrder }
data: { id: result.insertId, name, icon, color, sort_order: sortOrder }
}
}
async function updateCategory(ctx) {
const categoryId = parseInt(ctx.params.id)
const { name, icon, sortOrder, isShow } = ctx.request.body
const { name, icon, color, sortOrder, isShow } = ctx.request.body
if (!name) {
ctx.body = {
@@ -61,8 +61,8 @@ async function updateCategory(ctx) {
}
const result = await query(
'UPDATE categories SET name = ?, icon = ?, sort_order = ?, is_show = ? WHERE id = ?',
[name, icon || '', parseInt(sortOrder) || 0, parseInt(isShow) || 1, categoryId]
'UPDATE categories SET name = ?, icon = ?, color = ?, sort_order = ?, is_show = ? WHERE id = ?',
[name, icon || '', color || '#1890ff', parseInt(sortOrder) || 0, parseInt(isShow) || 1, categoryId]
)
if (result.affectedRows > 0) {
+87
View File
@@ -0,0 +1,87 @@
const { query } = require('../config/database')
function toCSV(headers, rows) {
const escape = (v) => {
if (v === null || v === undefined) return ''
const s = String(v)
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"'
}
return s
}
const lines = [headers.map(escape).join(',')]
for (const row of rows) {
lines.push(headers.map(h => escape(row[h])).join(','))
}
return lines.join('\n')
}
async function exportGoods(ctx) {
const rows = await query(
'SELECT g.id, g.name, g.price, g.original_price, g.unit, g.stock, g.sales, g.pricing_type, c.name as category_name FROM goods g LEFT JOIN categories c ON g.category_id = c.id ORDER BY g.id ASC'
)
const csv = toCSV(
['id', 'name', 'price', 'original_price', 'unit', 'stock', 'sales', 'category_name'],
rows
)
ctx.set('Content-Type', 'text/csv; charset=utf-8')
ctx.set('Content-Disposition', 'attachment; filename="goods.csv"')
ctx.body = '\uFEFF' + csv
}
async function exportOrders(ctx) {
const statusMap = { pending: '待付款', paid: '已付款', completed: '已完成', cancelled: '已取消' }
const rows = await query(
'SELECT o.id, o.total_price, o.status, o.created_at, u.phone as user_phone FROM orders o LEFT JOIN users u ON o.user_id = u.id ORDER BY o.created_at DESC'
)
const mapped = rows.map(r => ({
...r,
status: statusMap[r.status] || r.status,
total_price: parseFloat(r.total_price)
}))
const csv = toCSV(
['id', 'total_price', 'status', 'created_at', 'user_phone'],
mapped
)
ctx.set('Content-Type', 'text/csv; charset=utf-8')
ctx.set('Content-Disposition', 'attachment; filename="orders.csv"')
ctx.body = '\uFEFF' + csv
}
async function exportStock(ctx) {
const rows = await query(
'SELECT g.id, g.name, g.price, COALESCE(s.quantity, 0) as quantity FROM goods g LEFT JOIN stock s ON g.id = s.goods_id ORDER BY quantity ASC'
)
const csv = toCSV(
['id', 'name', 'price', 'quantity'],
rows
)
ctx.set('Content-Type', 'text/csv; charset=utf-8')
ctx.set('Content-Disposition', 'attachment; filename="stock.csv"')
ctx.body = '\uFEFF' + csv
}
async function exportPurchases(ctx) {
const rows = await query(
'SELECT p.id, p.supplier_name, p.total, p.status, p.created_at FROM purchases p ORDER BY p.created_at DESC'
)
const mapped = rows.map(r => ({
...r,
status: r.status === 0 ? '待入库' : r.status === 1 ? '已入库' : String(r.status),
total: parseFloat(r.total)
}))
const csv = toCSV(
['id', 'supplier_name', 'total', 'status', 'created_at'],
mapped
)
ctx.set('Content-Type', 'text/csv; charset=utf-8')
ctx.set('Content-Disposition', 'attachment; filename="purchases.csv"')
ctx.body = '\uFEFF' + csv
}
module.exports = {
exportGoods,
exportOrders,
exportStock,
exportPurchases
}
+83
View File
@@ -0,0 +1,83 @@
const { query, transaction } = require('../config/database')
async function getSpecs(ctx) {
const goodsId = ctx.query.goods_id
if (!goodsId) {
ctx.body = { code: 400, message: '缺少 goods_id 参数' }
return
}
const rows = await query('SELECT * FROM goods_specs WHERE goods_id = ? ORDER BY price ASC', [goodsId])
ctx.body = { code: 200, data: rows }
}
async function createSpec(ctx) {
const { goods_id, spec_name, price, stock = 0 } = ctx.request.body
if (!goods_id || !spec_name || price === undefined) {
ctx.body = { code: 400, message: '缺少必填参数' }
return
}
const result = await query(
'INSERT INTO goods_specs (goods_id, spec_name, price, stock) VALUES (?, ?, ?, ?)',
[goods_id, spec_name, price, stock]
)
ctx.body = { code: 200, data: { id: result.insertId } }
}
async function updateSpec(ctx) {
const id = ctx.params.id
const { spec_name, price, stock } = ctx.request.body
const fields = []
const params = []
if (spec_name !== undefined) { fields.push('spec_name = ?'); params.push(spec_name) }
if (price !== undefined) { fields.push('price = ?'); params.push(price) }
if (stock !== undefined) { fields.push('stock = ?'); params.push(stock) }
if (fields.length > 0) {
params.push(id)
await query(`UPDATE goods_specs SET ${fields.join(', ')} WHERE id = ?`, params)
}
ctx.body = { code: 200, message: '更新成功' }
}
async function deleteSpec(ctx) {
const id = ctx.params.id
await query('DELETE FROM goods_specs WHERE id = ?', [id])
ctx.body = { code: 200, message: '删除成功' }
}
async function batchSave(ctx) {
const { goods_id, specs } = ctx.request.body
if (!goods_id || !Array.isArray(specs)) {
ctx.body = { code: 400, message: '参数错误' }
return
}
await transaction(async (conn) => {
await conn.execute('DELETE FROM goods_specs WHERE goods_id = ?', [goods_id])
for (const spec of specs) {
if (spec.spec_name && spec.price !== undefined) {
await conn.execute(
'INSERT INTO goods_specs (goods_id, spec_name, price, stock) VALUES (?, ?, ?, ?)',
[goods_id, spec.spec_name, spec.price, spec.stock || 0]
)
}
}
})
ctx.body = { code: 200, message: '保存成功' }
}
module.exports = {
getSpecs,
createSpec,
updateSpec,
deleteSpec,
batchSave
}
+38 -6
View File
@@ -1,5 +1,23 @@
const { query } = require('../config/database')
const { toRelativeUrl } = require('../utils/image-url')
const { toRelativeUrl, processGoodsImages } = require('../utils/image-url')
const { paginate } = require('../utils/pagination')
const path = require('path')
const fs = require('fs')
function parseImages(images) {
if (!images) return []
try {
const parsed = typeof images === 'string' ? JSON.parse(images) : images
return Array.isArray(parsed) ? parsed : []
} catch { return [] }
}
function deleteImageFiles(urls) {
for (const url of urls) {
const filePath = path.join(__dirname, '..', 'public', url.replace(/^\//, ''))
fs.unlink(filePath, () => {})
}
}
async function getGoods(ctx) {
let sql = 'SELECT * FROM goods WHERE 1=1'
@@ -37,16 +55,20 @@ async function getGoods(ctx) {
sql += ' ORDER BY id DESC'
}
if (ctx.query.limit) {
if (ctx.query.limit && !ctx.query.page) {
sql += ' LIMIT ?'
params.push(parseInt(ctx.query.limit))
const goods = await query(sql, params)
ctx.body = { code: 200, data: processGoodsImages(goods) }
return
}
const goods = await query(sql, params)
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
if (result.data) result.data = processGoodsImages(result.data)
ctx.body = {
code: 200,
data: goods
...result
}
}
@@ -57,7 +79,7 @@ async function getGoodsById(ctx) {
if (goods.length > 0) {
ctx.body = {
code: 200,
data: goods[0]
data: processGoodsImages(goods)[0]
}
} else {
ctx.body = {
@@ -129,7 +151,6 @@ async function updateGoods(ctx) {
return
}
// 将图片URL转换为相对路径存储
const relativeImages = (images || []).map(img => toRelativeUrl(img))
const sql = `UPDATE goods SET
@@ -153,8 +174,14 @@ async function updateGoods(ctx) {
]
try {
const existing = await query('SELECT images FROM goods WHERE id = ?', [goodsId])
const result = await query(sql, 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: '更新成功'
@@ -178,8 +205,13 @@ async function deleteGoods(ctx) {
const goodsId = parseInt(ctx.params.id)
try {
const existing = await query('SELECT images FROM goods WHERE id = ?', [goodsId])
const result = await query('DELETE FROM goods WHERE id = ?', [goodsId])
if (result.affectedRows > 0) {
if (existing.length > 0) {
const oldImages = parseImages(existing[0].images)
deleteImageFiles(oldImages.filter(u => u.startsWith('/uploads/')))
}
ctx.body = {
code: 200,
message: '删除成功'
+79 -35
View File
@@ -1,22 +1,37 @@
const { query } = require('../config/database')
const { query, transaction } = require('../config/database')
const { paginate } = require('../utils/pagination')
const orderService = require('../services/orderService')
async function getOrders(ctx) {
const orders = await query('SELECT * FROM orders ORDER BY created_at DESC')
const { page, pageSize, status } = ctx.query
let sql = 'SELECT * FROM orders WHERE 1=1'
const params = []
if (status) {
sql += ' AND status = ?'
params.push(status)
}
sql += ' ORDER BY created_at DESC'
const result = await paginate(query, sql, params, page, pageSize)
const rows = result.data || []
await orderService.attachOrderItems(rows)
ctx.body = {
code: 200,
data: orders
...result
}
}
async function getOrderById(ctx) {
const orderId = ctx.params.id
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length > 0) {
const order = orders[0]
order.items = await orderService.getOrderItems(orderId)
ctx.body = {
code: 200,
data: orders[0]
data: order
}
} else {
ctx.body = {
@@ -27,59 +42,88 @@ async function getOrderById(ctx) {
}
async function createOrder(ctx) {
const { totalPrice, cart, userInfo } = ctx.request.body
const newOrder = {
id: `order_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
status: 'pending',
total_price: totalPrice,
cart: typeof cart === 'string' ? cart : JSON.stringify(cart),
user_info: typeof userInfo === 'string' ? userInfo : JSON.stringify(userInfo || {})
const { id, userId, totalPrice, cart, remark, customerName, customerPhone, orderType, status } = ctx.request.body
const orderId = id || `order_${Date.now()}_${Math.floor(Math.random() * 1000)}`
const userInfo = JSON.stringify({
remark: remark || '',
customerName: customerName || '',
customerPhone: customerPhone || '',
orderType: orderType || 'customer'
})
await transaction(async (conn) => {
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]
)
await orderService.insertOrderItems(conn, orderId, cart)
})
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
const created = orders[0]
created.items = await orderService.getOrderItems(orderId)
if (created.status === 'completed') {
setImmediate(() => orderService.processOrderComplete(created))
}
await query(
'INSERT INTO orders (id, status, total_price, cart, user_info) VALUES (?, ?, ?, ?, ?)',
[newOrder.id, newOrder.status, newOrder.total_price, newOrder.cart, newOrder.user_info]
)
ctx.body = {
code: 200,
data: newOrder
data: created
}
}
async function updateOrder(ctx) {
const orderId = ctx.params.id
const updates = ctx.request.body
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length > 0) {
const prevStatus = orders[0].status
const updateFields = []
const updateParams = []
if (updates.status !== undefined) {
updateFields.push('status = ?')
updateParams.push(updates.status)
}
if (updates.total_price !== undefined) {
const totalPrice = updates.total_price !== undefined ? updates.total_price : updates.totalPrice
if (totalPrice !== undefined) {
updateFields.push('total_price = ?')
updateParams.push(updates.total_price)
updateParams.push(totalPrice)
}
if (updates.cart !== undefined) {
const cartStr = typeof updates.cart === 'string' ? updates.cart : JSON.stringify(updates.cart)
updateFields.push('cart = ?')
updateParams.push(typeof updates.cart === 'string' ? updates.cart : JSON.stringify(updates.cart))
updateParams.push(cartStr)
}
updateParams.push(orderId)
await query(`UPDATE orders SET ${updateFields.join(', ')} WHERE id = ?`, updateParams)
if (updateFields.length > 0) {
updateParams.push(orderId)
await query(`UPDATE orders SET ${updateFields.join(', ')} WHERE id = ?`, updateParams)
}
if (updates.cart !== undefined) {
await transaction(async (conn) => {
await conn.execute('DELETE FROM order_items WHERE order_id = ?', [orderId])
await orderService.insertOrderItems(conn, orderId, updates.cart)
})
}
const updatedOrders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
const completed = updatedOrders[0]
completed.items = await orderService.getOrderItems(orderId)
if (completed.status === 'completed' && prevStatus !== 'completed') {
setImmediate(() => orderService.processOrderComplete(completed))
}
ctx.body = {
code: 200,
data: updatedOrders[0]
data: completed
}
} else {
ctx.body = {
@@ -94,4 +138,4 @@ module.exports = {
getOrderById,
createOrder,
updateOrder
}
}
+18 -9
View File
@@ -1,4 +1,5 @@
const { query } = require('../config/database')
const { query, transaction } = require('../config/database')
const { paginate } = require('../utils/pagination')
async function getPointsGoods(ctx) {
let sql = 'SELECT * FROM points_goods WHERE 1=1'
@@ -9,6 +10,13 @@ async function getPointsGoods(ctx) {
}
sql += ' ORDER BY points ASC'
if (ctx.query.page) {
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
ctx.body = { code: 200, ...result }
return
}
const goods = await query(sql, params)
ctx.body = {
@@ -115,14 +123,15 @@ async function exchangePointsGoods(ctx) {
return
}
const newPoints = user.points - totalPoints
await query('UPDATE users SET points = ? WHERE id = ?', [newPoints, userId])
await query('UPDATE points_goods SET stock = stock - ? WHERE id = ?', [qty, goodsId])
await query(
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
[userId, 'spend', totalPoints, `兑换「${goodsItem.name}」x${qty}`]
)
await transaction(async (conn) => {
const newPoints = user.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(
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
[userId, 'spend', totalPoints, `兑换「${goodsItem.name}」x${qty}`]
)
})
ctx.body = {
code: 200,
+39 -35
View File
@@ -1,4 +1,5 @@
const { query } = require('../config/database')
const { query, transaction } = require('../config/database')
const { paginate } = require('../utils/pagination')
async function getPurchases(ctx) {
let sql = `
@@ -14,11 +15,11 @@ async function getPurchases(ctx) {
}
sql += ' ORDER BY p.created_at DESC'
const purchases = await query(sql, params)
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
ctx.body = {
code: 200,
data: purchases
...result
}
}
@@ -69,26 +70,28 @@ async function createPurchase(ctx) {
total += (item.purchase_price || 0) * (item.quantity || 0)
}
const purchaseResult = await query(
'INSERT INTO purchases (supplier_id, supplier_name, total, remarks) VALUES (?, ?, ?, ?)',
[supplier_id, supplier.name, total, remarks || '']
)
const purchaseId = purchaseResult.insertId
for (const item of items) {
const goods = await query('SELECT name FROM goods WHERE id = ?', [item.goods_id])
const goodsName = goods.length > 0 ? goods[0].name : ''
await query(
'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]
const result = await transaction(async (conn) => {
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
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 : ''
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]
)
}
return purchaseId
})
ctx.body = {
code: 200,
message: '采购单创建成功',
data: { id: purchaseId }
data: { id: result }
}
}
@@ -109,26 +112,27 @@ async function inboundPurchase(ctx) {
const items = await query('SELECT * FROM purchase_items WHERE purchase_id = ?', [id])
for (const item of items) {
const existing = await query('SELECT * FROM stock WHERE goods_id = ?', [item.goods_id])
if (existing.length > 0) {
await query(
'UPDATE stock SET quantity = quantity + ? WHERE goods_id = ?',
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) {
await conn.execute(
'UPDATE stock SET quantity = quantity + ? WHERE goods_id = ?',
[item.quantity, item.goods_id]
)
} else {
await conn.execute(
'INSERT INTO stock (goods_id, quantity, warehouse) VALUES (?, ?, ?)',
[item.goods_id, item.quantity, '默认仓库']
)
}
await conn.execute(
'UPDATE goods SET stock = stock + ? WHERE id = ?',
[item.quantity, item.goods_id]
)
} else {
await query(
'INSERT INTO stock (goods_id, quantity, warehouse) VALUES (?, ?, ?)',
[item.goods_id, item.quantity, '默认仓库']
)
}
await query(
'UPDATE goods SET stock = stock + ? WHERE id = ?',
[item.quantity, item.goods_id]
)
}
await query('UPDATE purchases SET status = 1 WHERE id = ?', [id])
await conn.execute('UPDATE purchases SET status = 1 WHERE id = ?', [id])
})
ctx.body = {
code: 200,
+78
View File
@@ -0,0 +1,78 @@
const { query } = require('../config/database')
const fetch = require('node-fetch')
const { AI_VL_MODEL, AI_RECOGNIZE_LIMIT } = require('../config/constants')
const AI_API_KEY = process.env.DASHSCOPE_API_KEY
const AI_API_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
async function getByBarcode(ctx) {
try {
const { barcode } = ctx.request.body
if (!barcode) {
ctx.body = { code: 400, message: '请提供条形码' }
return
}
const goods = await query('SELECT * FROM goods WHERE barcode = ? LIMIT 1', [barcode])
if (goods.length > 0) {
ctx.body = { code: 200, data: goods[0] }
} else {
ctx.body = { code: 404, message: '未找到该商品' }
}
} catch (error) {
console.error('条码查询失败:', error)
ctx.body = { code: 500, message: '查询失败' }
}
}
async function recognizeImage(ctx) {
try {
const { imageData } = ctx.request.body
if (!imageData) {
ctx.body = { code: 400, message: '请提供图片数据' }
return
}
if (!AI_API_KEY) {
ctx.body = { code: 500, message: 'AI 识别未配置' }
return
}
const response = await fetch(AI_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: AI_VL_MODEL,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: '请识别这张图片中的商品,返回商品名称。只返回名称,不要其他内容。' },
{ type: 'image_url', image_url: { url: imageData } }
]
}
]
})
})
const result = await response.json()
const name = result?.choices?.[0]?.message?.content?.trim() || ''
const goods = name
? await query('SELECT * FROM goods WHERE name LIKE ? LIMIT ?', [`%${name}%`, AI_RECOGNIZE_LIMIT])
: []
ctx.body = {
code: 200,
data: {
message: name ? `识别到: ${name}` : '未识别到商品',
goods: goods || []
}
}
} catch (error) {
console.error('图片识别失败:', error)
ctx.body = { code: 500, message: '识别失败' }
}
}
module.exports = { getByBarcode, recognizeImage }
+131
View File
@@ -0,0 +1,131 @@
const { query } = require('../config/database')
const { COST_RATIO, PROFIT_RATIO } = require('../config/constants')
async function getSalesTrend(ctx) {
const days = parseInt(ctx.query.days) || 30
const group = ctx.query.group || 'day'
let dateFormat
if (group === 'week') {
dateFormat = 'DATE_FORMAT(created_at, \'%x-W%v\')'
} else if (group === 'month') {
dateFormat = 'DATE_FORMAT(created_at, \'%Y-%m\')'
} else {
dateFormat = 'DATE(created_at)'
}
const rows = await query(
`SELECT ${dateFormat} as period,
COUNT(*) as order_count,
COALESCE(SUM(total_price), 0) as total_sales
FROM orders
WHERE status IN ('paid', 'completed')
AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)
GROUP BY period
ORDER BY period ASC`,
[days]
)
ctx.body = { code: 200, data: rows }
}
async function getHotProducts(ctx) {
const limit = parseInt(ctx.query.limit) || 10
const rows = await query(
`SELECT id, name, price, sales, stock,
COALESCE(s.quantity, 0) as stock_qty
FROM goods
LEFT JOIN stock s ON goods.id = s.goods_id
ORDER BY sales DESC
LIMIT ?`,
[limit]
)
ctx.body = { code: 200, data: rows }
}
async function getProfitAnalysis(ctx) {
const days = parseInt(ctx.query.days) || 30
const revenueRows = await query(
`SELECT DATE(created_at) as date,
COUNT(*) as order_count,
COALESCE(SUM(total_price), 0) as revenue
FROM orders
WHERE status IN ('paid', 'completed')
AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC`,
[days]
)
const avgCost = await query(
`SELECT COALESCE(AVG(purchase_price), 0) as avg_cost
FROM purchase_items`
)
const costPerUnit = parseFloat(avgCost[0]?.avg_cost || 0)
const enriched = revenueRows.map(row => ({
...row,
revenue: parseFloat(row.revenue),
estimated_cost: parseFloat(row.revenue) * COST_RATIO,
estimated_profit: parseFloat(row.revenue) * PROFIT_RATIO
}))
const totalRevenue = enriched.reduce((s, r) => s + r.revenue, 0)
const totalCost = enriched.reduce((s, r) => s + r.estimated_cost, 0)
const totalProfit = enriched.reduce((s, r) => s + r.estimated_profit, 0)
ctx.body = {
code: 200,
data: {
days,
summary: {
total_revenue: totalRevenue,
total_cost: totalCost,
total_profit: totalProfit,
profit_margin: totalRevenue > 0 ? ((totalProfit / totalRevenue) * 100).toFixed(1) : '0.0',
avg_purchase_cost: costPerUnit
},
details: enriched
}
}
}
async function getInventoryTurnover(ctx) {
const rows = await query(
`SELECT g.id, g.name, g.price, g.sales,
COALESCE(s.quantity, 0) as stock_qty,
CASE
WHEN COALESCE(s.quantity, 0) <= 0 THEN g.sales
ELSE ROUND(g.sales / s.quantity, 2)
END as turnover_ratio
FROM goods g
LEFT JOIN stock s ON g.id = s.goods_id
ORDER BY turnover_ratio DESC`
)
const lowStock = rows.filter(r => r.stock_qty <= 5)
const outOfStock = rows.filter(r => r.stock_qty <= 0)
const slowMoving = rows.filter(r => r.turnover_ratio < 0.1 && r.sales > 0)
ctx.body = {
code: 200,
data: {
total_items: rows.length,
low_stock_count: lowStock.length,
out_of_stock_count: outOfStock.length,
slow_moving_count: slowMoving.length,
items: rows
}
}
}
module.exports = {
getSalesTrend,
getHotProducts,
getProfitAnalysis,
getInventoryTurnover
}
+3 -2
View File
@@ -1,4 +1,5 @@
const { query } = require('../config/database')
const { paginate } = require('../utils/pagination')
// 获取库存列表
async function getStockList(ctx) {
@@ -32,11 +33,11 @@ async function getStockList(ctx) {
sql += ' ORDER BY s.quantity ASC'
const stockList = await query(sql, params)
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
ctx.body = {
code: 200,
data: stockList
...result
}
}
+37
View File
@@ -0,0 +1,37 @@
const { query } = require('../config/database')
async function bindOpenId(ctx) {
const { userId, openid } = ctx.request.body
if (!userId || !openid) {
ctx.body = { code: 400, message: '缺少参数' }
return
}
await query('UPDATE users SET openid = ? WHERE id = ?', [openid, userId])
ctx.body = { code: 200, message: '绑定成功' }
}
async function notifyOrder(ctx) {
const { orderId } = ctx.request.body
const orders = await query(
'SELECT o.*, u.openid FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.id = ?',
[orderId]
)
if (!orders.length) {
ctx.body = { code: 404, message: '订单不存在' }
return
}
const order = orders[0]
if (!order.openid) {
ctx.body = { code: 400, message: '用户未绑定微信' }
return
}
try {
const { sendOrderStatusNotification } = require('../utils/wechat')
const result = await sendOrderStatusNotification(order.openid, order.id, order.status, order.total_price)
ctx.body = { code: 200, data: result }
} catch (e) {
ctx.body = { code: 500, message: e.message }
}
}
module.exports = { bindOpenId, notifyOrder }
+3 -2
View File
@@ -1,4 +1,5 @@
const { query } = require('../config/database')
const { paginate } = require('../utils/pagination')
async function getSuppliers(ctx) {
let sql = 'SELECT * FROM suppliers WHERE 1=1'
@@ -10,11 +11,11 @@ async function getSuppliers(ctx) {
}
sql += ' ORDER BY id DESC'
const suppliers = await query(sql, params)
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
ctx.body = {
code: 200,
data: suppliers
...result
}
}
+72 -96
View File
@@ -1,5 +1,7 @@
const { query } = 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')
@@ -59,6 +61,7 @@ async function login(ctx) {
}
const token = generateToken()
await query('UPDATE users SET token = ? WHERE id = ?', [token, user.id])
const userInfo = {
id: user.id,
@@ -79,129 +82,113 @@ async function login(ctx) {
// 用户注册(普通用户)
async function register(ctx) {
const { phone, password, name } = ctx.request.body
if (!phone || !password || !name) {
ctx.body = {
code: 400,
message: '请填写完整信息'
}
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: '该手机号已注册'
}
ctx.body = { code: 400, message: '该手机号已注册' }
return
}
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(password), name, '', 0, 0]
)
ctx.body = {
code: 200,
message: '注册成功',
data: { id: result.insertId, phone, name, avatar: '', points: 0, role: 0 }
}
}
async function requireStaffAuth(ctx) {
async function requireStaffAuth(ctx) {
const authHeader = ctx.headers.authorization || ''
const token = authHeader.replace('Bearer ', '')
if (!token) {
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: '权限不足,仅店员可操作' }
return null
}
return operators[0]
}
async function createUser(phone, name, role) {
const existing = await query('SELECT * FROM users WHERE phone = ?', [phone])
if (existing.length > 0) return { conflict: true }
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(DEFAULT_PASSWORD), name, '', 0, role]
)
return {
conflict: false,
data: {
id: result.insertId,
phone,
name,
avatar: '',
points: 0,
role: 0
role
}
}
}
// 店员注册(由店员帮助用户注册或店员自己注册
// 店员注册(需要店员权限
async function registerStaff(ctx) {
const { phone, name } = ctx.request.body
if (!phone || !name) {
ctx.body = {
code: 400,
message: '请填写手机号和姓名'
}
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: '该手机号已注册'
}
const operator = await requireStaffAuth(ctx)
if (!operator) return
const result = await createUser(phone, name, 1)
if (result.conflict) {
ctx.body = { code: 400, message: '该手机号已注册' }
return
}
// 默认密码 123456
const defaultPassword = '123456'
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(defaultPassword), name, '', 0, 1]
)
ctx.body = {
code: 200,
message: '店员注册成功,默认密码为123456',
data: {
id: result.insertId,
phone,
name,
avatar: '',
points: 0,
role: 1
}
message: `店员注册成功,默认密码为${DEFAULT_PASSWORD}`,
data: result.data
}
}
// 店员帮助用户注册
// 店员帮助用户注册(需要店员权限)
async function registerByStaff(ctx) {
const { phone, name } = ctx.request.body
if (!phone || !name) {
ctx.body = {
code: 400,
message: '请填写手机号和姓名'
}
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: '该手机号已注册'
}
const operator = await requireStaffAuth(ctx)
if (!operator) return
const result = await createUser(phone, name, 0)
if (result.conflict) {
ctx.body = { code: 400, message: '该手机号已注册' }
return
}
// 默认密码 123456
const defaultPassword = '123456'
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(defaultPassword), name, '', 0, 0]
)
ctx.body = {
code: 200,
message: '用户注册成功,默认密码为123456',
data: {
id: result.insertId,
phone,
name,
avatar: '',
points: 0,
role: 0
}
message: `用户注册成功,默认密码为${DEFAULT_PASSWORD}`,
data: result.data
}
}
@@ -210,18 +197,7 @@ async function getUserInfo(ctx) {
const userId = parseInt(ctx.query.id)
if (!userId) {
// 返回默认用户信息
ctx.body = {
code: 200,
data: {
id: 1,
phone: '13800138000',
name: '张三',
avatar: '',
points: 1000,
role: 0
}
}
ctx.body = { code: 400, message: '缺少用户ID' }
return
}
@@ -265,11 +241,11 @@ async function getUsers(ctx) {
sql += ' ORDER BY created_at DESC'
const users = await query(sql, params)
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
ctx.body = {
code: 200,
data: users
...result
}
}
@@ -394,14 +370,14 @@ async function resetPassword(ctx) {
return
}
const defaultPassword = '123456'
const defaultPassword = DEFAULT_PASSWORD
const result = await query('UPDATE users SET password = ? WHERE id = ?', [md5(defaultPassword), userId])
if (result.affectedRows > 0) {
ctx.body = {
code: 200,
message: '密码已重置为123456'
message: `密码已重置为${DEFAULT_PASSWORD}`
}
} else {
ctx.body = {
-1
View File
@@ -1 +0,0 @@
[{"id": 1, "name": "生鲜果蔬", "icon": "🍎", "is_show": 1, "sort_order": 1}, {"id": 2, "name": "休闲零食", "icon": "🍪", "is_show": 1, "sort_order": 2}, {"id": 3, "name": "酒水饮料", "icon": "🍺", "is_show": 1, "sort_order": 3}, {"id": 4, "name": "粮油调味", "icon": "🍚", "is_show": 1, "sort_order": 4}, {"id": 5, "name": "日用百货", "icon": "🧴", "is_show": 1, "sort_order": 5}, {"id": 10, "name": "乳制品", "icon": "🥛", "is_show": 1, "sort_order": 6}, {"id": 13, "name": "冷冻食品", "icon": "🧊", "is_show": 1, "sort_order": 7}, {"id": 37, "name": "速食方便", "icon": "🍜", "is_show": 1, "sort_order": 8}, {"id": 42, "name": "肉禽蛋品", "icon": "🥩", "is_show": 1, "sort_order": 9}, {"id": 49, "name": "海鲜水产", "icon": "🦐", "is_show": 1, "sort_order": 10}, {"id": 11, "name": "烘焙糕点", "icon": "🍰", "is_show": 0, "sort_order": 11}, {"id": 12, "name": "保健品", "icon": "💊", "is_show": 0, "sort_order": 12}]
-1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
[{"id": 1, "phone": "13800138000", "password": "e10adc3949ba59abbe56e057f20f883e", "name": "张三", "avatar": "", "points": 1000, "created_at": "2024-01-01T00:00:00Z"}, {"id": 2, "phone": "13900139000", "password": "e10adc3949ba59abbe56e057f20f883e", "name": "李四", "avatar": "", "points": 500, "created_at": "2024-02-01T00:00:00Z"}]
-65
View File
@@ -1,65 +0,0 @@
server {
listen 80;
server_name donghy.top www.donghy.top;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name donghy.top www.donghy.top;
ssl_certificate /etc/nginx/ssl/donghy.top_bundle.pem;
ssl_certificate_key /etc/nginx/ssl/donghy.top.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
keepalive_timeout 65;
keepalive_requests 100;
tcp_nodelay on;
tcp_nopush on;
location /img/ {
alias /var/www/node/services/public/uploads/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin *;
}
location /api/ {
proxy_pass http://127.0.0.1:3006/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
}
location / {
root /var/www/html;
index index.html index.htm;
}
}
+9 -2
View File
@@ -5,7 +5,9 @@
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "node app.js"
"dev": "node app.js",
"test": "jest --forceExit --detectOpenHandles",
"test:watch": "jest --watch"
},
"dependencies": {
"@koa/cors": "^4.0.0",
@@ -21,5 +23,10 @@
},
"keywords": [],
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"eslint": "^9.0.0",
"jest": "^29.0.0",
"prettier": "^3.5.0"
}
}
+13
View File
@@ -0,0 +1,13 @@
const Router = require('koa-router')
const addressController = require('../controllers/addresses')
const router = new Router()
router.get('/', addressController.getAddresses)
router.get('/:id', addressController.getAddressById)
router.post('/', addressController.createAddress)
router.put('/:id', addressController.updateAddress)
router.delete('/:id', addressController.deleteAddress)
router.put('/:id/default', addressController.setDefault)
module.exports = router.routes()
+11
View File
@@ -0,0 +1,11 @@
const Router = require('koa-router')
const exportController = require('../controllers/export')
const router = new Router()
router.get('/goods', exportController.exportGoods)
router.get('/orders', exportController.exportOrders)
router.get('/stock', exportController.exportStock)
router.get('/purchases', exportController.exportPurchases)
module.exports = router.routes()
+12
View File
@@ -0,0 +1,12 @@
const Router = require('koa-router')
const specController = require('../controllers/goods-specs')
const router = new Router()
router.get('/', specController.getSpecs)
router.post('/', specController.createSpec)
router.put('/:id', specController.updateSpec)
router.delete('/:id', specController.deleteSpec)
router.post('/batch', specController.batchSave)
module.exports = router.routes()
+3 -82
View File
@@ -1,87 +1,8 @@
const Router = require('koa-router')
const { query } = require('../config/database')
const fetch = require('node-fetch')
require('dotenv').config()
const router = new Router()
const AI_API_KEY = process.env.DASHSCOPE_API_KEY
const AI_API_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
const { getByBarcode, recognizeImage } = require('../controllers/recognize')
router.post('/barcode', async (ctx) => {
try {
const { barcode } = ctx.request.body
if (!barcode) {
ctx.body = { code: 400, message: '请提供条形码' }
return
}
const goods = await query(
'SELECT * FROM goods WHERE barcode = ? LIMIT 1',
[barcode]
)
if (goods.length > 0) {
ctx.body = { code: 200, data: goods[0] }
} else {
ctx.body = { code: 404, message: '未找到该商品' }
}
} catch (error) {
console.error('Barcode lookup failed:', error)
ctx.body = { code: 500, message: '查询失败' }
}
})
router.post('/image', async (ctx) => {
try {
const { imageData } = ctx.request.body
if (!imageData) {
ctx.body = { code: 400, message: '请提供图片数据' }
return
}
if (!AI_API_KEY) {
ctx.body = { code: 500, message: 'AI 识别未配置' }
return
}
const response = await fetch(AI_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'qwen-vl-max',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: '请识别这张图片中的商品,返回商品名称。只返回名称,不要其他内容。' },
{ type: 'image_url', image_url: { url: imageData } }
]
}
]
})
})
const result = await response.json()
const name = result?.choices?.[0]?.message?.content?.trim() || ''
const goods = name
? await query('SELECT * FROM goods WHERE name LIKE ? LIMIT 5', [`%${name}%`])
: []
ctx.body = {
code: 200,
data: {
message: name ? `识别到: ${name}` : '未识别到商品',
goods: goods || []
}
}
} catch (error) {
console.error('Image recognition failed:', error)
ctx.body = { code: 500, message: '识别失败' }
}
})
router.post('/barcode', getByBarcode)
router.post('/image', recognizeImage)
module.exports = router.routes()
+11
View File
@@ -0,0 +1,11 @@
const Router = require('koa-router')
const reportsController = require('../controllers/reports')
const router = new Router()
router.get('/sales-trend', reportsController.getSalesTrend)
router.get('/hot-products', reportsController.getHotProducts)
router.get('/profit', reportsController.getProfitAnalysis)
router.get('/inventory-turnover', reportsController.getInventoryTurnover)
module.exports = router.routes()
+8
View File
@@ -0,0 +1,8 @@
const Router = require('koa-router')
const router = new Router()
const { bindOpenId, notifyOrder } = require('../controllers/subscribe')
router.post('/bind-openid', bindOpenId)
router.post('/orders/notify', notifyOrder)
module.exports = router.routes()
+24 -15
View File
@@ -5,16 +5,19 @@ const fs = require('fs')
const router = new Router()
// 确保上传目录存在
const uploadDir = path.join(__dirname, '..', 'public', 'uploads')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
const MAX_SIZE = 5 * 1024 * 1024
const uploadDir = path.join(__dirname, '..', 'public', 'uploads')
// 配置 multer
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir)
const type = req.query.type || 'goods'
const dir = path.join(uploadDir, type)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
cb(null, dir)
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9)
@@ -23,21 +26,27 @@ const storage = multer.diskStorage({
}
})
const upload = multer({ storage })
const upload = multer({
storage,
limits: { fileSize: MAX_SIZE },
fileFilter: (req, file, cb) => {
if (ALLOWED_TYPES.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('不支持的文件类型,仅支持 jpg/png/gif/webp'))
}
}
})
// 上传接口
router.post('/', upload.single('file'), async (ctx) => {
if (!ctx.file) {
ctx.status = 400
ctx.body = {
code: 400,
message: '没有上传文件'
}
ctx.body = { code: 400, message: '没有上传文件' }
return
}
// 存储相对路径,前端使用时拼接域名
const fileUrl = `/uploads/${ctx.file.filename}`
const type = ctx.query.type || 'goods'
const fileUrl = `/uploads/${type}/${ctx.file.filename}`
ctx.body = {
code: 200,
message: '上传成功',
-101
View File
@@ -1,101 +0,0 @@
const mysql = require('mysql2/promise');
const config = require('../config/database');
const { DOMAIN_CONFIG } = require('../config/domain');
const { toRelativeUrl } = require('../utils/image-url');
async function convertToRelativeUrls() {
try {
const connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功\n');
console.log('🔍 检查数据库中的图片URL...');
const [goods] = await connection.execute('SELECT id, images FROM goods');
console.log(`找到 ${goods.length} 条商品记录\n`);
let updatedCount = 0;
// 更新图片URL
for (const item of goods) {
if (item.images) {
let oldImages = item.images;
let newImages = oldImages;
try {
// 尝试解析JSON
let imagesArray = JSON.parse(oldImages);
// 转换每个URL
let convertedArray = imagesArray.map(url => toRelativeUrl(url));
newImages = JSON.stringify(convertedArray);
if (oldImages !== newImages) {
await connection.execute(
'UPDATE goods SET images = ? WHERE id = ?',
[newImages, item.id]
);
console.log(`商品ID ${item.id}:`);
console.log(` 旧: ${oldImages}`);
console.log(` 新: ${newImages}`);
console.log('---');
updatedCount++;
}
} catch (e) {
// 如果不是有效JSON,直接尝试转换
let converted = toRelativeUrl(oldImages);
if (converted !== oldImages) {
newImages = converted;
await connection.execute(
'UPDATE goods SET images = ? WHERE id = ?',
[newImages, item.id]
);
console.log(`商品ID ${item.id} (非JSON格式):`);
console.log(` 旧: ${oldImages}`);
console.log(` 新: ${newImages}`);
console.log('---');
updatedCount++;
}
}
}
}
// 检查积分商品
const [pointsGoods] = await connection.execute('SELECT id, images FROM points_goods');
if (pointsGoods.length > 0) {
console.log(`\n检查 ${pointsGoods.length} 条积分商品记录...`);
for (const item of pointsGoods) {
if (item.images) {
let oldImages = item.images;
let newImages = oldImages;
try {
let imagesArray = JSON.parse(oldImages);
let convertedArray = imagesArray.map(url => toRelativeUrl(url));
newImages = JSON.stringify(convertedArray);
} catch (e) {
newImages = toRelativeUrl(oldImages);
}
if (oldImages !== newImages) {
await connection.execute(
'UPDATE points_goods SET images = ? WHERE id = ?',
[newImages, item.id]
);
console.log(`积分商品ID ${item.id}: 已更新`);
updatedCount++;
}
}
}
}
await connection.end();
console.log(`\n✅ 完成!共更新了 ${updatedCount} 条记录`);
} catch (error) {
console.error('❌ 更新失败:', error);
process.exit(1);
}
}
convertToRelativeUrls();
-70
View File
@@ -1,70 +0,0 @@
const mysql = require('mysql2/promise');
const config = require('../config/database');
const { DOMAIN_CONFIG } = require('../config/domain');
async function fixImageUrls() {
try {
const connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功\n');
// 获取当前配置的域名
const targetDomain = DOMAIN_CONFIG.BASE_URL;
console.log(`🎯 目标域名: ${targetDomain}\n`);
// 查找包含旧地址的记录
console.log('🔍 检查数据库中的图片URL...');
const [goods] = await connection.execute('SELECT id, images FROM goods WHERE images LIKE "%localhost%"');
console.log(`找到 ${goods.length} 条包含 localhost 的记录\n`);
// 更新图片URL
for (const item of goods) {
if (item.images) {
const oldImages = item.images;
// 替换所有 localhost:3000 和 localhost:3006
let newImages = oldImages
.replace(/http:\/\/localhost:3000/g, targetDomain)
.replace(/http:\/\/localhost:3006/g, targetDomain);
await connection.execute(
'UPDATE goods SET images = ? WHERE id = ?',
[newImages, item.id]
);
console.log(`商品ID ${item.id}:`);
console.log(` 旧: ${oldImages}`);
console.log(` 新: ${newImages}`);
console.log('---');
}
}
// 检查其他表(如有需要)
const [pointsGoods] = await connection.execute('SELECT id, images FROM points_goods WHERE images LIKE "%localhost%"');
if (pointsGoods.length > 0) {
console.log(`\n找到 ${pointsGoods.length} 条积分商品记录...`);
for (const item of pointsGoods) {
if (item.images) {
const oldImages = item.images;
let newImages = oldImages
.replace(/http:\/\/localhost:3000/g, targetDomain)
.replace(/http:\/\/localhost:3006/g, targetDomain);
await connection.execute(
'UPDATE points_goods SET images = ? WHERE id = ?',
[newImages, item.id]
);
console.log(`积分商品ID ${item.id}: 已更新`);
}
}
}
await connection.end();
console.log('\n✅ 所有图片URL更新完成!');
} catch (error) {
console.error('❌ 更新失败:', error);
process.exit(1);
}
}
fixImageUrls();
-89
View File
@@ -1,89 +0,0 @@
const fs = require('fs')
const path = require('path')
const mysql = require('mysql2/promise')
require('dotenv').config({ path: path.join(__dirname, '../.env') })
const categoriesData = require('../data/categories.json')
const goodsData = require('../data/goods.json')
const usersData = require('../data/users.json')
function requireEnv(name, fallback) {
const value = process.env[name] || fallback
if (!value && !fallback) {
throw new Error(`Missing ${name} in .env`)
}
return value
}
const config = {
host: requireEnv('DB_HOST'),
port: parseInt(requireEnv('DB_PORT', '3306')),
user: 'root',
password: requireEnv('DB_ROOT_PASSWORD'),
database: requireEnv('DB_NAME', 'miniprogram')
}
async function run() {
let connection = null
try {
console.log('Connecting to MySQL...')
connection = await mysql.createConnection({
host: config.host,
port: config.port,
user: config.user,
password: config.password
})
console.log('Creating database...')
await connection.query(`CREATE DATABASE IF NOT EXISTS ${config.database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`)
await connection.query(`USE ${config.database}`)
console.log('Granting permissions...')
await connection.query('GRANT ALL PRIVILEGES ON miniprogram.* TO admin@"%";')
await connection.query('FLUSH PRIVILEGES;')
console.log('Creating tables...')
const schema = fs.readFileSync(path.join(__dirname, '../config/schema.sql'), 'utf8')
const statements = schema.split(';').filter(s => s.trim())
for (const statement of statements) {
await connection.query(statement)
}
console.log('Inserting categories...')
for (const cat of categoriesData) {
await connection.query(
'INSERT INTO categories (id, name, icon, is_show, sort_order) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name=VALUES(name), icon=VALUES(icon), is_show=VALUES(is_show), sort_order=VALUES(sort_order)',
[cat.id, cat.name, cat.icon, cat.is_show, cat.sort_order]
)
}
console.log('Inserting goods...')
for (const good of goodsData) {
await connection.query(
'INSERT INTO goods (id, name, price, original_price, unit, category_id, images, stock, sales, is_hot, is_new, pricing_type, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name=VALUES(name), price=VALUES(price), original_price=VALUES(original_price)',
[good.id, good.name, good.price, good.original_price, good.unit, good.category_id, good.images, good.stock, good.sales, good.is_hot, good.is_new, good.pricing_type, good.description]
)
}
console.log('Inserting users...')
for (const user of usersData) {
await connection.query(
'INSERT INTO users (id, phone, password, name, avatar, points) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name=VALUES(name), points=VALUES(points)',
[user.id, user.phone, user.password, user.name, user.avatar, user.points]
)
}
console.log('Database initialization completed successfully!')
process.exit(0)
} catch (error) {
console.error('Database initialization failed:', error)
process.exit(1)
} finally {
if (connection) {
await connection.end()
}
}
}
run()
-47
View File
@@ -1,47 +0,0 @@
const crypto = require('crypto')
const { query } = require('../config/database')
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex')
}
async function registerStaff() {
try {
const phone = '12370109282'
const name = '董海洋'
const password = md5('123456')
const existing = await query('SELECT * FROM users WHERE phone = ?', [phone])
if (existing.length > 0) {
console.log('⚠️ 该手机号已注册')
const user = existing[0]
if (user.role === 1) {
console.log('该账号已经是店员身份')
} else {
console.log('该账号是普通用户,正在升级为店员...')
await query('UPDATE users SET role = 1 WHERE phone = ?', [phone])
console.log('✅ 已升级为店员身份')
}
return
}
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role, status) VALUES (?, ?, ?, ?, ?, ?, ?)',
[phone, password, name, '', 0, 1, 1]
)
console.log('✅ 店员注册成功!')
console.log('姓名: ' + name)
console.log('手机号: ' + phone)
console.log('密码: 123456')
process.exit(0)
} catch (error) {
console.error('注册失败:', error)
process.exit(1)
}
}
registerStaff()
-50
View File
@@ -1,50 +0,0 @@
const crypto = require('crypto')
const { query } = require('../config/database')
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex')
}
async function registerStaff() {
try {
const phone = '13070109282'
const name = '店员' // 可以改成你想要的名字
const password = md5('123456') // 默认密码,需要 md5 加密
// 检查是否已存在
const existing = await query('SELECT * FROM users WHERE phone = ?', [phone])
if (existing.length > 0) {
console.log('⚠️ 该手机号已注册')
// 如果已存在,检查是否是店员
const user = existing[0]
if (user.role === 1) {
console.log('该账号已经是店员身份,无需重复注册')
} else {
console.log('该账号是普通用户,正在升级为店员...')
await query('UPDATE users SET role = 1 WHERE phone = ?', [phone])
console.log('✅ 已升级为店员身份')
}
return
}
// 插入店员账号
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role, status) VALUES (?, ?, ?, ?, ?, ?, ?)',
[phone, password, name, '', 0, 1, 1] // role=1 表示店员
)
console.log('✅ 店员注册成功!')
console.log('手机号: ' + phone)
console.log('姓名: ' + name)
console.log('默认密码: 123456')
process.exit(0)
} catch (error) {
console.error('注册失败:', error)
process.exit(1)
}
}
registerStaff()
-36
View File
@@ -1,36 +0,0 @@
const mysql = require('mysql2/promise');
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../.env') });
const config = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'miniprogram'
};
async function testConnection() {
try {
console.log('尝试连接到:', config.host, config.port);
const connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功\n');
// 检查所有商品的图片URL
const [goods] = await connection.execute('SELECT id, name, images FROM goods LIMIT 10');
console.log(`找到 ${goods.length} 条商品记录\n`);
for (const item of goods) {
console.log(`商品ID ${item.id}: ${item.name}`);
console.log(` 图片: ${item.images}`);
}
await connection.end();
console.log('\n✅ 检查完成');
} catch (error) {
console.error('❌ 连接失败:', error.message);
process.exit(1);
}
}
testConnection();
+112
View File
@@ -0,0 +1,112 @@
const { query, transaction } = require('../config/database')
const { POINTS_RATE } = require('../config/constants')
function parseCart(cart) {
if (typeof cart === 'string') {
try { return JSON.parse(cart) } catch { return [] }
}
return Array.isArray(cart) ? cart : []
}
async function insertOrderItems(conn, orderId, cart) {
const items = parseCart(cart)
if (items.length === 0) return
for (const item of items) {
const qty = item.pricingType === 2 ? 1 : (item.quantity || 1)
const price = parseFloat(item.price || 0)
const subtotal = item.subtotal !== undefined ? parseFloat(item.subtotal) : price * qty
await conn.execute(
'INSERT INTO order_items (order_id, goods_id, goods_name, price, quantity, weight, subtotal, unit) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[orderId, item.id || item.goods_id, item.name || item.goods_name, price, qty, item.weight || null, subtotal, item.unit || '']
)
}
}
async function getOrderItems(orderId) {
return query('SELECT * FROM order_items WHERE order_id = ?', [orderId])
}
async function attachOrderItems(orders) {
if (!Array.isArray(orders)) orders = [orders]
const ids = orders.map(o => o.id)
if (ids.length === 0) return orders
const rows = await query('SELECT * FROM order_items WHERE order_id IN (' + ids.map(() => '?').join(',') + ')', ids)
const grouped = {}
for (const row of rows) {
if (!grouped[row.order_id]) grouped[row.order_id] = []
grouped[row.order_id].push(row)
}
for (const o of orders) {
o.items = grouped[o.id] || []
}
return orders
}
async function updateStockForOrder(order) {
const cart = typeof order.cart === 'string' ? JSON.parse(order.cart) : order.cart
if (!Array.isArray(cart) || cart.length === 0) return
let items = cart
if (order.items && order.items.length > 0) {
items = order.items
}
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)
await conn.execute('UPDATE goods SET stock = GREATEST(0, stock - ?), sales = COALESCE(sales, 0) + ? WHERE id = ?', [qty, qty, goodsId])
await conn.execute('UPDATE stock SET quantity = GREATEST(0, quantity - ?) WHERE goods_id = ?', [qty, goodsId])
}
})
}
async function addPoints(userId, orderId, totalPrice) {
if (!userId) return
const points = Math.floor(parseFloat(totalPrice) * POINTS_RATE)
if (points <= 0) return
await transaction(async (conn) => {
const userRows = await conn.execute('SELECT points FROM users WHERE id = ? AND status = 1', [userId])
if (userRows[0].length > 0) {
const newPoints = (userRows[0][0].points || 0) + points
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', points, `订单 ${orderId} 消费自动累计`]
)
}
})
}
async function sendWechatNotification(userId, orderId, status, totalPrice) {
try {
const rows = await query('SELECT openid FROM users WHERE id = ?', [userId])
const openid = rows.length > 0 ? rows[0].openid : undefined
if (openid) {
const { sendOrderStatusNotification } = require('../utils/wechat')
await sendOrderStatusNotification(openid, orderId, status, totalPrice).catch(err => { console.error('发送微信通知失败:', err) })
}
} catch (err) { console.error('发送微信通知失败:', err) }
}
async function processOrderComplete(order) {
try {
await updateStockForOrder(order)
await addPoints(order.user_id, order.id, order.total_price)
await sendWechatNotification(order.user_id, order.id, 'completed', order.total_price)
} catch (err) {
console.error('订单完成自动联动失败:', err)
}
}
module.exports = {
parseCart,
insertOrderItems,
getOrderItems,
attachOrderItems,
updateStockForOrder,
addPoints,
processOrderComplete
}
+20
View File
@@ -0,0 +1,20 @@
async function paginate(queryFn, sql, params, page = 1, pageSize = 20) {
const p = Math.max(1, parseInt(page) || 1)
const ps = Math.min(100, Math.max(1, parseInt(pageSize) || 20))
const countResult = await queryFn(
`SELECT COUNT(*) as total FROM (${sql}) AS _paged`,
params
)
const total = countResult[0].total
const totalPages = Math.ceil(total / ps)
const data = await queryFn(
`${sql} LIMIT ? OFFSET ?`,
[...params, ps, (p - 1) * ps]
)
return { data, total, page: p, pageSize: ps, totalPages }
}
module.exports = { paginate }
+87
View File
@@ -0,0 +1,87 @@
const fetch = require('node-fetch')
const APPID = process.env.WECHAT_APPID
const SECRET = process.env.WECHAT_SECRET
let accessToken = null
let tokenExpiresAt = 0
async function getAccessToken() {
if (!APPID || !SECRET) {
throw new Error('WECHAT_APPID and WECHAT_SECRET must be configured in .env')
}
if (accessToken && Date.now() < tokenExpiresAt) {
return accessToken
}
const res = await fetch(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${SECRET}`
)
const data = await res.json()
if (data.errcode) {
throw new Error(`微信API错误: ${data.errmsg}`)
}
accessToken = data.access_token
tokenExpiresAt = Date.now() + (data.expires_in - 300) * 1000
return accessToken
}
async function sendSubscribeMessage(openid, templateId, data, page = '') {
const token = await getAccessToken()
const body = {
touser: openid,
template_id: templateId,
page,
data,
miniprogram_state: 'formal'
}
const res = await fetch(
`https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}
)
const result = await res.json()
if (result.errcode && result.errcode !== 0) {
console.error('发送订阅消息失败:', result)
return { success: false, error: result.errmsg }
}
return { success: true }
}
async function sendOrderStatusNotification(openid, orderId, status, totalPrice) {
const statusText = {
paid: '已付款',
completed: '已完成',
cancelled: '已取消'
}
const templateId = process.env.WECHAT_ORDER_TEMPLATE_ID
if (!templateId) {
console.warn('WECHAT_ORDER_TEMPLATE_ID not configured, skipping notification')
return { success: false, error: 'template not configured' }
}
return sendSubscribeMessage(openid, templateId, {
thing1: { value: `订单 ${orderId}` },
phrase2: { value: statusText[status] || status },
amount3: { value: `¥${totalPrice}` },
date4: { value: new Date().toLocaleString('zh-CN') },
thing5: { value: '点击查看订单详情' }
}, `/pages/customer/order-detail/order-detail?id=${orderId}`)
}
module.exports = {
getAccessToken,
sendSubscribeMessage,
sendOrderStatusNotification
}