Hyperf对接报表 在 HyperF 项目中,如何设计帆布报表模板的版本管理机制,以支持报表模板的灰度发布、回滚及多版本并行运行?
2026/4/16 19:36:55 网站建设 项目流程
帆布报表模板版本管理系统 选型: hyperf/database + z-song/hyperf-repository 仓储模式 + Redis 灰度路由 + 事件驱动回滚 --- 架构总览 模板请求 └─ VersionRouter# 灰度路由:按租户/用户/流量比例分流├─ stable v1.2# 稳定版(默认)├─ canary v1.3# 灰度版(5% 流量)└─ shadow v1.4# 影子版(仅记录,不返回)↓ TemplateEngine# 渲染引擎,版本无关↓ MetricsCollector# 采集各版本渲染指标 → 自动回滚判断--- 一、数据库设计 -- 模板主表 CREATE TABLE report_templates(idINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code VARCHAR(64)NOT NULL UNIQUE, -- 业务标识,跨版本稳定 name VARCHAR(128)NOT NULL, tenant_id INT UNSIGNED NOT NULL, created_by INT UNSIGNED NOT NULL, created_at DATETIME NOT NULL, INDEX idx_tenant(tenant_id));-- 版本表(不可变,只增不改) CREATE TABLE report_template_versions(idINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, template_id INT UNSIGNED NOT NULL, version VARCHAR(20)NOT NULL, -- semver:1.2.0 config JSON NOT NULL, -- 列定义/SQL/样式 changelog TEXT, status TINYINT NOT NULL DEFAULT0, --0=draft1=canary2=stable3=deprecated created_by INT UNSIGNED NOT NULL, created_at DATETIME NOT NULL, UNIQUE KEY uk_ver(template_id, version), INDEX idx_status(template_id, status));-- 灰度规则表 CREATE TABLE canary_rules(idINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, template_id INT UNSIGNED NOT NULL, version_id INT UNSIGNED NOT NULL, rule_type VARCHAR(20)NOT NULL, -- percent/tenant/user rule_value JSON NOT NULL, --{"percent":5}/{"tenant_ids":[1,2]}enabled TINYINT NOT NULL DEFAULT1, UNIQUE KEY uk_tpl_ver(template_id, version_id));--- 二、版本路由器 — 灰度核心<?php // app/Template/VersionRouter.php namespace App\Template;use Hyperf\Context\Context;use Hyperf\DbConnection\Db;class VersionRouter{// 路由优先级:用户白名单>租户白名单>流量百分比>稳定版 publicfunctionresolve(int$templateId): array{$auth=Context::get('auth');$cacheKey="ver_route:{$templateId}:{$auth['user_id']}";// 同一请求内复用路由结果return\App\Cache\RequestCache::remember($cacheKey,function()use($templateId,$auth){return$this->doResolve($templateId,$auth);});}privatefunctiondoResolve(int$templateId, array$auth): array{$canaries=$this->getCanaryRules($templateId);foreach($canariesas$rule){if($this->match($rule,$auth)){return$this->getVersion($rule['version_id']);}}return$this->getStableVersion($templateId);}privatefunctionmatch(array$rule, array$auth): bool{$val=$rule['rule_value'];returnmatch($rule['rule_type']){'user'=>in_array($auth['user_id'],$val['user_ids']??[]),'tenant'=>in_array($auth['tenant_id'],$val['tenant_ids']??[]),'percent'=>crc32($auth['user_id'].$rule['id'])%100<($val['percent']??0), default=>false,};}// Redis 缓存灰度规则,5 分钟 TTL privatefunctiongetCanaryRules(int$templateId): array{$key="canary_rules:{$templateId}";if($cached=redis()->get($key))returnunserialize($cached);$rules=Db::table('canary_rules as cr')->join('report_template_versions as v','v.id','cr.version_id')->where('cr.template_id',$templateId)->where('cr.enabled',1)->where('v.status',1)//status=1canary ->orderBy('cr.id')->get(['cr.*','cr.rule_value'])->map(fn($r)=>[...(array)$r,'rule_value'=>json_decode($r->rule_value,true)])->all();redis()->setex($key,300, serialize($rules));return$rules;}privatefunctiongetVersion(int$versionId): array{return(array)Db::table('report_template_versions')->find($versionId);}privatefunctiongetStableVersion(int$templateId): array{return(array)Db::table('report_template_versions')->where('template_id',$templateId)->where('status',2)// stable ->orderByDesc('id')->first();}}--- 三、版本服务 — 发布 / 回滚<?php // app/Service/TemplateVersionService.php namespace App\Service;use App\Event\TemplateRolledBack;use Hyperf\DbConnection\Db;use Psr\EventDispatcher\EventDispatcherInterface;class TemplateVersionService{publicfunction__construct(privatereadonlyEventDispatcherInterface$events){}// 发布为灰度 publicfunctionpromoteToCanary(int$versionId, array$rule): void{Db::transaction(function()use($versionId,$rule){Db::table('report_template_versions')->where('id',$versionId)->update(['status'=>1]);$templateId=Db::table('report_template_versions')->where('id',$versionId)->value('template_id');Db::table('canary_rules')->updateOrInsert(['template_id'=>$templateId,'version_id'=>$versionId],['rule_type'=>$rule['type'],'rule_value'=>json_encode($rule['value']),'enabled'=>1]);$this->flushRouteCache($templateId);});}// 灰度转稳定(全量发布) publicfunctionpromoteToStable(int$versionId): void{$version=Db::table('report_template_versions')->find($versionId);Db::transaction(function()use($version){// 旧稳定版 → deprecated Db::table('report_template_versions')->where('template_id',$version->template_id)->where('status',2)->update(['status'=>3]);// 新版本 → stable,关闭灰度规则 Db::table('report_template_versions')->where('id',$version->id)->update(['status'=>2]);Db::table('canary_rules')->where('version_id',$version->id)->update(['enabled'=>0]);$this->flushRouteCache($version->template_id);});}// 回滚到指定版本 publicfunctionrollback(int$templateId, int$targetVersionId): void{Db::transaction(function()use($templateId,$targetVersionId){// 当前 stable → deprecated Db::table('report_template_versions')->where('template_id',$templateId)->where('status',2)->update(['status'=>3]);// 目标版本 → stable Db::table('report_template_versions')->where('id',$targetVersionId)->update(['status'=>2]);// 关闭所有灰度规则 Db::table('canary_rules')->where('template_id',$templateId)->update(['enabled'=>0]);$this->flushRouteCache($templateId);});$this->events->dispatch(new TemplateRolledBack($templateId,$targetVersionId));}// 自动回滚:错误率超阈值触发 publicfunctionautoRollbackIfNeeded(int$templateId): void{$metrics=$this->getCanaryMetrics($templateId);if(!$metrics)return;$errorRate=$metrics['errors']/ max($metrics['total'],1);if($errorRate>0.05||$metrics['p99_ms']>5000){$lastStable=Db::table('report_template_versions')->where('template_id',$templateId)->where('status',3)// deprecated=上一个稳定版 ->orderByDesc('id')->value('id');$lastStable&&$this->rollback($templateId,$lastStable);}}privatefunctiongetCanaryMetrics(int$templateId): ?array{$key="canary_metrics:{$templateId}";$raw=redis()->get($key);return$raw? json_decode($raw,true):null;}privatefunctionflushRouteCache(int$templateId): void{redis()->del("canary_rules:{$templateId}");}}--- 四、指标采集 — 驱动自动回滚<?php // app/Template/MetricsCollector.php namespace App\Template;class MetricsCollector{publicfunctionrecord(int$templateId, int$versionId, int$durationMs, bool$success): void{$key="canary_metrics:{$templateId}";// Redis Hash 原子累加,窗口10分钟$pipe=redis()->pipeline();$pipe->hIncrBy($key,'total',1);$pipe->hIncrBy($key,'errors',$success?0:1);$pipe->expire($key,600);$pipe->execute();// 滑动窗口 P99(简化:用 sortedset存延迟样本)$tsKey="canary_latency:{$templateId}";redis()->zAdd($tsKey, time(),"{$durationMs}:".uniqid());redis()->zRemRangeByScore($tsKey,0, time()-600);}publicfunctionp99(int$templateId): int{$key="canary_latency:{$templateId}";$total=redis()->zCard($key);$idx=(int)($total*0.99);$samples=redis()->zRange($key,$idx,$idx);return$samples?(int)explode(':',$samples[0])[0]:0;}}--- 五、渲染入口 — 版本透明<?php // app/Service/ReportRenderService.php namespace App\Service;use App\Template\MetricsCollector;use App\Template\VersionRouter;use OpenSpout\Writer\XLSX\Writer;use OpenSpout\Common\Entity\Row;class ReportRenderService{publicfunction__construct(privatereadonlyVersionRouter$router, privatereadonlyMetricsCollector$metrics, privatereadonlyTemplateVersionService$versions,){}publicfunctionrender(int$templateId, array$filters, string$outPath): void{$version=$this->router->resolve($templateId);$config=json_decode($version['config'],true);$start=microtime(true);$success=true;try{$writer=new Writer();$writer->openToFile($outPath);$writer->addRow(Row::fromValues(array_column($config['columns'],'label')));foreach($this->fetchData($config,$filters)as$batch){$writer->addRows(array_map(fn($r)=>Row::fromValues(array_values((array)$r)),$batch));}$writer->close();}catch(\Throwable$e){$success=false;throw$e;}finally{$ms=(int)((microtime(true)-$start)*1000);$this->metrics->record($templateId,$version['id'],$ms,$success);//每次渲染后检查是否需要自动回滚 $this->versions->autoRollbackIfNeeded($templateId);} } private function fetchData(array $config,array $filters):\Generator { $lastId=0;do { $rows=\Hyperf\DbConnection\Db::table($config['table'])->select(array_column($config['columns'],'field'))->where($filters)->where('id','>',$lastId)->orderBy('id')->limit(5000)->get();if($rows->isEmpty())break;$lastId=$rows->last()->id;yield$rows->all();}while(true);}}--- 六、Controller — 版本管理 API<?php // app/Controller/TemplateVersionController.php namespace App\Controller;use App\Service\TemplateVersionService;use Hyperf\HttpServer\Annotation\{Controller, Post, Get};use Hyperf\HttpServer\Contract\RequestInterface;#[Controller(prefix: '/template/version')]class TemplateVersionController{publicfunction__construct(privatereadonlyTemplateVersionService$svc){}#[Post('/canary/{versionId}', options: ['permission' => 'template:publish'])]publicfunctioncanary(int$versionId, RequestInterface$req): array{$this->svc->promoteToCanary($versionId,$req->post('rule'));return['ok'=>true];}#[Post('/stable/{versionId}', options: ['permission' => 'template:publish'])]publicfunctionstable(int$versionId): array{$this->svc->promoteToStable($versionId);return['ok'=>true];}#[Post('/rollback/{templateId}/{versionId}', options: ['permission' => 'template:rollback'])]publicfunctionrollback(int$templateId, int$versionId): array{$this->svc->rollback($templateId,$versionId);return['ok'=>true];}#[Get('/list/{templateId}')]publicfunctionlist(int$templateId): array{return\Hyperf\DbConnection\Db::table('report_template_versions')->where('template_id',$templateId)->orderByDesc('id')->get(['id','version','status','changelog','created_at'])->toArray();}}--- 七、完整状态机 draft(0)──promoteToCanary──► canary(1)──promoteToStable──► stable(2)│ │ │ autoRollback/rollback │ rollback ▼ ▼ deprecated(3)◄──────────────── deprecated(3)灰度流量分配示意: ┌─────────────────────────────────────────┐ │ 全量用户 │ │ ├─user_ids=[1,2,3]→ v1.3(白名单)│ │ ├─tenant_ids=[5]→ v1.3(租户)│ │ ├─percent=5% → v1.3(随机)│ │ └─ 其余95% → v1.2(stable)│ └─────────────────────────────────────────┘ 自动回滚触发条件: 错误率>5% OR P99 延迟>5000ms → 回滚到上一个 deprecated 版本 --- 八、关键设计决策 ┌────────────┬──────────────────────┬────────────────────────────────────────────┐ │ 问题 │ 决策 │ 原因 │ ├────────────┼──────────────────────┼────────────────────────────────────────────┤ │ 版本存储 │ 只增不改 JSON config │ 历史版本可随时回放,审计可追溯 │ ├────────────┼──────────────────────┼────────────────────────────────────────────┤ │ 灰度路由 │ crc32 取模 │ 同一用户始终路由到同一版本,体验一致 │ ├────────────┼──────────────────────┼────────────────────────────────────────────┤ │ 自动回滚 │ 错误率+P99 双指标 │ 单指标误判率高,双指标更稳健 │ ├────────────┼──────────────────────┼────────────────────────────────────────────┤ │ 路由缓存 │ Redis 5min + 请求级 │ 减少 DB 查询,灰度规则变更 5min 内生效 │ ├────────────┼──────────────────────┼────────────────────────────────────────────┤ │ 多版本并行 │ status 字段区分 │ 同一模板可同时存在 canary+stable,互不干扰 │ └────────────┴──────────────────────┴────────────────────────────────────────────┘

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询