Browse Source

恢复 双数据库架构,金山项目、嘉定项目同时存在

C 6 days ago
parent
commit
d17971a2a2
5 changed files with 178 additions and 60 deletions
  1. 19 5
      server/db.js
  2. 32 28
      server/routes/orders.js
  3. 62 17
      web/src/App.vue
  4. 13 1
      web/src/api/orders.js
  5. 52 9
      web/src/components/Sidebar.vue

+ 19 - 5
server/db.js

@@ -1,6 +1,7 @@
 const { Pool } = require('pg');
 
-const pool = new Pool({
+// 金山烟草(默认)
+const jsPool = new Pool({
   host: process.env.DB_HOST || '121.37.183.152',
   port: parseInt(process.env.DB_PORT || '5432'),
   database: process.env.DB_NAME || 'lp-card-test',
@@ -11,10 +12,23 @@ const pool = new Pool({
   connectionTimeoutMillis: 5000,
 });
 
-pool.on('error', (err) => console.error('数据库连接池异常:', err.message));
+// 嘉定烟草
+const jdPool = new Pool({
+  host: process.env.JD_DB_HOST || 'pgm-uf69f8n00s9mqn91go.pg.rds.aliyuncs.com',
+  port: parseInt(process.env.JD_DB_PORT || '5432'),
+  database: process.env.JD_DB_NAME || 'gt1_fmzgo',
+  user: process.env.JD_DB_USER || 'gt1fmzgo',
+  password: process.env.JD_DB_PASSWORD || 'Bm123456@',
+  max: 20,
+  idleTimeoutMillis: 30000,
+  connectionTimeoutMillis: 5000,
+});
+
+jsPool.on('error', (err) => console.error('金山数据库连接池异常:', err.message));
+jdPool.on('error', (err) => console.error('嘉定数据库连接池异常:', err.message));
 
-function getPool() {
-  return pool;
+function getPool(project) {
+  return project === 'jd' ? jdPool : jsPool;
 }
 
-module.exports = { getPool, pool };
+module.exports = { getPool, jsPool, jdPool };

+ 32 - 28
server/routes/orders.js

@@ -27,15 +27,19 @@ const USERS = [
   { username: 'user', password: 'user123', role: 'user' },
 ];
 
-// 获取数据库连接池
-const db = getPool();
+// 根据请求头选择数据库中间件
+function dbPool(req, res, next) {
+  req.db = getPool(req.headers['x-project']);
+  next();
+}
+
 // 登录接口
 router.post('/login', async (req, res) => {
   const { username, password } = req.body;
   const user = USERS.find(u => u.username === username && u.password === password);
   if (user) {
     const token = Buffer.from(`${username}:${user.role}:${Date.now()}`).toString('base64');
-    await logOperation(db, user.username, user.role, '用户登录', `${user.username} 登录系统`, req.ip);
+    await logOperation(getPool('js'), user.username, user.role, '用户登录', `${user.username} 登录系统`, req.ip);
     res.json({ success: true, token, username: user.username, role: user.role });
   } else {
     res.json({ success: false, message: '用户名或密码错误' });
@@ -97,7 +101,7 @@ const STATUS_MAP = {
 };
 
 // 动态查询订单
-router.post('/query', authMiddleware, async (req, res) => {
+router.post('/query', authMiddleware, dbPool, async (req, res) => {
   try {
     const { receiver_name, receiver_phone, order_no, start_date, end_date, status, page = 1, pageSize = 20 } = req.body;
 
@@ -173,7 +177,7 @@ router.post('/query', authMiddleware, async (req, res) => {
     }
 
     const countSql = sql.replace(/SELECT[\s\S]*?FROM/, 'SELECT COUNT(*) AS total FROM');
-    const countResult = await db.query(countSql, params);
+    const countResult = await req.db.query(countSql, params);
     const total = parseInt(countResult.rows[0].total);
 
     const pageNum = Math.max(1, parseInt(page));
@@ -182,8 +186,8 @@ router.post('/query', authMiddleware, async (req, res) => {
     sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
     params.push(pageSizeNum, (pageNum - 1) * pageSizeNum);
 
-    const result = await db.query(sql, params);
-    await logOperation(db, req.user.username, req.user.role, '查询订单', `条件: 状态=${status||'全部'}, 收货人=${receiver_name||'-'}, 手机号=${receiver_phone||'-'}, 订单号=${order_no||'-'}, 日期=${start_date||'-'}~${end_date||'-'}, 第${pageNum}页`, req.ip);
+    const result = await req.db.query(sql, params);
+    await logOperation(req.db, req.user.username, req.user.role, '查询订单', `条件: 状态=${status||'全部'}, 收货人=${receiver_name||'-'}, 手机号=${receiver_phone||'-'}, 订单号=${order_no||'-'}, 日期=${start_date||'-'}~${end_date||'-'}, 第${pageNum}页`, req.ip);
     res.json({ success: true, data: result.rows, total, page: pageNum, pageSize: pageSizeNum });
   } catch (err) {
     console.error('查询订单失败:', err);
@@ -192,7 +196,7 @@ router.post('/query', authMiddleware, async (req, res) => {
 });
 
 // 导出 Excel
-router.post('/export/excel', authMiddleware, async (req, res) => {
+router.post('/export/excel', authMiddleware, dbPool, async (req, res) => {
   try {
     const { batch_no, card_no, order_no, start_date, end_date, status } = req.body;
 
@@ -264,7 +268,7 @@ router.post('/export/excel', authMiddleware, async (req, res) => {
 
     sql += ' ORDER BY c.batch_no, o.create_time, o.order_no';
 
-    const result = await db.query(sql, params);
+    const result = await req.db.query(sql, params);
 
     const workbook = new ExcelJS.Workbook();
     const sheet = workbook.addWorksheet('订单列表');
@@ -321,7 +325,7 @@ router.post('/export/excel', authMiddleware, async (req, res) => {
 
     await workbook.xlsx.write(res);
     res.end();
-    await logOperation(db, req.user.username, req.user.role, '导出Excel', `导出${result.rows.length}条订单`, req.ip);
+    await logOperation(req.db, req.user.username, req.user.role, '导出Excel', `导出${result.rows.length}条订单`, req.ip);
   } catch (err) {
     console.error('导出Excel失败:', err);
     res.status(500).json({ success: false, message: err.message });
@@ -329,7 +333,7 @@ router.post('/export/excel', authMiddleware, async (req, res) => {
 });
 
 // 导入运单信息 - 批量更新物流(不允许重复发货)
-router.post('/import/logistics', authMiddleware, upload.single('file'), async (req, res) => {
+router.post('/import/logistics', authMiddleware, dbPool, upload.single('file'), async (req, res) => {
   try {
     if (!req.file) {
       return res.status(400).json({ success: false, message: '请上传文件' });
@@ -404,16 +408,16 @@ router.post('/import/logistics', authMiddleware, upload.single('file'), async (r
       }
 
       try {
-        await db.query('BEGIN');
+        await req.db.query('BEGIN');
 
         // 查询订单是否存在及状态
-        const orderResult = await db.query(
+        const orderResult = await req.db.query(
           `SELECT id, status FROM gift_order WHERE order_no = $1 AND del_flag = '0'`,
           [orderNo]
         );
 
         if (orderResult.rows.length === 0) {
-          await db.query('ROLLBACK');
+          await req.db.query('ROLLBACK');
           results.failed++;
           results.errors.push(`第${i + 2}行: 订单 ${orderNo} 不存在`);
           continue;
@@ -424,27 +428,27 @@ router.post('/import/logistics', authMiddleware, upload.single('file'), async (r
 
         // 不允许重复发货/已完成
         if (orderStatus === 3) {
-          await db.query('ROLLBACK');
+          await req.db.query('ROLLBACK');
           results.failed++;
           results.errors.push(`第${i + 2}行: 订单 ${orderNo} 已发货,不能重复导入`);
           continue;
         }
         if (orderStatus === 1) {
-          await db.query('ROLLBACK');
+          await req.db.query('ROLLBACK');
           results.failed++;
           results.errors.push(`第${i + 2}行: 订单 ${orderNo} 已完成,不能导入`);
           continue;
         }
 
         // 更新订单状态为已发货,同时更新备注
-        await db.query(
+        await req.db.query(
           `UPDATE gift_order SET status = 3, remark = COALESCE($1, remark), update_time = now(), update_by = $2
            WHERE id = $3 AND del_flag = '0'`,
           [remark, 'import', orderId]
         );
 
         // 插入或更新物流信息
-        await db.query(
+        await req.db.query(
           `INSERT INTO gift_logistics (order_id, order_no, company, tracking_no, status, waybill_status, create_time, create_by, update_time, update_by, del_flag)
            VALUES ($1, $2, $3, $4, 0, '已发货', now(), 'import', now(), 'import', '0')
            ON CONFLICT (order_id) DO UPDATE
@@ -452,16 +456,16 @@ router.post('/import/logistics', authMiddleware, upload.single('file'), async (r
           [orderId, orderNo, company || '', trackingNo]
         );
 
-        await db.query('COMMIT');
+        await req.db.query('COMMIT');
         results.success++;
       } catch (err) {
-        await db.query('ROLLBACK');
+        await req.db.query('ROLLBACK');
         results.failed++;
         results.errors.push(`第${i + 2}行: ${err.message}`);
       }
     }
 
-    await logOperation(db, req.user.username, req.user.role, '导入运单', `成功${results.success}条, 失败${results.failed}条`, req.ip);
+    await logOperation(req.db, req.user.username, req.user.role, '导入运单', `成功${results.success}条, 失败${results.failed}条`, req.ip);
     res.json({ success: true, results });
   } catch (err) {
     console.error('导入运单失败:', err);
@@ -470,7 +474,7 @@ router.post('/import/logistics', authMiddleware, upload.single('file'), async (r
 });
 
 // 下载导入模板
-router.get('/import/template', authMiddleware, async (req, res) => {
+router.get('/import/template', authMiddleware, dbPool, async (req, res) => {
   try {
     const workbook = new ExcelJS.Workbook();
     const sheet = workbook.addWorksheet('运单导入模板');
@@ -509,7 +513,7 @@ router.get('/import/template', authMiddleware, async (req, res) => {
 
     await workbook.xlsx.write(res);
     res.end();
-    await logOperation(db, req.user.username, req.user.role, '下载导入模板', '下载运单导入模板', req.ip);
+    await logOperation(req.db, req.user.username, req.user.role, '下载导入模板', '下载运单导入模板', req.ip);
   } catch (err) {
     console.error('下载模板失败:', err);
     res.status(500).json({ success: false, message: err.message });
@@ -517,9 +521,9 @@ router.get('/import/template', authMiddleware, async (req, res) => {
 });
 
 // 统计数据接口
-router.get('/stats', async (req, res) => {
+router.get('/stats', dbPool, async (req, res) => {
   try {
-    const result = await db.query(`
+    const result = await req.db.query(`
       SELECT
         COUNT(*) AS total,
         COUNT(*) FILTER (WHERE o.status = 0) AS pending,
@@ -547,7 +551,7 @@ router.get('/stats', async (req, res) => {
 });
 
 // 查询操作日志(仅admin)
-router.get('/logs', authMiddleware, adminOnly, async (req, res) => {
+router.get('/logs', authMiddleware, adminOnly, dbPool, async (req, res) => {
   try {
     const { page = 1, pageSize = 20, username, action, start_date, end_date } = req.query;
     let sql = `SELECT * FROM sys_operation_log WHERE 1=1`;
@@ -577,14 +581,14 @@ router.get('/logs', authMiddleware, adminOnly, async (req, res) => {
 
     // 总数
     const countSql = sql.replace('SELECT *', 'SELECT COUNT(*) as total');
-    const countResult = await db.query(countSql, params);
+    const countResult = await req.db.query(countSql, params);
     const total = parseInt(countResult.rows[0].total);
 
     sql += ' ORDER BY create_time DESC';
     sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
     params.push(pageSize, (page - 1) * pageSize);
 
-    const result = await db.query(sql, params);
+    const result = await req.db.query(sql, params);
     res.json({ success: true, data: result.rows, total, page: parseInt(page), pageSize: parseInt(pageSize) });
   } catch (err) {
     console.error('查询日志失败:', err);

+ 62 - 17
web/src/App.vue

@@ -8,7 +8,7 @@
       :currentPage="currentPage"
       :username="username"
       :role="userRole"
-      :pendingCount="stats.pending || 0"
+      :pendingCount="{ js: jsStats.pending || 0, jd: jdStats.pending || 0 }"
       @navigate="navigateTo"
       @logout="handleLogout"
     />
@@ -23,20 +23,20 @@
       <div class="content-area">
         <!-- 订单管理 -->
         <OrderManagement
-          v-show="currentPage === 'orders'"
+          v-show="activePage === 'orders'"
           ref="orderRef"
           :tableData="tableData"
           :total="totalCount"
           :loading="loading"
-          :stats="stats"
+          :stats="activeStats"
           @query="handleQuery"
           @exportExcel="handleExportExcel"
-          @openImport="navigateTo('import')"
+          @openImport="navigateTo(currentProject + '-import')"
         />
 
         <!-- 物流导入 -->
         <LogisticsImport
-          v-show="currentPage === 'import'"
+          v-show="activePage === 'import'"
           :importing="importing"
           :importResult="importResult"
           @import="handleImport"
@@ -45,7 +45,7 @@
 
         <!-- 操作日志 -->
         <OperationLogs
-          v-if="userRole === 'admin' && currentPage === 'logs'"
+          v-if="userRole === 'admin' && activePage === 'logs'"
           ref="logRef"
           :tableData="logData"
           :total="logTotal"
@@ -67,11 +67,11 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { Top } from '@element-plus/icons-vue'
 import {
-  setToken, getToken, removeToken,
+  setToken, getToken, removeToken, setCurrentProject,
   queryOrders, exportExcel, importLogistics, downloadTemplate, queryLogs, getStats
 } from './api/orders.js'
 
@@ -88,15 +88,31 @@ const username = ref('')
 const userRole = ref('')
 const theme = ref('light')
 
-// 页面导航
-const currentPage = ref('orders')
+// 项目与页面导航
+const currentProject = ref('js')
+const currentPage = ref('js-orders')
+
+const activePage = computed(() => {
+  const p = currentPage.value
+  return p.substring(p.indexOf('-') + 1)
+})
+
+const activeStats = computed(() => {
+  return currentProject.value === 'jd' ? jdStats.value : jsStats.value
+})
+
+function setProject(project) {
+  currentProject.value = project
+  setCurrentProject(project)
+}
 
 // 订单数据
 const orderRef = ref(null)
 const tableData = ref([])
 const totalCount = ref(0)
 const loading = ref(false)
-const stats = ref({})
+const jsStats = ref({})
+const jdStats = ref({})
 
 // 导入
 const importing = ref(false)
@@ -126,6 +142,7 @@ onMounted(() => {
     userRole.value = localStorage.getItem('role') || 'user'
     theme.value = localStorage.getItem('theme') || 'light'
     applyTheme()
+    setProject('js')
     handleQuery()
     loadStats()
   }
@@ -146,7 +163,8 @@ function handleLoginSuccess(data) {
   importResult.value = null
   logData.value = []
   logTotal.value = 0
-  currentPage.value = 'orders'
+  setProject('js')
+  currentPage.value = 'js-orders'
   handleQuery()
   loadStats()
 }
@@ -162,13 +180,33 @@ function handleLogout() {
   importResult.value = null
   logData.value = []
   logTotal.value = 0
-  currentPage.value = 'orders'
+  currentProject.value = 'js'
+  currentPage.value = 'js-orders'
   ElMessage.success('已安全退出')
 }
 
 function navigateTo(page) {
+  const project = page.substring(0, 2)
+  const pageType = page.substring(page.indexOf('-') + 1)
+  const projectChanged = project !== currentProject.value
+
+  setProject(project)
   currentPage.value = page
-  if (page === 'logs') handleQueryLogs()
+
+  if (projectChanged) {
+    tableData.value = []
+    totalCount.value = 0
+    importResult.value = null
+    logData.value = []
+    logTotal.value = 0
+  }
+
+  if (pageType === 'orders') {
+    handleQuery()
+    if (projectChanged) loadStats()
+  } else if (pageType === 'logs') {
+    handleQueryLogs()
+  }
 }
 
 // 主题
@@ -185,7 +223,13 @@ function applyTheme() {
 async function loadStats() {
   try {
     const res = await getStats()
-    if (res.data.success) stats.value = res.data.data
+    if (res.data.success) {
+      if (currentProject.value === 'jd') {
+        jdStats.value = res.data.data
+      } else {
+        jsStats.value = res.data.data
+      }
+    }
   } catch (e) { /* ignore */ }
 }
 
@@ -285,8 +329,9 @@ async function handleQueryLogs() {
 }
 
 function handleRefresh() {
-  if (currentPage.value === 'orders') handleQuery()
-  else if (currentPage.value === 'logs') handleQueryLogs()
+  const pageType = activePage.value
+  if (pageType === 'orders') handleQuery()
+  else if (pageType === 'logs') handleQueryLogs()
   loadStats()
   ElMessage.success('数据已刷新')
 }

+ 13 - 1
web/src/api/orders.js

@@ -5,8 +5,20 @@ const api = axios.create({
   timeout: 30000,
 })
 
-// 请求拦截器:自动附加 token
+// 当前项目(js=金山烟草, jd=嘉定烟草)
+let currentProject = 'js'
+
+export function setCurrentProject(project) {
+  currentProject = project
+}
+
+export function getCurrentProject() {
+  return currentProject
+}
+
+// 请求拦截器:自动附加项目标识和 token
 api.interceptors.request.use((config) => {
+  config.headers['X-Project'] = currentProject
   const token = localStorage.getItem('auth_token')
   if (token) {
     config.headers['Authorization'] = token

+ 52 - 9
web/src/components/Sidebar.vue

@@ -9,11 +9,12 @@
     </div>
 
     <nav class="sidebar-nav">
-      <div class="nav-section-label">主要功能</div>
+      <!-- 金山烟草 -->
+      <div class="nav-section-label">金山烟草</div>
       <div
         class="nav-item"
-        :class="{ active: currentPage === 'orders' }"
-        @click="$emit('navigate', 'orders')"
+        :class="{ active: currentPage === 'js-orders' }"
+        @click="$emit('navigate', 'js-orders')"
       >
         <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
           <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
@@ -21,12 +22,12 @@
           <path d="M16 10a4 4 0 0 1-8 0"/>
         </svg>
         <span>订单管理</span>
-        <span v-if="pendingCount > 0" class="nav-badge">{{ pendingCount }}</span>
+        <span v-if="pendingCount.js > 0" class="nav-badge">{{ pendingCount.js }}</span>
       </div>
       <div
         class="nav-item"
-        :class="{ active: currentPage === 'import' }"
-        @click="$emit('navigate', 'import')"
+        :class="{ active: currentPage === 'js-import' }"
+        @click="$emit('navigate', 'js-import')"
       >
         <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
           <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
@@ -38,8 +39,50 @@
       <div
         v-if="role === 'admin'"
         class="nav-item"
-        :class="{ active: currentPage === 'logs' }"
-        @click="$emit('navigate', 'logs')"
+        :class="{ active: currentPage === 'js-logs' }"
+        @click="$emit('navigate', 'js-logs')"
+      >
+        <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
+          <polyline points="14 2 14 8 20 8"/>
+          <line x1="16" y1="13" x2="8" y2="13"/>
+          <line x1="16" y1="17" x2="8" y2="17"/>
+        </svg>
+        <span>操作日志</span>
+      </div>
+
+      <!-- 嘉定烟草 -->
+      <div class="nav-section-label" style="margin-top: var(--space-3);">嘉定烟草</div>
+      <div
+        class="nav-item"
+        :class="{ active: currentPage === 'jd-orders' }"
+        @click="$emit('navigate', 'jd-orders')"
+      >
+        <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
+          <line x1="3" y1="6" x2="21" y2="6"/>
+          <path d="M16 10a4 4 0 0 1-8 0"/>
+        </svg>
+        <span>订单管理</span>
+        <span v-if="pendingCount.jd > 0" class="nav-badge">{{ pendingCount.jd }}</span>
+      </div>
+      <div
+        class="nav-item"
+        :class="{ active: currentPage === 'jd-import' }"
+        @click="$emit('navigate', 'jd-import')"
+      >
+        <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
+          <polyline points="17 8 12 3 7 8"/>
+          <line x1="12" y1="3" x2="12" y2="15"/>
+        </svg>
+        <span>物流导入</span>
+      </div>
+      <div
+        v-if="role === 'admin'"
+        class="nav-item"
+        :class="{ active: currentPage === 'jd-logs' }"
+        @click="$emit('navigate', 'jd-logs')"
       >
         <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
           <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
@@ -73,7 +116,7 @@ defineProps({
   currentPage: String,
   username: String,
   role: String,
-  pendingCount: { type: Number, default: 0 }
+  pendingCount: { type: Object, default: () => ({ js: 0, jd: 0 }) }
 })
 defineEmits(['navigate', 'logout'])
 </script>