1. 项目概述:药品商城系统的核心价值
去年帮本地连锁药店做线上化改造时,我深刻体会到医药电商系统的特殊性。这个基于ThinkPHP的药品管理系统,不仅要满足常规电商功能,还要处理处方药审核、医保对接等医药行业特有的业务流程。系统包含后台药品进销存管理、前台H5购药商城、药师咨询模块三大核心板块,日均要处理200+处方单的电子审核流程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计解析
2.1 技术栈选型考量
选择ThinkPHP6框架主要基于三个现实因素:
- 药店现有技术团队主要擅长PHP
- 框架自带的ORM和验证器完美适配药品数据校验需求
- 丰富的扩展库支持微信支付、医保接口对接
数据库采用MySQL主从架构,商品表单独做了分库处理。这里有个细节:药品基础信息表(medicine_base)与库存表(medicine_stock)必须分离,因为同一药品在不同门店的库存和价格策略可能不同。
2.2 核心数据模型设计
药品类目树的设计值得特别注意:
php复制// 三级类目结构示例
class Category extends Model
{
// 一级类目:处方药/非处方药/医疗器械...
public function parent()
{
return $this->belongsTo(Category::class);
}
// 特殊字段
protected $schema = [
'is_prescription' => 'bool', // 是否处方药
'medical_insurance' => 'bool' // 是否医保目录
];
}
处方药需要额外关联的审核流程表:
php复制class Prescription extends Model
{
const STATUS_PENDING = 0; // 待审核
const STATUS_APPROVED = 1; // 已通过
public function orders()
{
return $this->hasMany(Order::class);
}
}
3. 关键业务逻辑实现
3.1 药品库存的分布式管理
采用Redis+Lua脚本实现库存原子性操作:
lua复制-- 库存扣减脚本
local key = KEYS[1]
local change = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key))
if current >= change then
return redis.call('DECRBY', key, change)
else
return -1
end
在PHP中调用:
php复制$result = Redis::eval(
$luaScript,
1,
'stock_'.$skuId,
$purchaseQuantity
);
3.2 处方药审核流程
审核状态机设计要点:
- 用户上传处方照片后自动OCR识别关键信息
- 系统校验处方有效期和医师签名
- 药师后台二次确认用药合理性
php复制class PrescriptionService
{
public function approve($prescriptionId, $pharmacistId)
{
DB::transaction(function() use ($prescriptionId, $pharmacistId) {
$prescription = Prescription::lockForUpdate()->find($prescriptionId);
if ($prescription->status != Prescription::STATUS_PENDING) {
throw new Exception('非法状态变更');
}
$prescription->update([
'status' => Prescription::STATUS_APPROVED,
'approver_id' => $pharmacistId,
'approved_at' => now()
]);
// 触发订单状态变更
OrderService::unlockPrescriptionOrders($prescriptionId);
});
}
}
4. 特殊业务场景处理
4.1 医保支付对接方案
与地方医保平台对接的三大难点:
- 加密验签使用SM2/SM3国密算法
- 需要处理医保目录对照关系
- 结算数据需要保留5年以上
我们通过中间件处理加密通信:
php复制class MedicalInsuranceMiddleware
{
public function handle($request, Closure $next)
{
// 请求体SM2加密
$encrypted = SM2::encrypt(
$request->getContent(),
config('medical_insurance.public_key')
);
$request->headers->set(
'X-Medical-Sign',
SM3::hash($encrypted)
);
return $next($request);
}
}
4.2 药品效期预警机制
在库存管理模块添加定时任务:
php复制class ExpireAlertCommand extends Command
{
protected function schedule(Schedule $schedule)
{
$schedule->call(function() {
$alertItems = MedicineStock::where(
'expire_date', '<', now()->addMonth()
)->where('expire_alert_sent', false)->get();
foreach ($alertItems as $item) {
// 发送预警通知
AlertService::sendExpireWarning($item);
$item->update(['expire_alert_sent' => true]);
}
})->dailyAt('08:00');
}
}
5. 安全与合规要点
5.1 敏感数据保护措施
- 处方图片存储使用私有云OSS
- 数据库字段加密采用AES-256
- 操作日志保留180天以上
php复制class PrescriptionController
{
public function upload(Request $request)
{
$file = $request->file('prescription');
// 文件加密存储
$path = Storage::disk('medical_private')
->putFileAs(
'prescriptions/'.date('Ym'),
$file,
Str::random(40).'.enc'
);
// 记录审计日志
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'prescription_upload',
'ip' => $request->ip()
]);
return response()->json([
'path' => encrypt($path)
]);
}
}
5.2 药品经营许可证校验
在商户入驻模块强制验证:
php复制class MerchantService
{
public function register(array $data)
{
Validator::make($data, [
'drug_license' => [
'required',
function ($attribute, $value, $fail) {
if (!DrugLicense::validate($value)) {
$fail('药品经营许可证无效');
}
}
]
])->validate();
// ...注册逻辑
}
}
6. 性能优化实践
6.1 药品搜索方案
采用Elasticsearch实现多维度搜索:
json复制{
"mappings": {
"properties": {
"name": {"type": "text", "analyzer": "ik_max_word"},
"pinyin": {"type": "text"},
"category_id": {"type": "integer"},
"is_prescription": {"type": "boolean"},
"price": {"type": "scaled_float", "scaling_factor": 100}
}
}
}
搜索接口示例:
php复制class SearchController
{
public function index(Request $request)
{
$params = [
'bool' => [
'must' => [
['match' => ['name' => $request->input('q')]],
['term' => ['is_prescription' => false]] // 默认不展示处方药
]
]
];
if ($request->has('category')) {
$params['bool']['filter'][] = [
'term' => ['category_id' => (int)$request->input('category')]
];
}
return MedicineSearch::searchQuery($params)
->paginate(15);
}
}
6.2 高并发订单处理
使用RabbitMQ实现订单异步处理:
php复制class OrderCreatedListener
{
public function handle($event)
{
$order = $event->order;
AMQP::publish('order_created', json_encode([
'order_id' => $order->id,
'user_id' => $order->user_id,
'created_at' => $order->created_at->toDateTimeString()
]), [
'persistent' => true
]);
}
}
消费者端处理逻辑:
php复制class OrderProcessWorker
{
public function process($message)
{
$data = json_decode($message->body, true);
try {
DB::transaction(function() use ($data) {
// 库存预占
InventoryService::holdItems($data['order_id']);
// 支付超时定时任务
ScheduleService::setPaymentTimeoutJob(
$data['order_id'],
now()->addMinutes(30)
);
});
$message->ack();
} catch (Exception $e) {
$message->nack();
}
}
}
7. 移动端适配方案
7.1 混合开发实践
使用Uniapp实现三端同步:
javascript复制// 药品详情页核心逻辑
export default {
data() {
return {
specs: [],
selectedSpec: null
}
},
methods: {
async loadDetail() {
const res = await this.$http.get(`/api/medicine/${this.id}`, {
params: {
include: 'specs,inventory'
}
});
this.specs = res.data.specs.map(item => {
return {
...item,
disabled: item.inventory <= 0
};
});
}
}
}
7.2 微信小程序特殊处理
药品类目需要特殊报备:
javascript复制// app.js
App({
onLaunch() {
wx.request({
url: '/api/wechat/check_category',
success(res) {
if (!res.data.hasMedicalPermission) {
wx.showModal({
title: '提示',
content: '当前小程序未开通药品类目'
});
}
}
});
}
});
处方上传组件示例:
html复制<template>
<view class="prescription-upload">
<image
v-for="(item,index) in tempFiles"
:key="index"
:src="item.path"
mode="aspectFill"
/>
<button
@click="chooseImage"
:disabled="tempFiles.length >= 3"
>上传处方</button>
</view>
</template>
8. 运维监控体系
8.1 业务指标监控
使用Prometheus监控关键指标:
yaml复制# prometheus.yml 配置示例
scrape_configs:
- job_name: 'pharmacy'
metrics_path: '/metrics'
static_configs:
- targets: ['app:9100']
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox:9115
业务指标示例:
php复制class OrderMetrics
{
public function handleOrderCreated()
{
$counter = Prometheus::counter()
->withName('orders_created_total')
->withHelp('Total created orders')
->withLabels(['payment_type']);
$counter->inc([
$this->order->payment_type
]);
}
}
8.2 日志分析方案
ELK日志处理流程:
python复制# logstash 配置片段
filter {
grok {
match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] %{WORD:env}\.%{LOGLEVEL:level}: %{GREEDYDATA:message}" }
}
if [path] =~ "prescription" {
mutate {
add_tag => [ "sensitive" ]
}
}
}
关键日志查询示例:
sql复制# Kibana Discover查询
status:500 AND path:"/api/order/*"
| sort by @timestamp desc
| limit 100
9. 项目部署实践
9.1 容器化部署方案
Docker-compose核心配置:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8000:8000"
volumes:
- ./storage:/app/storage
depends_on:
- redis
- mysql
environment:
- QUEUE_CONNECTION=redis
mysql:
image: mysql:5.7
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
9.2 持续集成流程
Gitlab CI示例:
yaml复制stages:
- test
- build
- deploy
phpunit:
stage: test
script:
- composer install
- php artisan test
docker-build:
stage: build
script:
- docker build -t registry.example.com/pharmacy:${CI_COMMIT_SHORT_SHA} .
- docker push registry.example.com/pharmacy:${CI_COMMIT_SHORT_SHA}
production-deploy:
stage: deploy
when: manual
script:
- ssh deploy@production "docker pull registry.example.com/pharmacy:${CI_COMMIT_SHORT_SHA}"
- ssh deploy@production "docker-compose up -d"
10. 实际运营经验
10.1 用户行为分析
发现三个关键模式:
- 晚上8-10点是处方药下单高峰
- 用户平均查看5.3个药品页才会下单
- 医保支付订单的退货率比普通订单低62%
对应的优化策略:
php复制class RecommendationService
{
public function forUser($userId)
{
$history = $this->getBrowseHistory($userId);
return Medicine::query()
->whereIn('category_id', $history->pluck('category_id'))
->where('is_prescription', false)
->orderBy('sales_7days', 'desc')
->limit(6)
->get();
}
}
10.2 营销活动设计
药品促销的特殊限制:
- 处方药不得参与任何促销
- 医疗器械折扣不得低于备案价
- 需要额外审核促销文案
php复制class PromotionService
{
public function createCampaign(array $data)
{
$medicines = Medicine::findMany($data['item_ids']);
if ($medicines->contains('is_prescription', true)) {
throw new InvalidArgumentException('处方药不可参与促销');
}
return DB::transaction(function() use ($data) {
$campaign = Promotion::create([
'name' => $data['name'],
'start_time' => $data['start_time'],
'end_time' => $data['end_time'],
'status' => 'pending_review'
]);
foreach ($data['items'] as $item) {
PromotionItem::create([
'promotion_id' => $campaign->id,
'medicine_id' => $item['id'],
'discount_type' => $item['discount_type'],
'discount_value' => $item['discount_value']
]);
}
return $campaign;
});
}
}
