From 5e68d971278d13d6af17ca7d3c7cabe576f2b58a Mon Sep 17 00:00:00 2001 From: houhuan Date: Tue, 9 Dec 2025 13:28:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.log | 1 + backend/app.py | 135 ++++++++++++++++++++++++++++++++++++++----------- config.json | 3 +- data/data.db | Bin 16384 -> 16384 bytes 4 files changed, 108 insertions(+), 31 deletions(-) diff --git a/app.log b/app.log index 15cd563..6ad123d 100644 --- a/app.log +++ b/app.log @@ -2,3 +2,4 @@ 准备发送消息: 【益选便利店】2025-12-08的营业额:3402.6 准备发送消息: 【益选便利店】2025-12-08的营业额:3629.76 准备发送消息: 【益选便利店】2025-12-06的营业额:1803.09 +准备发送消息: 【益选便利店】2025-12-09的营业额:3462.53 diff --git a/backend/app.py b/backend/app.py index 1ef44f7..d6ab73d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -23,23 +23,24 @@ load_dotenv() app = Flask(__name__, static_folder="../frontend", static_url_path="/static") CORS(app) -# 强制使用绝对路径解决 Windows 路径问题 base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) -db_path = os.path.join(base_dir, "data", "data.db") +default_db_path = os.path.join(base_dir, "data", "data.db") if os.name == 'nt': - # Windows 需要转义反斜杠,或者使用 4 个斜杠 + 驱动器号 - # SQLAlchemy 在 Windows 上通常接受 sqlite:///C:\path\to\file - app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{db_path}" + default_db_url = f"sqlite:///{default_db_path}" else: - app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:////{db_path}" + default_db_url = f"sqlite:////{default_db_path}" +db_url = os.getenv('DATABASE_URL') or default_db_url +app.config['SQLALCHEMY_DATABASE_URI'] = db_url -def _ensure_sqlite_dir(url): - # 已通过绝对路径计算,直接检查 data 目录 - d = os.path.dirname(db_path) - if not os.path.exists(d): +def _ensure_sqlite_dir(url: str): + if not isinstance(url, str) or not url.startswith('sqlite'): + return + p = url.replace('sqlite:////', '/').replace('sqlite:///', '') + d = os.path.dirname(p) + if d and not os.path.exists(d): os.makedirs(d, exist_ok=True) -_ensure_sqlite_dir(app.config['SQLALCHEMY_DATABASE_URI']) +_ensure_sqlite_dir(db_url) db = SQLAlchemy(app) class DailyRevenue(db.Model): @@ -125,19 +126,33 @@ def daily_job(target_date=None): def settle_today_if_due(): cfg = load_config() - cutoff = cfg.get("cutoff_hour", 23) + ct = cfg.get("cutoff_time") + ch = cfg.get("cutoff_hour", 23) + cm = 0 + if isinstance(ct, str) and re.match(r'^\d{1,2}:\d{2}$', ct): + try: + p = ct.split(':') + ch = int(p[0]); cm = int(p[1]) + except Exception: + pass try: - cutoff = int(cutoff) + ch = int(ch) except Exception: - cutoff = 23 - # 只有当前时间 >= cutoff 才尝试结算今天 - if datetime.now().hour >= cutoff: + ch = 23 + if ch < 0 or ch > 23: + ch = 23 + if cm < 0 or cm > 59: + cm = 0 + local_tz = get_localzone() + now = datetime.now(local_tz) + if (now.hour > ch) or (now.hour == ch and now.minute >= cm): daily_job() def settle_past_days(): """启动检查:补录过去3天未定版的数据(防止服务器宕机漏单)""" with app.app_context(): - today = datetime.now().date() + local_tz = get_localzone() + today = datetime.now(local_tz).date() for i in range(1, 4): d = today - timedelta(days=i) existing = DailyRevenue.query.filter_by(date=d).first() @@ -209,7 +224,9 @@ def get_periods(today: date): def api_metrics(): cfg = load_config() shop_name = cfg.get("shop_name", "益选便利店") - today_local = datetime.now().date() + local_tz = get_localzone() + now_local = datetime.now(local_tz) + today_local = now_local.date() periods = get_periods(today_local) yday = periods["yesterday"] day_before = periods["day_before"] @@ -230,11 +247,33 @@ def api_metrics(): cur += timedelta(days=1) return round(total, 2) weekdays = ['周一','周二','周三','周四','周五','周六','周日'] + def get_cutoff_hm(): + ct = cfg.get("cutoff_time") + h = cfg.get("cutoff_hour", 23) + m = 0 + if isinstance(ct, str) and re.match(r'^\d{1,2}:\d{2}$', ct): + try: + p = ct.split(':') + h = int(p[0]); m = int(p[1]) + except Exception: + pass + try: + h = int(h) + except Exception: + h = 23 + if h < 0 or h > 23: + h = 23 + if m < 0 or m > 59: + m = 0 + return h, m + ch, cm = get_cutoff_hm() + show_today = (now_local.hour > ch) or (now_local.hour == ch and now_local.minute >= cm) out = { "shop_name": shop_name, - "server_now": datetime.now().isoformat(timespec='seconds'), - "cutoff_hour": 23, - "today": {"date": today_local.isoformat(), "weekday": weekdays[today_local.weekday()], "amount": amt_for(today_local)}, + "server_now": now_local.isoformat(timespec='seconds'), + "cutoff_hour": ch, + "cutoff_time": f"{ch:02d}:{cm:02d}", + "today": {"date": today_local.isoformat(), "weekday": weekdays[today_local.weekday()], "amount": (amt_for(today_local) if show_today else None)}, "yesterday": {"date": yday.isoformat(), "amount": amt_for(yday)}, "day_before": {"date": day_before.isoformat(), "amount": amt_for(day_before)}, "this_week": {"start": tw_s.isoformat(), "end": tw_e.isoformat(), "total": sum_final(tw_s, tw_e)}, @@ -245,16 +284,36 @@ def api_metrics(): @app.route("/api/series7") def api_series7(): - today_local = datetime.now().date() + local_tz = get_localzone() + now_local = datetime.now(local_tz) + today_local = now_local.date() days = int(request.args.get('days', 7)) days = max(7, min(days, 90)) - # 结束日期:若今日已定版则含今日,否则到昨日 + # 结束日期:若到达截止时间且今日已定版则含今日,否则到昨日 end = today_local r_today = DailyRevenue.query.filter_by(date=today_local).first() - if not (r_today and r_today.is_final): + cfg = load_config() + ct = cfg.get("cutoff_time") + ch = cfg.get("cutoff_hour", 23) + cm = 0 + if isinstance(ct, str) and re.match(r'^\d{1,2}:\d{2}$', ct): + try: + p = ct.split(':') + ch = int(p[0]); cm = int(p[1]) + except Exception: + pass + try: + ch = int(ch) + except Exception: + ch = 23 + if ch < 0 or ch > 23: + ch = 23 + if cm < 0 or cm > 59: + cm = 0 + show_today = (now_local.hour > ch) or (now_local.hour == ch and now_local.minute >= cm) + if not (show_today and r_today and r_today.is_final): end = today_local - timedelta(days=1) start = end - timedelta(days=days-1) - cfg = load_config() series = [] cur = start while cur <= end: @@ -416,7 +475,8 @@ def auto_import_csv_on_start(): def sync_log_to_db(): """启动时将 app.log 中缺失的数据同步到 DB(只同步今天之前)""" log_map = parse_app_log() - today_local = datetime.now().date() + local_tz = get_localzone() + today_local = datetime.now(local_tz).date() for ds, amt in log_map.items(): d = datetime.strptime(ds, '%Y-%m-%d').date() if d >= today_local: @@ -430,11 +490,25 @@ def sync_log_to_db(): if __name__ == "__main__": local_tz = get_localzone() scheduler = BackgroundScheduler(timezone=local_tz) + cfg = load_config() + ct = cfg.get("cutoff_time") + ch = cfg.get("cutoff_hour", 23) + cm = 0 + if isinstance(ct, str) and re.match(r'^\d{1,2}:\d{2}$', ct): + try: + p = ct.split(':') + ch = int(p[0]); cm = int(p[1]) + except Exception: + pass try: - cutoff = int(load_config().get("cutoff_hour", 23)) + ch = int(ch) except Exception: - cutoff = 23 - scheduler.add_job(daily_job, "cron", hour=cutoff, minute=0) + ch = 23 + if ch < 0 or ch > 23: + ch = 23 + if cm < 0 or cm > 59: + cm = 0 + scheduler.add_job(daily_job, "cron", hour=ch, minute=cm) scheduler.start() with app.app_context(): sync_log_to_db() @@ -447,7 +521,8 @@ if __name__ == "__main__": def sse_events(): def event_stream(): while True: - now = datetime.now() + local_tz = get_localzone() + now = datetime.now(local_tz) payload = {"type": "tick", "server_now": now.isoformat(timespec='seconds')} yield f"data: {json.dumps(payload)}\n\n" if now.minute in (0, 1): diff --git a/config.json b/config.json index eb6f2cc..92d00ed 100644 --- a/config.json +++ b/config.json @@ -11,5 +11,6 @@ 1600, 2000 ], - "cutoff_hour": 11 + "cutoff_hour": 13, + "cutoff_time": "13:18" } diff --git a/data/data.db b/data/data.db index 567374e811c83253331c267f5677aee8c6dcadf3..d11dbbffef27b2552eb45a614fefdee62820745d 100644 GIT binary patch delta 242 zcmZo@U~Fh$oFL7}I#I@%k#%Fj5`G>QzG)2nOZmO{KJZQ3EGW>%7Zt(6$)L#Y$muB0 zU}Ruqs%vPZYhdZHnwLLD)&|Nzi_}BBt@pJR-0J>`hpKKt= fT@37+lCq8n#V~hG*09&$LpaFP+|pz+xBXWD83Q|u delta 83 zcmZo@U~Fh$oFL7}GEv5vk!54T5`Hdbz7GuiOZmO{K5Q0Lc*M8)gPaH>7c>7A2L3Dj lpZFgG1-I}|oFFv$ioO9*n45urJ%1cOH&FO5-(+t4uK*&78NUDk