⚠️ MVP 阶段说明
本项目为全新项目,尚未上线,当前处于 MVP(最小可行产品)阶段。
核心业务:药店采购比价系统,爬取各医药B2B平台药品价格,生成采购链接。
目标用户:药店采购人员、药师,目标日活20万+。📅 版本:v1.0-MVP | 📆 更新日期:2026-06-16 | ✅ 状态:开发中
本次更新为爬虫系统添加了全渠道医药平台区分功能,支持记录每次爬取的采购平台来源:
| 价值点 | 说明 | 收益 |
|---|---|---|
| 🎯 最优价格采购 | 覆盖34个主流医药渠道 | 降低采购成本10%-30% |
| 🔗 一键跳转购买 | 生成各平台采购链接 | 提升转化率50%+ |
| 📊 跨平台对比 | 实时价格对比分析 | 提高决策效率 |
| 👥 邀请裂变 | 通过分享获得额外配额 | 提升用户注册率和日活 |
🏭 B2B批发平台(14个)- 药店主要采购渠道
├─ 核心平台:药师帮、药京采、健之佳
└─ 扩展平台:1药网、康爱多、阿里健康等
🏪 B2C零售平台(8个)- 小批量采购/急单
├─ O2O即时配送:叮当快药、美团买药
└─ 连锁药房:老百姓、大参林、益丰等
🛒 综合电商医药频道(7个)- 价格参考
└─ 京东健康、天猫医药、拼多多医药等
👨⚕️ 垂直医药平台(4个)- 特色渠道
└─ 微医、平安好医生、春雨医生等
// 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: 展示比价结果,点击跳转到对应平台购买
// 消耗配额(必须传递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;
};
接口信息:
POST /api/crawler/consume请求参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| count | Number | 是 | - | 消耗次数(通常传1) |
| useCouponFirst | Boolean | 否 | false | 是否优先使用优惠券 |
| platform | String | 是 | - | 平台代码(见下方枚举) |
MVP核心平台枚举:
const MVP_PLATFORMS = {
YAOSHBANG: 'yaoshibang', // 药师帮 - 国内最大医药B2B ⭐
YAOBANGMANG: 'yaobangmang', // 药帮忙 - 西南地区领先 ⭐
YIYAOCHENG: 'yiyaocheng' // 1药城 - 华中地区知名 ⭐
};
完整平台列表(Phase 2-6扩展):
响应示例:
{
"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字段!
});
接口信息:
GET /api/crawler/logs?days=7查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| 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 || [];
}
接口信息:
GET /api/admin/crawler/user-detail?userId=1查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 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
}
]
}
}
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;
.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) │<───────────────────────────┤
│<─────────────────────────────┤ │
❌ 错误做法:
// 不传platform参数
await fetch('/api/crawler/consume', {
body: JSON.stringify({ count: 1 })
});
✅ 正确做法:
// 必须传递platform参数
await fetch('/api/crawler/consume', {
body: JSON.stringify({
count: 1,
platform: 'yaoshibang' // 必填
})
});
yaoshibang ✅药师帮 ❌Yaoshibang ❌try {
await consumeQuota(platform);
} catch (error) {
// 常见错误:
// - 配额不足:code=400, message="每日配额已用完"
// - 未登录:code=401, message="未认证"
// - 参数错误:code=400, message="平台代码无效"
if (error.message.includes('配额')) {
// 引导用户升级会员或等待明天
showUpgradeModal();
} else {
// 显示错误提示
showError(error.message);
}
}
platform 字段可能为 nulllog.platform || '未知平台'A: 用于记录用户主要在哪些平台进行比价,便于:
A: 逐个平台调用,每次调用传递对应的platform参数:
for (const platform of selectedPlatforms) {
await consumeQuota(platform); // 每次传不同的platform
const priceData = await crawlPrice(drugName, platform);
}
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);
A: 捕获错误并引导用户:
try {
await consumeQuota(platform);
} catch (error) {
if (error.message.includes('配额')) {
// 显示升级弹窗
Modal.confirm({
title: '配额不足',
content: '每日配额已用完,升级VIP可获得更多配额',
okText: '立即升级',
onOk: () => navigate('/upgrade')
});
}
}
A: 可以,但不建议。建议每次爬取一个平台就调用一次接口:
// ✅ 推荐:逐个平台调用
for (const platform of platforms) {
await consumeQuota(platform); // count=1
}
// ❌ 不推荐:一次性消耗多次
await consumeQuota('yaoshibang', { count: 3 });
如有问题,请联系:
最后更新:2026-06-16
文档版本:v1.0-MVP
维护团队:后端开发组