列名统一增加后缀(🈲勿手改)+优化空值覆盖逻辑
This commit is contained in:
parent
104198e918
commit
21e0160b76
@ -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
|
||||
|
||||
@ -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
2462
install.cmd
Normal file
File diff suppressed because one or more lines are too long
@ -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链接是否存在
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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', '')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user