Browse Source

提升/api/tracker 的性能

zxlie 9 months ago
parent
commit
9dd1b4a113
4 changed files with 135 additions and 22 deletions
  1. 11 4
      apps/background/statistics.js
  2. 118 3
      server/admin/js/admin.js
  3. 0 9
      server/api/track.js
  4. 6 6
      server/models/track.js

+ 11 - 4
apps/background/statistics.js

@@ -122,8 +122,6 @@ let Statistics = (function() {
             userAgent: nav.userAgent || '',
             language: nav.language || '',
             platform: nav.platform || '',
-            vendor: nav.vendor || '',
-            online: nav.onLine,
             extensionVersion: chrome.runtime.getManifest().version,
             ...tabInfo
         };
@@ -141,9 +139,18 @@ let Statistics = (function() {
         const payload = {
             event: eventName,
             userId: uid,
-            ...clientInfo,
-            ...params
+            ...clientInfo
         };
+        // 只允许 TrackSchema 里的字段
+        const allowedFields = [
+            'tool_name', 'extensionVersion', 'browser', 'browserVersion', 'os', 'osVersion',
+            'IP', 'country', 'province', 'city', 'pageUrl', 'pageTitle', 'language', 'platform'
+        ];
+        for (const key of allowedFields) {
+            if (params[key] !== undefined) {
+                payload[key] = params[key];
+            }
+        }
         try {
             fetch(SERVER_TRACK_URL, {
                 method: 'POST',

+ 118 - 3
server/admin/js/admin.js

@@ -46,6 +46,7 @@ const toolNameMap = {
 
 // 顶部导航栏(无打赏按钮,仅限本人使用)
 const HeaderNav = defineComponent({
+  emits: ['show-query-modal'],
   template: `
     <header class="w-full h-14 bg-white shadow flex items-center justify-between px-6 fixed top-0 left-0 z-10">
       <div class="flex items-center space-x-3">
@@ -54,6 +55,7 @@ const HeaderNav = defineComponent({
       </div>
       <div class="flex items-center space-x-4">
         <span class="text-gray-500 text-sm">仅限本人使用</span>
+        <button @click="$emit('show-query-modal')" class="ml-4 px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">查询</button>
         <svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A13.937 13.937 0 0112 15c2.485 0 4.797.607 6.879 1.804M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
       </div>
     </header>
@@ -293,9 +295,119 @@ const LoginModal = defineComponent({
   `
 });
 
+// 查询模态框组件
+const QueryModal = defineComponent({
+  props: ['show'],
+  emits: ['close'],
+  setup(props, { emit }) {
+    const toolName = ref('');
+    const event = ref('');
+    const userId = ref('');
+    const startTime = ref('');
+    const endTime = ref('');
+    const page = ref(1);
+    const pageSize = ref(20);
+    const total = ref(0);
+    const list = ref([]);
+    const loading = ref(false);
+
+    // 固定字段顺序
+    const fields = [
+      'userId',
+      'extensionVersion',
+      'tool_name',
+      'browser',
+      'os',
+      'language',
+      'country',
+      'province',
+      'city',
+      'pageUrl',
+      'pageTitle'
+    ];
+
+    const doQuery = async () => {
+      loading.value = true;
+      try {
+        const params = new URLSearchParams();
+        if (toolName.value) params.append('tool_name', toolName.value);
+        if (event.value) params.append('event', event.value);
+        if (userId.value) params.append('userId', userId.value);
+        if (startTime.value) params.append('startTime', startTime.value);
+        if (endTime.value) params.append('endTime', endTime.value);
+        params.append('page', page.value);
+        params.append('pageSize', pageSize.value);
+        const res = await fetch(`/api/admin/raw?${params.toString()}`, { credentials: 'include' });
+        const data = await res.json();
+        list.value = data.list || [];
+        total.value = data.total || 0;
+      } finally {
+        loading.value = false;
+      }
+    };
+
+    const handlePageChange = (newPage) => {
+      page.value = newPage;
+      doQuery();
+    };
+
+    const close = () => {
+      emit('close');
+    };
+
+    return {
+      toolName, event, userId, startTime, endTime, page, pageSize, total, list, loading,
+      doQuery, handlePageChange, close, fields
+    };
+  },
+  template: `
+    <div v-if="show" class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
+      <div class="bg-white rounded-xl shadow-lg p-8 w-[1200px] max-h-[90vh] overflow-auto relative">
+        <button @click="close" class="absolute top-4 right-4 text-gray-400 hover:text-red-500 text-2xl">&times;</button>
+        <div class="text-lg font-bold mb-4">数据查询</div>
+        <div class="flex flex-wrap gap-4 mb-4">
+          <input v-model="toolName" class="border rounded px-3 py-2" placeholder="工具名" />
+          <input v-model="event" class="border rounded px-3 py-2" placeholder="事件类型" />
+          <input v-model="userId" class="border rounded px-3 py-2" placeholder="用户ID" />
+          <input v-model="startTime" type="date" class="border rounded px-3 py-2" placeholder="开始日期" />
+          <input v-model="endTime" type="date" class="border rounded px-3 py-2" placeholder="结束日期" />
+          <button @click="doQuery" class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">查询</button>
+        </div>
+        <div v-if="loading" class="text-center text-gray-500 py-8">加载中...</div>
+        <table v-else class="min-w-full text-xs border border-gray-200 border-collapse mb-4">
+          <thead>
+            <tr>
+              <th v-for="field in fields" :key="field" class="px-2 py-1 border border-gray-200 bg-gray-50">{{field}}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="item in list" :key="item._id">
+              <td v-for="field in fields" :key="field" class="px-2 py-1 border border-gray-200">
+                <span v-if="typeof item[field] === 'object' && item[field] !== null">{{ JSON.stringify(item[field]) }}</span>
+                <span v-else>{{ item[field] }}</span>
+              </td>
+            </tr>
+            <tr v-if="list.length === 0">
+              <td :colspan="fields.length" class="text-center border border-gray-200 py-4">暂无数据</td>
+            </tr>
+          </tbody>
+        </table>
+        <div class="flex justify-between items-center">
+          <span>共 {{total}} 条</span>
+          <div>
+            <button :disabled="page<=1" @click="handlePageChange(page-1)" class="px-2 py-1 border rounded mr-2">上一页</button>
+            <span>第 {{page}} 页</span>
+            <button :disabled="page*pageSize>=total" @click="handlePageChange(page+1)" class="px-2 py-1 border rounded ml-2">下一页</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  `
+});
+
 // App组件定义,保持在所有子组件定义之后
 const App = defineComponent({
-  components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable, LoginModal },
+  components: { HeaderNav, OverviewPanel, TopTools, SimpleTable, ErrorAlert, EventTrendTable, LoginModal, QueryModal },
   setup() {
     // 数据定义
     const overview = ref({});
@@ -315,6 +427,7 @@ const App = defineComponent({
     const loading = ref(true);
     const loggedIn = ref(false);
     const loginError = ref(null);
+    const showQueryModal = ref(false);
 
     // API请求工具函数
     const fetchApi = async (url) => {
@@ -466,13 +579,15 @@ const App = defineComponent({
       tools, eventDist, errorMsg, loading, eventTrend,
       loggedIn,
       loginError,
-      handleLogin
+      handleLogin,
+      showQueryModal
     };
   },
   template: `
     <LoginModal :show="!loggedIn" :error="loginError" @login="handleLogin" />
     <div v-if="loggedIn">
-      <HeaderNav />
+      <HeaderNav @show-query-modal="showQueryModal = true" />
+      <QueryModal :show="showQueryModal" @close="showQueryModal = false" />
       <main class="pt-16 px-6 max-w-7xl mx-auto">
         <ErrorAlert :message="errorMsg" />
         <OverviewPanel :overview="overview" />

+ 0 - 9
server/api/track.js

@@ -19,9 +19,6 @@ router.post('/', async (req, res) => {
         if (ip.startsWith('::ffff:')) ip = ip.replace('::ffff:', '');
         const geo = geoip.lookup(ip) || {};
 
-        // 设备类型判断
-        const deviceType = uaResult.device.type || (uaResult.os.name && uaResult.os.name.match(/Android|iOS|iPhone|iPad/i) ? 'mobile' : 'desktop');
-
         // 只保留 TrackSchema 中定义的字段
         const data = {
             // 用户与会话
@@ -40,11 +37,7 @@ router.post('/', async (req, res) => {
             browserVersion: uaResult.browser.version || '',
             os: uaResult.os.name || '',
             osVersion: uaResult.os.version || '',
-            deviceType: deviceType || '',
-            deviceVendor: uaResult.device.vendor || '',
-            deviceModel: uaResult.device.model || '',
             language: body.language || headers['accept-language'] || '',
-            timezone: body.timezone || '',
             platform: body.platform || uaResult.os.name || '',
 
             // 网络与地理
@@ -52,11 +45,9 @@ router.post('/', async (req, res) => {
             country: geo.country || '',
             province: geo.region || '',
             city: geo.city || '',
-            online: typeof body.online === 'boolean' ? body.online : null,
 
             // 扩展相关
             extensionVersion: body.extensionVersion || '',
-            previous_version: body.previous_version || '',
             tool_name: body.tool_name || '',
         };
         

+ 6 - 6
server/models/track.js

@@ -10,20 +10,20 @@ const TrackSchema = new mongoose.Schema({
     browserVersion: String,
     os: String,
     osVersion: String,
-    deviceType: String,
-    deviceVendor: String,
-    deviceModel: String,
     language: String,
-    timezone: String,
     platform: String,
     IP: String,
     country: String,
     province: String,
     city: String,
-    online: Boolean,
     extensionVersion: String,
-    previous_version: String,
     tool_name: String,
 }, { timestamps: true });
 
+TrackSchema.index({ userId: 1 });
+TrackSchema.index({ tool_name: 1 });
+TrackSchema.index({ event: 1 });
+
+mongoose.set('autoIndex', false);
+
 module.exports = mongoose.models.Track || mongoose.model('Track', TrackSchema);