列名统一增加后缀(🈲勿手改)+优化空值覆盖逻辑

This commit is contained in:
zelong 2026-01-20 17:09:41 +08:00
parent 104198e918
commit 21e0160b76
6 changed files with 2604 additions and 56 deletions

View File

@ -9,7 +9,8 @@ workspace_id = 58335167
# 支持配置多个docid用逗号分隔例如
# docid = doc1,doc2,doc3
# 同步时会依次对所有表格进行同步
docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg,dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA
#docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg
docid =dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA
[Schedule]
# 同步频率(分钟)
@ -19,4 +20,4 @@ sync_interval = 30
# 企业微信应用ID
agentid = 1000615
# 接收人列表用户ID多个用|分隔,@all表示全部成员
receivers = 04636 4
receivers = 046364

View File

@ -1,4 +1,4 @@
{
"access_token": "wTDckSfv2tq6P_xM2lBqSirg8qEsUHRjLqisLLQxNX4ZDEhOxXhR9v5dLF0nV5obkkTSa87p1R_Cu-FLcVp439_xVYbFb66ENSmuJcTT-2sYoJ_4JkSbAPmfKy_8-6jqF995MOmJcoCbZU1PdB0QbLyIWe_U1du7nnZ3o0uqFdPI_cAVa1EWPLjWNvkxYrjW5rVWBIPwPvlK7VaACfsSzevBwvEO89-UbHBrZLn07dI",
"fetch_time": 1768380602.07453
"access_token": "sAnGXgA8F4wUIwz6SzTBejU4QcdCfgzffkmQWjsEaa8G7sWNIddugZZwJ0UsReuvR6b6b4mMAwYrq_Si9Lhh8ssqmjtkRrwDOqlkS1NceMMjM3eKRSTAi0Ah5PEgrU0m6Eb04icBKOKjIgc2_PA_Z_zvWOB_eJjMsCxegio1vecev-OAg3ZVNO6A7Ctt1j4Soz_2lnwN1_fapOGdIRTf__kPzTcfimqA-L35unooOSQ",
"fetch_time": 1768893512.5036647
}

2462
install.cmd Normal file

File diff suppressed because one or more lines are too long

View File

@ -26,13 +26,13 @@ from src2.logger import get_task2_logger
# ============================================================
# 字段名称常量(与智能表格列名完全一致)
# ============================================================
FIELD_TAPD_LINK = "TAPD链接" # 用户填写,解析单号
FIELD_TAPD_STATUS = "TAPD状态" # 工具回写
FIELD_OWNER = "处理人" # 工具回写
FIELD_BEGIN_DATE = "TAPD预计开始日期" # 工具回写
FIELD_DUE_DATE = "TAPD预计完成日期" # 工具回写
FIELD_PLAN = "计划" # 工具回写TAPD计划字段
FIELD_SYNC_STATUS = "同步状态" # 工具回写,标记同步结果
FIELD_TAPD_LINK = "TAPD链接(🈲勿手改)" # 用户填写,解析单号
FIELD_TAPD_STATUS = "TAPD状态(🈲勿手改)" # 工具回写
FIELD_OWNER = "处理人(🈲勿手改)" # 工具回写
FIELD_BEGIN_DATE = "TAPD预计开始日期(🈲勿手改)" # 工具回写
FIELD_DUE_DATE = "TAPD预计完成日期(🈲勿手改)" # 工具回写
FIELD_PLAN = "计划(🈲勿手改)" # 工具回写TAPD计划字段
FIELD_SYNC_STATUS = "同步状态(🈲勿手改)" # 工具回写,标记同步结果
# 必要字段列表
REQUIRED_FIELDS = [
@ -184,25 +184,46 @@ class SmartSheetSync:
"""
values = {}
# 只添加非空的字段,每个字段值需要包含 type 和 text
# 跳过 None 和空字符串
if status is not None and status != "":
values[FIELD_TAPD_STATUS] = [{"type": "text", "text": status}]
# 处理字段值:
# - None: 不更新该字段(跳过)
# - 空字符串 "": 清空该字段(传入空数组)
# - 非空字符串: 更新为新值
if owner is not None and owner != "":
values[FIELD_OWNER] = [{"type": "text", "text": owner}]
if status is not None:
if status == "":
values[FIELD_TAPD_STATUS] = []
else:
values[FIELD_TAPD_STATUS] = [{"type": "text", "text": status}]
if begin_date is not None and begin_date != "":
values[FIELD_BEGIN_DATE] = [{"type": "text", "text": begin_date}]
if owner is not None:
if owner == "":
values[FIELD_OWNER] = []
else:
values[FIELD_OWNER] = [{"type": "text", "text": owner}]
if due_date is not None and due_date != "":
values[FIELD_DUE_DATE] = [{"type": "text", "text": due_date}]
if begin_date is not None:
if begin_date == "":
values[FIELD_BEGIN_DATE] = []
else:
values[FIELD_BEGIN_DATE] = [{"type": "text", "text": begin_date}]
if plan is not None and plan != "":
values[FIELD_PLAN] = [{"type": "text", "text": plan}]
if due_date is not None:
if due_date == "":
values[FIELD_DUE_DATE] = []
else:
values[FIELD_DUE_DATE] = [{"type": "text", "text": due_date}]
if sync_status is not None and sync_status != "":
values[FIELD_SYNC_STATUS] = [{"type": "text", "text": sync_status}]
if plan is not None:
if plan == "":
values[FIELD_PLAN] = []
else:
values[FIELD_PLAN] = [{"type": "text", "text": plan}]
if sync_status is not None:
if sync_status == "":
values[FIELD_SYNC_STATUS] = []
else:
values[FIELD_SYNC_STATUS] = [{"type": "text", "text": sync_status}]
return {
"record_id": record_id,
@ -337,9 +358,12 @@ class SmartSheetSync:
skipped_terminal_count = 0
for record in all_records:
# 检查同步状态是否为"成功"
# 检查同步状态是否为成功(兼容新旧格式)
# 旧格式: "成功"
# 新格式: "✅ 同步成功 01-14 15:30"
sync_status = self.api.get_field_value_by_title(record, FIELD_SYNC_STATUS)
if sync_status != "成功":
sync_status_str = str(sync_status) if sync_status else ""
if not (sync_status == "成功" or "同步成功" in sync_status_str):
continue
# 检查TAPD链接是否存在

View File

@ -11,7 +11,7 @@
import sys
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
from datetime import datetime, timedelta, timezone
# 将项目根目录添加到 Python 路径
project_root = Path(__file__).parent.parent
@ -20,7 +20,7 @@ sys.path.insert(0, str(project_root))
from src.token_manager import TokenManager
from src2.config import Task2ConfigManager
from src2.logger import get_task2_logger
from src2.tapd_api import TAPDStoryApi, TERMINAL_STATUSES
from src2.tapd_api import TAPDStoryApi, TERMINAL_STATUSES, StoryNotFoundException
from src2.smartsheet_sync import SmartSheetSync, REQUIRED_FIELDS
@ -75,6 +75,18 @@ class SyncService:
print(f" ✓ 同步服务初始化完成")
def _get_beijing_time_str(self) -> str:
"""
获取北京时间格式字符串MM-dd HH:mm
Returns:
str: 格式化的北京时间字符串例如 "01-14 15:30"
"""
# 北京时间是 UTC+8
beijing_tz = timezone(timedelta(hours=8))
beijing_time = datetime.now(beijing_tz)
return beijing_time.strftime("%m-%d %H:%M")
def sync_once(self) -> Dict[str, Any]:
"""
执行一次完整的同步流程遍历所有配置的智能表格
@ -269,7 +281,7 @@ class SyncService:
if records_with_link:
print(f"\n--- 新记录同步 ---")
success_records = [] # 成功同步的记录
failed_record_ids = [] # 失败的记录ID列表
failed_records_info = [] # 失败的记录信息列表包含ID和状态
for record_info in records_with_link:
record_result = self._process_record(record_info)
@ -283,7 +295,12 @@ class SyncService:
sheet_result["records_updated"] += 1
else:
sheet_result["records_failed"] += 1
failed_record_ids.append(record_info["record_id"])
# 收集失败记录的ID和对应的同步状态
failed_records_info.append({
"record_id": record_info["record_id"],
"sync_status": record_result.get("sync_status", "⚠️ 同步失败请联系PM")
})
# 收集失败记录的详细信息用于推送
failed_record = {
@ -303,16 +320,16 @@ class SyncService:
except Exception as e:
print(f" ✗ 成功记录回写失败: {e}")
# 5. 批量回写失败记录
if failed_record_ids:
print(f"\n正在回写 {len(failed_record_ids)} 条失败记录的状态...")
# 5. 批量回写失败记录(根据不同失败原因回写不同状态)
if failed_records_info:
print(f"\n正在回写 {len(failed_records_info)} 条失败记录的状态...")
try:
failed_updates = [
self.current_smartsheet.build_update_record(
record_id=record_id,
sync_status="失败"
record_id=info["record_id"],
sync_status=info["sync_status"]
)
for record_id in failed_record_ids
for info in failed_records_info
]
self.current_smartsheet.batch_update_records(sheet_id, failed_updates)
print(f" ✓ 失败记录状态回写完成")
@ -351,12 +368,14 @@ class SyncService:
"success": False,
"update_record": None,
"error_message": None,
"story_info": None
"story_info": None,
"sync_status": None # 用于失败记录的状态回写
}
# 检查链接解析结果
if not record_info["parse_success"]:
record_result["error_message"] = record_info.get("parse_error", "链接解析失败")
record_result["sync_status"] = "❌ 单号无效"
# 记录链接解析失败日志
self.logger.log_api_call(
api_type="task2",
@ -378,14 +397,14 @@ class SyncService:
story_info = self.tapd_api.get_story(story_id)
record_result["story_info"] = story_info
# 提取需要同步的字段
status = story_info.get('status', '')
owner = story_info.get('owner', '')
begin_date = story_info.get('begin', '')
due_date = story_info.get('due', '')
# 提取需要同步的字段None 转为空字符串)
status = story_info.get('status') or ''
owner = story_info.get('owner') or ''
begin_date = story_info.get('begin') or ''
due_date = story_info.get('due') or ''
# 提取计划字段并转换为中文名称
plan_id = story_info.get('custom_plan_field_1', '')
plan_id = story_info.get('custom_plan_field_1') or ''
plan_name = self.tapd_api.map_plan_id_to_name(plan_id)
# 获取当前字段值,判断是否需要更新
@ -395,7 +414,11 @@ class SyncService:
current_values, status, owner, begin_date, due_date, plan_name
)
# 构造更新记录(包含业务字段 + 同步状态=成功)
# 生成同步成功状态(包含时间戳)
time_str = self._get_beijing_time_str()
sync_status_success = f"✅ 同步成功 {time_str}"
# 构造更新记录(包含业务字段 + 同步状态=成功+时间戳)
# 即使业务字段没有变化,也要写入同步状态
update_record = self.current_smartsheet.build_update_record(
record_id=record_info["record_id"],
@ -404,14 +427,33 @@ class SyncService:
begin_date=begin_date,
due_date=due_date,
plan=plan_name,
sync_status="成功"
sync_status=sync_status_success
)
record_result["update_record"] = update_record
record_result["success"] = True
except Exception as e:
except StoryNotFoundException as e:
# 单号无效TAPD中未找到该需求
record_result["error_message"] = str(e)
record_result["sync_status"] = "❌ 单号无效"
# 记录TAPD查询失败日志
self.logger.log_api_call(
api_type="task2",
operation="sync_record_failure",
request_data={
"record_id": record_info["record_id"],
"story_id": story_id
},
response_data={},
success=False,
error_message=record_result["error_message"]
)
except Exception as e:
# 其他异常API调用失败等
record_result["error_message"] = str(e)
record_result["sync_status"] = "⚠️ 同步失败请联系PM"
# 记录TAPD查询失败日志
self.logger.log_api_call(
api_type="task2",
@ -515,28 +557,39 @@ class SyncService:
# 查询TAPD最新状态
story_info = self.tapd_api.get_story(record_info["story_id"])
# 提取最新字段值
new_status = story_info.get('status', '')
new_owner = story_info.get('owner', '')
new_begin = story_info.get('begin', '')
new_due = story_info.get('due', '')
plan_id = story_info.get('custom_plan_field_1', '')
# 提取最新字段值None 转为空字符串)
new_status = story_info.get('status') or ''
new_owner = story_info.get('owner') or ''
new_begin = story_info.get('begin') or ''
new_due = story_info.get('due') or ''
plan_id = story_info.get('custom_plan_field_1') or ''
new_plan = self.tapd_api.map_plan_id_to_name(plan_id)
# 获取当前值并比较
current = self.current_smartsheet.get_current_field_values(record_info["record"])
# 调试:打印当前值和新值(含类型)
print(f"\n [DEBUG] 记录 {record_info['record_id']} 字段比较:")
print(f" 结束日期: 当前='{current.get('TAPD预计完成日期')}' (type={type(current.get('TAPD预计完成日期'))}) vs 新='{new_due}' (type={type(new_due)})")
needs_update = self._check_needs_update(
current, new_status, new_owner, new_begin, new_due, new_plan
)
print(f" needs_update = {needs_update}")
if needs_update:
# 生成同步成功状态(包含时间戳)
time_str = self._get_beijing_time_str()
sync_status_success = f"✅ 同步成功 {time_str}"
update_record = self.current_smartsheet.build_update_record(
record_id=record_info["record_id"],
status=new_status,
owner=new_owner,
begin_date=new_begin,
due_date=new_due,
plan=new_plan
plan=new_plan,
sync_status=sync_status_success
)
updates.append(update_record)

View File

@ -16,6 +16,14 @@ from requests.auth import HTTPBasicAuth
from src2.logger import get_task2_logger
# ============================================================
# 自定义异常类
# ============================================================
class StoryNotFoundException(Exception):
"""当TAPD需求Story不存在时抛出的异常"""
pass
# TAPD状态值映射表
# 将API返回的状态代码转换为中文显示文本
STATUS_MAPPING = {
@ -225,7 +233,7 @@ class TAPDStoryApi:
data = result.get('data', [])
if not isinstance(data, list) or len(data) == 0:
raise RuntimeError(f"未找到需求: {story_id}")
raise StoryNotFoundException(f"未找到需求: {story_id}")
# 取第一个元素
first_item = data[0]
@ -237,7 +245,7 @@ class TAPDStoryApi:
raise RuntimeError(f"API返回数据格式异常: {first_item}")
if not story_info:
raise RuntimeError(f"未找到需求: {story_id}")
raise StoryNotFoundException(f"未找到需求: {story_id}")
# 转换状态为中文
raw_status = story_info.get('status', '')