Features: - PatientWechatCallbackController for URL verification and message handling - PatientWechatService for template and customer messages - Support for secure mode (message encryption/decryption) - Simplified route /wechat/patient/callback for WeChat config - Event handlers for subscribe/unsubscribe/text messages - Template message for visit reminders Technical details: - Reuse @wecom/crypto for encryption (compatible with Official Account) - Relaxed Fastify schema validation to prevent early request blocking - Access token caching (7000s with 5min pre-refresh) - Comprehensive logging for debugging Testing: Local URL verification passed, ready for SAE deployment Status: Code complete, waiting for WeChat platform configuration
224 lines
5.6 KiB
Markdown
224 lines
5.6 KiB
Markdown
# 工具C - Pivot列顺序优化总结
|
||
|
||
## 📋 问题描述
|
||
|
||
**用户需求**:长宽转换后,列的排序应该与上传文件时的列顺序保持一致。
|
||
|
||
**当前问题**:系统按字母顺序排列转换后的列,导致顺序与原文件不一致。
|
||
|
||
---
|
||
|
||
## 🎯 解决方案:方案A - Python端排序
|
||
|
||
### 核心思路
|
||
1. Node.js后端从session获取**原始列顺序**
|
||
2. Node.js后端从数据中提取**透视列值的原始顺序**(按首次出现顺序)
|
||
3. 传递给Python
|
||
4. Python在pivot后,按原始顺序重排列
|
||
|
||
---
|
||
|
||
## 🛠️ 实现细节
|
||
|
||
### 1. Python端(pivot.py)
|
||
|
||
**新增参数**:
|
||
- `original_column_order: List[str]`:原始列顺序(如`['Record ID', 'Event Name', 'FMA', '体重', '收缩压', ...]`)
|
||
- `pivot_value_order: List[str]`:透视列值的原始顺序(如`['基线', '1个月', '2个月', ...]`)
|
||
|
||
**排序逻辑**:
|
||
```python
|
||
if original_column_order:
|
||
# 1. 索引列始终在最前面
|
||
final_cols = [index_column]
|
||
|
||
# 2. 按原始列顺序添加转换后的列
|
||
for orig_col in original_column_order:
|
||
if orig_col in value_columns:
|
||
# 找出所有属于这个原列的新列
|
||
related_cols = [c for c in df_pivot.columns if c.startswith(f'{orig_col}___')]
|
||
|
||
# ✨ 按透视列的原始顺序排序
|
||
if pivot_value_order:
|
||
pivot_order_map = {val: idx for idx, val in enumerate(pivot_value_order)}
|
||
related_cols_sorted = sorted(
|
||
related_cols,
|
||
key=lambda c: pivot_order_map.get(c.split('___')[1], 999)
|
||
)
|
||
else:
|
||
related_cols_sorted = sorted(related_cols)
|
||
|
||
final_cols.extend(related_cols_sorted)
|
||
|
||
# 3. 添加未选择的列(保持原始顺序)
|
||
if keep_unused_columns:
|
||
for orig_col in original_column_order:
|
||
if orig_col in df_pivot.columns and orig_col not in final_cols:
|
||
final_cols.append(orig_col)
|
||
|
||
# 4. 重排列
|
||
df_pivot = df_pivot[final_cols]
|
||
```
|
||
|
||
### 2. Python端(main.py)
|
||
|
||
**PivotRequest模型**:
|
||
```python
|
||
class PivotRequest(BaseModel):
|
||
# ... 原有字段 ...
|
||
original_column_order: List[str] = [] # ✨ 新增
|
||
pivot_value_order: List[str] = [] # ✨ 新增
|
||
```
|
||
|
||
**调用pivot_long_to_wide**:
|
||
```python
|
||
result_df = pivot_long_to_wide(
|
||
df,
|
||
request.index_column,
|
||
request.pivot_column,
|
||
request.value_columns,
|
||
request.aggfunc,
|
||
request.column_mapping,
|
||
request.keep_unused_columns,
|
||
request.unused_agg_method,
|
||
request.original_column_order, # ✨ 新增
|
||
request.pivot_value_order # ✨ 新增
|
||
)
|
||
```
|
||
|
||
### 3. Node.js后端(QuickActionController.ts)
|
||
|
||
**获取原始列顺序**:
|
||
```typescript
|
||
const originalColumnOrder = session.columns || [];
|
||
```
|
||
|
||
**获取透视列值的原始顺序**:
|
||
```typescript
|
||
const pivotColumn = params.pivotColumn;
|
||
const seenPivotValues = new Set();
|
||
const pivotValueOrder: string[] = [];
|
||
|
||
for (const row of fullData) {
|
||
const pivotValue = row[pivotColumn];
|
||
if (pivotValue !== null && pivotValue !== undefined && !seenPivotValues.has(pivotValue)) {
|
||
seenPivotValues.add(pivotValue);
|
||
pivotValueOrder.push(String(pivotValue));
|
||
}
|
||
}
|
||
```
|
||
|
||
**传递给QuickActionService**:
|
||
```typescript
|
||
executeResult = await quickActionService.executePivot(
|
||
fullData,
|
||
params,
|
||
session.columnMapping,
|
||
originalColumnOrder, // ✨ 新增
|
||
pivotValueOrder // ✨ 新增
|
||
);
|
||
```
|
||
|
||
### 4. Node.js后端(QuickActionService.ts)
|
||
|
||
**方法签名**:
|
||
```typescript
|
||
async executePivot(
|
||
data: any[],
|
||
params: PivotParams,
|
||
columnMapping?: any[],
|
||
originalColumnOrder?: string[], // ✨ 新增
|
||
pivotValueOrder?: string[] // ✨ 新增
|
||
): Promise<OperationResult>
|
||
```
|
||
|
||
**传递给Python**:
|
||
```typescript
|
||
const response = await axios.post(`${PYTHON_SERVICE_URL}/api/operations/pivot`, {
|
||
// ... 原有参数 ...
|
||
original_column_order: originalColumnOrder || [], // ✨ 新增
|
||
pivot_value_order: pivotValueOrder || [], // ✨ 新增
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 效果对比
|
||
|
||
### 修改前(按字母顺序)
|
||
```
|
||
Record ID | FMA___基线 | FMA___1个月 | 收缩压___基线 | 收缩压___1个月 | 体重___基线 | 体重___1个月
|
||
↑ ↑ ↑ ↑ ↑ ↑ ↑
|
||
索引列 F开头 F开头 S开头(拼音) S开头 T开头 T开头
|
||
```
|
||
|
||
### 修改后(按原始顺序)
|
||
```
|
||
Record ID | FMA___基线 | FMA___1个月 | 体重___基线 | 体重___1个月 | 收缩压___基线 | 收缩压___1个月
|
||
↑ ↑ ↑ ↑ ↑ ↑ ↑
|
||
索引列 原文件第3列 原文件第3列 原文件第4列 原文件第4列 原文件第5列 原文件第5列
|
||
```
|
||
|
||
### 透视值内部顺序(按原始出现顺序)
|
||
```
|
||
FMA___基线 | FMA___1个月 | FMA___2个月
|
||
↑ ↑ ↑
|
||
首次出现 第二次出现 第三次出现
|
||
(而不是按"1个月"、"2个月"、"基线"的字母顺序)
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ 开发完成
|
||
|
||
### 修改文件清单
|
||
1. ✅ `extraction_service/operations/pivot.py`
|
||
2. ✅ `extraction_service/main.py`
|
||
3. ✅ `backend/src/modules/dc/tool-c/controllers/QuickActionController.ts`
|
||
4. ✅ `backend/src/modules/dc/tool-c/services/QuickActionService.ts`
|
||
|
||
### 优势
|
||
- ✅ 列顺序与原文件一致(用户熟悉)
|
||
- ✅ 透视值顺序按时间顺序(基线→1个月→2个月)
|
||
- ✅ 未选择的列也保持原始顺序
|
||
- ✅ 导出Excel时顺序正确
|
||
|
||
---
|
||
|
||
**开发时间**:2025-12-09
|
||
**状态**:✅ 已完成,等待测试
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|