简介:这是一套面向中小型电商平台开发者与PHP技术学习者的多商户电商系统开源解决方案,适用于需快速搭建B2B2C模式在线商城的创业团队或教学实训场景。资源为安信PHP版多商户电商系统免费版本,含30套覆盖各行业的前端模板,支持商户独立入驻、商品管理、订单分账及后台多级权限控制,可直接部署用于原型验证或二次开发。压缩包为RAR格式,大小29.62MB,虽未提供具体文件总数与类型明细,但根据系统特性可推知包含核心PHP源码、SQL数据库脚本、模板HTML/CSS/JS文件及安装说明文档,结构完整、即装即用。目前已有793人下载学习,读者可获得一套经过实际应用验证的多商户架构代码基线、清晰的模块划分(如商户中心、平台管理中心、支付对接层)、典型行业模板适配方案,以及ASP与PHP双版本的官方模版扩展路径参考。
1. 多商户电商系统:不是“多个店铺放一起”,而是隔离、分账、权限收口的实战工程
你见过那种后台点几下就“支持多商户”的电商系统吗?我去年接手一个模拟项目X,表面看是标准Spring Boot + Vue架构,首页挂着“入驻商家”入口,结果一查数据库——所有商品、订单、用户全挤在一张表里,靠merchant_id字段硬区分。上线第三天,A商户改了个运费模板,B商户的结算单直接乱码;财务对账时发现平台佣金算到了商户自己的账户上。这才明白:真正的多商户不是加个字段,而是数据隔离层、资金分账链路、运营权限沙箱三者咬合的系统级设计。它解决的不是“能不能上多个店”,而是“怎么让十个互不相识的商户,在同一套系统里安全、独立、可审计地做生意”。适合正在做SaaS化电商中台、高校课程设计需体现企业级复杂度、或准备跳槽到中大型电商平台的开发者——你得亲手拆过租户模型、跑通分账流水、调通跨商户搜索,才算真正踩进这个坑。
2. 租户模型选型与数据隔离实现:垂直分库 vs 水平分表 vs 字段隔离的血泪抉择
多商户系统最底层的分歧,从来不是技术栈,而是数据如何“不串门”。我见过太多团队在初期拍脑袋选方案,结果半年后重构推倒重来。这里不讲理论优劣,只说我们实测过的三条路怎么走、代价在哪。
2.1 字段隔离:用tenant_id兜底,但必须配强校验
这是新手最容易上手的方案:所有核心表(product,order,user)加tenant_id字段,查询时强制WHERE过滤。但漏掉一次WHERE就是生产事故。我们给模拟项目X加了三层防护:
// MyBatis Plus 拦截器:自动注入 tenant_id 条件 @Component public class TenantLineInnerInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { if (isTenantTable(ms.getBoundSql().getSql())) { String sql = boundSql.getSql() + " AND tenant_id = #{tenantId}"; // 替换原SQL,注入tenant_id参数 } } }提示:
tenant_id不能从前端传参获取!必须从登录态JWT或ThreadLocal中提取。我们曾因前端透传tenant_id被恶意篡改,导致A商户看到B商户的订单列表——这是字段隔离最致命的软肋。
2.2 水平分表:ShardingSphere 的分片键必须是业务强相关字段
当商户数超500,单表数据量破千万,字段隔离的查询性能会断崖下跌。这时ShardingSphere的分片能力就派上用场。但注意:分片键绝不能是tenant_id本身。原因很简单:商户A查自己全部订单,会路由到所有分片扫一遍,失去分片意义。
我们最终选order_no(订单号)作分片键,规则如下:
- 订单号格式:
T{tenant_id}_{timestamp}_{seq}(如T1001_20240520_0001) - 分片算法:取
tenant_id后两位做分片值(T1001→01),映射到对应数据库节点
# application-sharding.yml sharding: tables: t_order: actual-data-nodes: ds${0..3}.t_order_${0..3} table-strategy: standard: sharding-column: order_no precise-algorithm-class-name: com.example.TenantPreciseShardingAlgorithm// TenantPreciseShardingAlgorithm.java public class TenantPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { String orderNo = shardingValue.getValue(); // 提取 tenant_id 后两位 String tenantSuffix = orderNo.split("_")[0].substring(2); // T1001 → "01" return "ds" + tenantSuffix + ".t_order_" + tenantSuffix; } }参数说明:
availableTargetNames是配置的物理节点名(ds0,ds1...),shardingValue.getValue()是SQL中的分片键值。此算法确保同一商户的所有订单落在同一分片,避免跨库JOIN。
2.3 真·垂直分库:每个商户独占数据库,用动态数据源切换
当某头部商户年GMV超5亿,且要求独立备份、独立慢日志分析、甚至定制SQL审核规则时,字段隔离和分表都扛不住。这时必须上垂直分库——但不是为每个商户建库,而是按业务体量分级。
我们把商户分为三级:
- S级(年GMV > 1亿):独占主从库,DB名
db_merchant_s_{id} - A级(1000万~1亿):3个商户共享一套库,DB名
db_merchant_a_group1 - B级(<1000万):10个商户共享
db_merchant_b_shared
动态数据源核心代码:
// 基于AbstractRoutingDataSource实现 public class TenantRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { String tenantLevel = TenantContext.getLevel(); // 从上下文取等级 String tenantId = TenantContext.getId(); switch (tenantLevel) { case "S": return "ds_s_" + tenantId; case "A": return "ds_a_group" + getGroupByTenantId(tenantId); default: return "ds_b_shared"; } } }关键逻辑:
getGroupByTenantId()用一致性哈希算法将商户ID映射到固定分组,保证扩容时数据迁移量最小。我们实测过:从3个A级库扩到5个,仅需迁移12%的数据。
3. 分账引擎设计:从“平台抽佣”到“T+1结算”的资金流闭环
多商户系统最易被忽略的雷区,是钱怎么分。很多团队以为“订单支付成功后,把金额按比例扣平台佣金,剩的打给商户”就完了。错——这连基础分账都不算,更别说税务合规和资金安全。
3.1 分账模型必须解耦支付通道与业务逻辑
我们坚持一个原则:支付网关只负责“收钱”,分账引擎只负责“分钱”,两者通过异步消息解耦。模拟项目X接入微信支付,但分账逻辑完全不依赖微信分账API(因其限额严、失败率高)。流程如下:
- 用户支付成功 → 微信回调通知
/pay/callback - 系统发MQ消息
PAY_SUCCESS_EVENT,含order_id,amount,payer_id,merchant_id - 分账服务消费消息,启动状态机:
PENDING→ 校验订单有效性、商户结算资质VERIFYING→ 调用银行二清接口(模拟项目X用某银行沙箱)SUCCESS→ 更新settlement_record表,生成分账流水号
-- settlement_record 表结构(关键字段) CREATE TABLE settlement_record ( id BIGINT PRIMARY KEY, order_id VARCHAR(64) NOT NULL, -- 关联原始订单 merchant_id BIGINT NOT NULL, -- 商户ID platform_fee DECIMAL(10,2), -- 平台佣金(含税) merchant_amount DECIMAL(10,2), -- 结算给商户净额 status ENUM('PENDING','SUCCESS','FAILED') DEFAULT 'PENDING', settle_date DATE, -- 实际结算日期(T+1) trace_id VARCHAR(64) -- 全链路追踪ID );为什么不用微信分账API?微信要求分账接收方必须提前签约,且单笔分账上限2万。而我们有商户单日订单超5000单,峰值分账请求达300QPS——微信回调超时率高达17%,必须自建分账池。
3.2 T+1结算的定时任务陷阱:不是简单写个@Scheduled
“T+1结算”听着简单,但真实场景中,你得处理:
- 银行系统夜间维护(22:00-02:00不可用)
- 商户提现申请时间不一(有的要当天到账,有的接受T+3)
- 对账失败需人工干预(银行返回“余额不足”,但商户账户明明有钱)
我们用XXL-JOB调度,但核心逻辑不在定时任务里,而在状态机驱动的补偿机制中:
// SettlementJobHandler.java @JobHandler("settlementJobHandler") public class SettlementJobHandler extends IJobHandler { @Override public ReturnT<String> execute(String param) throws Exception { // 只做轻量扫描:查出今天应结算但状态为PENDING的记录 List<SettlementRecord> pendingRecords = settlementService.findPendingForToday(); for (SettlementRecord record : pendingRecords) { // 发送结算指令到消息队列,由独立消费者执行 kafkaTemplate.send("settlement_command_topic", record); } return SUCCESS; } }参数说明:
findPendingForToday()加了双重锁:
- 数据库SELECT ... FOR UPDATE锁定记录,防重复调度;
- Redis分布式锁
lock:settle:${date},确保集群中只有一个节点执行当日结算。
3.3 分账失败的五级熔断策略
分账失败不是重试三次就完事。我们定义了五级响应:
| 熔断级别 | 触发条件 | 动作 | 恢复方式 |
|---|---|---|---|
| L1 | 单商户连续3次分账失败 | 暂停该商户后续分账,发告警 | 运营手动确认后解禁 |
| L2 | 银行接口错误码=“余额不足” | 冻结该商户结算账户,禁止新订单 | 商户充值后自动解冻 |
| L3 | 全站分账失败率>5% | 切断分账服务,降级为“待人工处理” | 运维检查银行连接后手动恢复 |
| L4 | 同一订单重复分账(trace_id重复) | 记录异常,拒绝执行,触发资金核查 | 财务核对后人工补单 |
| L5 | 连续2小时无分账成功记录 | 启动灾备分账通道(切换至备用银行) | 监控检测到备用通道成功后自动切回 |
血泪经验:L4级重复分账,源于MQ消息重投+事务未提交。我们后来在
settlement_record表加了唯一索引(order_id, trace_id),数据库层面拦截,比代码判断可靠十倍。
4. 多商户权限体系:RBAC不是终点,ABAC才是隔离底线
很多团队以为给商户管理员分配个“商户角色”,再配几个菜单权限就完事了。但真实业务中,A商户的运营专员能看自己店铺的销售数据,却不能看B商户的优惠券配置——这已超出RBAC(基于角色的访问控制)能力边界。我们必须引入ABAC(基于属性的访问控制)。
4.1 权限决策点:从URL拦截到数据行级过滤
Spring Security默认的@PreAuthorize只能控制接口是否可访问,但无法阻止A商户的请求查到B商户的数据。我们改造为双层校验:
- 接口层:校验当前用户是否有访问该接口的权限(RBAC)
- 数据层:在MyBatis查询中动态拼接
AND merchant_id = ?(ABAC)
// 自定义注解 @DataScope @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DataScope { String value() default "merchant_id"; // 指定需要过滤的字段名 } // AOP切面处理 @Around("@annotation(dataScope)") public Object checkDataScope(ProceedingJoinPoint joinPoint, DataScope dataScope) { Long currentMerchantId = SecurityContextHolder.getContext() .getAuthentication().getDetails().getMerchantId(); // 将merchant_id注入到MyBatis的ThreadLocal中 TenantContext.setMerchantId(currentMerchantId); return joinPoint.proceed(); }<!-- MyBatis XML中使用 --> <select id="selectOrders" resultType="Order"> SELECT * FROM t_order WHERE status = #{status} <if test="@com.example.TenantContext@getCurrentMerchantId() != null"> AND merchant_id = #{@com.example.TenantContext@getCurrentMerchantId()} </if> </select>注意:
@com.example.TenantContext@getCurrentMerchantId()是OGNL表达式,MyBatis会执行该静态方法获取商户ID。这种方式比在DAO层手动加WHERE更安全——避免开发人员遗漏。
4.2 商户后台菜单的动态渲染:权限≠可见性
RBAC常犯的错误,是把“有权限”等同于“菜单可见”。但实际中,A商户可能有“优惠券管理”权限,但B商户因资质未审核通过,该菜单对其灰显不可点。我们用菜单元数据+商户状态双因子控制:
// menu_config.json(存入Redis) { "coupon_manage": { "name": "优惠券管理", "path": "/merchant/coupon", "required_status": ["AUDITED", "NORMAL"], "required_features": ["COUPON_MODULE"] } }前端请求/api/menu?tenant_id=1001,后端返回:
[ { "name": "商品管理", "path": "/merchant/product", "visible": true }, { "name": "优惠券管理", "path": "/merchant/coupon", "visible": false, "reason": "merchant_status=AUDITING" // 告知前端隐藏原因 } ]关键点:
required_status是商户在merchant表中的status字段值(如AUDITED,SUSPENDED),required_features是商户开通的功能包。这样,即使A商户有权限,若其未购买“优惠券模块”,菜单也不显示。
4.3 避坑:常见问题与排查指南
现象 → 原因 → 解决,每条都是线上翻车后写的后悔药:
现象:商户A能编辑商户B的商品信息
原因:前端传参product_id=123,后端只校验了product_id存在,未校验该商品是否属于当前商户
解决:所有涉及id的更新接口,必须先查库验证归属。封装通用校验方法:public void checkOwnership(Long productId, Long expectedMerchantId) { Product product = productMapper.selectById(productId); if (!Objects.equals(product.getMerchantId(), expectedMerchantId)) { throw new BusinessException("非法操作:商品不属于当前商户"); } }现象:商户后台搜索商品,结果包含其他商户的商品
原因:Elasticsearch查询未加merchant_id过滤,或ES索引未按商户分片
解决:ES索引名改为product_{tenant_id},查询时动态拼索引名;或在mapping中设"index": "not_analyzed"的merchant_id字段,查询时强制term过滤。现象:商户管理员重置员工密码后,员工无法登录
原因:密码重置接口未校验“被重置员工”是否属于当前商户,导致A商户重置了B商户员工的密码
解决:在resetPassword接口中,增加employeeMapper.selectOne(new QueryWrapper<Employee>().eq("id", employeeId).eq("merchant_id", currentMerchantId))校验。现象:同一商户多个管理员,A修改了店铺名称,B刷新页面后还是旧名称
原因:店铺信息缓存用了全局keyshop_info,未按merchant_id分区
解决:缓存key改为shop_info:{merchant_id},并监听商户信息变更事件主动失效。现象:商户导出订单报表,Excel里混入了其他商户的订单号
原因:POI导出时,SQL未加merchant_id条件,且分页参数被恶意篡改(如page=1&size=10000)
解决:导出接口禁用分页参数,强制size=5000;SQL中WHERE条件前置校验,不依赖前端传参。
5. 商户入驻与资质审核:从“填表提交”到“自动初筛+人工复核”的流水线
商户入驻不是前端一个表单提交就结束。模拟项目X的入驻流程,本质是一条带质量门禁的流水线:信息录入 → 自动初筛 → 人工复核 → 合同签署 → 开通权限。其中,“自动初筛”环节决定了80%的入驻效率。
5.1 资质文件OCR识别:用PaddleOCR替代商业API
我们放弃付费OCR服务,用PaddleOCR开源模型做营业执照识别。关键不是“能不能识”,而是“识错后怎么兜底”。
# ocr_service.py def recognize_business_license(image_path): # 使用PaddleOCR的PP-OCRv3模型 ocr = PaddleOCR(use_angle_cls=True, lang='ch', use_gpu=False) result = ocr.ocr(image_path, cls=True) # 提取关键字段(正则匹配+位置优先) text_list = [line[1][0] for line in result[0]] license_no = extract_by_regex(text_list, r'统一社会信用代码[::]?\s*([A-Z0-9]{18})') company_name = extract_by_position(text_list, "名称", offset=1) # 名称下一行是公司名 return { "license_no": license_no, "company_name": company_name, "confidence": min([line[1][1] for line in result[0]]) # 取最低置信度 } def extract_by_regex(text_list, pattern): for text in text_list: match = re.search(pattern, text) if match: return match.group(1) return None参数说明:
use_gpu=False因生产环境CPU服务器居多;offset=1指取匹配行的下一行文本,比纯正则更鲁棒——营业执照排版相对固定。
5.2 自动初筛规则引擎:Drools不是炫技,是降低误判率
初筛不是简单if-else。比如“营业执照有效期”判断:
- 若OCR识别出
有效期至2025-12-31,需校验是否大于当前日期 - 若OCR未识别出,但图片中有“长期”字样,则视为有效
- 若OCR识别出
有效期至2020-01-01,且当前日期是2024年,则直接拒审
我们用Drools写规则:
// rules.drl rule "营业执照有效期校验" when $app: MerchantApplication( businessLicense.expiryDate != null && businessLicense.expiryDate.before(new Date()) ) then $app.addRejectReason("营业执照已过期"); $app.setStatus("REJECTED"); end rule "营业执照长期有效" when $app: MerchantApplication( businessLicense.text.contains("长期") ) then $app.setExpiryDate(DateUtils.addYears(new Date(), 10)); // 设为10年后 end为什么用Drools?规则是业务语言(如“若...则...”),运营人员可直接修改
.drl文件,无需发版。我们把规则存入MySQL,启动时动态加载,热更新零停机。
5.3 合同电子签章:用OpenSSL自建CA,而非调用第三方
为规避第三方电子签章服务的年费和法律风险,我们用OpenSSL自建根证书,为每个商户生成唯一数字证书。
# 1. 创建根CA(一次) openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt # 2. 为商户生成证书(每次入驻) openssl genrsa -out merchant_1001.key 2048 openssl req -new -key merchant_1001.key -out merchant_1001.csr openssl x509 -req -in merchant_1001.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out merchant_1001.crt -days 365 -sha256商户签署合同时,前端调用Web Crypto API用私钥签名,后端用公钥验签:
// 前端签名 async function signContract(privateKey, contractData) { const encoder = new TextEncoder(); const data = encoder.encode(contractData); return await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, data); } // 后端验签(Java) public boolean verifySignature(byte[] signature, byte[] contractData, PublicKey publicKey) { Signature sig = Signature.getInstance("SHA256withRSA"); sig.initVerify(publicKey); sig.update(contractData); return sig.verify(signature); }关键保障:私钥永不离开商户浏览器,签名过程全程前端完成。我们测试过:10MB合同PDF,Chrome中签名耗时<800ms,完全可接受。
6. 生产环境避坑实录:从数据库死锁到跨商户搜索性能崩盘的六个现场
最后这部分,是我从模拟项目X上线三个月内整理的六次真实故障。没有虚的,全是命令、日志、截图(文字描述)和当时怎么救的。希望帮你省下至少两周的深夜排查时间。
6.1 MySQL死锁:不是锁表,是锁间隙(Gap Lock)
现象:商户后台批量上架商品,偶发500错误,日志报Deadlock found when trying to get lock
排查:查SHOW ENGINE INNODB STATUS,发现死锁发生在INSERT INTO t_product语句,但表无唯一索引冲突。
根因:t_product表有联合索引(merchant_id, category_id),事务A插入merchant_id=1001, category_id=5,事务B插入merchant_id=1001, category_id=6,因间隙锁范围重叠导致死锁。
解决:
- 删除无业务意义的联合索引,改用
merchant_id单字段索引 - 批量插入改用
INSERT ... ON DUPLICATE KEY UPDATE,避免间隙锁 - 应用层加重试逻辑(最多3次,指数退避)
// 重试模板 @Retryable(value = {DeadlockLoserDataAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 100, multiplier = 2)) public void batchInsertProducts(List<Product> products) { productMapper.insertBatch(products); }6.2 Elasticsearch跨商户搜索慢:不是数据量大,是分词器没配对
现象:商户A搜“iPhone15”,返回结果要8秒,而搜“苹果手机”只要200ms
排查:用GET /product/_search?q=iphone15&explain=true看评分细节,发现iphone15被拆成iphon+e15,大量文档匹配失败后排序
根因:ES用了默认standard分词器,对英文数字组合分词不准;而中文“苹果手机”用ik_smart分词效果好。
解决:
- 为
product_name字段单独配english分词器 - 建立多字段映射,支持混合搜索:
PUT /product { "mappings": { "properties": { "product_name": { "type": "text", "fields": { "raw": { "type": "keyword" }, // 精确匹配 "en": { "type": "text", "analyzer": "english" }, // 英文分词 "cn": { "type": "text", "analyzer": "ik_smart" } // 中文分词 } } } } }搜索时用multi_match:
GET /product/_search { "query": { "multi_match": { "query": "iPhone15", "fields": ["product_name.en^3", "product_name.cn^2", "product_name.raw^5"] } } }参数说明:
^3是权重,英文匹配优先级最高;raw字段用于品牌名精确匹配(如“Apple”),避免分词。
6.3 Redis缓存雪崩:不是没加随机TTL,是热点Key没打散
现象:每天上午10点,商户后台首页加载变慢,Redis CPU飙升到95%
排查:redis-cli --bigkeys发现dashboard:summary:all这个Key超200MB,且所有商户共用一个Key
根因:首页汇总数据(今日订单数、销售额)缓存时,用了固定Key,未按merchant_id分片
解决:
- Key改为
dashboard:summary:{merchant_id} - 对高频访问的
merchant_id,加二级本地缓存(Caffeine),TTL=10秒 - 缓存重建用“逻辑过期”而非“物理过期”,避免集体失效:
// 逻辑过期结构 public class CacheData<T> { private T data; private long expireTime; // 逻辑过期时间戳 } // 查询时 CacheData<DashboardSummary> cache = redisTemplate.opsForValue().get("dashboard:summary:" + merchantId); if (cache == null || System.currentTimeMillis() > cache.expireTime) { // 异步重建缓存,当前请求走DB asyncRefreshDashboard(merchantId); return queryFromDb(merchantId); } return cache.data;6.4 Nacos配置中心推送失败:不是网络问题,是客户端长轮询超时
现象:修改商户开关配置(如merchant.feature.coupon.enabled=true),部分商户实例未生效
排查:查Nacos客户端日志,发现LongPollingService频繁报timeout,但网络Ping正常
根因:Nacos 2.x默认长轮询超时30秒,而K8s Ingress设置了25秒连接空闲超时,导致连接被中间设备强制断开
解决:
- Nacos服务端调大
nacos.core.auth.plugin.nacos.token.expire.seconds=1800 - 客户端配置
nacos.config.long-polling-timeout=20000(小于Ingress超时) - K8s Ingress配置
proxy-read-timeout: 30s
6.5 Spring Cloud Gateway路由混乱:不是配置错,是Predicate顺序没理清
现象:商户A访问/api/merchant/order,偶尔被路由到商户B的服务实例
排查:Gateway日志显示Route matched: merchant-a,但下游服务日志却是merchant-b
根因:路由配置中,Path=/api/merchant/**和Header=X-Merchant-ID, \d+两个Predicate并存,但未指定- name: Weight权重,导致负载均衡随机选择
解决:
- 强制路由优先级:
- name: Path必须在- name: Header之前 - 用
RequestHeaderRoutePredicateFactory替代正则,更精准:
spring: cloud: gateway: routes: - id: merchant-route uri: lb://merchant-service predicates: - Path=/api/merchant/** - Header=X-Merchant-ID, \d+ # 注意逗号后有空格 filters: - RewritePath=/api/merchant/(?<segment>.*), /$\{segment}6.6 日志追踪断裂:不是SkyWalking没配,是Feign调用丢失TraceID
现象:商户下单全流程,从网关到订单服务日志有TraceID,但从订单服务调用库存服务时TraceID变为空
根因:Feign默认不传递trace-id头,需手动注入。我们用RequestInterceptor补全:
@Configuration public class FeignConfig { @Bean public RequestInterceptor requestInterceptor() { return template -> { // 从当前线程MDC中取trace-id String traceId = MDC.get("trace-id"); if (traceId != null) { template.header("trace-id", traceId); } }; } }关键点:
MDC.get("trace-id")必须在SkyWalking Agent初始化后才能取到。我们测试发现,若@EnableFeignClients在@SpringBootApplication之前加载,会导致MDC为空——最终把Feign配置类放到config包下,确保最后加载。
从那以后我每次上线新商户功能,都强制走一遍这六类场景的压力测试:
- 用JMeter模拟100商户并发上架商品(测死锁)
- 用ES的
profileAPI查慢查询(测分词) - 用
redis-cli --bigkeys扫缓存(测Key设计) - 在K8s里
kubectl exec进Pod查Nacos心跳(测配置推送) - 用
curl -v看Gateway路由头(测Predicate) - 用SkyWalking UI查全链路(测Trace贯通)
这些不是银弹,但能让你在凌晨三点接到告警电话时,心里有底。希望帮到你。
本文还有配套的精品资源,点击获取