feat(飞书通知): 实现营业额数据推送至飞书的功能
添加完整的飞书推送功能,包括卡片消息、富文本消息和纯文本消息三种形式 支持与昨日数据的对比展示,包含金额差值和百分比变化 添加请求签名和错误处理机制,确保推送可靠性
This commit is contained in:
parent
940e493e39
commit
166109a1c8
190
backend/app.py
190
backend/app.py
@ -81,7 +81,100 @@ with app.app_context():
|
||||
db.session.commit()
|
||||
|
||||
def push_feishu(date_str: str, amount: float, reason: str):
|
||||
return
|
||||
cfg = load_config()
|
||||
url = cfg.get("feishu_webhook_url")
|
||||
if not url:
|
||||
return
|
||||
shop = cfg.get("shop_name", "益选便利店")
|
||||
try:
|
||||
d = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except Exception:
|
||||
d = datetime.now().date()
|
||||
y = d - timedelta(days=1)
|
||||
r = DailyRevenue.query.filter_by(date=y).first()
|
||||
y_amt = (r.amount if (r and r.is_final) else None)
|
||||
arrow = ''
|
||||
diff_str = ''
|
||||
pct_str = ''
|
||||
if isinstance(y_amt, (int, float)):
|
||||
diff = amount - y_amt
|
||||
arrow = '🔺' if diff >= 0 else '🔻'
|
||||
diff_str = f"{'+' if diff >= 0 else '-'}{abs(diff):.2f}"
|
||||
if y_amt != 0:
|
||||
pct = abs(diff / y_amt) * 100.0
|
||||
pct_str = f"({'+' if diff >= 0 else '-'}{pct:.2f}%)"
|
||||
today_line = f"**今日**:¥{amount:.2f}"
|
||||
if isinstance(y_amt, (int, float)):
|
||||
today_line += f" {arrow} {diff_str} {pct_str}".strip()
|
||||
y_line = f"**昨日**:{('暂无数据' if y_amt is None else '¥' + format(y_amt, '.2f'))}"
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"elements": [
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"📊 **{shop}** 营业额通知"}},
|
||||
{"tag": "hr"},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"**日期**:{date_str}"}},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": today_line}},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": y_line}},
|
||||
{"tag": "note", "elements": [
|
||||
{"tag": "plain_text", "content": f"原因:{reason} | 时间:{datetime.now().isoformat(timespec='seconds')}"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
payload = {"msg_type": "interactive", "card": card}
|
||||
secret = cfg.get("feishu_secret")
|
||||
if secret:
|
||||
ts = str(int(time.time()))
|
||||
sign_src = ts + "\n" + secret
|
||||
sign = base64.b64encode(hmac.new(secret.encode(), sign_src.encode(), digestmod=hashlib.sha256).digest()).decode()
|
||||
payload.update({"timestamp": ts, "sign": sign})
|
||||
def _log(s: str):
|
||||
p = os.path.join(os.path.dirname(__file__), "..", "app.log")
|
||||
with open(p, 'a', encoding='utf-8') as f:
|
||||
f.write(s + "\n")
|
||||
try:
|
||||
print(s, flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
def _post_json(u: str, payload_obj: dict):
|
||||
body = json.dumps(payload_obj, ensure_ascii=False).encode('utf-8')
|
||||
return requests.post(u, data=body, headers={'Content-Type': 'application/json; charset=utf-8'}, timeout=5)
|
||||
try:
|
||||
is_feishu = ('open.feishu.cn' in url)
|
||||
ok = False
|
||||
if is_feishu:
|
||||
resp = _post_json(url, payload)
|
||||
ok = (200 <= resp.status_code < 300)
|
||||
_log(f"飞书推送卡片{'成功' if ok else '失败'}: status={resp.status_code} body={resp.text[:500]}")
|
||||
if not ok:
|
||||
post_payload = {
|
||||
"msg_type": "post",
|
||||
"content": {
|
||||
"post": {
|
||||
"zh_cn": {
|
||||
"title": f"{shop} 营业额通知",
|
||||
"content": [[
|
||||
{"tag":"text","text": f"日期:{date_str}\n"},
|
||||
{"tag":"text","text": f"今日:¥{amount:.2f}"},
|
||||
*( [{"tag":"text","text": f" {arrow} {diff_str} {pct_str}"}] if isinstance(y_amt,(int,float)) else [] ),
|
||||
],[
|
||||
{"tag":"text","text": f"原因:{reason}"}
|
||||
]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resp_post = _post_json(url, post_payload)
|
||||
ok = (200 <= resp_post.status_code < 300)
|
||||
_log(f"飞书推送POST{'成功' if ok else '失败'}: status={resp_post.status_code} body={resp_post.text[:500]}")
|
||||
if not ok:
|
||||
text = f"{shop}\n日期:{date_str}\n今日:¥{amount:.2f}"
|
||||
if isinstance(y_amt, (int, float)):
|
||||
text += f" {arrow} {diff_str} {pct_str}".strip()
|
||||
text += f"\n原因:{reason}"
|
||||
resp2 = _post_json(url, {"msg_type":"text","content":{"text": text}})
|
||||
_log(f"飞书推送文本{'成功' if (200 <= resp2.status_code < 300) else '失败'}: status={resp2.status_code} body={resp2.text[:500]}")
|
||||
except Exception as e:
|
||||
_log(f"飞书推送异常: {str(e)[:200]}")
|
||||
|
||||
def generate_mock_revenue():
|
||||
"""保持原有逻辑:生成当日模拟营业额"""
|
||||
@ -579,98 +672,3 @@ def sse_events():
|
||||
yield "data: {\"type\": \"force_refresh\"}\n\n"
|
||||
time.sleep(30)
|
||||
return Response(stream_with_context(event_stream()), mimetype='text/event-stream')
|
||||
def push_feishu(date_str: str, amount: float, reason: str):
|
||||
cfg = load_config()
|
||||
url = cfg.get("feishu_webhook_url")
|
||||
if not url:
|
||||
return
|
||||
shop = cfg.get("shop_name", "益选便利店")
|
||||
try:
|
||||
d = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except Exception:
|
||||
d = datetime.now().date()
|
||||
y = d - timedelta(days=1)
|
||||
r = DailyRevenue.query.filter_by(date=y).first()
|
||||
y_amt = (r.amount if (r and r.is_final) else None)
|
||||
arrow = ''
|
||||
diff_str = ''
|
||||
pct_str = ''
|
||||
if isinstance(y_amt, (int, float)):
|
||||
diff = amount - y_amt
|
||||
arrow = '🔺' if diff >= 0 else '🔻'
|
||||
diff_str = f"{'+' if diff >= 0 else '-'}{abs(diff):.2f}"
|
||||
if y_amt != 0:
|
||||
pct = abs(diff / y_amt) * 100.0
|
||||
pct_str = f"({'+' if diff >= 0 else '-'}{pct:.2f}%)"
|
||||
today_line = f"**今日**:¥{amount:.2f}"
|
||||
if isinstance(y_amt, (int, float)):
|
||||
today_line += f" {arrow} {diff_str} {pct_str}".strip()
|
||||
y_line = f"**昨日**:{('暂无数据' if y_amt is None else '¥' + format(y_amt, '.2f'))}"
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"elements": [
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"📊 **{shop}** 营业额通知"}},
|
||||
{"tag": "hr"},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"**日期**:{date_str}"}},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": today_line}},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": y_line}},
|
||||
{"tag": "note", "elements": [
|
||||
{"tag": "plain_text", "content": f"原因:{reason} | 时间:{datetime.now().isoformat(timespec='seconds')}"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
payload = {"msg_type": "interactive", "card": card}
|
||||
secret = cfg.get("feishu_secret")
|
||||
if secret:
|
||||
ts = str(int(time.time()))
|
||||
sign_src = ts + "\n" + secret
|
||||
sign = base64.b64encode(hmac.new(secret.encode(), sign_src.encode(), digestmod=hashlib.sha256).digest()).decode()
|
||||
payload.update({"timestamp": ts, "sign": sign})
|
||||
def _log(s: str):
|
||||
p = os.path.join(os.path.dirname(__file__), "..", "app.log")
|
||||
with open(p, 'a', encoding='utf-8') as f:
|
||||
f.write(s + "\n")
|
||||
try:
|
||||
print(s, flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
def _post_json(u: str, payload_obj: dict):
|
||||
body = json.dumps(payload_obj, ensure_ascii=False).encode('utf-8')
|
||||
return requests.post(u, data=body, headers={'Content-Type': 'application/json; charset=utf-8'}, timeout=5)
|
||||
try:
|
||||
is_feishu = ('open.feishu.cn' in url)
|
||||
ok = False
|
||||
if is_feishu:
|
||||
resp = _post_json(url, payload)
|
||||
ok = (200 <= resp.status_code < 300)
|
||||
_log(f"飞书推送卡片{'成功' if ok else '失败'}: status={resp.status_code} body={resp.text[:500]}")
|
||||
if not ok:
|
||||
post_payload = {
|
||||
"msg_type": "post",
|
||||
"content": {
|
||||
"post": {
|
||||
"zh_cn": {
|
||||
"title": f"{shop} 营业额通知",
|
||||
"content": [[
|
||||
{"tag":"text","text": f"日期:{date_str}\n"},
|
||||
{"tag":"text","text": f"今日:¥{amount:.2f}"},
|
||||
*( [{"tag":"text","text": f" {arrow} {diff_str} {pct_str}"}] if isinstance(y_amt,(int,float)) else [] ),
|
||||
],[
|
||||
{"tag":"text","text": f"原因:{reason}"}
|
||||
]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resp_post = _post_json(url, post_payload)
|
||||
ok = (200 <= resp_post.status_code < 300)
|
||||
_log(f"飞书推送POST{'成功' if ok else '失败'}: status={resp_post.status_code} body={resp_post.text[:500]}")
|
||||
if not ok:
|
||||
text = f"{shop}\n日期:{date_str}\n今日:¥{amount:.2f}"
|
||||
if isinstance(y_amt, (int, float)):
|
||||
text += f" {arrow} {diff_str} {pct_str}".strip()
|
||||
text += f"\n原因:{reason}"
|
||||
resp2 = _post_json(url, {"msg_type":"text","content":{"text": text}})
|
||||
_log(f"飞书推送文本{'成功' if (200 <= resp2.status_code < 300) else '失败'}: status={resp2.status_code} body={resp2.text[:500]}")
|
||||
except Exception as e:
|
||||
_log(f"飞书推送异常: {str(e)[:200]}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user