Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
746edeb72c | ||
|
|
9c13ab6d3b | ||
|
|
1e4afacac7 |
@@ -0,0 +1,198 @@
|
|||||||
|
# OneOS 日里程与累计里程差额排查报告
|
||||||
|
|
||||||
|
- 排查日期:2026-09-16(Asia/Shanghai)
|
||||||
|
- 涉及数据日期:2026-09-11 至 2026-09-15
|
||||||
|
- 接收方:OneOS 开放平台及里程计算服务维护人员
|
||||||
|
- 状态:已复现;待上游核查计算基准、跨日缺报及协议选源逻辑
|
||||||
|
|
||||||
|
## 1. 问题概述
|
||||||
|
|
||||||
|
BI 用户发现:昨日累计总里程减去前日累计总里程,不等于昨日的区间里程;继续向前检查,多日均存在差额。
|
||||||
|
|
||||||
|
本次在相同车辆范围、相同协议优先级下复现,并绕过 BI 直接请求 OneOS 单日接口核对异常车辆。上游返回本身就存在相邻日期累计差与日里程不一致的情况,不能仅用 BI 显示取整解释。
|
||||||
|
|
||||||
|
已确认的现象:
|
||||||
|
|
||||||
|
1. 某查询日返回的累计里程对应更早日期的报文;后续更新后的累计差跨越多天,与当日日里程不一致。
|
||||||
|
2. 同一车辆相邻日期切换 MQTT / GB32960,累计读数发生跳变,且切换后可能选中很早的读数。
|
||||||
|
|
||||||
|
尚未确认:日里程的内部计算公式、有效数据判定、跨日补算规则及异常过滤规则。本文不将差额直接认定为漏算里程,也不将累计差直接认定为真实日行驶里程。
|
||||||
|
|
||||||
|
## 2. 查询范围与 BI 计算口径
|
||||||
|
|
||||||
|
本次 BI 查询未附加客户、部门、车牌或里程筛选条件,每天返回 916 台车辆。
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/mileage/monitoring
|
||||||
|
?startDate=2026-09-15
|
||||||
|
&endDate=2026-09-15
|
||||||
|
&sourcePriority=instrument
|
||||||
|
&limit=2000
|
||||||
|
```
|
||||||
|
|
||||||
|
`instrument` 对应上游协议优先级:
|
||||||
|
|
||||||
|
```json
|
||||||
|
["GB32960", "MQTT"]
|
||||||
|
```
|
||||||
|
|
||||||
|
BI 当前计算方式:
|
||||||
|
|
||||||
|
| 指标 | 计算方式 |
|
||||||
|
| --- | --- |
|
||||||
|
| 单车累计里程 | OneOS `totalMileageKm`,不以日里程反推 |
|
||||||
|
| 单车单日里程 | OneOS `dailyMileageKm` |
|
||||||
|
| 单车区间里程 | 区间内每日 `dailyMileageKm` 求和 |
|
||||||
|
| 累计总里程 | 查询范围内车辆的累计里程求和;空值按 0 参与汇总 |
|
||||||
|
| 当日总里程 | 查询范围内车辆的日里程求和;汇总接口对结果取整 |
|
||||||
|
|
||||||
|
以下对账表使用逐车日里程原始数值求和,保留三位小数,避免整数显示带来的误差。
|
||||||
|
|
||||||
|
## 3. 汇总对账结果
|
||||||
|
|
||||||
|
单位:km。定义 `差额 = 当天累计总里程 - 前一天累计总里程 - 当天日里程合计`。
|
||||||
|
|
||||||
|
| 日期 | 累计总里程 | 相邻日累计差 | 当日日里程合计 | 差额 |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| 2026-09-11 | 48,448,486.800 | — | 98,182.208 | — |
|
||||||
|
| 2026-09-12 | 48,546,128.300 | 97,641.500 | 97,321.774 | +319.726 |
|
||||||
|
| 2026-09-13 | 48,619,784.800 | 73,656.500 | 82,322.847 | −8,666.347 |
|
||||||
|
| 2026-09-14 | 48,721,190.600 | 101,405.800 | 94,242.687 | +7,163.113 |
|
||||||
|
| 2026-09-15 | 48,824,917.800 | 103,727.200 | 103,554.604 | +172.596 |
|
||||||
|
|
||||||
|
按车牌匹配相邻日期,差额来源分解如下:
|
||||||
|
|
||||||
|
| 日期 | 两日累计非空且协议相同的车辆数 / 净差额 | 两日累计非空且协议变化的车辆数 / 净差额 | 至少一日累计为空的车辆数 / 净差额 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 09-12 | 759 / +319.726 | 0 / 0 | 157 / 0 |
|
||||||
|
| 09-13 | 758 / +674.653 | 1 / −9,341.000 | 157 / 0 |
|
||||||
|
| 09-14 | 759 / +7,163.113 | 0 / 0 | 157 / 0 |
|
||||||
|
| 09-15 | 759 / +172.596 | 0 / 0 | 157 / 0 |
|
||||||
|
|
||||||
|
本次样本中,空累计值组对差额净贡献为 0;同协议数据也存在明显不一致,不能只修复协议切换问题。
|
||||||
|
|
||||||
|
## 4. 异常样本一:累计读数跨日,日里程未与累计差对齐
|
||||||
|
|
||||||
|
以下记录已通过 OneOS 单日接口直接复核,响应均为 HTTP 200、`code=SUCCESS`,记录状态均为 `NORMAL`。
|
||||||
|
|
||||||
|
### 4.1 粤AGQ8377:9 月 15 日差额 +70.2 km
|
||||||
|
|
||||||
|
| 查询日期 | sourceProtocol | totalMileageKm | dailyMileageKm | dataTime(+08:00) |
|
||||||
|
| --- | --- | ---: | ---: | --- |
|
||||||
|
| 2026-09-13 | GB32960 | 14,366.0 | 6.1 | 2026-09-13 07:17:27 |
|
||||||
|
| 2026-09-14 | GB32960 | 14,366.0 | 0 | 2026-09-13 07:17:27 |
|
||||||
|
| 2026-09-15 | GB32960 | 14,467.4 | 31.2 | 2026-09-15 20:57:04 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
累计差 = 14,467.4 - 14,366.0 = 101.4
|
||||||
|
差额 = 101.4 - 31.2 = 70.2 km
|
||||||
|
```
|
||||||
|
|
||||||
|
查询 9 月 14 日时,累计读数仍来自 9 月 13 日。请核查 9 月 15 日日里程的起点读数及其时间,并解释 70.2 km 在日统计中的归属或过滤原因。
|
||||||
|
|
||||||
|
### 4.2 粤AGR6839:9 月 15 日差额 +38.0 km
|
||||||
|
|
||||||
|
| 查询日期 | sourceProtocol | totalMileageKm | dailyMileageKm | dataTime(+08:00) |
|
||||||
|
| --- | --- | ---: | ---: | --- |
|
||||||
|
| 2026-09-14 | GB32960 | 45,122.2 | 0 | 2026-09-11 11:10:53 |
|
||||||
|
| 2026-09-15 | GB32960 | 45,162.8 | 2.6 | 2026-09-15 10:34:02 |
|
||||||
|
|
||||||
|
累计差为 40.6 km,日里程为 2.6 km,差额为 38.0 km。请核查跨多日无新读数后的统计基准及补算策略。
|
||||||
|
|
||||||
|
### 4.3 粤AGP3609:9 月 14 日差额 +808.7 km
|
||||||
|
|
||||||
|
| 查询日期 | sourceProtocol | totalMileageKm | dailyMileageKm | dataTime(+08:00) |
|
||||||
|
| --- | --- | ---: | ---: | --- |
|
||||||
|
| 2026-09-12 | GB32960 | 48,102.9 | 1,293.6 | 2026-09-12 15:08:47 |
|
||||||
|
| 2026-09-13 | GB32960 | 48,102.9 | 0 | 2026-09-12 15:08:47 |
|
||||||
|
| 2026-09-14 | GB32960 | 48,981.7 | 70.1 | 2026-09-14 23:59:51 |
|
||||||
|
| 2026-09-15 | GB32960 | 50,349.1 | 1,367.4 | 2026-09-15 23:59:50 |
|
||||||
|
|
||||||
|
9 月 14 日累计差为 878.8 km,日里程为 70.1 km,差额为 808.7 km;9 月 15 日累计差与日里程则同为 1,367.4 km。该车适合对比正常日期与跨日缺报日期的计算路径。
|
||||||
|
|
||||||
|
## 5. 异常样本二:协议切换导致累计值倒退
|
||||||
|
|
||||||
|
车辆:沪A60591F。以下记录也已直接请求上游复核,均返回 `NORMAL`。
|
||||||
|
|
||||||
|
| 查询日期 | sourceProtocol | totalMileageKm | dailyMileageKm | dataTime(+08:00) |
|
||||||
|
| --- | --- | ---: | ---: | --- |
|
||||||
|
| 2026-09-12 | MQTT | 51,416 | 0 | 2026-09-12 04:53:49 |
|
||||||
|
| 2026-09-13 | GB32960 | 42,075 | 0 | 2026-07-16 23:59:59 |
|
||||||
|
| 2026-09-14 | GB32960 | 42,075 | 0 | 2026-07-16 23:59:59 |
|
||||||
|
| 2026-09-15 | GB32960 | 42,075 | 0 | 2026-07-16 23:59:59 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
9 月 13 日累计差 = 42,075 - 51,416 = -9,341 km
|
||||||
|
9 月 13 日日里程 = 0 km
|
||||||
|
```
|
||||||
|
|
||||||
|
该车单独贡献 −9,341 km,叠加其他同协议车辆 +674.653 km 的差额后,形成当日全量净差额 −8,666.347 km。
|
||||||
|
|
||||||
|
请重点核查:
|
||||||
|
|
||||||
|
1. 同一请求优先级下,9 月 12 日采用 MQTT,9 月 13 日为何改用 7 月 16 日的 GB32960 读数?
|
||||||
|
2. 协议“有效数据”的定义是否包含数据时间、新鲜度和查询日期边界?
|
||||||
|
3. `NORMAL` 是否仅表示读数数值有效,而不表示查询当日存在有效报文?
|
||||||
|
4. 两协议累计读数是否具有同一基准?切换时是否应提供不连续标识?
|
||||||
|
5. 请核查该车协议记录对应的 VIN、设备及绑定历史,排除历史绑定变化造成的读数混用。
|
||||||
|
|
||||||
|
## 6. 上游直接复现方法
|
||||||
|
|
||||||
|
接口:`POST https://open.d.lnoneos.com/api/v1/vehicles/mileage/query`。
|
||||||
|
|
||||||
|
使用已授权的 API Key,通过环境变量注入,不将真实凭证写入报告或日志。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail-with-body --silent --show-error \
|
||||||
|
'https://open.d.lnoneos.com/api/v1/vehicles/mileage/query' \
|
||||||
|
-H "Authorization: Bearer ${ONEOS_MILEAGE_API_KEY}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"date": "2026-09-15",
|
||||||
|
"plateNumbers": ["粤AGQ8377", "粤AGR6839", "粤AGP3609", "沪A60591F"],
|
||||||
|
"protocolPriority": ["GB32960", "MQTT"]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
依次将 `date` 替换为 `2026-09-12`、`2026-09-13`、`2026-09-14`、`2026-09-15`。
|
||||||
|
|
||||||
|
逐车对比字段:`date`、`plateNumber`、`vin`、`status`、`dailyMileageKm`、`totalMileageKm`、`sourceProtocol`、`dataTime`、`calculatedAt`、`updatedAt`。
|
||||||
|
|
||||||
|
本次直接复核保留了报告中的关键字段,未记录响应 `traceId`;上游重放时请保存完整响应及 `traceId`。历史数据如已重算或补报,当前返回值可能与本报告不同,请同时提供重算时间和修改前后结果。
|
||||||
|
|
||||||
|
## 7. 请上游提供的定位结果
|
||||||
|
|
||||||
|
### 7.1 明确字段契约
|
||||||
|
|
||||||
|
- `dailyMileageKm`:是当天首尾读数差、前日末值到当日末值的差,还是逐报文有效增量之和?是否丢弃跨日增量、异常跳变或负增量?
|
||||||
|
- `totalMileageKm`:是查询日最后读数、截至查询日最近读数,还是其他快照?无当日报文时是否延用历史值?
|
||||||
|
- `dataTime`:具体对应累计读数、日里程终点还是其他时间?
|
||||||
|
- 两个里程字段是否保证来自同一协议、同一设备、同一计算快照?
|
||||||
|
- 自然日边界是否统一为 Asia/Shanghai 的 00:00:00 至次日 00:00:00?
|
||||||
|
- 是否承诺相邻日累计差等于日里程;若不承诺,请明确哪些场景不成立及差额如何解释。
|
||||||
|
|
||||||
|
### 7.2 提供样本计算明细
|
||||||
|
|
||||||
|
对上述四辆车,请返回:
|
||||||
|
|
||||||
|
1. 各协议参与选源的候选记录、时间及未选中原因。
|
||||||
|
2. 日里程计算起止报文的时间、累计读数、设备/VIN 标识。
|
||||||
|
3. 有效增量、被过滤增量及过滤原因。
|
||||||
|
4. 缺报、补报、跨日增量归属,以及历史重算记录。
|
||||||
|
5. 对每个差额给出可核对的数值分解,不能只解释为“统计口径不同”。
|
||||||
|
|
||||||
|
## 8. 修复或口径确认后的验收建议
|
||||||
|
|
||||||
|
1. 使用同一车辆集合和协议配置,重放 9 月 11 至 15 日数据,逐车核对后再核对汇总。
|
||||||
|
2. 对连续上报且同协议的正常车辆,若契约约定可对账,应满足累计差与日里程一致;容差按上游实际数值精度明确。
|
||||||
|
3. 对跨日缺报车辆,明确跨日增量是否补算、归属哪天,并能解释全部差额。
|
||||||
|
4. 对协议切换车辆,避免将不同基准的累计值视为连续可减的序列;提供明确的协议切换和不连续信息。
|
||||||
|
5. 对陈旧读数,区分“历史累计值仍有效”和“当日有有效报文”,避免用单一 `NORMAL` 混淆两者。
|
||||||
|
6. 验证单日接口与区间接口对同一车、同一天的日里程、累计值和选源结果一致。
|
||||||
|
7. 如修复涉及历史重算,提供重算范围及完成时间;BI 历史接口缓存最长 6 小时,应在刷新后复核。
|
||||||
|
|
||||||
|
## 9. 本次排查边界
|
||||||
|
|
||||||
|
- 已检查 BI 里程映射、日/区间聚合和汇总逻辑,并对异常样本直接调用上游单日接口复核。
|
||||||
|
- 尚未访问 OneOS 原始报文、计算服务源码或内部存储,不能确定其内部算法根因。
|
||||||
|
- 本次未修改业务代码、未变更里程口径、未触发历史数据重算。
|
||||||
@@ -310,7 +310,7 @@ export default function FullscreenMonitoring({
|
|||||||
<td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td>
|
<td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
<span className={`text-xs font-mono font-bold ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-400' : 'text-blue-400') : 'text-amber-400'}`}>
|
<span className={`text-xs font-mono font-bold ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-400' : 'text-blue-400') : 'text-amber-400'}`}>
|
||||||
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400/70' : 'text-slate-500'}`}>km</span></> : <span className="text-[8px] text-amber-500/50">未对接</span>}
|
{(v.isDataSynced || v.totalKm != null) ? <>{v.mileageAnomaly ? '数据异常' : Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400/70' : 'text-slate-500'}`}>{v.mileageAnomaly ? '' : 'km'}</span></> : <span className="text-[8px] text-amber-500/50">未对接</span>}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ export default function VehicleList({
|
|||||||
<div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center">
|
<div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center">
|
||||||
<Truck size={14} className="text-slate-400" />
|
<Truck size={14} className="text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${v.isOnline ? 'bg-green-500' : 'bg-slate-300'}`} title={v.isOnline ? '在线' : '离线'}></div>
|
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${v.isOnline ? 'bg-green-500' : 'bg-slate-300'}`} title={v.mileageAnomaly ? '数据异常' : v.isOnline ? '在线' : '离线'}></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden flex-1">
|
<div className="overflow-hidden flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<span className="text-xs font-black text-slate-900 font-mono"><Blur>{v.plate}</Blur></span>
|
<span className="text-xs font-black text-slate-900 font-mono"><Blur>{v.plate}</Blur></span>
|
||||||
<span className={`text-[8px] px-1 rounded ${v.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
|
<span className={`text-[8px] px-1 rounded ${v.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
|
||||||
{v.isOnline ? '在线' : '离线'}
|
{v.mileageAnomaly ? '数据异常' : v.isOnline ? '在线' : '离线'}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex shrink-0 rounded px-1.5 py-0.5 text-[7px] font-black ${sourceDisplay.className}`}
|
className={`inline-flex shrink-0 rounded px-1.5 py-0.5 text-[7px] font-black ${sourceDisplay.className}`}
|
||||||
@@ -84,6 +84,7 @@ export default function VehicleList({
|
|||||||
{sourceDisplay.label}
|
{sourceDisplay.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{v.mileageAnomaly && <div className="text-[9px] font-bold text-amber-600" title={v.mileageAnomaly}>{v.mileageAnomaly === 'ODOMETER_SOURCE_CHANGED' ? '里程来源变化' : '上游里程异常'}</div>}
|
||||||
<div className={`text-[8px] font-bold ${statisticTime.color}`} title={statisticTime.title}>
|
<div className={`text-[8px] font-bold ${statisticTime.color}`} title={statisticTime.title}>
|
||||||
{statisticTime.label}
|
{statisticTime.label}
|
||||||
</div>
|
</div>
|
||||||
@@ -101,8 +102,8 @@ export default function VehicleList({
|
|||||||
</div>
|
</div>
|
||||||
<div className="hidden text-right md:block">
|
<div className="hidden text-right md:block">
|
||||||
<div className={`text-sm font-black tabular-nums ${highMileageAlert ? 'text-red-600' : 'text-blue-600'}`}>
|
<div className={`text-sm font-black tabular-nums ${highMileageAlert ? 'text-red-600' : 'text-blue-600'}`}>
|
||||||
{Math.max(0, v.dailyKm || 0).toLocaleString()}
|
{v.mileageAnomaly ? '数据异常' : Math.max(0, v.dailyKm || 0).toLocaleString()}
|
||||||
<span className="ml-1 text-[9px] font-bold text-slate-400">km</span>
|
<span className="ml-1 text-[9px] font-bold text-slate-400">{v.mileageAnomaly ? '' : 'km'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden text-right md:block">
|
<div className="hidden text-right md:block">
|
||||||
@@ -118,7 +119,7 @@ export default function VehicleList({
|
|||||||
)}
|
)}
|
||||||
<span className="text-[7px] font-black text-blue-600/50 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none" title={isRangeMode ? '区间内里程' : '当日里程'}>区</span>
|
<span className="text-[7px] font-black text-blue-600/50 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none" title={isRangeMode ? '区间内里程' : '当日里程'}>区</span>
|
||||||
<div className={`text-sm font-black leading-none ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-600' : 'text-blue-600') : 'text-amber-600'}`}>
|
<div className={`text-sm font-black leading-none ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-600' : 'text-blue-600') : 'text-amber-600'}`}>
|
||||||
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400' : 'text-slate-400'}`}>km</span></> : <span className="text-[7px] text-amber-500/70">未对接</span>}
|
{(v.isDataSynced || v.totalKm != null) ? <>{v.mileageAnomaly ? '数据异常' : Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400' : 'text-slate-400'}`}>{v.mileageAnomaly ? '' : 'km'}</span></> : <span className="text-[7px] text-amber-500/70">未对接</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface MonitoringVehicle {
|
|||||||
plate: string;
|
plate: string;
|
||||||
vin: string;
|
vin: string;
|
||||||
dailyKm: number;
|
dailyKm: number;
|
||||||
|
mileageAnomaly?: string | null;
|
||||||
dailyMileage?: Record<string, number>;
|
dailyMileage?: Record<string, number>;
|
||||||
totalKm: number | null;
|
totalKm: number | null;
|
||||||
source: string;
|
source: string;
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ export function registerHydrogenDailyRoute(
|
|||||||
const [stationRows] = await hydrogenPool.query<RowDataPacket[]>(
|
const [stationRows] = await hydrogenPool.query<RowDataPacket[]>(
|
||||||
`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS d,
|
`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS d,
|
||||||
COALESCE(b.station_id, 0) AS stationId,
|
COALESCE(b.station_id, 0) AS stationId,
|
||||||
COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name),
|
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')),
|
||||||
CASE WHEN MAX(b.station_id) IS NULL THEN '未关联站点'
|
CASE WHEN MAX(b.station_id) IS NULL THEN '未关联站点'
|
||||||
ELSE CONCAT('未知站点 #', MAX(b.station_id)) END) AS stationName,
|
ELSE CONCAT('未知站点 #', MAX(b.station_id)) END) AS stationName,
|
||||||
ROUND(SUM(b.amount_kg), 2) AS kg,
|
ROUND(SUM(b.amount_kg), 2) AS kg,
|
||||||
-- 单价:直接取订单中的成本价(不重算)。MAX 自然忽略 0 元的免费/赠送单
|
-- 单价:直接取订单中的成本价(不重算)。MAX 自然忽略 0 元的免费/赠送单
|
||||||
MAX(b.cost_price) AS pricePerKg
|
MAX(b.cost_price) AS pricePerKg
|
||||||
FROM ${HYDROGEN_TABLE} b
|
FROM ${HYDROGEN_TABLE} b
|
||||||
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||||||
WHERE ${where}
|
WHERE ${where}
|
||||||
GROUP BY d, COALESCE(b.station_id, 0)
|
GROUP BY d, COALESCE(b.station_id, 0)
|
||||||
ORDER BY d DESC, kg DESC`,
|
ORDER BY d DESC, kg DESC`,
|
||||||
|
|||||||
@@ -119,13 +119,13 @@ export function registerHydrogenOverviewRoute(
|
|||||||
// Top5 加氢站(指定年份)
|
// Top5 加氢站(指定年份)
|
||||||
const [top5Rows] = await hydrogenPool.query<RowDataPacket[]>(
|
const [top5Rows] = await hydrogenPool.query<RowDataPacket[]>(
|
||||||
`SELECT b.station_id AS id,
|
`SELECT b.station_id AS id,
|
||||||
COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name),
|
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')),
|
||||||
CASE WHEN b.station_id IS NULL THEN '未关联站点'
|
CASE WHEN b.station_id IS NULL THEN '未关联站点'
|
||||||
ELSE CONCAT('未知站点 #', b.station_id) END) AS name,
|
ELSE CONCAT('未知站点 #', b.station_id) END) AS name,
|
||||||
SUM(b.amount_kg) AS kg,
|
SUM(b.amount_kg) AS kg,
|
||||||
SUM(b.cost_total) AS fee
|
SUM(b.cost_total) AS fee
|
||||||
FROM ${HYDROGEN_TABLE} b
|
FROM ${HYDROGEN_TABLE} b
|
||||||
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||||||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||||||
AND b.${HYDROGEN_LOCAL} >= ?
|
AND b.${HYDROGEN_LOCAL} >= ?
|
||||||
AND YEAR(b.${HYDROGEN_LOCAL}) = ?
|
AND YEAR(b.${HYDROGEN_LOCAL}) = ?
|
||||||
@@ -146,13 +146,13 @@ export function registerHydrogenOverviewRoute(
|
|||||||
// 加氢站全量汇总(同年所有站,按加氢量降序)
|
// 加氢站全量汇总(同年所有站,按加氢量降序)
|
||||||
const [stationFullRows] = await hydrogenPool.query<RowDataPacket[]>(
|
const [stationFullRows] = await hydrogenPool.query<RowDataPacket[]>(
|
||||||
`SELECT b.station_id AS id,
|
`SELECT b.station_id AS id,
|
||||||
COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name),
|
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')),
|
||||||
CASE WHEN b.station_id IS NULL THEN '未关联站点'
|
CASE WHEN b.station_id IS NULL THEN '未关联站点'
|
||||||
ELSE CONCAT('未知站点 #', b.station_id) END) AS name,
|
ELSE CONCAT('未知站点 #', b.station_id) END) AS name,
|
||||||
SUM(b.amount_kg) AS kg,
|
SUM(b.amount_kg) AS kg,
|
||||||
SUM(b.fee_total) AS revenue
|
SUM(b.fee_total) AS revenue
|
||||||
FROM ${HYDROGEN_TABLE} b
|
FROM ${HYDROGEN_TABLE} b
|
||||||
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||||||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||||||
AND b.${HYDROGEN_LOCAL} >= ?
|
AND b.${HYDROGEN_LOCAL} >= ?
|
||||||
AND YEAR(b.${HYDROGEN_LOCAL}) = ?
|
AND YEAR(b.${HYDROGEN_LOCAL}) = ?
|
||||||
@@ -174,18 +174,18 @@ export function registerHydrogenOverviewRoute(
|
|||||||
const [regionRows] = await hydrogenPool.query<RowDataPacket[]>(
|
const [regionRows] = await hydrogenPool.query<RowDataPacket[]>(
|
||||||
`SELECT region, SUM(kg) AS kg FROM (
|
`SELECT region, SUM(kg) AS kg FROM (
|
||||||
SELECT CASE
|
SELECT CASE
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%嘉兴%' OR COALESCE(s.station_name, b.station_name, '') LIKE '%平湖%' THEN '嘉兴'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%嘉兴%' OR COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%平湖%' THEN '嘉兴'
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%广州%' THEN '广州'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%广州%' THEN '广州'
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%佛山%' THEN '佛山'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%佛山%' THEN '佛山'
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%成都%' THEN '成都'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%成都%' THEN '成都'
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%重庆%' THEN '重庆'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%重庆%' THEN '重庆'
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%乌鲁木齐%' THEN '乌鲁木齐'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%乌鲁木齐%' THEN '乌鲁木齐'
|
||||||
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%昆山%' THEN '昆山'
|
WHEN COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '') LIKE '%昆山%' THEN '昆山'
|
||||||
ELSE COALESCE(NULLIF(s.station_name, ''), NULLIF(b.station_name, ''), '未知')
|
ELSE COALESCE(NULLIF(s.site_name, ''), NULLIF(b.station_name, ''), '未知')
|
||||||
END AS region,
|
END AS region,
|
||||||
b.amount_kg AS kg
|
b.amount_kg AS kg
|
||||||
FROM ${HYDROGEN_TABLE} b
|
FROM ${HYDROGEN_TABLE} b
|
||||||
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||||||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||||||
AND b.${HYDROGEN_LOCAL} >= ?
|
AND b.${HYDROGEN_LOCAL} >= ?
|
||||||
AND YEAR(b.${HYDROGEN_LOCAL}) = ?
|
AND YEAR(b.${HYDROGEN_LOCAL}) = ?
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ test('能源路由继续按原顺序注册四个 GET 接口', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('四个处理器继续使用原缓存语义、数据库、SQL 与参数', async () => {
|
test('四个处理器保持缓存和参数契约,加氢站点使用新主数据表', async () => {
|
||||||
const hydrogenCalls: QueryCall[] = [];
|
const hydrogenCalls: QueryCall[] = [];
|
||||||
const electricCalls: QueryCall[] = [];
|
const electricCalls: QueryCall[] = [];
|
||||||
const cacheCalls: CacheCall[] = [];
|
const cacheCalls: CacheCall[] = [];
|
||||||
@@ -133,12 +133,12 @@ test('四个处理器继续使用原缓存语义、数据库、SQL 与参数', a
|
|||||||
[
|
[
|
||||||
'95f6d99b61c9',
|
'95f6d99b61c9',
|
||||||
'63a63c5f78bb',
|
'63a63c5f78bb',
|
||||||
'1367eddb3b7f',
|
'9cf6719a39c3',
|
||||||
'83a52a7f9336',
|
'089d8a10d666',
|
||||||
'f28fa4a3cb08',
|
'256ac8309d5a',
|
||||||
'dac9e86fc527',
|
'dac9e86fc527',
|
||||||
'a71da8f05765',
|
'a71da8f05765',
|
||||||
'b562668859ce',
|
'24b23780a259',
|
||||||
'226fd8f950a1',
|
'226fd8f950a1',
|
||||||
'6e4031b10926',
|
'6e4031b10926',
|
||||||
'4fc77463294c',
|
'4fc77463294c',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface MileageRow {
|
|||||||
vin: string;
|
vin: string;
|
||||||
daily_km: string;
|
daily_km: string;
|
||||||
total_km: string | null;
|
total_km: string | null;
|
||||||
|
mileage_anomaly?: string | null;
|
||||||
source: string;
|
source: string;
|
||||||
source_protocol: OneOsProtocol | null;
|
source_protocol: OneOsProtocol | null;
|
||||||
data_time: string | null;
|
data_time: string | null;
|
||||||
@@ -27,6 +28,7 @@ export interface DailyMileageRow {
|
|||||||
vin: string | null;
|
vin: string | null;
|
||||||
date: string;
|
date: string;
|
||||||
daily_km: string | number | null;
|
daily_km: string | number | null;
|
||||||
|
mileage_anomaly?: string | null;
|
||||||
source: string | null;
|
source: string | null;
|
||||||
source_protocol: OneOsProtocol | null;
|
source_protocol: OneOsProtocol | null;
|
||||||
data_time: string | null;
|
data_time: string | null;
|
||||||
@@ -105,7 +107,8 @@ export function toMileageRows(rows: OneOsDailyMileage[]): MileageRow[] {
|
|||||||
vin: row.vin,
|
vin: row.vin,
|
||||||
daily_km: String(row.dailyMileageKm ?? 0),
|
daily_km: String(row.dailyMileageKm ?? 0),
|
||||||
total_km: row.totalMileageKm == null ? null : String(row.totalMileageKm),
|
total_km: row.totalMileageKm == null ? null : String(row.totalMileageKm),
|
||||||
source: row.status === 'NORMAL' ? 'ONEOS_API' : 'NONE',
|
source: row.status !== 'NO_DATA' ? 'ONEOS_API' : 'NONE',
|
||||||
|
mileage_anomaly: row.status === 'DATA_ANOMALY' ? row.dataQuality || 'DATA_ANOMALY' : null,
|
||||||
source_protocol: row.sourceProtocol,
|
source_protocol: row.sourceProtocol,
|
||||||
data_time: row.dataTime,
|
data_time: row.dataTime,
|
||||||
calculated_at: row.calculatedAt,
|
calculated_at: row.calculatedAt,
|
||||||
@@ -174,6 +177,7 @@ export function mergeMonitoringVehicles(
|
|||||||
plate: info.plate,
|
plate: info.plate,
|
||||||
vin: mileage?.vin || info.vin || '',
|
vin: mileage?.vin || info.vin || '',
|
||||||
dailyKm,
|
dailyKm,
|
||||||
|
mileageAnomaly: mileage?.mileage_anomaly || null,
|
||||||
// Cumulative mileage is never backfilled from a different source here.
|
// Cumulative mileage is never backfilled from a different source here.
|
||||||
totalKm: mileage?.total_km != null ? Number(mileage.total_km) : null,
|
totalKm: mileage?.total_km != null ? Number(mileage.total_km) : null,
|
||||||
source,
|
source,
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ export async function queryRangeMileage(
|
|||||||
vin: row.vin,
|
vin: row.vin,
|
||||||
date,
|
date,
|
||||||
daily_km: row.dailyMileageKm,
|
daily_km: row.dailyMileageKm,
|
||||||
source: row.status === 'NORMAL' ? 'ONEOS_API' : 'NONE',
|
source: row.status !== 'NO_DATA' ? 'ONEOS_API' : 'NONE',
|
||||||
|
mileage_anomaly: row.status === 'DATA_ANOMALY' ? row.dataQuality || 'DATA_ANOMALY' : null,
|
||||||
source_protocol: row.sourceProtocol,
|
source_protocol: row.sourceProtocol,
|
||||||
data_time: row.dataTime,
|
data_time: row.dataTime,
|
||||||
calculated_at: row.calculatedAt,
|
calculated_at: row.calculatedAt,
|
||||||
@@ -216,6 +217,7 @@ export async function queryRangeMileage(
|
|||||||
vin: existing?.vin || row.vin || '',
|
vin: existing?.vin || row.vin || '',
|
||||||
daily_km: String((Number(existing?.daily_km) || 0) + km),
|
daily_km: String((Number(existing?.daily_km) || 0) + km),
|
||||||
total_km: null,
|
total_km: null,
|
||||||
|
mileage_anomaly: existing?.mileage_anomaly || row.mileage_anomaly || null,
|
||||||
source: existing?.source !== 'NONE' && existing?.source ? existing.source : (row.source || 'NONE'),
|
source: existing?.source !== 'NONE' && existing?.source ? existing.source : (row.source || 'NONE'),
|
||||||
source_protocol: row.source_protocol || existing?.source_protocol || null,
|
source_protocol: row.source_protocol || existing?.source_protocol || null,
|
||||||
data_time: row.data_time || existing?.data_time || null,
|
data_time: row.data_time || existing?.data_time || null,
|
||||||
|
|||||||
@@ -86,3 +86,15 @@ test('连续日期分组、车牌规范化和请求键保持稳定', () => {
|
|||||||
);
|
);
|
||||||
assert.throws(() => assertMileageDate('2026/08/12'), /Invalid mileage date/);
|
assert.throws(() => assertMileageDate('2026/08/12'), /Invalid mileage date/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('异常日里程不伪造为有效值,保留仪表累计和异常原因', () => {
|
||||||
|
const [row] = normalizeOneOsRows([{
|
||||||
|
plateNumber: '粤AGE4862', date: '2026-09-17', status: 'DATA_ANOMALY',
|
||||||
|
dataQuality: 'ODOMETER_SOURCE_CHANGED', dailyMileageKm: null,
|
||||||
|
totalMileageKm: 66852.4, sourceProtocol: 'GB32960',
|
||||||
|
}], '2026-09-17');
|
||||||
|
assert.equal(row.status, 'DATA_ANOMALY');
|
||||||
|
assert.equal(row.dailyMileageKm, null);
|
||||||
|
assert.equal(row.totalMileageKm, 66852.4);
|
||||||
|
assert.equal(row.dataQuality, 'ODOMETER_SOURCE_CHANGED');
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ export interface OneOsDailyMileage {
|
|||||||
date: string;
|
date: string;
|
||||||
dailyMileageKm: number | null;
|
dailyMileageKm: number | null;
|
||||||
totalMileageKm: number | null;
|
totalMileageKm: number | null;
|
||||||
status: 'NORMAL' | 'NO_DATA';
|
status: 'NORMAL' | 'NO_DATA' | 'DATA_ANOMALY';
|
||||||
|
dataQuality?: string;
|
||||||
dataTime: string | null;
|
dataTime: string | null;
|
||||||
calculatedAt: string | null;
|
calculatedAt: string | null;
|
||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
@@ -44,7 +45,7 @@ export function normalizeOneOsRows(value: unknown, requestedDate: string): OneOs
|
|||||||
const vin = typeof row.vin === 'string' ? row.vin.trim() : '';
|
const vin = typeof row.vin === 'string' ? row.vin.trim() : '';
|
||||||
const plateNumber = typeof row.plateNumber === 'string' ? row.plateNumber.trim() : '';
|
const plateNumber = typeof row.plateNumber === 'string' ? row.plateNumber.trim() : '';
|
||||||
const date = typeof row.date === 'string' ? row.date : requestedDate;
|
const date = typeof row.date === 'string' ? row.date : requestedDate;
|
||||||
const status = row.status === 'NORMAL' ? 'NORMAL' : 'NO_DATA';
|
const status = row.status === 'NORMAL' || row.status === 'DATA_ANOMALY' ? row.status : 'NO_DATA';
|
||||||
|
|
||||||
// The aliases keep the BI compatible while the Open API formalizes its fields.
|
// The aliases keep the BI compatible while the Open API formalizes its fields.
|
||||||
const dataTime = stringField(row, 'dataTime', 'statisticTime', 'recordTime');
|
const dataTime = stringField(row, 'dataTime', 'statisticTime', 'recordTime');
|
||||||
@@ -60,8 +61,9 @@ export function normalizeOneOsRows(value: unknown, requestedDate: string): OneOs
|
|||||||
plateNumber,
|
plateNumber,
|
||||||
date,
|
date,
|
||||||
dailyMileageKm: nonNegativeMileage(row.dailyMileageKm, status),
|
dailyMileageKm: nonNegativeMileage(row.dailyMileageKm, status),
|
||||||
totalMileageKm: nonNegativeMileage(row.totalMileageKm, status),
|
totalMileageKm: nonNegativeMileage(row.totalMileageKm, status === 'DATA_ANOMALY' ? 'NORMAL' : status),
|
||||||
status,
|
status,
|
||||||
|
...(status === 'DATA_ANOMALY' ? { dataQuality: stringField(row, 'dataQuality') || 'DATA_ANOMALY' } : {}),
|
||||||
dataTime,
|
dataTime,
|
||||||
calculatedAt,
|
calculatedAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface CachedVehicle {
|
|||||||
plate: string;
|
plate: string;
|
||||||
vin: string;
|
vin: string;
|
||||||
dailyKm: number;
|
dailyKm: number;
|
||||||
|
mileageAnomaly?: string | null;
|
||||||
dailyMileage?: Record<string, number>;
|
dailyMileage?: Record<string, number>;
|
||||||
totalKm: number | null;
|
totalKm: number | null;
|
||||||
source: string;
|
source: string;
|
||||||
|
|||||||
+21
-17
@@ -1,3 +1,7 @@
|
|||||||
|
# 凭据通过 Woodpecker Secret 注入(HARBOR_USERNAME / HARBOR_PASSWORD),
|
||||||
|
# 不再把 Harbor 账号密码 base64 后写进流水线脚本。
|
||||||
|
# 镜像推送只在 push / manual 触发:pull_request 不应向生产 registry 推送。
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: npm-build
|
- name: npm-build
|
||||||
image: node:22-alpine
|
image: node:22-alpine
|
||||||
@@ -12,14 +16,17 @@ steps:
|
|||||||
- main
|
- main
|
||||||
- feature/asset-statistics
|
- feature/asset-statistics
|
||||||
commands: |
|
commands: |
|
||||||
cd $CI_WORKSPACE
|
set -eu
|
||||||
npm ci
|
cd "$CI_WORKSPACE"
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
npm ci --registry=https://registry.npmmirror.com/ --no-audit --no-fund --loglevel=verbose
|
||||||
npm run lint
|
npm run lint
|
||||||
npm test
|
npm test
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# 获取分支名
|
# 获取分支名
|
||||||
BRANCH_NAME=$(echo $CI_COMMIT_BRANCH | tr / -)
|
BRANCH_NAME=$(echo ${CI_COMMIT_BRANCH:-manual} | tr / -)
|
||||||
echo "Branch name: $BRANCH_NAME"
|
echo "Branch name: $BRANCH_NAME"
|
||||||
|
|
||||||
# 版本号: 分支名-package.json版本
|
# 版本号: 分支名-package.json版本
|
||||||
@@ -30,10 +37,10 @@ steps:
|
|||||||
|
|
||||||
- name: docker-build
|
- name: docker-build
|
||||||
image: docker:24.0.5-cli
|
image: docker:24.0.5-cli
|
||||||
|
depends_on: [npm-build]
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
|
||||||
- manual
|
- manual
|
||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
@@ -42,25 +49,22 @@ steps:
|
|||||||
- feature/asset-statistics
|
- feature/asset-statistics
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
HARBOR_USERNAME:
|
||||||
|
from_secret: HARBOR_USERNAME
|
||||||
|
HARBOR_PASSWORD:
|
||||||
|
from_secret: HARBOR_PASSWORD
|
||||||
commands: |
|
commands: |
|
||||||
PROJECT_VERSION=$(cat $CI_WORKSPACE/project_version.txt)
|
PROJECT_VERSION=$(cat $CI_WORKSPACE/project_version.txt)
|
||||||
MODULE_NAME=ln-bi
|
MODULE_NAME=ln-bi
|
||||||
|
REGISTRY=harbor.lnh2e.com
|
||||||
|
PROJECT=lingniu-v1
|
||||||
|
|
||||||
echo "Building Docker image: $MODULE_NAME:$PROJECT_VERSION"
|
echo "Building Docker image: $MODULE_NAME:$PROJECT_VERSION"
|
||||||
|
|
||||||
cd $CI_WORKSPACE
|
cd $CI_WORKSPACE
|
||||||
|
docker build -t $REGISTRY/$PROJECT/$MODULE_NAME:$PROJECT_VERSION .
|
||||||
|
|
||||||
docker build -t harbor.lnh2e.com/lingniu-v1/$MODULE_NAME:$PROJECT_VERSION .
|
echo "$HARBOR_PASSWORD" | docker login $REGISTRY -u "$HARBOR_USERNAME" --password-stdin
|
||||||
|
|
||||||
mkdir -p /root/.docker
|
docker push $REGISTRY/$PROJECT/$MODULE_NAME:$PROJECT_VERSION
|
||||||
cat > /root/.docker/config.json <<EOF
|
|
||||||
{
|
|
||||||
"auths": {
|
|
||||||
"harbor.lnh2e.com": {
|
|
||||||
"auth": "$(echo Y2ljZDpMbkBjaWNkMDE=)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
docker push harbor.lnh2e.com/lingniu-v1/$MODULE_NAME:$PROJECT_VERSION
|
|
||||||
|
|||||||
Reference in New Issue
Block a user