v1.0.71cc8d2bed2974dc41bf82fe43de2148f0dbae0ca35509cab5bcc22f4a8659a982749ba021f27afb3egit diff 仅命中 1 个文件 src/services/data_service.rs(新增 ~55 行实现 + ~40 行测试,另少量 let → let mut),无其他文件改动。codescan.issues_diff 的真实结果;代码逻辑结论来自对 data_service.rs 相关函数的逐段阅读。本次是一次单一功能迭代:为数据占位符接口(GET /api/v1/data/{articleId}/{shortCode})返回的表格响应新增「环比」列(MoM),口径为“与同一系列上一期相比的百分比”,对应工单 dzs-admin#33 v2 带环比卡。
核心新增件(src/services/data_service.rs):
| 新增件 | 位置(head) | 作用 |
|---|---|---|
pub const MOM_COLUMN_KEY / MOM_COLUMN_LABEL | data_service.rs:2191 | 列 key=mom、label=环比,供客户端 isMomColumn 判据 |
fn with_mom(rows, value_index, series_index) | data_service.rs:2203 | 给按期升序的行追加环比单元格;按系列分组,计算 (cur-prev)/prev,保留 2 位小数;无前值/前值为 0/本期无值 → null |
fn mom_column() | data_service.rs:~2237 | 统一构造 DataColumn{key:"mom",label:"环比"},避免各处 key/label 漂移 |
| 3 个单元测试 | data_service.rs:~4089+ | mom_column_follows_previous_period_within_series、mom_column_never_crosses_series、mom_column_guards_zero_prev_and_rounds |
设计契约(注释声明):响应结构“只加不改”——老客户端忽略多出来的这一列,已发布文章不受影响。
resolve_metric,约 data_service.rs:708 处)// 环比列(带环比卡 / 标准表「值 + 环比」):与同系列上一期比(行已按期升序)
let value_index = if attr_prefix.is_some() { 2 } else { 1 };
let table_rows = with_mom(table_rows, value_index, attr_prefix.map(|_| 1));
columns.push(mom_column());
[期, 值](value_index=1、单系列);有分类 [期, 分类, 值](value_index=2、按“分类”列分组)。下标与 metric_columns 一致,正确。empty_metric_resp 在此之前 return,因此空数据响应不含 mom 列(见风险 R3)。price_buckets,约 data_service.rs:885)let table_rows = with_mom(table_rows, 2, Some(1)); // 行 [日期, 属性组合, 价格]
columns.push(mom_column());
public_price_rows 按 price_date DESC, id DESC 查询后 rows.reverse() → 全局按日期升序,同一“属性组合”跨期有序;分组键为列 1 的属性组合标签(见风险 R1)。price_daily,约 data_service.rs:1005)// 环比列:仅均值表加(PriceDaily::Range 无单一数值列,加了也没有可比口径)
let (mut columns, table_rows) = if matches!(mode, PriceDaily::Range) {
(columns, table_rows)
} else {
let mut cols = columns;
cols.push(mom_column());
(cols, with_mom(table_rows, 1, None)) // 行 [日期, 均价, 最低, 最高],取“均价”列,单系列
};
Average / WholeCommodity 加环比,Range 口径有意不加(注释已说明,合理)。index_series,约 data_service.rs:1770)// 环比列:多字段(现货/期货指数并排)按「指数」列分组,单字段恒一条系列
let value_index = if multi { 2 } else { 1 };
let table_rows = with_mom(table_rows, value_index, multi.then_some(1));
columns.push(mom_column());
if multi { table_rows.sort_by((date, label)) } 之后,行按 (日期, 指数名) 升序,同一指数跨期有序;multi=false 时 resolve_index 只解析单个行业、单字段 → 单系列,None 正确。with_mom 实现要点(正确性核验,均通过)cells 里按原下标读值、算环比,最后统一 row.push(cell),不会因边算边 push 造成下标漂移。prev != 0.0 守卫,前值为 0 → null;serde_json 的 Value::from(NaN/inf) 落为 Null,不会 panic。((cur-prev)/prev*10_000.0).round()/100.0 即“百分数保留 2 位小数”(测试用 1.224 → 1.22 验证)。columns 与每行都各 +1,长度对齐。codescan.issues_diff)工具判定:修复 10 条,全部集中在 src/services/data_service.rs,类型均为 Medium/Complexity 或 Low/Maintainability:
| # | 类型 | base 位置 | 名称/说明 |
|---|---|---|---|
| 1 | Medium/Complexity | data_service.rs:1770 | 嵌套深度过高 |
| 2 | Medium/Complexity | data_service.rs:2445 | 函数过长 |
| 3 | Low/Maintainability | data_service.rs:2461 | 行过长 |
| 4 | Medium/Complexity | data_service.rs:2718 | 函数过长 |
| 5 | Medium/Complexity | data_service.rs:3066 | 函数过长 |
| 6 | Medium/Complexity | data_service.rs:3183 | 函数过长 |
| 7 | Medium/Complexity |
⚠️ 复核结论(重要):这 10 条并非真正的修复,而是行号平移导致的“假修复”。本次在
data_service.rs前部插入了若干行(各调用点 +with_mom块),使原问题的行号整体后移(+78行的那些:2445→2523、2718→2796、3066→3144、3183→3261、3456→3534、3823→3901、3934→4012;前部的 1770→1793 为+23)。工具以“行号”匹配问题,行号一变即被同时记为“删除(修复)”与“新增”。对照第四章可见它们一一对应、性质完全一致。
codescan.issues_diff)工具判定:新增 11 条,全部为 Medium/Low,无 Critical/High:
| # | 严重度/类型 | head 位置 | 说明 | 是否与变更直接相关 |
|---|---|---|---|---|
| 1 | Medium/Complexity | data_service.rs:1793 | 嵌套深度过高 | 否(=base:1770 平移) |
| 2 | Medium/Dependency | data_service.rs:2203 | with_mom 直接依赖 15 个函数(阈值 10) | 是(本次新增函数) |
| 3 | Medium/Complexity | data_service.rs:2523 | anchor_is_time_precise_not_day_truncated 函数过长(95行) | 否(=base:2445 平移) |
| 4 | Low/Maintainability | data_service.rs:2539 | 行过长 | 否(= 平移) |
结论:真正由本次变更引入的新问题只有 1 条——with_mom 的“高耦合”告警。
对该唯一新增告警的判定:大概率误报(FP,低价值)。
with_mom 是一个纯函数(输入 Vec<Vec<Value>> → 输出同类型),其“依赖 15 个函数”统计的是对 Value::from / as_f64 / as_str / HashMap::get / insert / round / ... 等标准库/内建方法与 Value 相关 API 的调用,属启发式“函数级扇出”计数,不代表真实模块耦合。describe 依赖 19、latest_value 依赖 12、load_attr_resolver 依赖 31……),with_mom 的 15 与代码库既有风格一致,不构成新增风险。with_mom(无收益);若要求清零,可将其保留为当前形态并在告警白名单中登记。以下为阅读代码后发现的语义/健壮性风险,扫描器未报告,供评审参考:
R1|系列分组用“显示标签”,而非稳定 key(建议关注,中)
with_mom 的分组键取自表格里的显示文本列(指标=“分类”名、价格=“属性组合”描述、指数=“指数”名),而各调用点构造 series 用的是稳定 key(attr_value_code / bucket_key / source_type)。二者不一致:
attr_values / 不同 source_type)解析出的显示文本相同(例如属性名、属性值名重名导致 describe() / value_name() 输出碰撞),with_mom 会把它们当作同一条系列,从而产生跨系列的错误环比。with_mom 接受稳定的系列 key(或让调用点传入 key 列并在输出前剥离),或在代码注释中显式声明“标签在同一响应内唯一”为前置条件。R2|空值/缺失期采用“跨空期 carry-forward”,与“上一期”字面口径可能不符(建议确认,中)
实现中当某期数值为 null 时,不更新 last[key];因此下一期的环比会与“最近一个非空期”比较,而非“相邻上一期”。同理,若中间整期缺失(行不存在),环比也跨期比较。测试用例仅覆盖了“末尾为 null”,未覆盖“中间空洞”。若产品口径要求“环比=与日历相邻上一期”,则空期后应输出 null 而非跨期百分比。建议:与产品确认,并补一条“中间空期”的单测固化预期。
R3|空数据响应不含 mom 列,列集合前后不一致(轻微,低)
empty_metric_resp / empty_price_resp / empty_futures_resp 均在 columns.push(mom_column()) 之前返回,故空数据时无 mom 列。对有缓存/按列渲染的前端,可能造成同一短码“有数据/无数据”两种列头。功能上影响有限(空表无行),但属契约不一致,建议确认前端是否依赖列集合稳定。
R4|期货表未加环比,范围一致性存疑(观察,低)
本次给 指标 / 公开价分桶 / 公开价均值 / 指数 四类表加了 mom,但 futures_main_series(行 [交易日, 值])与 futures_contract_series(行 [交易日, 合约, 值],形态与 price_buckets 几乎一致)未加。若工单期望“所有标准表”都有环比,则期货表属覆盖遗漏;若有意排除,建议在注释中标注范围边界。建议:与需求方核对期货表是否在范围内。
R5|已核验为“无问题”的点(正向确认)
serde_json 对 NaN/inf → Null 的行为,不会 panic / 不会产出非法数值;Range 口径显式不加环比(口径正确);各调用点 value_index 与实际列结构匹配;附:版本内存在的 Critical 0 条 / High 15 条(
market_service/cache.rs、metric_service/cache.rs的 MD5 弱算法,response_cache.rs的 token 日志泄露,redis_util.rs不可达代码)均为存量问题,不在本次 diff 范围内,本报告不计入增量;如需处理请另行立项。
data_service.rs,为表格响应“只加不改”地追加环比列,逻辑正确、边界(除零/空值/舍入)已由单测覆盖。with_mom 高耦合)判定为启发式误报,无需重构。data_service.rs:3456 |
| 嵌套深度过高 |
| 8 | Medium/Complexity | data_service.rs:3456 | 函数过长 |
| 9 | Medium/Complexity | data_service.rs:3823 | 函数过长 |
| 10 | Medium/Complexity | data_service.rs:3934 | 嵌套深度过高 |
base:2461| 5 | Medium/Complexity | data_service.rs:2796 | short_codes_are_deterministic_... 函数过长(221行) | 否(=base:2718 平移) |
| 6 | Medium/Complexity | data_service.rs:3144 | resolves_futures_from_quote_table 函数过长(113行) | 否(=base:3066 平移) |
| 7 | Medium/Complexity | data_service.rs:3261 | resolves_index_from_sector_index_table 函数过长(114行) | 否(=base:3183 平移) |
| 8 | Medium/Complexity | data_service.rs:3534 | 嵌套深度过高 | 否(=base:3456 平移) |
| 9 | Medium/Complexity | data_service.rs:3534 | discover_upper_bound_target 函数过长(181行) | 否(=base:3456 平移) |
| 10 | Medium/Complexity | data_service.rs:3901 | assert_upper_bound 函数过长(96行) | 否(=base:3823 平移) |
| 11 | Medium/Complexity | data_service.rs:4012 | 嵌套深度过高 | 否(=base:3934 平移) |