提交 385dcafc authored 作者: lidongxu's avatar lidongxu

分别清洗数据完成

上级 3b2e71ec
---
name: project-docs-workflow
description: Enforces reading project markdown under docs/ before implementing features in this repository, and updating those docs after code changes. Use when changing Python under code/, adding API routes, data conversion, or audit/clean pipelines; use when the user mentions 项目说明、架构、数据流、文档同步.
---
# 项目文档先行与收尾
## 开工前
1. 阅读仓库根目录下的 **`docs/PROJECT_INDEX.md`**(索引与模块分类)。
2. 按任务打开索引中链接的 **模块文档**(如 `docs/api.md``docs/team-conversion.md`),再读相关源码;用户规则要求改代码前也要读目标文件。
3. 索引或模块文档缺失、与代码明显不一致时:在实现过程中**顺带补一节或修正**(仍遵守「注释简洁」与「不大段复制源码」)。
## 完工后
1. 若行为、接口、数据流或目录职责有变:更新对应 **`docs/*.md`**,必要时更新 **`docs/PROJECT_INDEX.md`** 的模块列表或说明。
2. 新增可复用模块或新流水线:在索引中增加分类与链接,并新增或扩写模块文档。
## 文档原则
- **简洁**:架构、数据流用短句 + mermaid 即可;细节用文件路径指向代码。
- **渐进**:Skill 保持短小;长说明放在 `docs/` 按需阅读。
- **调试中脚本****勿**`code/py_/audit/point_sale/data_chengyu_puling.py` 写入项目文档,除非用户明确要求纳入。
## 文档根路径
- 索引:`docs/PROJECT_INDEX.md`
......@@ -7,4 +7,4 @@ __pycache__/
venv/
.env
# 团队转换默认输出目录
code/cache/
code/cache/
\ No newline at end of file
"""动态加载浦零/诚予宽表转换脚本(与 team_conversion_loader 同方式)。"""
import importlib.util
from pathlib import Path
from typing import Any, Callable
_CODE_BASE = Path(__file__).resolve().parent.parent
_CP_SCRIPT = _CODE_BASE / "py_" / "audit" / "point_sale" / "data_chengyu_puling.py"
def _load_mod() -> Any:
spec = importlib.util.spec_from_file_location("chengyu_puling_data", _CP_SCRIPT)
if spec is None or spec.loader is None:
raise RuntimeError(f"无法加载浦零/诚予转换模块: {_CP_SCRIPT}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
if getattr(mod, "run_puling_conversion", None) is None:
raise RuntimeError("data_chengyu_puling 中缺少 run_puling_conversion")
return mod
_mod = _load_mod()
run_puling_conversion: Callable[..., dict[str, Any]] = _mod.run_puling_conversion
default_puling_target_path: Callable[[], str] = _mod.default_puling_target_path
default_chengyu_target_path: Callable[[], str] = _mod.default_chengyu_target_path
PRODUCT_GROUPS: list = _mod.PRODUCT_GROUPS
PRODUCT_GROUPS_CY: list = _mod.PRODUCT_GROUPS_CY
"""清洗相关 HTTP 路由:校验入参、调用团队转换、映射业务错误到 HTTP 状态码。"""
"""清洗相关 HTTP 路由:校验入参、团队/浦零/诚予转换、映射业务错误到 HTTP 状态码。"""
from typing import Any
from fastapi import APIRouter, HTTPException
from api.chengyu_puling_loader import (
PRODUCT_GROUPS,
PRODUCT_GROUPS_CY,
default_chengyu_target_path,
default_puling_target_path,
run_puling_conversion,
)
from api.response import ApiEnvelope, ok
from api.schemas import CleanRequestBody
from api.team_conversion_loader import default_team_target_path, run_team_conversion
......@@ -42,16 +51,49 @@ def post_clean(body: CleanRequestBody) -> ApiEnvelope:
)
team_url = (body.team_url or "").strip()
team_target = (body.team_target_path or "").strip() or default_team_target_path()
if not team_url:
puling_url = (body.puling_url or "").strip()
chengyu_url = (body.chengyu_url or "").strip()
if not team_url and not puling_url and not chengyu_url:
raise HTTPException(
status_code=400,
detail={"ok": False, "error": "team_url 不能为空"},
detail={
"ok": False,
"error": "team_url、puling_url、chengyu_url 至少填写一个非空地址",
},
)
audit_date_str = _audit_date_str_from_body(body)
result = run_team_conversion(team_url, team_target, audit_date_str)
if result.get("ok"):
return ok(data=result, msg="成功")
_raise_http_for_failed_result(result)
return ok(data=result, msg=str(result.get("message") or ""))
data: dict[str, Any] = {"team": None, "puling": None, "chengyu": None}
if team_url:
team_target = (body.team_target_path or "").strip() or default_team_target_path()
r = run_team_conversion(team_url, team_target, audit_date_str)
if not r.get("ok"):
_raise_http_for_failed_result(r)
data["team"] = r
if puling_url:
r = run_puling_conversion(
puling_url,
default_puling_target_path(),
audit_date_str,
yname="浦零",
product_groups=PRODUCT_GROUPS,
)
if not r.get("ok"):
_raise_http_for_failed_result(r)
data["puling"] = r
if chengyu_url:
r = run_puling_conversion(
chengyu_url,
default_chengyu_target_path(),
audit_date_str,
yname="诚予",
product_groups=PRODUCT_GROUPS_CY,
)
if not r.get("ok"):
_raise_http_for_failed_result(r)
data["chengyu"] = r
return ok(data=data, msg="成功")
......@@ -8,7 +8,7 @@ class CleanRequestBody(BaseModel):
year: int | None = None
month: int | None = None
day: int | None = None
team_url: str | None = None
team_url: str | None = None # 非空则走 data_conversion.run_team_conversion
team_target_path: str | None = None # 默认:项目下 cache/team_时间戳.xlsx
puling_url: str | None = None
chengyu_url: str | None = None
puling_url: str | None = None # 非空则走 data_chengyu_puling(浦零列布局)
chengyu_url: str | None = None # 非空则走 data_chengyu_puling(诚予列布局)
# clean_data 项目说明(索引)
面向 Agent 与人类:改代码前先读本索引,再点进对应模块文档。实现后若行为有变,请同步更新本文或子文档。
## 仓库结构(概览)
| 路径 | 职责 |
|------|------|
| `code/` | 运行时代码:FastAPI、`api/``utils/`、稽查转换脚本目录 `py_/audit/...` |
| `code/cache/` | 默认团队转换输出目录(`team_时间戳.xlsx`) |
| `docs/` | 本索引与各模块说明 |
## 模块文档
| 文档 | 内容 |
|------|------|
| [api.md](api.md) | HTTP API、路由、统一响应与错误映射 |
| [team-conversion.md](team-conversion.md) | 团队宽表 URL → 窄表 → `合并后` sheet 流水线 |
| [utils.md](utils.md) | `utils/dates``utils/excel_http` 职责 |
## 架构(逻辑分层)
```mermaid
flowchart TB
subgraph http [HTTP]
A[FastAPI index.py]
R[api/routes_clean.py]
A --> R
end
subgraph load [加载]
L[api/team_conversion_loader.py]
R --> L
end
subgraph biz [业务转换]
D[py_/audit/point_sale/data_conversion.py]
L --> D
end
subgraph util [工具]
U1[utils/excel_http]
U2[utils/dates]
D --> U1
D --> U2
end
D --> X[(本地 xlsx 合并后)]
```
## 数据流(当前已接线:团队清洗)
```mermaid
sequenceDiagram
participant C as 客户端
participant API as POST /api/v1/clean
participant R as routes_clean
participant T as team_conversion_loader
participant D as data_conversion.run_team_conversion
participant URL as team_url xlsx
participant FS as 目标 xlsx
C->>API: JSON body
API->>R: 校验 department / team_url
R->>T: run_team_conversion(...)
T->>D: 动态加载并调用
D->>URL: HTTP 读宽表
D->>D: 宽转窄 + 临期/新鲜度等
D->>FS: 写入/覆盖「合并后」sheet
D-->>R: dict ok / error
R-->>C: ApiEnvelope 或 HTTP 错误
```
## 需求 / 能力拆分(现状)
- **已落地**`department = 风控稽查数据清洗` + `team_url` → 团队宽表转换并落盘;可选 `year/month/day` 拼稽查日期;可选 `team_target_path`(默认 `code/cache/team_{时间戳}.xlsx`)。
- **请求体预留**`puling_url``chengyu_url` 等在 `api/schemas.py` 中已声明,**当前路由未使用**;后续接线时再在子文档中补充数据流(调试中的脚本不写入本文档,除非明确纳入)。
## 运行入口说明
- FastAPI 应用实例在 **`code/index.py`**`app`
- `code/README.md` 中若仍为 `uvicorn main:app`,以实际文件为准:一般在 `code` 目录执行 `uvicorn index:app`(或配置等价模块路径)。
## 维护约定
- 改接口契约、状态码语义、转换步骤或输出文件格式 → 更新 [api.md](api.md) / [team-conversion.md](team-conversion.md)
- 新增独立工具模块 → 更新 [utils.md](utils.md) 并在上表增加一行链接。
# HTTP API(`code/api/`)
## 应用装配
- **`code/index.py`**`FastAPI` 实例 `app`,注册 `HTTPException` / `RequestValidationError` 处理器,挂载 `api_router`
## 路由
- **前缀**`/api`
- **清洗**`POST /api/v1/clean``routes_clean.py`
### POST /api/v1/clean
- **department**:必须为 **`风控稽查数据清洗`**(常量 `DEPARTMENT_RISK_AUDIT_CLEAN`),否则 400。
- **team_url / puling_url / chengyu_url****至少一个**非空;各 URL 须为 `http://``https://`(由下游校验),否则 400;读 URL 失败等映射规则见下。
- **team_url**:若提供 → `team_conversion_loader` 加载的 **`data_conversion.run_team_conversion`**
- **puling_url**:若提供 → **`data_chengyu_puling.run_puling_conversion`**`yname=浦零``PRODUCT_GROUPS`)。
- **chengyu_url**:若提供 → 同一脚本 **`run_puling_conversion`**`yname=诚予``PRODUCT_GROUPS_CY`)。
- **team_target_path**:可选;仅团队分支使用;为空则 `default_team_target_path()``code/cache/team_{时间戳}.xlsx`
- **浦零 / 诚予** 输出路径:分别为 `default_puling_target_path()``default_chengyu_target_path()``code/cache/puling_*.xlsx``chengyu_*.xlsx`
- **year / month / day**:可选;若均提供则拼为 `YYYYMMDD` 传入各清洗分支作为稽查日期线索。
请求体模型见 **`code/api/schemas.py`**`CleanRequestBody`)。
**成功时 `data` 形态**`{ "team": dict | null, "puling": dict | null, "chengyu": dict | null }`,仅对本次请求中**非空 URL** 对应的分支写入结果,其余为 `null`
## 统一响应
- 成功封装:**`ApiEnvelope`**`code/api/response.py`):`code=0``data``msg`
- `HTTPException` / 校验失败:**`exception_handlers.py`** 将详情映射为同类 JSON(`code` 可能为 HTTP 状态码或 422)。
## 团队转换失败 → HTTP
`_raise_http_for_failed_result``routes_clean.py`)根据返回 `dict``error` 文案区分:
-`source_url 须为` → 400
- 含「从 URL 读取源表失败」或「读取源表失败」前缀 → 502
- `message` 存在且无 `error`(如无有效数据)→ 不抛异常,正常包进 `ApiEnvelope`
## 相关文件
- `routes_clean.py``schemas.py``response.py``exception_handlers.py``team_conversion_loader.py``chengyu_puling_loader.py`
# 团队稽查宽表 → 窄表(`data_conversion.py`)
## 位置与加载方式
- 脚本路径:**`code/py_/audit/point_sale/data_conversion.py`**
- **`api/team_conversion_loader.py`**`importlib` 按文件路径动态加载,导出 **`run_team_conversion`**,避免包名与历史中文文件名纠缠。
## 入口:`run_team_conversion`
参数要点:
- **`source_url`**:团队宽表 xlsx 的 URL;须 `http(s)`,否则返回 `ok: False`
- **`target_path`**:输出 xlsx 路径。
- **`audit_date_str`**:可选;与宽表/目标表内稽查日期列协同解析(见 `_resolve_audit_date`)。
流程摘要:
1. **`read_team_source_from_url`**`utils/excel_http.read_excel_from_url_skip1_with_header_row` — 跳过第 1 行,第 2 行起为数据;从第 1 行识别「稽核日期/稽查日期」列索引,失败则用回退列索引。
2. **`main`**:读或建目标工作簿的 **`合并后`** sheet,按 **`PRODUCT_GROUPS_JC`** 将「价格 + 多口味生产月份列」展开为多行窄表,写门店维度列,计算 **临期 / 大日期 / 新鲜度**`rDate`)。
3. 返回 `dict``ok``records_added``target_file``error` / `message`
## 依赖
- **pandas / openpyxl / python-dateutil**(见 `code/requirements.txt`
- **`code/utils/dates.py`****`code/utils/excel_http.py`**`data_conversion` 会把 `code` 上级目录插入 `sys.path` 以 import `utils`
## 与 HTTP 的衔接
[api.md](api.md)`POST /api/v1/clean` 在校验通过后调用 `run_team_conversion(team_url, team_target, audit_date_str)`
# 工具层(`code/utils/`)
## `excel_http.py`
- **`read_excel_from_url`**`urllib` 拉取 xlsx 到内存,`pandas.read_excel`,可配置 `skiprows` / `header`
- **`read_excel_from_url_skip1_with_header_row`**:团队宽表专用 — 返回 **(数据 DataFrame, 第 1 行表头 Series)**,数据从第 2 行起、`header=None`,列下标与业务 `iloc` 约定对齐。
## `dates.py`
- **`to_yyyy_mm_dd`**:单元格值 → `YYYY-MM-DD`
- **`first_yyyy_mm_dd_in_iloc` / `first_yyyy_mm_dd_in_dataframe`**:在宽表或目标表中解析稽查日期。
- **`normalize_year_month_to_day01`**:生产月份字符串规范为 `YYYY-MM-01`
- **`approx_gap_months_calendar`**:到期日相对稽查日的剩余月数近似(供临期/新鲜度逻辑使用)。
业务侧主要使用者:**`py_/audit/point_sale/data_conversion.py`**
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论