前端集成指南-爬虫平台功能.md 24 KB

药店采购比价系统 - 前端集成指南(爬虫平台功能)

⚠️ MVP 阶段说明
本项目为全新项目,尚未上线,当前处于 MVP(最小可行产品)阶段
核心业务:药店采购比价系统,爬取各医药B2B平台药品价格,生成采购链接。
目标用户:药店采购人员、药师,目标日活20万+。

📅 版本:v1.0-MVP | 📆 更新日期:2026-06-16 | ✅ 状态:开发中


📋 目录


🎯 功能概述

核心价值

本次更新为爬虫系统添加了全渠道医药平台区分功能,支持记录每次爬取的采购平台来源:

价值点 说明 收益
🎯 最优价格采购 覆盖34个主流医药渠道 降低采购成本10%-30%
🔗 一键跳转购买 生成各平台采购链接 提升转化率50%+
📊 跨平台对比 实时价格对比分析 提高决策效率
👥 邀请裂变 通过分享获得额外配额 提升用户注册率和日活

平台分类

🏭 B2B批发平台(14个)- 药店主要采购渠道
   ├─ 核心平台:药师帮、药京采、健之佳
   └─ 扩展平台:1药网、康爱多、阿里健康等

🏪 B2C零售平台(8个)- 小批量采购/急单
   ├─ O2O即时配送:叮当快药、美团买药
   └─ 连锁药房:老百姓、大参林、益丰等

🛒 综合电商医药频道(7个)- 价格参考
   └─ 京东健康、天猫医药、拼多多医药等

👨‍⚕️ 垂直医药平台(4个)- 特色渠道
   └─ 微医、平安好医生、春雨医生等

🚀 快速开始

1. MVP阶段核心流程

// Step 1: 用户搜索药品
const drugName = '阿莫西林胶囊';

// Step 2: 选择比价平台(MVP推荐3个核心平台)
const platforms = ['yaoshibang', 'yaobangmang', 'yiyaocheng'];

// Step 3: 逐个平台爬取价格
for (const platform of platforms) {
  // 消耗配额
  await consumeQuota(platform);
  
  // 爬取价格并生成采购链接
  const priceData = await crawlPrice(drugName, platform);
}

// Step 4: 展示比价结果,点击跳转到对应平台购买

2. 关键代码片段

// 消耗配额(必须传递platform参数)
const consumeQuota = async (platform) => {
  const response = await fetch('/api/crawler/consume', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`
    },
    body: JSON.stringify({
      count: 1,
      useCouponFirst: false,
      platform: platform  // ⚠️ 必填:平台代码
    })
  });
  
  const result = await response.json();
  
  if (result.code !== 200) {
    throw new Error(result.message || '配额不足');
  }
  
  return result.data;
};

📡 接口详解

1. 消耗爬虫次数 ⭐⭐⭐

接口信息:

  • URL: POST /api/crawler/consume
  • 认证: 需要 JWT Token
  • 用途: 用户点击"比价"按钮时调用,扣减配额并记录平台

请求参数:

字段 类型 必填 默认值 说明
count Number - 消耗次数(通常传1)
useCouponFirst Boolean false 是否优先使用优惠券
platform String - 平台代码(见下方枚举)

MVP核心平台枚举:

const MVP_PLATFORMS = {
  YAOSHBANG: 'yaoshibang',       // 药师帮 - 国内最大医药B2B ⭐
  YAOBANGMANG: 'yaobangmang',    // 药帮忙 - 西南地区领先 ⭐
  YIYAOCHENG: 'yiyaocheng'       // 1药城 - 华中地区知名 ⭐
};

完整平台列表(Phase 2-6扩展):

点击查看34个平台完整列表 ```javascript const ALL_PLATFORMS = { // B2B批发平台(14个) yaoshibang: '药师帮', yaojingcai: '药京采', jianzhijia: '健之佳', '1yao': '1药网', kangaiduoduo: '康爱多', alihealth: '阿里健康', sinopharm: '国药控股', jointown: '九州通', zhencheng: '珍诚医药', yaoyitong: '药易通', hezong: '合纵药易购', yicaotang: '宜草堂', yaodu: '药都在线', yaocaiying: '药材盈', // B2C零售平台(8个) dingdang: '叮当快药', quanyuantang: '泉源堂', dashenlin: '大参林', laobaixing: '老百姓大药房', yixintang: '一心堂', yifeng: '益丰大药房', haiwang: '海王星辰', guoda: '国大药房', // 综合电商医药频道(7个) jd_health: '京东健康', tmall_pharma: '天猫医药', pdd_pharma: '拼多多医药', douyin_pharma: '抖音医药', kuaishou_pharma: '快手医药', meituan_pharma: '美团买药', eleme_pharma: '饿了么医药', // 垂直医药平台(4个) weyi: '微医', pingan_good: '平安好医生', chunyu: '春雨医生', haodf: '好大夫在线' }; ```

响应示例:

{
  "code": 200,
  "message": "success",
  "data": true
}

错误响应:

{
  "code": 400,
  "message": "每日配额已用完,请升级会员或等待明天",
  "data": null
}

前端调用示例:

// ✅ 正确用法
async function startPriceComparison(drugName, platform) {
  try {
    // 1. 消耗配额
    await fetch('/api/crawler/consume', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify({
        count: 1,
        platform: platform  // 必须传递
      })
    });
    
    // 2. 爬取价格(调用后端爬虫服务)
    const priceData = await crawlDrugPrice(drugName, platform);
    
    return priceData;
  } catch (error) {
    console.error('比价失败:', error);
    throw error;
  }
}

// ❌ 错误用法:不传platform参数
await fetch('/api/crawler/consume', {
  body: JSON.stringify({ count: 1 })  // 缺少platform字段!
});

2. 获取爬虫使用记录

接口信息:

  • URL: GET /api/crawler/logs?days=7
  • 认证: 需要 JWT Token
  • 用途: 展示用户的历史爬取记录

查询参数:

参数 类型 必填 默认值 说明
days Number 7 查询最近N天的记录

响应示例:

{
  "code": 200,
  "message": "success",
  "data": [
    {
      "id": 1,
      "userId": 1,
      "usageDate": "2026-06-16",
      "usageCount": 3,
      "platform": "yaoshibang",      // ✅ 平台代码
      "source": "QUOTA",              // QUOTA:配额 COUPON:优惠券
      "remark": null,
      "createTime": "2026-06-16T10:30:00"
    }
  ]
}

前端调用示例:

async function fetchUsageLogs(days = 7) {
  const response = await fetch(`/api/crawler/logs?days=${days}`, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  const result = await response.json();
  return result.data || [];
}

3. 查看用户爬虫详情(运营后台专用)

接口信息:

  • URL: GET /api/admin/crawler/user-detail?userId=1
  • 认证: 需要管理员 JWT Token
  • 用途: 运营后台查看用户的平台使用分布统计

查询参数:

参数 类型 必填 说明
userId Long 用户ID

响应示例:

{
  "code": 200,
  "message": "success",
  "data": {
    "userId": 1,
    "nickname": "测试用户",
    "phone": "138****8000",
    "todayTotalUsage": 8,
    "monthlyTotalUsage": 45,
    "platformStats": [
      {
        "platformCode": "yaoshibang",
        "platformName": "药师帮",
        "todayUsage": 3,
        "monthlyUsage": 15,
        "totalUsage": 50
      },
      {
        "platformCode": "yaobangmang",
        "platformName": "药帮忙",
        "todayUsage": 2,
        "monthlyUsage": 10,
        "totalUsage": 30
      }
    ]
  }
}

💻 前端实现示例

React组件:药品比价功能(MVP简化版)

import React, { useState } from 'react';

// MVP核心平台配置(3个)
const PLATFORM_CONFIG = {
  yaoshibang: { 
    name: '药师帮', 
    icon: '💊', 
    color: '#1890ff',
    description: '国内最大医药B2B平台'
  },
  yaobangmang: { 
    name: '药帮忙', 
    icon: '🏥', 
    color: '#52c41a',
    description: '西南地区领先平台'
  },
  yiyaocheng: { 
    name: '1药城', 
    icon: '💉', 
    color: '#faad14',
    description: '华中地区知名平台'
  }
};

function DrugPriceComparison() {
  const [drugName, setDrugName] = useState('');
  const [selectedPlatforms, setSelectedPlatforms] = useState(['yaoshibang']);
  const [isComparing, setIsComparing] = useState(false);
  const [priceResults, setPriceResults] = useState([]);
  const [quotaInfo, setQuotaInfo] = useState(null);

  // 启动比价
  const startComparison = async () => {
    if (!drugName.trim()) {
      alert('请输入药品名称');
      return;
    }

    if (selectedPlatforms.length === 0) {
      alert('请至少选择一个平台');
      return;
    }

    try {
      setIsComparing(true);
      const results = [];

      // 逐个平台爬取价格
      for (const platform of selectedPlatforms) {
        try {
          // 1. 消耗配额
          await consumeQuota(platform);
          
          // 2. 爬取该平台药品价格
          const priceData = await crawlDrugPrice(drugName, platform);
          results.push(priceData);
        } catch (error) {
          console.error(`${PLATFORM_CONFIG[platform].name}爬取失败:`, error);
          // 继续处理其他平台
        }
      }

      // 按价格从低到高排序
      results.sort((a, b) => a.price - b.price);
      setPriceResults(results);
      
      // 刷新配额状态
      await refreshQuotaStatus();
    } catch (error) {
      console.error('比价失败:', error);
      alert('操作失败,请重试');
    } finally {
      setIsComparing(false);
    }
  };

  // 消耗配额
  const consumeQuota = async (platform) => {
    const response = await fetch('/api/crawler/consume', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
      },
      body: JSON.stringify({
        count: 1,
        useCouponFirst: false,
        platform: platform  // ✅ 必填
      })
    });

    const result = await response.json();
    
    if (result.code !== 200) {
      throw new Error(result.message || `${PLATFORM_CONFIG[platform].name}配额不足`);
    }
    
    return result.data;
  };

  // 爬取药品价格(实际应调用后端爬虫服务)
  const crawlDrugPrice = async (drugName, platform) => {
    // TODO: 调用后端爬虫接口
    // const response = await fetch(`/api/crawler/search?drug=${drugName}&platform=${platform}`);
    
    // 模拟数据
    return {
      platform,
      drugName,
      price: Math.random() * 100 + 10,
      purchaseLink: `https://${platform}.com/buy?drug=${encodeURIComponent(drugName)}`,
      supplier: '某某医药公司',
      stock: '有货',
      specification: '0.25g*24粒'
    };
  };

  // 刷新配额状态
  const refreshQuotaStatus = async () => {
    const response = await fetch('/api/crawler/status', {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
      }
    });
    const result = await response.json();
    setQuotaInfo(result.data);
  };

  return (
    <div className="drug-price-comparison">
      <h2>💊 药品采购比价</h2>
      
      {/* 配额信息 */}
      {quotaInfo && (
        <div className="quota-info">
          <span>今日剩余: {quotaInfo.dailyRemaining}/{quotaInfo.dailyLimit}次</span>
          <span>本月剩余: {quotaInfo.monthlyRemaining}/{quotaInfo.monthlyLimit}次</span>
        </div>
      )}
      
      {/* 药品搜索 */}
      <div className="search-box">
        <input 
          type="text"
          placeholder="输入药品名称(如:阿莫西林)"
          value={drugName}
          onChange={(e) => setDrugName(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && startComparison()}
        />
      </div>

      {/* 平台选择 */}
      <div className="platform-selector">
        <p>选择比价平台:</p>
        <div className="platform-grid">
          {Object.entries(PLATFORM_CONFIG).map(([code, info]) => (
            <label key={code} className={`platform-card ${selectedPlatforms.includes(code) ? 'selected' : ''}`}>
              <input
                type="checkbox"
                checked={selectedPlatforms.includes(code)}
                onChange={(e) => {
                  if (e.target.checked) {
                    setSelectedPlatforms([...selectedPlatforms, code]);
                  } else {
                    setSelectedPlatforms(selectedPlatforms.filter(p => p !== code));
                  }
                }}
              />
              <div className="platform-info">
                <span className="platform-icon">{info.icon}</span>
                <span className="platform-name">{info.name}</span>
                <span className="platform-desc">{info.description}</span>
              </div>
            </label>
          ))}
        </div>
      </div>

      {/* 开始比价按钮 */}
      <button 
        className="compare-btn"
        onClick={startComparison}
        disabled={isComparing || !drugName.trim() || selectedPlatforms.length === 0}
      >
        {isComparing ? '🔍 比价中...' : '🚀 开始比价'}
      </button>

      {/* 比价结果 */}
      {priceResults.length > 0 && (
        <div className="price-results">
          <h3>比价结果(按价格从低到高)</h3>
          <table className="price-table">
            <thead>
              <tr>
                <th>排名</th>
                <th>平台</th>
                <th>供应商</th>
                <th>规格</th>
                <th>价格</th>
                <th>库存</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody>
              {priceResults.map((result, index) => (
                <tr key={result.platform} className={index === 0 ? 'best-price' : ''}>
                  <td>
                    {index === 0 && <span className="badge">🏆 最低价</span>}
                    {index + 1}
                  </td>
                  <td>
                    <span className="platform-badge" style={{ backgroundColor: PLATFORM_CONFIG[result.platform].color }}>
                      {PLATFORM_CONFIG[result.platform].icon} {PLATFORM_CONFIG[result.platform].name}
                    </span>
                  </td>
                  <td>{result.supplier}</td>
                  <td>{result.specification}</td>
                  <td className="price">¥{result.price.toFixed(2)}</td>
                  <td>{result.stock}</td>
                  <td>
                    <a 
                      href={result.purchaseLink} 
                      target="_blank" 
                      rel="noopener noreferrer"
                      className="buy-link"
                    >
                      🔗 去采购
                    </a>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
          <p className="tip">💡 点击"去采购"将跳转到对应平台的购买页面</p>
        </div>
      )}
    </div>
  );
}

export default DrugPriceComparison;

CSS样式(配套)

.drug-price-comparison {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.quota-info {
  background: #f0f5ff;
  padding: 12px;
  border-radius: 8px;
  margin-bottom: 20px;
  display: flex;
  gap: 20px;
  font-size: 14px;
}

.search-box input {
  width: 100%;
  padding: 12px;
  font-size: 16px;
  border: 2px solid #d9d9d9;
  border-radius: 8px;
  margin-bottom: 20px;
}

.platform-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px;
  margin-top: 12px;
}

.platform-card {
  display: flex;
  align-items: center;
  padding: 12px;
  border: 2px solid #d9d9d9;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}

.platform-card.selected {
  border-color: #1890ff;
  background: #f0f5ff;
}

.platform-card:hover {
  border-color: #1890ff;
}

.platform-info {
  display: flex;
  flex-direction: column;
  margin-left: 8px;
}

.platform-name {
  font-weight: bold;
}

.platform-desc {
  font-size: 12px;
  color: #999;
}

.compare-btn {
  width: 100%;
  padding: 14px;
  font-size: 16px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  margin: 20px 0;
}

.compare-btn:disabled {
  background: #d9d9d9;
  cursor: not-allowed;
}

.price-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 16px;
}

.price-table th,
.price-table td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #f0f0f0;
}

.best-price {
  background: #fff7e6;
}

.badge {
  background: #ff4d4f;
  color: white;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
}

.platform-badge {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 16px;
  color: white;
  font-size: 14px;
}

.price {
  color: #ff4d4f;
  font-weight: bold;
  font-size: 16px;
}

.buy-link {
  color: #1890ff;
  text-decoration: none;
}

.buy-link:hover {
  text-decoration: underline;
}

.tip {
  margin-top: 12px;
  color: #999;
  font-size: 14px;
}

📊 数据流说明

完整业务流程

┌─────────────┐
│ 用户搜索药品 │
└──────┬──────┘
       │
       ▼
┌──────────────────┐
│ 选择比价平台     │ ← MVP推荐3个核心平台
└──────┬───────────┘
       │
       ▼
┌──────────────────────────┐
│ 循环每个平台:           │
│ 1. 调用 /consume 接口    │ ← 扣减配额,记录platform
│ 2. 调用爬虫服务          │ ← 爬取价格数据
│ 3. 生成采购链接          │ ← 返回购买URL
└──────┬───────────────────┘
       │
       ▼
┌──────────────────┐
│ 按价格排序展示   │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│ 用户点击购买     │ → 跳转到对应平台
└──────────────────┘

数据流转图

前端                          后端                        数据库
 │                              │                            │
 │ POST /consume                │                            │
 │ {platform: 'yaoshibang'}     │                            │
 ├─────────────────────────────>│                            │
 │                              │ 验证配额                    │
 │                              ├───────────────────────────>│
 │                              │                            │
 │                              │ 扣减配额                    │
 │                              │ INSERT usage_log           │
 │                              │ (含platform字段)           │
 │                              │                            │
 │                              │<───────────────────────────┤
 │                              │                            │
 │ 返回成功                     │                            │
 │<─────────────────────────────┤                            │
 │                              │                            │
 │ GET /logs                    │                            │
 ├─────────────────────────────>│                            │
 │                              │ 查询使用记录               │
 │                              ├───────────────────────────>│
 │                              │ SELECT * FROM usage_log   │
 │                              │ WHERE platform IS NOT NULL │
 │                              │                            │
 │ 返回记录(含platform)       │<───────────────────────────┤
 │<─────────────────────────────┤                            │

⚠️ 注意事项

1. 必填参数

❌ 错误做法:

// 不传platform参数
await fetch('/api/crawler/consume', {
  body: JSON.stringify({ count: 1 })
});

✅ 正确做法:

// 必须传递platform参数
await fetch('/api/crawler/consume', {
  body: JSON.stringify({ 
    count: 1,
    platform: 'yaoshibang'  // 必填
  })
});

2. 平台代码规范

  • 使用小写字母和下划线:yaoshibang
  • 不要使用中文:药师帮
  • 不要使用大写:Yaoshibang

3. 错误处理

try {
  await consumeQuota(platform);
} catch (error) {
  // 常见错误:
  // - 配额不足:code=400, message="每日配额已用完"
  // - 未登录:code=401, message="未认证"
  // - 参数错误:code=400, message="平台代码无效"
  
  if (error.message.includes('配额')) {
    // 引导用户升级会员或等待明天
    showUpgradeModal();
  } else {
    // 显示错误提示
    showError(error.message);
  }
}

4. 性能优化

  • 并发控制:建议串行爬取,避免同时发起多个请求导致配额快速耗尽
  • 缓存策略:相同药品的比价结果可缓存5分钟,减少重复爬取
  • 懒加载:先展示部分平台结果,其余平台异步加载

5. 向后兼容

  • 历史数据的 platform 字段可能为 null
  • 前端需要做空值判断:log.platform || '未知平台'

❓ 常见问题

Q1: 为什么要传递platform参数?

A: 用于记录用户主要在哪些平台进行比价,便于:

  • 运营分析用户偏好
  • 优化爬虫资源分配
  • 提供个性化推荐

Q2: 如果用户选择了多个平台,如何调用接口?

A: 逐个平台调用,每次调用传递对应的platform参数:

for (const platform of selectedPlatforms) {
  await consumeQuota(platform);  // 每次传不同的platform
  const priceData = await crawlPrice(drugName, platform);
}

Q3: 平台代码从哪里获取?

A: 建议使用常量定义:

// constants/platforms.js
export const PLATFORM_CODES = {
  YAOSHBANG: 'yaoshibang',
  YAOBANGMANG: 'yaobangmang',
  YIYAOCHENG: 'yiyaocheng'
};

// 使用时
import { PLATFORM_CODES } from '@/constants/platforms';
await consumeQuota(PLATFORM_CODES.YAOSHBANG);

Q4: 如何处理配额不足的情况?

A: 捕获错误并引导用户:

try {
  await consumeQuota(platform);
} catch (error) {
  if (error.message.includes('配额')) {
    // 显示升级弹窗
    Modal.confirm({
      title: '配额不足',
      content: '每日配额已用完,升级VIP可获得更多配额',
      okText: '立即升级',
      onOk: () => navigate('/upgrade')
    });
  }
}

Q5: 能否一次性消耗多次配额?

A: 可以,但不建议。建议每次爬取一个平台就调用一次接口:

// ✅ 推荐:逐个平台调用
for (const platform of platforms) {
  await consumeQuota(platform);  // count=1
}

// ❌ 不推荐:一次性消耗多次
await consumeQuota('yaoshibang', { count: 3 });

📞 技术支持

如有问题,请联系:

  • 📧 邮箱:dev@zhijiayun.com
  • 💬 企业微信:药店采购比价系统技术群
  • 📱 电话:400-XXX-XXXX

最后更新:2026-06-16
文档版本:v1.0-MVP
维护团队:后端开发组