Ai config
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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`);
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
@@ -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 +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}]
|
||||
File diff suppressed because one or more lines are too long
@@ -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"}]
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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: '上传成功',
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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();
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user